Skip to content

Commit ec29407

Browse files
committed
feat(cli): add sync command as a shorthand for backfill import
`codetime sync` runs `backfill import --source all`. The bare `backfill` command defaults to a dry plan, which surprised users who expected the obvious command to actually upload their history; `sync` is the friendly front door for that without changing backfill's own semantics.
1 parent a31c3e1 commit ec29407

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

packages/cli/src/cli.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,23 @@ function createCli(ctx: RunContext, registry: AdapterRegistry) {
175175
.option('--force', 'Force full re-import: clear watermark and re-process all files')
176176
.action((action, options) => backfillCommand({ ...normalizeOptions(options), action }, ctx, registry))
177177

178+
// `sync` is the friendly front door to the upload path: it just runs
179+
// `backfill import --source all`. Most users want "send my local
180+
// history now", and the bare `backfill` defaults to a dry plan — this
181+
// removes that surprise without changing backfill's own semantics.
182+
cli.command('sync', 'Import and upload all local agent history (shorthand for `backfill import --source all`)')
183+
.option('--source <source>', 'Limit to one source (default: all)')
184+
.option('--since <time>', 'Only include history after this time')
185+
.option('--until <time>', 'Only include history before this time')
186+
.option('--project <name>', 'Project filter')
187+
.option('--batch-size <count>', 'Max rollups per request (also bounded by --batch-bytes)')
188+
.option('--force', 'Force full re-import: clear watermark and re-process all files')
189+
.option('--dry-run', 'Print the planned import without uploading')
190+
.action((options) => {
191+
const opts = normalizeOptions(options)
192+
return backfillCommand({ ...opts, action: 'import', source: stringOption(opts.source) || 'all' }, ctx, registry)
193+
})
194+
178195
// Browser login (device-code flow): opens `<remote>/cli/auth?code=…`,
179196
// polls until the user approves it there, then writes the upload token
180197
// to config — the one-click alternative to `token set`. Works over SSH
@@ -1536,6 +1553,7 @@ Usage:
15361553
codetime detect [--json] [--home <path>]
15371554
codetime install [--target codex,claude,opencode,pi] [--all] [--dry-run] [--force] [--home <path>]
15381555
codetime hook --agent <name>
1556+
codetime sync [--source <source>] [--force] [--dry-run]
15391557
codetime backfill discover|plan|import|verify --source codex|claude-code|opencode|pi|all --dry-run [--json] [--batch-size <count>]
15401558
codetime login [--no-browser] [--remote <url>]
15411559
codetime token set <token>
@@ -1551,6 +1569,7 @@ Commands:
15511569
detect Show supported local targets and install status.
15521570
install Install integration files into detected or requested targets.
15531571
hook Read agent hook JSON from stdin and report a throttled event.
1572+
sync Import and upload all local agent history (backfill import --source all).
15541573
backfill Discover local history and create metadata-only import plans.
15551574
login Authorize this machine by signing in through your browser.
15561575
token Set, show, or clear the persisted API token.

packages/cli/test/cli.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,3 +1575,40 @@ test('login fails and writes no token when the code expires', async () => {
15751575
assert.equal(exitCode, 1)
15761576
await assert.rejects(readFile(path.join(home, '.codetime', 'config.json'), 'utf8'), { code: 'ENOENT' })
15771577
})
1578+
1579+
test('sync uploads local history via the import path', async () => {
1580+
const home = await createCodexBackfillHome()
1581+
const calls: Array<{ url: string, body: string }> = []
1582+
1583+
// `sync` is sugar for `backfill import --source all`; with codex
1584+
// fixtures present it must POST rollups to the ingest endpoint.
1585+
const exitCode = await run(['sync', '--home', home, '--api-url', 'http://example.test'], testContext({
1586+
stdout: { write: () => {} },
1587+
fetch: async (url, init) => {
1588+
const body = String(init?.body)
1589+
const rollups = JSON.parse(body).rollups
1590+
calls.push({ url: String(url), body })
1591+
return Response.json({ inserted: rollups.length, skipped: 0, conflicts: 0, conflictIds: [] }, { status: 200 })
1592+
},
1593+
}))
1594+
1595+
assert.equal(exitCode, 0)
1596+
assert.equal(calls.length, 1)
1597+
assert.match(calls[0].url, /\/v3\/agent\/ingest$/)
1598+
})
1599+
1600+
test('sync --dry-run plans without uploading', async () => {
1601+
const home = await createCodexBackfillHome()
1602+
const calls: string[] = []
1603+
1604+
const exitCode = await run(['sync', '--dry-run', '--home', home, '--api-url', 'http://example.test'], testContext({
1605+
stdout: { write: () => {} },
1606+
fetch: async (url) => {
1607+
calls.push(String(url))
1608+
return Response.json({}, { status: 200 })
1609+
},
1610+
}))
1611+
1612+
assert.equal(exitCode, 0)
1613+
assert.equal(calls.length, 0)
1614+
})

0 commit comments

Comments
 (0)