orizu 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ import { extractErrorMessage } from './error-response.js'
27
27
  import { parseGlobalFlags } from './global-flags.js'
28
28
  import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js'
29
29
  import { runLocalAppPreview } from './preview-runtime.js'
30
+ import { parseAssignmentManifestJsonl } from './task-assignment-manifest.js'
30
31
  import {
31
32
  assertSecureTokenTransport,
32
33
  authedFetch,
@@ -35,15 +36,32 @@ import {
35
36
  setGlobalFlags,
36
37
  } from './http.js'
37
38
  import {
39
+ computeSkillContentHash,
38
40
  getSkillInstallPath,
41
+ getSkillTargetStatus,
42
+ getTargetForAgent,
39
43
  installSkillTarget,
44
+ isSkillInstallAgent,
40
45
  isSkillInstallTarget,
46
+ resolveSkillSource,
47
+ SKILL_INSTALL_AGENTS,
41
48
  SKILL_INSTALL_TARGETS,
49
+ SkillInstallMode,
50
+ SkillInstallScope,
42
51
  SkillInstallTarget,
52
+ SkillTargetStatus,
43
53
  targetNeedsOverwrite,
44
54
  } from './skill-installer.js'
55
+ import { renderBanner } from './banner.js'
45
56
  import { formatTaskCreateError } from './task-create-error.js'
57
+ import { renderAgentSetupPrompt } from './setup-prompt.js'
46
58
  import { LoginResponse } from './types.js'
59
+ import {
60
+ getWorkspaceRoot,
61
+ initOrizuWorkspace,
62
+ WORKSPACE_DIR_NAME,
63
+ workspaceExists,
64
+ } from './workspace.js'
47
65
 
48
66
  interface Team {
49
67
  id: string
@@ -107,6 +125,8 @@ interface PromptSummary {
107
125
  id: string
108
126
  name: string
109
127
  role: string
128
+ status?: string
129
+ archivedAt?: string | null
110
130
  description?: string | null
111
131
  }
112
132
 
@@ -279,7 +299,16 @@ function printVersion() {
279
299
  printLine(`orizu ${getCliVersion()}`)
280
300
  }
281
301
 
302
+ function printBannerIfInteractive() {
303
+ if (!process.stdout.isTTY) {
304
+ return
305
+ }
306
+ printLine(renderBanner())
307
+ printLine('')
308
+ }
309
+
282
310
  function printUsage() {
311
+ printBannerIfInteractive()
283
312
  printLine(renderRootHelp())
284
313
  }
285
314
 
@@ -288,7 +317,7 @@ function printPreviewUsage() {
288
317
  }
289
318
 
290
319
  function printOptimizationUsage() {
291
- printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--num-threads auto|N] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
320
+ printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
292
321
  }
293
322
 
294
323
  let cliArgs = process.argv.slice(2)
@@ -492,15 +521,17 @@ function printPromptSummaries(items: PromptSummary[], emptyMessage: string) {
492
521
  id: sanitizeTerminalText(item.id),
493
522
  name: sanitizeTerminalText(item.name),
494
523
  role: sanitizeTerminalText(item.role),
524
+ status: sanitizeTerminalText(item.status || 'active'),
495
525
  }))
496
526
  const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length))
497
527
  const nameWidth = Math.max('NAME'.length, ...rows.map(row => row.name.length))
498
528
  const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
529
+ const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length))
499
530
 
500
- printLine(`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`)
501
- printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`)
531
+ printLine(`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)} ${'STATUS'.padEnd(statusWidth)}`)
532
+ printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)} ${'-'.repeat(statusWidth)}`)
502
533
  rows.forEach(row => {
503
- printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`)
534
+ printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)} ${row.status.padEnd(statusWidth)}`)
504
535
  })
505
536
  }
506
537
 
@@ -1168,6 +1199,10 @@ async function login() {
1168
1199
  assertSecureTokenTransport(baseUrl)
1169
1200
 
1170
1201
  if (hasArg('--no-prompt-if-logged-in') && getServerCredentials(baseUrl)) {
1202
+ if (hasJsonFlag()) {
1203
+ printJson({ status: 'already-logged-in', server: baseUrl })
1204
+ return
1205
+ }
1171
1206
  printLine(`Already logged in to ${sanitizeTerminalText(baseUrl)}.`)
1172
1207
  return
1173
1208
  }
@@ -1262,6 +1297,14 @@ async function login() {
1262
1297
  apiKey: loginData.apiKey,
1263
1298
  })
1264
1299
 
1300
+ if (hasJsonFlag()) {
1301
+ printJson({
1302
+ status: 'logged-in',
1303
+ server: baseUrl,
1304
+ user: { id: loginData.user.id, email: loginData.user.email ?? null },
1305
+ })
1306
+ return
1307
+ }
1265
1308
  printLine(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`)
1266
1309
  }
1267
1310
 
@@ -1272,6 +1315,10 @@ async function whoami() {
1272
1315
  }
1273
1316
 
1274
1317
  const data = await response.json() as { user: { id: string; email: string | null } }
1318
+ if (hasJsonFlag()) {
1319
+ printJson({ user: data.user, server: getBaseUrl() })
1320
+ return
1321
+ }
1275
1322
  printLine(sanitizeTerminalText(data.user.email ?? data.user.id))
1276
1323
  }
1277
1324
 
@@ -1279,6 +1326,10 @@ async function logout() {
1279
1326
  const baseUrl = getBaseUrl()
1280
1327
  const credentials = getServerCredentials(baseUrl)
1281
1328
  if (!credentials) {
1329
+ if (hasJsonFlag()) {
1330
+ printJson({ status: 'already-logged-out', server: baseUrl })
1331
+ return
1332
+ }
1282
1333
  printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`)
1283
1334
  return
1284
1335
  }
@@ -1306,6 +1357,10 @@ async function logout() {
1306
1357
  }
1307
1358
 
1308
1359
  clearServerCredentials(baseUrl)
1360
+ if (hasJsonFlag()) {
1361
+ printJson({ status: 'logged-out', server: baseUrl, remoteLogoutError })
1362
+ return
1363
+ }
1309
1364
  if (remoteLogoutError) {
1310
1365
  console.warn(`Warning: remote logout failed: ${remoteLogoutError}`)
1311
1366
  }
@@ -1320,6 +1375,15 @@ async function printEnv() {
1320
1375
  const projectId = project?.id || getArg('--project-id') || process.env.ORIZU_PROJECT_ID || ''
1321
1376
  const projectSlug = project ? `${project.teamSlug}/${project.slug}` : process.env.ORIZU_PROJECT || ''
1322
1377
 
1378
+ if (hasJsonFlag()) {
1379
+ printJson({
1380
+ ORIZU_API_URL: baseUrl,
1381
+ ORIZU_TOKEN: token,
1382
+ ORIZU_PROJECT_ID: projectId,
1383
+ ...(projectSlug ? { ORIZU_PROJECT: projectSlug } : {}),
1384
+ })
1385
+ return
1386
+ }
1323
1387
  printLine(`export ORIZU_API_URL=${shellQuote(baseUrl)}`)
1324
1388
  printLine(`export ORIZU_TOKEN=${shellQuote(token)}`)
1325
1389
  printLine(`export ORIZU_PROJECT_ID=${shellQuote(projectId)}`)
@@ -1361,28 +1425,52 @@ async function logOptimizationEvent() {
1361
1425
  }
1362
1426
 
1363
1427
  const data = await parseJsonResponse<{ eventId: string }>(response, 'Optimization event log')
1428
+ if (hasJsonFlag()) {
1429
+ printJson({ eventId: data.eventId })
1430
+ return
1431
+ }
1364
1432
  printLine(`Logged event ${sanitizeTerminalText(data.eventId)}`)
1365
1433
  }
1366
1434
 
1367
1435
  async function listPrompts() {
1368
1436
  const project = getArg('--project') || await resolveProjectSlug(null)
1369
- const response = await authedFetch(`/api/cli/prompts?project=${encodeURIComponent(project)}`)
1437
+ const status = getArg('--status') || 'active'
1438
+ if (!['active', 'archived', 'all'].includes(status)) {
1439
+ throw new Error('Usage: orizu prompts list --project <team/project> [--status active|archived|all]')
1440
+ }
1441
+
1442
+ const params = new URLSearchParams({ project, status })
1443
+ const response = await authedFetch(`/api/cli/prompts?${params.toString()}`)
1370
1444
  if (!response.ok) {
1371
1445
  throw new Error(`Failed to fetch prompts: ${await response.text()}`)
1372
1446
  }
1373
1447
 
1374
1448
  const data = await parseJsonResponse<{ prompts: PromptSummary[] }>(response, 'Prompts list')
1449
+ if (hasJsonFlag()) {
1450
+ printJson(data as unknown as Record<string, unknown>)
1451
+ return
1452
+ }
1375
1453
  printPromptSummaries(data.prompts, 'No prompts found.')
1376
1454
  }
1377
1455
 
1378
1456
  async function listJudges() {
1379
1457
  const project = getArg('--project') || await resolveProjectSlug(null)
1380
- const response = await authedFetch(`/api/cli/judges?project=${encodeURIComponent(project)}`)
1458
+ const status = getArg('--status') || 'active'
1459
+ if (!['active', 'archived', 'all'].includes(status)) {
1460
+ throw new Error('Usage: orizu judges list --project <team/project> [--status active|archived|all]')
1461
+ }
1462
+
1463
+ const params = new URLSearchParams({ project, status })
1464
+ const response = await authedFetch(`/api/cli/judges?${params.toString()}`)
1381
1465
  if (!response.ok) {
1382
1466
  throw new Error(`Failed to fetch judges: ${await response.text()}`)
1383
1467
  }
1384
1468
 
1385
1469
  const data = await parseJsonResponse<{ judges: PromptSummary[] }>(response, 'Judges list')
1470
+ if (hasJsonFlag()) {
1471
+ printJson(data as unknown as Record<string, unknown>)
1472
+ return
1473
+ }
1386
1474
  printPromptSummaries(data.judges, 'No judges found.')
1387
1475
  }
1388
1476
 
@@ -1425,6 +1513,10 @@ async function listScorers() {
1425
1513
  }
1426
1514
 
1427
1515
  const data = await parseJsonResponse<{ scorers: ScorerSummary[] }>(response, 'Scorers list')
1516
+ if (hasJsonFlag()) {
1517
+ printJson(data as unknown as Record<string, unknown>)
1518
+ return
1519
+ }
1428
1520
  printScorerSummaries(data.scorers)
1429
1521
  }
1430
1522
 
@@ -1775,6 +1867,40 @@ async function setPromptLabel() {
1775
1867
  printLine(`Moved ${sanitizeTerminalText(label)} to ${sanitizeTerminalText(promptVersionId)}`)
1776
1868
  }
1777
1869
 
1870
+ async function setPromptArchivedState(archived: boolean) {
1871
+ const promptRef = getPositionalArg(2)
1872
+ const project = getArg('--project') || await resolveProjectSlug(null)
1873
+ const command = archived ? 'archive' : 'restore'
1874
+
1875
+ if (!promptRef) {
1876
+ throw new Error(`Usage: orizu prompts ${command} <prompt-id-or-name> --project <team/project> [--json]`)
1877
+ }
1878
+
1879
+ const response = await authedFetch(`/api/cli/prompts/${encodeURIComponent(promptRef)}?project=${encodeURIComponent(project)}`, {
1880
+ method: 'PATCH',
1881
+ headers: { 'Content-Type': 'application/json' },
1882
+ body: JSON.stringify({ archived }),
1883
+ })
1884
+
1885
+ if (!response.ok) {
1886
+ throw new Error(`Failed to ${command} prompt: ${await response.text()}`)
1887
+ }
1888
+
1889
+ const data = await parseJsonResponse<{
1890
+ prompt: {
1891
+ id: string
1892
+ name: string
1893
+ status: string
1894
+ }
1895
+ }>(response, `Prompt ${command}`)
1896
+ if (hasJsonFlag()) {
1897
+ printJson(data)
1898
+ return
1899
+ }
1900
+
1901
+ printLine(`${archived ? 'Archived' : 'Restored'} prompt ${sanitizeTerminalText(data.prompt.name)} (${sanitizeTerminalText(data.prompt.id)})`)
1902
+ }
1903
+
1778
1904
  async function bindPromptScorer(role: 'headline' | 'tracked') {
1779
1905
  const promptId = getPositionalArg(3)
1780
1906
  const project = getArg('--project') || await resolveProjectSlug(null)
@@ -2032,6 +2158,10 @@ async function submitRunResults() {
2032
2158
  perRowResultsStoragePath: string
2033
2159
  }
2034
2160
  }>(response, 'Run submit')
2161
+ if (hasJsonFlag()) {
2162
+ printJson({ run: data.run })
2163
+ return
2164
+ }
2035
2165
  printLine(
2036
2166
  `Submitted run ${sanitizeTerminalText(data.run.id)} ` +
2037
2167
  `(${formatPercent(data.run.aggregateScore)}) -> ` +
@@ -2595,10 +2725,19 @@ async function runGepaOptimization() {
2595
2725
  if (result.status !== 0) {
2596
2726
  throw new Error(`orizu-gepa failed with exit code ${result.status}`)
2597
2727
  }
2728
+
2729
+ if (hasJsonFlag()) {
2730
+ printJson({ status: 'completed', exitCode: 0 })
2731
+ }
2598
2732
  }
2599
2733
 
2600
2734
  async function listTeams() {
2601
- printTeams(await fetchTeams())
2735
+ const teams = await fetchTeams()
2736
+ if (hasJsonFlag()) {
2737
+ printJson({ teams })
2738
+ return
2739
+ }
2740
+ printTeams(teams)
2602
2741
  }
2603
2742
 
2604
2743
  async function resolveTeamSlug(teamSlugArg: string | null): Promise<string> {
@@ -2643,13 +2782,22 @@ async function createTeam() {
2643
2782
  }
2644
2783
 
2645
2784
  const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
2785
+ if (hasJsonFlag()) {
2786
+ printJson({ team: data.team })
2787
+ return
2788
+ }
2646
2789
  printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
2647
2790
  }
2648
2791
 
2649
2792
  async function listProjects() {
2650
2793
  const teamSlugArg = getArg('--team')
2651
2794
  const teamSlug = teamSlugArg ? normalizeSlugInput(teamSlugArg) : null
2652
- printProjects(await fetchProjects(teamSlug || undefined))
2795
+ const projects = await fetchProjects(teamSlug || undefined)
2796
+ if (hasJsonFlag()) {
2797
+ printJson({ projects })
2798
+ return
2799
+ }
2800
+ printProjects(projects)
2653
2801
  }
2654
2802
 
2655
2803
  async function createProject() {
@@ -2684,17 +2832,31 @@ async function createProject() {
2684
2832
  const data = await parseJsonResponse<{
2685
2833
  project: { id: string; name: string; slug: string; teamSlug: string }
2686
2834
  }>(response, 'Project create')
2835
+ if (hasJsonFlag()) {
2836
+ printJson({ project: data.project })
2837
+ return
2838
+ }
2687
2839
  printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
2688
2840
  }
2689
2841
 
2690
2842
  async function listTasks() {
2691
2843
  const project = getArg('--project')
2692
- printTasks(await fetchTasks(project || undefined))
2844
+ const tasks = await fetchTasks(project || undefined)
2845
+ if (hasJsonFlag()) {
2846
+ printJson({ tasks })
2847
+ return
2848
+ }
2849
+ printTasks(tasks)
2693
2850
  }
2694
2851
 
2695
2852
  async function listApps() {
2696
2853
  const project = getArg('--project') || await resolveProjectSlug(null)
2697
- printApps(await fetchApps(project))
2854
+ const apps = await fetchApps(project)
2855
+ if (hasJsonFlag()) {
2856
+ printJson({ apps })
2857
+ return
2858
+ }
2859
+ printApps(apps)
2698
2860
  }
2699
2861
 
2700
2862
  function readSourceFile(pathArg: string): string {
@@ -2759,29 +2921,97 @@ async function askYesNo(question: string, defaultYes: boolean): Promise<boolean>
2759
2921
  }
2760
2922
  }
2761
2923
 
2762
- async function promptSkillInstallTargets(): Promise<SkillInstallTarget[]> {
2924
+ const SKILL_INSTALL_USAGE =
2925
+ `Usage: orizu install-skill [--agent <${SKILL_INSTALL_AGENTS.join('|')}>]... [--scope global|project] [--mode auto|link|copy] [--target <${SKILL_INSTALL_TARGETS.join('|')}>]... [--yes] [--dry-run]`
2926
+
2927
+ interface SkillInstallChoice {
2928
+ label: string
2929
+ target: SkillInstallTarget
2930
+ defaultYes: boolean
2931
+ }
2932
+
2933
+ function describeSkillTarget(target: SkillInstallTarget): string {
2934
+ if (target === 'claude-user') return 'Claude Code, for you across all projects'
2935
+ if (target === 'claude-project') return 'Claude Code, only for this project'
2936
+ if (target === 'agent-user') return 'Codex / Open Agent Skills, for you across all projects'
2937
+ if (target === 'agents-project') return 'Codex / Open Agent Skills, only for this project'
2938
+ if (target === 'codex-project') return 'Codex legacy project folder (.codex/skills)'
2939
+ return 'Project instructions, managed Orizu section in AGENTS.md'
2940
+ }
2941
+
2942
+ async function promptSkillInstallTargets(scope: SkillInstallScope): Promise<SkillInstallTarget[]> {
2763
2943
  if (!isInteractiveTerminal()) {
2764
- throw new Error(
2765
- `Usage: orizu install-skill --target <${SKILL_INSTALL_TARGETS.join('|')}> [--yes] [--dry-run]`
2766
- )
2944
+ throw new Error(SKILL_INSTALL_USAGE)
2767
2945
  }
2768
2946
 
2769
- printLine('Where should the Orizu CLI skill be installed?')
2947
+ const choices: SkillInstallChoice[] = [
2948
+ { label: describeSkillTarget(getTargetForAgent('claude', scope)), target: getTargetForAgent('claude', scope), defaultYes: true },
2949
+ { label: describeSkillTarget(getTargetForAgent('codex', scope)), target: getTargetForAgent('codex', scope), defaultYes: true },
2950
+ { label: describeSkillTarget('agents-md'), target: 'agents-md', defaultYes: false },
2951
+ ]
2952
+
2953
+ printLine('Where should Orizu be available?')
2770
2954
  const targets: SkillInstallTarget[] = []
2771
- for (const target of SKILL_INSTALL_TARGETS) {
2772
- const defaultYes = target === 'agent-user'
2773
- if (await askYesNo(` Install to ${target}?`, defaultYes)) {
2774
- targets.push(target)
2955
+ for (const choice of choices) {
2956
+ const installPath = getSkillInstallPath(choice.target)
2957
+ if (await askYesNo(` ${choice.label}\n ${installPath}?`, choice.defaultYes)) {
2958
+ targets.push(choice.target)
2959
+ }
2960
+ }
2961
+
2962
+ if (await askYesNo(' Show advanced targets?', false)) {
2963
+ for (const target of SKILL_INSTALL_TARGETS) {
2964
+ if (targets.includes(target)) {
2965
+ continue
2966
+ }
2967
+ const installPath = getSkillInstallPath(target)
2968
+ if (await askYesNo(` ${describeSkillTarget(target)} [${target}]\n ${installPath}?`, false)) {
2969
+ targets.push(target)
2970
+ }
2775
2971
  }
2776
2972
  }
2777
2973
 
2778
2974
  return targets
2779
2975
  }
2780
2976
 
2781
- function parseSkillInstallTargets(): SkillInstallTarget[] {
2782
- const rawTargets = getArgs('--target')
2977
+ function parseSkillInstallScope(): SkillInstallScope {
2978
+ const raw = getArg('--scope')
2979
+ if (raw === null || raw === 'global') {
2980
+ return 'global'
2981
+ }
2982
+ if (raw === 'project') {
2983
+ return 'local'
2984
+ }
2985
+ throw new Error(`Unknown --scope '${raw}'. Choices: global, project.`)
2986
+ }
2987
+
2988
+ function parseSkillInstallMode(): SkillInstallMode {
2989
+ const raw = getArg('--mode')
2990
+ if (raw === null) {
2991
+ return 'auto'
2992
+ }
2993
+ if (raw !== 'auto' && raw !== 'link' && raw !== 'copy') {
2994
+ throw new Error(`Unknown --mode '${raw}'. Choices: auto, link, copy.`)
2995
+ }
2996
+ return raw
2997
+ }
2998
+
2999
+ function parseSkillInstallTargets(scope: SkillInstallScope): SkillInstallTarget[] {
2783
3000
  const targets: SkillInstallTarget[] = []
2784
- for (const target of rawTargets) {
3001
+
3002
+ for (const agent of getArgs('--agent')) {
3003
+ if (!isSkillInstallAgent(agent)) {
3004
+ throw new Error(
3005
+ `Unknown agent '${agent}'. Available agents: ${SKILL_INSTALL_AGENTS.join(', ')}`
3006
+ )
3007
+ }
3008
+ const target = getTargetForAgent(agent, scope)
3009
+ if (!targets.includes(target)) {
3010
+ targets.push(target)
3011
+ }
3012
+ }
3013
+
3014
+ for (const target of getArgs('--target')) {
2785
3015
  if (!isSkillInstallTarget(target)) {
2786
3016
  throw new Error(
2787
3017
  `Unknown skill install target '${target}'. Available targets: ${SKILL_INSTALL_TARGETS.join(', ')}`
@@ -2791,6 +3021,7 @@ function parseSkillInstallTargets(): SkillInstallTarget[] {
2791
3021
  targets.push(target)
2792
3022
  }
2793
3023
  }
3024
+
2794
3025
  return targets
2795
3026
  }
2796
3027
 
@@ -2801,41 +3032,446 @@ function formatSkillInstallAction(action: string): string {
2801
3032
  return 'Would update'
2802
3033
  }
2803
3034
 
2804
- async function installSkillCommand() {
2805
- const skipConfirm = hasArg('--yes')
2806
- const dryRun = hasArg('--dry-run')
2807
- let targets = parseSkillInstallTargets()
2808
- if (targets.length === 0) {
2809
- targets = await promptSkillInstallTargets()
3035
+ function describePlannedWrite(target: SkillInstallTarget, action: string, mode: string, path: string): string {
3036
+ const verb = action.includes('create') || action === 'created' ? 'create' : 'replace'
3037
+ if (target === 'agents-md') {
3038
+ return ` ${path} (${verb} managed Orizu section)`
3039
+ }
3040
+ if (mode === 'link') {
3041
+ return ` ${path} (${verb}, symlink to the CLI-managed skill)`
2810
3042
  }
3043
+ return ` ${path} (${verb}, full copy with sync metadata)`
3044
+ }
2811
3045
 
2812
- if (targets.length === 0) {
2813
- printLine('No targets selected; nothing to do.')
2814
- return
3046
+ interface SkillInstallOutcome {
3047
+ target: SkillInstallTarget
3048
+ path: string
3049
+ action: 'created' | 'updated' | 'skipped' | 'failed'
3050
+ mode: 'link' | 'copy' | 'section' | null
3051
+ error?: string
3052
+ }
3053
+
3054
+ async function applySkillInstallTargets(
3055
+ targets: SkillInstallTarget[],
3056
+ options: { mode: SkillInstallMode, skipConfirm: boolean, dryRun: boolean }
3057
+ ): Promise<SkillInstallOutcome[]> {
3058
+ const cliVersion = getCliVersion()
3059
+ const jsonMode = hasJsonFlag()
3060
+ const planned = targets.map(target =>
3061
+ installSkillTarget(target, { dryRun: true, mode: options.mode, cliVersion })
3062
+ )
3063
+
3064
+ if (!jsonMode) {
3065
+ printLine('Planned writes:')
3066
+ for (const plan of planned) {
3067
+ printLine(describePlannedWrite(plan.target, plan.action, plan.mode, plan.path))
3068
+ }
3069
+ printLine('')
2815
3070
  }
2816
3071
 
3072
+ if (options.dryRun) {
3073
+ if (!jsonMode) {
3074
+ printLine('Dry run: no files were changed.')
3075
+ }
3076
+ return planned.map(plan => ({
3077
+ target: plan.target,
3078
+ path: plan.path,
3079
+ action: 'skipped',
3080
+ mode: plan.mode,
3081
+ }))
3082
+ }
3083
+
3084
+ const outcomes: SkillInstallOutcome[] = []
2817
3085
  for (const target of targets) {
2818
- let overwrite = skipConfirm
3086
+ let overwrite = options.skipConfirm
2819
3087
  const installPath = getSkillInstallPath(target)
2820
- if (!dryRun && !overwrite && targetNeedsOverwrite(target)) {
3088
+ if (!overwrite && targetNeedsOverwrite(target)) {
2821
3089
  if (!isInteractiveTerminal()) {
2822
3090
  throw new Error(`${installPath} already exists. Pass --yes to replace it.`)
2823
3091
  }
2824
3092
  overwrite = await askYesNo(`Replace ${installPath}?`, true)
2825
3093
  if (!overwrite) {
2826
- printLine(`Skipped ${installPath}`)
3094
+ if (!jsonMode) {
3095
+ printLine(`Skipped ${installPath}`)
3096
+ }
3097
+ outcomes.push({ target, path: installPath, action: 'skipped', mode: null })
2827
3098
  continue
2828
3099
  }
2829
3100
  }
2830
3101
 
3102
+ try {
3103
+ const result = installSkillTarget(target, {
3104
+ overwrite,
3105
+ mode: options.mode,
3106
+ cliVersion,
3107
+ })
3108
+ if (!jsonMode) {
3109
+ const suffix = result.mode === 'link' ? ' (symlink)' : ''
3110
+ printLine(`${formatSkillInstallAction(result.action)} ${result.path}${suffix}`)
3111
+ }
3112
+ outcomes.push({
3113
+ target,
3114
+ path: result.path,
3115
+ action: result.action as 'created' | 'updated',
3116
+ mode: result.mode,
3117
+ })
3118
+ } catch (error: any) {
3119
+ const message = error?.message || String(error)
3120
+ if (!jsonMode) {
3121
+ printLine(`Failed ${installPath}: ${message}`)
3122
+ }
3123
+ outcomes.push({ target, path: installPath, action: 'failed', mode: null, error: message })
3124
+ }
3125
+ }
3126
+
3127
+ return outcomes
3128
+ }
3129
+
3130
+ async function installSkillCommand() {
3131
+ const skipConfirm = hasArg('--yes')
3132
+ const dryRun = hasArg('--dry-run')
3133
+ const scope = parseSkillInstallScope()
3134
+ const mode = parseSkillInstallMode()
3135
+ let targets = parseSkillInstallTargets(scope)
3136
+ if (targets.length === 0) {
3137
+ targets = await promptSkillInstallTargets(scope)
3138
+ }
3139
+
3140
+ if (targets.length === 0) {
3141
+ if (hasJsonFlag()) {
3142
+ printJson({ installs: [] })
3143
+ return
3144
+ }
3145
+ printLine('No targets selected; nothing to do.')
3146
+ return
3147
+ }
3148
+
3149
+ const outcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun })
3150
+ if (hasJsonFlag()) {
3151
+ printJson({ dryRun, installs: outcomes })
3152
+ }
3153
+ }
3154
+
3155
+ function describeSkillTargetState(status: SkillTargetStatus): string {
3156
+ if (status.state === 'current') return 'current'
3157
+ if (status.state === 'stale') return 'stale (run `orizu skills update`)'
3158
+ if (status.state === 'broken-link') return 'broken symlink (run `orizu skills update`)'
3159
+ if (status.state === 'unmanaged') return 'exists without a managed Orizu section'
3160
+ return 'not installed'
3161
+ }
3162
+
3163
+ function skillsStatusCommand() {
3164
+ const source = resolveSkillSource()
3165
+ const statuses = SKILL_INSTALL_TARGETS.map(target => getSkillTargetStatus(target))
3166
+
3167
+ if (hasJsonFlag()) {
3168
+ printJson({
3169
+ name: source.name,
3170
+ root: source.root,
3171
+ source: source.source,
3172
+ cliVersion: getCliVersion(),
3173
+ sourceHash: computeSkillContentHash(source.root),
3174
+ targets: statuses.map(status => ({
3175
+ target: status.target,
3176
+ path: status.path,
3177
+ state: status.state,
3178
+ mode: status.mode,
3179
+ linkTarget: status.linkTarget,
3180
+ installedHash: status.installedHash,
3181
+ meta: status.meta,
3182
+ })),
3183
+ })
3184
+ return
3185
+ }
3186
+
3187
+ printLine(`Skill source: ${source.root} (${source.source})`)
3188
+ printLine('')
3189
+ for (const status of statuses) {
3190
+ const mode = status.mode ? `, ${status.mode}` : ''
3191
+ printLine(` ${status.target.padEnd(16)} ${describeSkillTargetState(status)}${mode}`)
3192
+ printLine(` ${''.padEnd(16)} ${status.path}`)
3193
+ }
3194
+ }
3195
+
3196
+ async function skillsUpdateCommand() {
3197
+ const dryRun = hasArg('--dry-run')
3198
+ const cliVersion = getCliVersion()
3199
+ const updates: Array<{ target: SkillInstallTarget, path: string, action: string }> = []
3200
+
3201
+ for (const target of SKILL_INSTALL_TARGETS) {
3202
+ const status = getSkillTargetStatus(target)
3203
+ if (status.state === 'missing' || status.state === 'unmanaged') {
3204
+ continue
3205
+ }
3206
+
3207
+ if (status.state === 'current') {
3208
+ updates.push({ target, path: status.path, action: 'already-current' })
3209
+ continue
3210
+ }
3211
+
3212
+ if (dryRun) {
3213
+ updates.push({ target, path: status.path, action: 'would-update' })
3214
+ continue
3215
+ }
3216
+
3217
+ const refreshMode: SkillInstallMode = status.mode === 'link' ? 'auto' : 'copy'
2831
3218
  const result = installSkillTarget(target, {
2832
- overwrite,
2833
- dryRun,
3219
+ overwrite: true,
3220
+ mode: refreshMode,
3221
+ cliVersion,
3222
+ })
3223
+ updates.push({
3224
+ target,
3225
+ path: status.path,
3226
+ action: status.state === 'broken-link' ? 'relinked' : result.action,
2834
3227
  })
2835
- printLine(`${formatSkillInstallAction(result.action)} ${result.path}`)
3228
+ }
3229
+
3230
+ if (hasJsonFlag()) {
3231
+ printJson({ updates })
3232
+ return
3233
+ }
3234
+
3235
+ if (updates.length === 0) {
3236
+ printLine('No Orizu skill installs found. Run `orizu install-skill` first.')
3237
+ return
3238
+ }
3239
+
3240
+ for (const update of updates) {
3241
+ if (update.action === 'already-current') {
3242
+ printLine(`Current ${update.path}`)
3243
+ } else if (update.action === 'would-update') {
3244
+ printLine(`Would update ${update.path}`)
3245
+ } else if (update.action === 'relinked') {
3246
+ printLine(`Relinked ${update.path}`)
3247
+ } else {
3248
+ printLine(`Updated ${update.path}`)
3249
+ }
2836
3250
  }
2837
3251
  }
2838
3252
 
3253
+ function skillsPathCommand() {
3254
+ const source = resolveSkillSource()
3255
+
3256
+ if (hasJsonFlag()) {
3257
+ printJson({
3258
+ name: source.name,
3259
+ root: source.root,
3260
+ skillMd: source.skillMd,
3261
+ source: source.source,
3262
+ cliVersion: getCliVersion(),
3263
+ skillHash: computeSkillContentHash(source.root),
3264
+ })
3265
+ return
3266
+ }
3267
+
3268
+ printLine(hasArg('--skill-md') ? source.skillMd : source.root)
3269
+ }
3270
+
3271
+ const LAUNCHABLE_AGENTS: Record<string, string> = {
3272
+ claude: 'claude',
3273
+ codex: 'codex',
3274
+ }
3275
+
3276
+ function findExecutable(name: string): string | null {
3277
+ const pathValue = process.env.PATH || ''
3278
+ for (const dir of pathValue.split(delimiter)) {
3279
+ if (!dir) {
3280
+ continue
3281
+ }
3282
+ const candidate = join(dir, name)
3283
+ if (existsSync(candidate)) {
3284
+ return candidate
3285
+ }
3286
+ }
3287
+ return null
3288
+ }
3289
+
3290
+ function describeSetupAuthState(): { state: 'signed-in' | 'signed-out', baseUrl: string } {
3291
+ const baseUrl = getBaseUrl()
3292
+ return {
3293
+ state: getServerCredentials(baseUrl) ? 'signed-in' : 'signed-out',
3294
+ baseUrl,
3295
+ }
3296
+ }
3297
+
3298
+ async function setupCommand() {
3299
+ const dryRun = hasArg('--dry-run')
3300
+ const skipConfirm = hasArg('--yes')
3301
+ const noInput = hasArg('--no-input') || !isInteractiveTerminal()
3302
+
3303
+ printBannerIfInteractive()
3304
+ printLine('Orizu setup')
3305
+ printLine('')
3306
+
3307
+ // Phase 1: login
3308
+ printLine('1. Authentication')
3309
+ let auth = describeSetupAuthState()
3310
+ if (hasArg('--skip-login')) {
3311
+ printLine(' Skipped (--skip-login).')
3312
+ } else if (auth.state === 'signed-in') {
3313
+ printLine(` Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`)
3314
+ } else if (noInput || dryRun) {
3315
+ printLine(' Not signed in. Run `orizu login` to authenticate.')
3316
+ } else if (await askYesNo(' Sign in to Orizu now?', true)) {
3317
+ await login()
3318
+ auth = describeSetupAuthState()
3319
+ } else {
3320
+ printLine(' Skipped. Run `orizu login` when ready.')
3321
+ }
3322
+ printLine('')
3323
+
3324
+ // Phase 2: agent integration
3325
+ printLine('2. Agent integration')
3326
+ const scope = parseSkillInstallScope()
3327
+ const mode = parseSkillInstallMode()
3328
+ let targets = hasArg('--no-install') ? [] : parseSkillInstallTargets(scope)
3329
+ if (targets.length === 0 && !hasArg('--no-install')) {
3330
+ if (noInput) {
3331
+ printLine(' No agents selected. Pass --agent claude and/or --agent codex, or rerun interactively.')
3332
+ } else {
3333
+ targets = await promptSkillInstallTargets(scope)
3334
+ }
3335
+ }
3336
+
3337
+ let installOutcomes: SkillInstallOutcome[] = []
3338
+ if (targets.length > 0) {
3339
+ installOutcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun })
3340
+ } else if (hasArg('--no-install')) {
3341
+ printLine(' Skipped (--no-install).')
3342
+ }
3343
+ printLine('')
3344
+
3345
+ // Phase 3: local workspace
3346
+ printLine('3. Local workspace')
3347
+ let workspaceState: 'created' | 'exists' | 'skipped' | 'would-create' = 'skipped'
3348
+ if (hasArg('--no-workspace')) {
3349
+ printLine(' Skipped (--no-workspace).')
3350
+ } else if (workspaceExists()) {
3351
+ workspaceState = 'exists'
3352
+ printLine(` ${WORKSPACE_DIR_NAME}/ already initialized.`)
3353
+ } else {
3354
+ const wantsWorkspace = hasArg('--workspace') ||
3355
+ (!noInput && await askYesNo(` Create a gitignored ${WORKSPACE_DIR_NAME}/ workspace for local Orizu artifacts?`, true))
3356
+ if (wantsWorkspace) {
3357
+ const result = initOrizuWorkspace({
3358
+ baseUrl: auth.baseUrl,
3359
+ cliVersion: getCliVersion(),
3360
+ dryRun,
3361
+ })
3362
+ workspaceState = result.state === 'would-create' ? 'would-create' : result.state
3363
+ for (const action of result.actions) {
3364
+ printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`)
3365
+ }
3366
+ } else {
3367
+ printLine(` Skipped. Rerun with --workspace to create ${WORKSPACE_DIR_NAME}/.`)
3368
+ }
3369
+ }
3370
+ printLine('')
3371
+
3372
+ // Phase 4: coding-agent handoff
3373
+ const handoffPrompt = renderAgentSetupPrompt({
3374
+ workspacePath: workspaceState === 'created' || workspaceState === 'exists'
3375
+ ? getWorkspaceRoot()
3376
+ : null,
3377
+ })
3378
+ if (!hasArg('--no-handoff')) {
3379
+ printLine('4. Coding-agent handoff')
3380
+ printLine(' Give your coding agent this prompt to plan repo-specific Orizu adoption:')
3381
+ printLine('')
3382
+ printLine('--- prompt start ---')
3383
+ printLine(handoffPrompt)
3384
+ printLine('--- prompt end ---')
3385
+ printLine('')
3386
+
3387
+ const detected = Object.keys(LAUNCHABLE_AGENTS).filter(agent => findExecutable(LAUNCHABLE_AGENTS[agent]))
3388
+ if (detected.length > 0) {
3389
+ printLine(` Detected agents: ${detected.join(', ')}. Launch one with the prompt, e.g.:`)
3390
+ for (const agent of detected) {
3391
+ printLine(` ${LAUNCHABLE_AGENTS[agent]} "$(orizu setup prompt)"`)
3392
+ }
3393
+ }
3394
+
3395
+ const launchAgent = getArg('--launch')
3396
+ if (launchAgent) {
3397
+ if (!(launchAgent in LAUNCHABLE_AGENTS)) {
3398
+ throw new Error(`Unknown --launch agent '${launchAgent}'. Choices: ${Object.keys(LAUNCHABLE_AGENTS).join(', ')}.`)
3399
+ }
3400
+ const binary = findExecutable(LAUNCHABLE_AGENTS[launchAgent])
3401
+ if (!isInteractiveTerminal()) {
3402
+ printLine(' Not launching: --launch requires an interactive terminal.')
3403
+ } else if (!binary) {
3404
+ printLine(` Not launching: '${LAUNCHABLE_AGENTS[launchAgent]}' was not found on PATH.`)
3405
+ } else if (dryRun) {
3406
+ printLine(` Would launch ${binary} with the setup prompt.`)
3407
+ } else if (skipConfirm || await askYesNo(` Launch ${LAUNCHABLE_AGENTS[launchAgent]} with the setup prompt now?`, true)) {
3408
+ printLine(` Launching ${binary}…`)
3409
+ spawnSync(binary, [handoffPrompt], { stdio: 'inherit' })
3410
+ }
3411
+ }
3412
+ printLine('')
3413
+ }
3414
+
3415
+ // Setup summary (success criteria checklist)
3416
+ const summaryTargets = installOutcomes.length > 0
3417
+ ? installOutcomes
3418
+ .filter(outcome => outcome.action !== 'failed')
3419
+ .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target) }))
3420
+ : []
3421
+ const source = resolveSkillSource()
3422
+
3423
+ if (hasJsonFlag()) {
3424
+ printJson({
3425
+ auth: { state: auth.state, server: auth.baseUrl },
3426
+ integrations: installOutcomes.map(outcome => ({
3427
+ target: outcome.target,
3428
+ path: outcome.path,
3429
+ action: outcome.action,
3430
+ mode: outcome.mode,
3431
+ error: outcome.error,
3432
+ })),
3433
+ skill: {
3434
+ root: source.root,
3435
+ source: source.source,
3436
+ cliVersion: getCliVersion(),
3437
+ skillHash: computeSkillContentHash(source.root),
3438
+ },
3439
+ workspace: { state: workspaceState, path: getWorkspaceRoot() },
3440
+ })
3441
+ return
3442
+ }
3443
+
3444
+ printLine('Setup summary')
3445
+ printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`)
3446
+ if (summaryTargets.length > 0) {
3447
+ printLine(' Integrations:')
3448
+ for (const { outcome, status } of summaryTargets) {
3449
+ const modeLabel = status.mode ? `, ${status.mode}` : ''
3450
+ printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`)
3451
+ }
3452
+ } else {
3453
+ printLine(' Integrations: none installed this run — `orizu install-skill` sets them up.')
3454
+ }
3455
+ const failed = installOutcomes.filter(outcome => outcome.action === 'failed')
3456
+ for (const failure of failed) {
3457
+ printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`)
3458
+ }
3459
+ printLine(` Skill source: ${source.root} (${source.source})`)
3460
+ printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : `${getWorkspaceRoot()} (${workspaceState})`}`)
3461
+ printLine(' Next: hand the prompt above to your coding agent to plan repo-specific Orizu adoption.')
3462
+ }
3463
+
3464
+ function setupPromptCommand() {
3465
+ const prompt = renderAgentSetupPrompt({
3466
+ workspacePath: workspaceExists() ? getWorkspaceRoot() : null,
3467
+ })
3468
+ if (hasJsonFlag()) {
3469
+ printJson({ prompt })
3470
+ return
3471
+ }
3472
+ printLine(prompt)
3473
+ }
3474
+
2839
3475
  function readJsonFile(pathArg: string): Record<string, unknown> {
2840
3476
  const raw = readSourceFile(pathArg)
2841
3477
  try {
@@ -3100,6 +3736,10 @@ async function createAppFromFile() {
3100
3736
  app: { id: string; name: string; versionNum: number; componentName?: string; url?: string }
3101
3737
  warnings?: string[]
3102
3738
  }>(response, 'App create')
3739
+ if (hasJsonFlag()) {
3740
+ printJson({ app: data.app, warnings: data.warnings || [] })
3741
+ return
3742
+ }
3103
3743
  printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
3104
3744
  if (data.app.url) {
3105
3745
  printLine(`View app: ${formatTerminalLink(data.app.url)}`)
@@ -3140,6 +3780,14 @@ async function previewAppFromFile() {
3140
3780
  keepOpen,
3141
3781
  })
3142
3782
 
3783
+ if (hasJsonFlag()) {
3784
+ printJson({
3785
+ url: result.url,
3786
+ screenshotPath: result.screenshotPath || null,
3787
+ warnings: result.warnings,
3788
+ })
3789
+ return
3790
+ }
3143
3791
  printLine(`Preview rendered: ${formatTerminalLink(result.url)}`)
3144
3792
  if (result.screenshotPath) {
3145
3793
  printLine(`Screenshot: ${sanitizeTerminalText(result.screenshotPath)}`)
@@ -3191,6 +3839,10 @@ async function updateAppFromFile() {
3191
3839
  app: { id: string; name: string; versionNum: number; componentName?: string }
3192
3840
  warnings?: string[]
3193
3841
  }>(response, 'App update')
3842
+ if (hasJsonFlag()) {
3843
+ printJson({ app: data.app, warnings: data.warnings || [] })
3844
+ return
3845
+ }
3194
3846
  printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`)
3195
3847
  if (data.warnings?.length) {
3196
3848
  printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
@@ -3238,6 +3890,10 @@ async function linkAppDataset() {
3238
3890
  versionNum: number
3239
3891
  }>(response, 'App link dataset')
3240
3892
 
3893
+ if (hasJsonFlag()) {
3894
+ printJson({ app: data.app, linkedDataset: data.linkedDataset, versionNum: data.versionNum })
3895
+ return
3896
+ }
3241
3897
  printLine(
3242
3898
  `Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`
3243
3899
  )
@@ -3253,48 +3909,82 @@ function parseCommaSeparated(value: string | null): string[] {
3253
3909
  .filter(Boolean)
3254
3910
  }
3255
3911
 
3912
+ function readAssignmentManifest(path: string | null): ReturnType<typeof parseAssignmentManifestJsonl> | null {
3913
+ if (!path) {
3914
+ return null
3915
+ }
3916
+
3917
+ const content = readFileSync(expandHomePath(path), 'utf8')
3918
+ return parseAssignmentManifestJsonl(content)
3919
+ }
3920
+
3256
3921
  async function createTask() {
3257
3922
  const projectSlug = getArg('--project')
3258
3923
  const datasetId = getArg('--dataset')
3259
3924
  const appId = getArg('--app')
3260
3925
  const title = getArg('--title')
3261
3926
  const assignees = parseCommaSeparated(getArg('--assignees'))
3927
+ const assignmentFile = getArg('--assignment-file')
3262
3928
  const publish = hasArg('--publish')
3263
3929
  const versionArg = getArg('--version')
3264
3930
  const instructions = getArg('--instructions')
3265
3931
  const labelsPerItemArg = getArg('--labels-per-item')
3266
- const labelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1
3932
+ const parsedLabelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1
3933
+ const labelsPerItem =
3934
+ Number.isInteger(parsedLabelsPerItem) && parsedLabelsPerItem > 0
3935
+ ? parsedLabelsPerItem
3936
+ : null
3267
3937
  const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN
3268
3938
  const versionNum =
3269
3939
  Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null
3270
3940
 
3271
- if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0)) {
3272
- throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]')
3941
+ if (assignmentFile && assignees.length > 0) {
3942
+ throw new Error('Use either --assignment-file or --assignees, not both.')
3943
+ }
3944
+
3945
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
3946
+
3947
+ if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0 && !explicitAssignments)) {
3948
+ throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]')
3273
3949
  }
3274
3950
 
3275
3951
  if (versionArg && versionNum === null) {
3276
3952
  throw new Error('--version must be a positive integer')
3277
3953
  }
3278
3954
 
3955
+ if (labelsPerItemArg && labelsPerItem === null) {
3956
+ throw new Error('--labels-per-item must be a positive integer')
3957
+ }
3958
+
3959
+ const requestBody: Record<string, unknown> = {
3960
+ projectSlug,
3961
+ datasetId,
3962
+ appId,
3963
+ versionNum,
3964
+ title,
3965
+ publish,
3966
+ instructions,
3967
+ }
3968
+
3969
+ if (explicitAssignments) {
3970
+ requestBody.explicitAssignments = explicitAssignments
3971
+ } else {
3972
+ requestBody.memberIds = assignees
3973
+ }
3974
+
3975
+ if (!explicitAssignments || labelsPerItemArg) {
3976
+ requestBody.requiredAssignmentsPerRow = labelsPerItem
3977
+ }
3978
+
3279
3979
  const response = await authedFetch('/api/cli/tasks', {
3280
3980
  method: 'POST',
3281
3981
  headers: { 'Content-Type': 'application/json' },
3282
- body: JSON.stringify({
3283
- projectSlug,
3284
- datasetId,
3285
- appId,
3286
- versionNum,
3287
- title,
3288
- memberIds: assignees,
3289
- publish,
3290
- instructions,
3291
- requiredAssignmentsPerRow: labelsPerItem,
3292
- }),
3982
+ body: JSON.stringify(requestBody),
3293
3983
  })
3294
3984
 
3295
3985
  if (!response.ok) {
3296
3986
  const cliError = await formatTaskCreateError(response)
3297
- if (hasArg('--json')) {
3987
+ if (hasJsonFlag()) {
3298
3988
  const payload = cliError.structuredPayload ?? { error: cliError.message }
3299
3989
  printLine(JSON.stringify({
3300
3990
  ...payload,
@@ -3318,7 +4008,7 @@ async function createTask() {
3318
4008
  warning?: string
3319
4009
  }>(response, 'Task create')
3320
4010
 
3321
- if (hasArg('--json')) {
4011
+ if (hasJsonFlag()) {
3322
4012
  printLine(JSON.stringify({
3323
4013
  taskId: data.task.id,
3324
4014
  datasetId,
@@ -3327,13 +4017,16 @@ async function createTask() {
3327
4017
  taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
3328
4018
  title: data.task.title,
3329
4019
  status: data.task.status,
4020
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
3330
4021
  requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
3331
4022
  assignmentsCreated: data.assignmentsCreated,
3332
4023
  draft: data.task.status === 'draft',
3333
4024
  ...(data.task.status === 'draft'
3334
4025
  ? {
3335
4026
  message: 'Draft created. Test it manually before assigning.',
3336
- publishCommand: `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
4027
+ publishCommand: explicitAssignments
4028
+ ? `orizu tasks publish --task ${data.task.id} --assignment-file ${assignmentFile ? shellQuote(assignmentFile) : '<path>'}`
4029
+ : `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
3337
4030
  }
3338
4031
  : {}),
3339
4032
  ...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
@@ -3350,12 +4043,17 @@ async function createTask() {
3350
4043
  `\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
3351
4044
  `\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
3352
4045
  `\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
4046
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
3353
4047
  `\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
3354
4048
  `\n Assignments: ${data.assignmentsCreated}` +
3355
4049
  (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
3356
4050
  `\n URL: ${sanitizeTerminalText(taskUrl)}` +
3357
4051
  (isDraft
3358
- ? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`
4052
+ ? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: ${
4053
+ explicitAssignments
4054
+ ? `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignment-file ${sanitizeTerminalText(assignmentFile || '<path>')}`
4055
+ : `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`
4056
+ }`
3359
4057
  : '')
3360
4058
  )
3361
4059
  }
@@ -3363,15 +4061,26 @@ async function createTask() {
3363
4061
  async function publishTask() {
3364
4062
  const taskId = getArg('--task')
3365
4063
  const assignees = parseCommaSeparated(getArg('--assignees'))
4064
+ const assignmentFile = getArg('--assignment-file')
3366
4065
 
3367
- if (!taskId || assignees.length === 0) {
3368
- throw new Error('Usage: orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]')
4066
+ if (assignmentFile && assignees.length > 0) {
4067
+ throw new Error('Use either --assignment-file or --assignees, not both.')
4068
+ }
4069
+
4070
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
4071
+
4072
+ if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
4073
+ throw new Error('Usage: orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]')
3369
4074
  }
3370
4075
 
3371
4076
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/publish`, {
3372
4077
  method: 'POST',
3373
4078
  headers: { 'Content-Type': 'application/json' },
3374
- body: JSON.stringify({ memberIds: assignees }),
4079
+ body: JSON.stringify(
4080
+ explicitAssignments
4081
+ ? { explicitAssignments }
4082
+ : { memberIds: assignees }
4083
+ ),
3375
4084
  })
3376
4085
 
3377
4086
  if (!response.ok) {
@@ -3390,10 +4099,11 @@ async function publishTask() {
3390
4099
  'Task publish'
3391
4100
  )
3392
4101
 
3393
- if (hasArg('--json')) {
4102
+ if (hasJsonFlag()) {
3394
4103
  printLine(JSON.stringify({
3395
4104
  taskId: data.task.id,
3396
4105
  status: data.task.status,
4106
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
3397
4107
  assignmentsCreated: data.assignmentsCreated,
3398
4108
  assignmentsProcessed: data.assignmentsProcessed,
3399
4109
  ...(data.assignmentsRemoved !== undefined
@@ -3409,6 +4119,7 @@ async function publishTask() {
3409
4119
 
3410
4120
  printLine(
3411
4121
  `Published task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]` +
4122
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
3412
4123
  `\n Assignments: ${data.assignmentsCreated}` +
3413
4124
  (data.assignmentsRemoved !== undefined
3414
4125
  ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
@@ -3420,23 +4131,65 @@ async function publishTask() {
3420
4131
  async function assignTask() {
3421
4132
  const taskId = getArg('--task')
3422
4133
  const assignees = parseCommaSeparated(getArg('--assignees'))
4134
+ const assignmentFile = getArg('--assignment-file')
4135
+
4136
+ if (assignmentFile && assignees.length > 0) {
4137
+ throw new Error('Use either --assignment-file or --assignees, not both.')
4138
+ }
4139
+
4140
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
4141
+
4142
+ if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
4143
+ throw new Error('Usage: orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]')
4144
+ }
3423
4145
 
3424
- if (!taskId || assignees.length === 0) {
3425
- throw new Error('Usage: orizu tasks assign --task <taskId> --assignees <userId1,userId2>')
4146
+ const requestBody: Record<string, unknown> = explicitAssignments
4147
+ ? { explicitAssignments }
4148
+ : { memberIds: assignees }
4149
+ if (hasArg('--replace-existing')) {
4150
+ requestBody.replaceExisting = true
3426
4151
  }
3427
4152
 
3428
4153
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/assign`, {
3429
4154
  method: 'POST',
3430
4155
  headers: { 'Content-Type': 'application/json' },
3431
- body: JSON.stringify({ memberIds: assignees }),
4156
+ body: JSON.stringify(requestBody),
3432
4157
  })
3433
4158
 
3434
4159
  if (!response.ok) {
3435
4160
  throw new Error(`Failed to assign task: ${await response.text()}`)
3436
4161
  }
3437
4162
 
3438
- const data = await parseJsonResponse<{ assignmentsCreated: number }>(response, 'Task assign')
3439
- printLine(`Created ${data.assignmentsCreated} assignments.`)
4163
+ const data = await parseJsonResponse<{
4164
+ assignmentsProcessed?: number
4165
+ assignmentsCreated: number
4166
+ assignmentsRemoved?: number
4167
+ assignmentShortfall?: number
4168
+ warning?: string
4169
+ }>(response, 'Task assign')
4170
+ if (hasJsonFlag()) {
4171
+ printJson({
4172
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
4173
+ assignmentsProcessed: data.assignmentsProcessed ?? data.assignmentsCreated,
4174
+ assignmentsCreated: data.assignmentsCreated,
4175
+ ...(data.assignmentsRemoved !== undefined
4176
+ ? { assignmentsRemoved: data.assignmentsRemoved }
4177
+ : {}),
4178
+ ...(data.assignmentShortfall !== undefined
4179
+ ? { assignmentShortfall: data.assignmentShortfall }
4180
+ : {}),
4181
+ ...(data.warning ? { warning: data.warning } : {}),
4182
+ })
4183
+ return
4184
+ }
4185
+ printLine(
4186
+ `Created ${data.assignmentsCreated} assignments.` +
4187
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
4188
+ (data.assignmentsRemoved !== undefined
4189
+ ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
4190
+ : '') +
4191
+ (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '')
4192
+ )
3440
4193
  }
3441
4194
 
3442
4195
  async function taskStatus() {
@@ -3448,7 +4201,7 @@ async function taskStatus() {
3448
4201
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`)
3449
4202
  if (!response.ok) {
3450
4203
  const rawBody = await response.text()
3451
- if (hasArg('--json')) {
4204
+ if (hasJsonFlag()) {
3452
4205
  let errorPayload: Record<string, unknown> = { error: rawBody }
3453
4206
  try {
3454
4207
  const parsed = JSON.parse(rawBody)
@@ -3475,7 +4228,7 @@ async function taskStatus() {
3475
4228
  }
3476
4229
 
3477
4230
  const data = await parseJsonResponse<TaskStatusPayload>(response, 'Task status')
3478
- if (hasArg('--json')) {
4231
+ if (hasJsonFlag()) {
3479
4232
  printLine(JSON.stringify(data, null, 2))
3480
4233
  return
3481
4234
  }
@@ -3502,6 +4255,10 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
3502
4255
  }
3503
4256
 
3504
4257
  const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
4258
+ if (hasJsonFlag()) {
4259
+ printJson({ task: data.task })
4260
+ return
4261
+ }
3505
4262
  const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
3506
4263
  printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
3507
4264
  }
@@ -3626,7 +4383,7 @@ async function appDetail() {
3626
4383
  throw new Error(`App '${appId}' not found in project '${projectSlug}'`)
3627
4384
  }
3628
4385
 
3629
- if (hasArg('--json')) {
4386
+ if (hasJsonFlag()) {
3630
4387
  printLine(JSON.stringify(detail, null, 2))
3631
4388
  return
3632
4389
  }
@@ -3687,6 +4444,14 @@ async function exportAppSource() {
3687
4444
  : defaultAppExportFilename(data.app.name, data.version.versionNum)
3688
4445
 
3689
4446
  writeTextFileEnsuringDir(filename, data.version.code)
4447
+ if (hasJsonFlag()) {
4448
+ printJson({
4449
+ app: { id: data.app.id, name: data.app.name },
4450
+ versionNum: data.version.versionNum,
4451
+ savedTo: filename,
4452
+ })
4453
+ return
4454
+ }
3690
4455
  printLine(
3691
4456
  `Saved app ${sanitizeTerminalText(data.app.name)} v${data.version.versionNum} source to ${sanitizeTerminalText(filename)}`
3692
4457
  )
@@ -3694,8 +4459,12 @@ async function exportAppSource() {
3694
4459
 
3695
4460
  async function listTeamMembers() {
3696
4461
  const teamSlug = await resolveTeamSlug(getArg('--team'))
3697
-
3698
- printTeamMembers(await fetchTeamMembers(teamSlug))
4462
+ const members = await fetchTeamMembers(teamSlug)
4463
+ if (hasJsonFlag()) {
4464
+ printJson({ team: teamSlug, members })
4465
+ return
4466
+ }
4467
+ printTeamMembers(members)
3699
4468
  }
3700
4469
 
3701
4470
  async function addTeamMember() {
@@ -3714,6 +4483,10 @@ async function addTeamMember() {
3714
4483
  throw new Error(`Failed to add team member: ${await response.text()}`)
3715
4484
  }
3716
4485
  const data = await parseJsonResponse<{ member: TeamMember }>(response, 'Team member add')
4486
+ if (hasJsonFlag()) {
4487
+ printJson({ member: data.member })
4488
+ return
4489
+ }
3717
4490
  printLine(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`)
3718
4491
  }
3719
4492
 
@@ -3737,6 +4510,10 @@ async function removeTeamMember() {
3737
4510
  if (!response.ok) {
3738
4511
  throw new Error(`Failed to remove team member: ${await response.text()}`)
3739
4512
  }
4513
+ if (hasJsonFlag()) {
4514
+ printJson({ removed: { id: member.id, email: member.email } })
4515
+ return
4516
+ }
3740
4517
  printLine(`Removed team member ${sanitizeTerminalText(member.email)}`)
3741
4518
  }
3742
4519
 
@@ -3771,6 +4548,10 @@ async function changeTeamMemberRole() {
3771
4548
  throw new Error(`Failed to update member role: ${await response.text()}`)
3772
4549
  }
3773
4550
 
4551
+ if (hasJsonFlag()) {
4552
+ printJson({ member: { id: member.id, email: member.email, role } })
4553
+ return
4554
+ }
3774
4555
  printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`)
3775
4556
  }
3776
4557
 
@@ -3873,6 +4654,10 @@ async function uploadJsonlDatasetInChunks(
3873
4654
  throw new Error('Dataset file contains no rows')
3874
4655
  }
3875
4656
 
4657
+ if (hasJsonFlag()) {
4658
+ printJson({ dataset })
4659
+ return
4660
+ }
3876
4661
  printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`)
3877
4662
  if (dataset.url) {
3878
4663
  printLine(`View dataset: ${formatTerminalLink(dataset.url)}`)
@@ -3902,6 +4687,10 @@ async function uploadDataset() {
3902
4687
  const { rows, sourceType } = parseDatasetFile(file)
3903
4688
  const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3904
4689
 
4690
+ if (hasJsonFlag()) {
4691
+ printJson({ dataset: data.dataset })
4692
+ return
4693
+ }
3905
4694
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
3906
4695
  if (data.dataset.url) {
3907
4696
  printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`)
@@ -4039,6 +4828,10 @@ async function downloadDataset() {
4039
4828
  const bytes = new Uint8Array(await response.arrayBuffer())
4040
4829
  writeFileSync(filename, bytes)
4041
4830
 
4831
+ if (hasJsonFlag()) {
4832
+ printJson({ datasetId, format, savedTo: filename })
4833
+ return
4834
+ }
4042
4835
  printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
4043
4836
  }
4044
4837
 
@@ -4204,6 +4997,10 @@ async function appendDatasetRows() {
4204
4997
  }
4205
4998
 
4206
4999
  if (lastResult) {
5000
+ if (hasJsonFlag()) {
5001
+ printJson({ dataset: lastResult.dataset, appendedCount: totalAppended })
5002
+ return
5003
+ }
4207
5004
  printLine(
4208
5005
  `Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
4209
5006
  )
@@ -4265,6 +5062,10 @@ async function editDatasetRows() {
4265
5062
  updatedCount: number
4266
5063
  }>(response, 'Dataset edit rows')
4267
5064
 
5065
+ if (hasJsonFlag()) {
5066
+ printJson({ dataset: data.dataset, updatedCount: data.updatedCount })
5067
+ return
5068
+ }
4268
5069
  printLine(
4269
5070
  `Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`
4270
5071
  )
@@ -4304,6 +5105,10 @@ async function deleteDatasetRows() {
4304
5105
  deletedCount: number
4305
5106
  }>(response, 'Dataset delete rows')
4306
5107
 
5108
+ if (hasJsonFlag()) {
5109
+ printJson({ dataset: data.dataset, deletedCount: data.deletedCount })
5110
+ return
5111
+ }
4307
5112
  printLine(
4308
5113
  `Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
4309
5114
  )
@@ -4358,6 +5163,10 @@ async function deleteDataset() {
4358
5163
  dataset: { id: string }
4359
5164
  }>(response, 'Dataset delete')
4360
5165
 
5166
+ if (hasJsonFlag()) {
5167
+ printJson({ dataset: data.dataset, deleted: true })
5168
+ return
5169
+ }
4361
5170
  printLine(`Deleted dataset ${sanitizeTerminalText(data.dataset.id)}.`)
4362
5171
  }
4363
5172
 
@@ -4394,6 +5203,10 @@ async function lockDataset() {
4394
5203
  }
4395
5204
  }>(response, 'Dataset lock')
4396
5205
 
5206
+ if (hasJsonFlag()) {
5207
+ printJson({ dataset: data.dataset })
5208
+ return
5209
+ }
4397
5210
  printLine(
4398
5211
  `Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`
4399
5212
  )
@@ -4431,6 +5244,10 @@ async function cloneDataset() {
4431
5244
  }
4432
5245
  }>(response, 'Dataset clone')
4433
5246
 
5247
+ if (hasJsonFlag()) {
5248
+ printJson({ dataset: data.dataset })
5249
+ return
5250
+ }
4434
5251
  printLine(
4435
5252
  `Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`
4436
5253
  )
@@ -4462,6 +5279,10 @@ async function downloadAnnotations() {
4462
5279
  const bytes = new Uint8Array(await response.arrayBuffer())
4463
5280
  writeFileSync(filename, bytes)
4464
5281
 
5282
+ if (hasJsonFlag()) {
5283
+ printJson({ taskId, format, savedTo: filename })
5284
+ return
5285
+ }
4465
5286
  printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
4466
5287
  }
4467
5288
 
@@ -4706,7 +5527,11 @@ async function runnersExec() {
4706
5527
  } else {
4707
5528
  writeFileSync(outPath, resultJsonl)
4708
5529
  }
4709
- printLine(`Wrote ${context.rows.length} runner results to ${sanitizeTerminalText(outPath)}`)
5530
+ if (hasJsonFlag()) {
5531
+ printJson({ rowCount: context.rows.length, out: outPath })
5532
+ } else {
5533
+ printLine(`Wrote ${context.rows.length} runner results to ${sanitizeTerminalText(outPath)}`)
5534
+ }
4710
5535
  } finally {
4711
5536
  materializedRunner.cleanup()
4712
5537
  }
@@ -4747,6 +5572,9 @@ export async function main(rawArgs = process.argv.slice(2)) {
4747
5572
 
4748
5573
  const helpTarget = extractHelpTarget(cliArgs)
4749
5574
  if (helpTarget !== null) {
5575
+ if (helpTarget.length === 0) {
5576
+ printBannerIfInteractive()
5577
+ }
4750
5578
  printLine(renderHelpForArgs(helpTarget))
4751
5579
  return
4752
5580
  }
@@ -4769,6 +5597,16 @@ export async function main(rawArgs = process.argv.slice(2)) {
4769
5597
  return
4770
5598
  }
4771
5599
 
5600
+ if (command === 'setup' && subcommand === 'prompt') {
5601
+ setupPromptCommand()
5602
+ return
5603
+ }
5604
+
5605
+ if (command === 'setup') {
5606
+ await setupCommand()
5607
+ return
5608
+ }
5609
+
4772
5610
  if (command === 'install-skill') {
4773
5611
  await installSkillCommand()
4774
5612
  return
@@ -4779,6 +5617,21 @@ export async function main(rawArgs = process.argv.slice(2)) {
4779
5617
  return
4780
5618
  }
4781
5619
 
5620
+ if (command === 'skills' && subcommand === 'path') {
5621
+ skillsPathCommand()
5622
+ return
5623
+ }
5624
+
5625
+ if (command === 'skills' && subcommand === 'status') {
5626
+ skillsStatusCommand()
5627
+ return
5628
+ }
5629
+
5630
+ if (command === 'skills' && subcommand === 'update') {
5631
+ await skillsUpdateCommand()
5632
+ return
5633
+ }
5634
+
4782
5635
  if (command === 'login') {
4783
5636
  await login()
4784
5637
  return
@@ -4862,6 +5715,16 @@ export async function main(rawArgs = process.argv.slice(2)) {
4862
5715
  return
4863
5716
  }
4864
5717
 
5718
+ if (command === 'prompts' && subcommand === 'archive') {
5719
+ await setPromptArchivedState(true)
5720
+ return
5721
+ }
5722
+
5723
+ if (command === 'prompts' && subcommand === 'restore') {
5724
+ await setPromptArchivedState(false)
5725
+ return
5726
+ }
5727
+
4865
5728
  if (command === 'prompts' && subcommand === 'labels' && cliArgs[2] === 'set') {
4866
5729
  await setPromptLabel()
4867
5730
  return