orizu 0.3.7 → 0.4.0

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
@@ -35,15 +35,32 @@ import {
35
35
  setGlobalFlags,
36
36
  } from './http.js'
37
37
  import {
38
+ computeSkillContentHash,
38
39
  getSkillInstallPath,
40
+ getSkillTargetStatus,
41
+ getTargetForAgent,
39
42
  installSkillTarget,
43
+ isSkillInstallAgent,
40
44
  isSkillInstallTarget,
45
+ resolveSkillSource,
46
+ SKILL_INSTALL_AGENTS,
41
47
  SKILL_INSTALL_TARGETS,
48
+ SkillInstallMode,
49
+ SkillInstallScope,
42
50
  SkillInstallTarget,
51
+ SkillTargetStatus,
43
52
  targetNeedsOverwrite,
44
53
  } from './skill-installer.js'
54
+ import { renderBanner } from './banner.js'
45
55
  import { formatTaskCreateError } from './task-create-error.js'
56
+ import { renderAgentSetupPrompt } from './setup-prompt.js'
46
57
  import { LoginResponse } from './types.js'
58
+ import {
59
+ getWorkspaceRoot,
60
+ initOrizuWorkspace,
61
+ WORKSPACE_DIR_NAME,
62
+ workspaceExists,
63
+ } from './workspace.js'
47
64
 
48
65
  interface Team {
49
66
  id: string
@@ -107,6 +124,8 @@ interface PromptSummary {
107
124
  id: string
108
125
  name: string
109
126
  role: string
127
+ status?: string
128
+ archivedAt?: string | null
110
129
  description?: string | null
111
130
  }
112
131
 
@@ -279,7 +298,16 @@ function printVersion() {
279
298
  printLine(`orizu ${getCliVersion()}`)
280
299
  }
281
300
 
301
+ function printBannerIfInteractive() {
302
+ if (!process.stdout.isTTY) {
303
+ return
304
+ }
305
+ printLine(renderBanner())
306
+ printLine('')
307
+ }
308
+
282
309
  function printUsage() {
310
+ printBannerIfInteractive()
283
311
  printLine(renderRootHelp())
284
312
  }
285
313
 
@@ -288,7 +316,7 @@ function printPreviewUsage() {
288
316
  }
289
317
 
290
318
  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]`)
319
+ 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
320
  }
293
321
 
294
322
  let cliArgs = process.argv.slice(2)
@@ -492,15 +520,17 @@ function printPromptSummaries(items: PromptSummary[], emptyMessage: string) {
492
520
  id: sanitizeTerminalText(item.id),
493
521
  name: sanitizeTerminalText(item.name),
494
522
  role: sanitizeTerminalText(item.role),
523
+ status: sanitizeTerminalText(item.status || 'active'),
495
524
  }))
496
525
  const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length))
497
526
  const nameWidth = Math.max('NAME'.length, ...rows.map(row => row.name.length))
498
527
  const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
528
+ const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length))
499
529
 
500
- printLine(`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`)
501
- printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`)
530
+ printLine(`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)} ${'STATUS'.padEnd(statusWidth)}`)
531
+ printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)} ${'-'.repeat(statusWidth)}`)
502
532
  rows.forEach(row => {
503
- printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`)
533
+ printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)} ${row.status.padEnd(statusWidth)}`)
504
534
  })
505
535
  }
506
536
 
@@ -1168,6 +1198,10 @@ async function login() {
1168
1198
  assertSecureTokenTransport(baseUrl)
1169
1199
 
1170
1200
  if (hasArg('--no-prompt-if-logged-in') && getServerCredentials(baseUrl)) {
1201
+ if (hasJsonFlag()) {
1202
+ printJson({ status: 'already-logged-in', server: baseUrl })
1203
+ return
1204
+ }
1171
1205
  printLine(`Already logged in to ${sanitizeTerminalText(baseUrl)}.`)
1172
1206
  return
1173
1207
  }
@@ -1262,6 +1296,14 @@ async function login() {
1262
1296
  apiKey: loginData.apiKey,
1263
1297
  })
1264
1298
 
1299
+ if (hasJsonFlag()) {
1300
+ printJson({
1301
+ status: 'logged-in',
1302
+ server: baseUrl,
1303
+ user: { id: loginData.user.id, email: loginData.user.email ?? null },
1304
+ })
1305
+ return
1306
+ }
1265
1307
  printLine(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`)
1266
1308
  }
1267
1309
 
@@ -1272,6 +1314,10 @@ async function whoami() {
1272
1314
  }
1273
1315
 
1274
1316
  const data = await response.json() as { user: { id: string; email: string | null } }
1317
+ if (hasJsonFlag()) {
1318
+ printJson({ user: data.user, server: getBaseUrl() })
1319
+ return
1320
+ }
1275
1321
  printLine(sanitizeTerminalText(data.user.email ?? data.user.id))
1276
1322
  }
1277
1323
 
@@ -1279,6 +1325,10 @@ async function logout() {
1279
1325
  const baseUrl = getBaseUrl()
1280
1326
  const credentials = getServerCredentials(baseUrl)
1281
1327
  if (!credentials) {
1328
+ if (hasJsonFlag()) {
1329
+ printJson({ status: 'already-logged-out', server: baseUrl })
1330
+ return
1331
+ }
1282
1332
  printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`)
1283
1333
  return
1284
1334
  }
@@ -1306,6 +1356,10 @@ async function logout() {
1306
1356
  }
1307
1357
 
1308
1358
  clearServerCredentials(baseUrl)
1359
+ if (hasJsonFlag()) {
1360
+ printJson({ status: 'logged-out', server: baseUrl, remoteLogoutError })
1361
+ return
1362
+ }
1309
1363
  if (remoteLogoutError) {
1310
1364
  console.warn(`Warning: remote logout failed: ${remoteLogoutError}`)
1311
1365
  }
@@ -1320,6 +1374,15 @@ async function printEnv() {
1320
1374
  const projectId = project?.id || getArg('--project-id') || process.env.ORIZU_PROJECT_ID || ''
1321
1375
  const projectSlug = project ? `${project.teamSlug}/${project.slug}` : process.env.ORIZU_PROJECT || ''
1322
1376
 
1377
+ if (hasJsonFlag()) {
1378
+ printJson({
1379
+ ORIZU_API_URL: baseUrl,
1380
+ ORIZU_TOKEN: token,
1381
+ ORIZU_PROJECT_ID: projectId,
1382
+ ...(projectSlug ? { ORIZU_PROJECT: projectSlug } : {}),
1383
+ })
1384
+ return
1385
+ }
1323
1386
  printLine(`export ORIZU_API_URL=${shellQuote(baseUrl)}`)
1324
1387
  printLine(`export ORIZU_TOKEN=${shellQuote(token)}`)
1325
1388
  printLine(`export ORIZU_PROJECT_ID=${shellQuote(projectId)}`)
@@ -1361,28 +1424,52 @@ async function logOptimizationEvent() {
1361
1424
  }
1362
1425
 
1363
1426
  const data = await parseJsonResponse<{ eventId: string }>(response, 'Optimization event log')
1427
+ if (hasJsonFlag()) {
1428
+ printJson({ eventId: data.eventId })
1429
+ return
1430
+ }
1364
1431
  printLine(`Logged event ${sanitizeTerminalText(data.eventId)}`)
1365
1432
  }
1366
1433
 
1367
1434
  async function listPrompts() {
1368
1435
  const project = getArg('--project') || await resolveProjectSlug(null)
1369
- const response = await authedFetch(`/api/cli/prompts?project=${encodeURIComponent(project)}`)
1436
+ const status = getArg('--status') || 'active'
1437
+ if (!['active', 'archived', 'all'].includes(status)) {
1438
+ throw new Error('Usage: orizu prompts list --project <team/project> [--status active|archived|all]')
1439
+ }
1440
+
1441
+ const params = new URLSearchParams({ project, status })
1442
+ const response = await authedFetch(`/api/cli/prompts?${params.toString()}`)
1370
1443
  if (!response.ok) {
1371
1444
  throw new Error(`Failed to fetch prompts: ${await response.text()}`)
1372
1445
  }
1373
1446
 
1374
1447
  const data = await parseJsonResponse<{ prompts: PromptSummary[] }>(response, 'Prompts list')
1448
+ if (hasJsonFlag()) {
1449
+ printJson(data as unknown as Record<string, unknown>)
1450
+ return
1451
+ }
1375
1452
  printPromptSummaries(data.prompts, 'No prompts found.')
1376
1453
  }
1377
1454
 
1378
1455
  async function listJudges() {
1379
1456
  const project = getArg('--project') || await resolveProjectSlug(null)
1380
- const response = await authedFetch(`/api/cli/judges?project=${encodeURIComponent(project)}`)
1457
+ const status = getArg('--status') || 'active'
1458
+ if (!['active', 'archived', 'all'].includes(status)) {
1459
+ throw new Error('Usage: orizu judges list --project <team/project> [--status active|archived|all]')
1460
+ }
1461
+
1462
+ const params = new URLSearchParams({ project, status })
1463
+ const response = await authedFetch(`/api/cli/judges?${params.toString()}`)
1381
1464
  if (!response.ok) {
1382
1465
  throw new Error(`Failed to fetch judges: ${await response.text()}`)
1383
1466
  }
1384
1467
 
1385
1468
  const data = await parseJsonResponse<{ judges: PromptSummary[] }>(response, 'Judges list')
1469
+ if (hasJsonFlag()) {
1470
+ printJson(data as unknown as Record<string, unknown>)
1471
+ return
1472
+ }
1386
1473
  printPromptSummaries(data.judges, 'No judges found.')
1387
1474
  }
1388
1475
 
@@ -1425,6 +1512,10 @@ async function listScorers() {
1425
1512
  }
1426
1513
 
1427
1514
  const data = await parseJsonResponse<{ scorers: ScorerSummary[] }>(response, 'Scorers list')
1515
+ if (hasJsonFlag()) {
1516
+ printJson(data as unknown as Record<string, unknown>)
1517
+ return
1518
+ }
1428
1519
  printScorerSummaries(data.scorers)
1429
1520
  }
1430
1521
 
@@ -1775,6 +1866,40 @@ async function setPromptLabel() {
1775
1866
  printLine(`Moved ${sanitizeTerminalText(label)} to ${sanitizeTerminalText(promptVersionId)}`)
1776
1867
  }
1777
1868
 
1869
+ async function setPromptArchivedState(archived: boolean) {
1870
+ const promptRef = getPositionalArg(2)
1871
+ const project = getArg('--project') || await resolveProjectSlug(null)
1872
+ const command = archived ? 'archive' : 'restore'
1873
+
1874
+ if (!promptRef) {
1875
+ throw new Error(`Usage: orizu prompts ${command} <prompt-id-or-name> --project <team/project> [--json]`)
1876
+ }
1877
+
1878
+ const response = await authedFetch(`/api/cli/prompts/${encodeURIComponent(promptRef)}?project=${encodeURIComponent(project)}`, {
1879
+ method: 'PATCH',
1880
+ headers: { 'Content-Type': 'application/json' },
1881
+ body: JSON.stringify({ archived }),
1882
+ })
1883
+
1884
+ if (!response.ok) {
1885
+ throw new Error(`Failed to ${command} prompt: ${await response.text()}`)
1886
+ }
1887
+
1888
+ const data = await parseJsonResponse<{
1889
+ prompt: {
1890
+ id: string
1891
+ name: string
1892
+ status: string
1893
+ }
1894
+ }>(response, `Prompt ${command}`)
1895
+ if (hasJsonFlag()) {
1896
+ printJson(data)
1897
+ return
1898
+ }
1899
+
1900
+ printLine(`${archived ? 'Archived' : 'Restored'} prompt ${sanitizeTerminalText(data.prompt.name)} (${sanitizeTerminalText(data.prompt.id)})`)
1901
+ }
1902
+
1778
1903
  async function bindPromptScorer(role: 'headline' | 'tracked') {
1779
1904
  const promptId = getPositionalArg(3)
1780
1905
  const project = getArg('--project') || await resolveProjectSlug(null)
@@ -2032,6 +2157,10 @@ async function submitRunResults() {
2032
2157
  perRowResultsStoragePath: string
2033
2158
  }
2034
2159
  }>(response, 'Run submit')
2160
+ if (hasJsonFlag()) {
2161
+ printJson({ run: data.run })
2162
+ return
2163
+ }
2035
2164
  printLine(
2036
2165
  `Submitted run ${sanitizeTerminalText(data.run.id)} ` +
2037
2166
  `(${formatPercent(data.run.aggregateScore)}) -> ` +
@@ -2595,10 +2724,19 @@ async function runGepaOptimization() {
2595
2724
  if (result.status !== 0) {
2596
2725
  throw new Error(`orizu-gepa failed with exit code ${result.status}`)
2597
2726
  }
2727
+
2728
+ if (hasJsonFlag()) {
2729
+ printJson({ status: 'completed', exitCode: 0 })
2730
+ }
2598
2731
  }
2599
2732
 
2600
2733
  async function listTeams() {
2601
- printTeams(await fetchTeams())
2734
+ const teams = await fetchTeams()
2735
+ if (hasJsonFlag()) {
2736
+ printJson({ teams })
2737
+ return
2738
+ }
2739
+ printTeams(teams)
2602
2740
  }
2603
2741
 
2604
2742
  async function resolveTeamSlug(teamSlugArg: string | null): Promise<string> {
@@ -2643,13 +2781,22 @@ async function createTeam() {
2643
2781
  }
2644
2782
 
2645
2783
  const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
2784
+ if (hasJsonFlag()) {
2785
+ printJson({ team: data.team })
2786
+ return
2787
+ }
2646
2788
  printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
2647
2789
  }
2648
2790
 
2649
2791
  async function listProjects() {
2650
2792
  const teamSlugArg = getArg('--team')
2651
2793
  const teamSlug = teamSlugArg ? normalizeSlugInput(teamSlugArg) : null
2652
- printProjects(await fetchProjects(teamSlug || undefined))
2794
+ const projects = await fetchProjects(teamSlug || undefined)
2795
+ if (hasJsonFlag()) {
2796
+ printJson({ projects })
2797
+ return
2798
+ }
2799
+ printProjects(projects)
2653
2800
  }
2654
2801
 
2655
2802
  async function createProject() {
@@ -2684,17 +2831,31 @@ async function createProject() {
2684
2831
  const data = await parseJsonResponse<{
2685
2832
  project: { id: string; name: string; slug: string; teamSlug: string }
2686
2833
  }>(response, 'Project create')
2834
+ if (hasJsonFlag()) {
2835
+ printJson({ project: data.project })
2836
+ return
2837
+ }
2687
2838
  printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
2688
2839
  }
2689
2840
 
2690
2841
  async function listTasks() {
2691
2842
  const project = getArg('--project')
2692
- printTasks(await fetchTasks(project || undefined))
2843
+ const tasks = await fetchTasks(project || undefined)
2844
+ if (hasJsonFlag()) {
2845
+ printJson({ tasks })
2846
+ return
2847
+ }
2848
+ printTasks(tasks)
2693
2849
  }
2694
2850
 
2695
2851
  async function listApps() {
2696
2852
  const project = getArg('--project') || await resolveProjectSlug(null)
2697
- printApps(await fetchApps(project))
2853
+ const apps = await fetchApps(project)
2854
+ if (hasJsonFlag()) {
2855
+ printJson({ apps })
2856
+ return
2857
+ }
2858
+ printApps(apps)
2698
2859
  }
2699
2860
 
2700
2861
  function readSourceFile(pathArg: string): string {
@@ -2759,29 +2920,97 @@ async function askYesNo(question: string, defaultYes: boolean): Promise<boolean>
2759
2920
  }
2760
2921
  }
2761
2922
 
2762
- async function promptSkillInstallTargets(): Promise<SkillInstallTarget[]> {
2923
+ const SKILL_INSTALL_USAGE =
2924
+ `Usage: orizu install-skill [--agent <${SKILL_INSTALL_AGENTS.join('|')}>]... [--scope global|project] [--mode auto|link|copy] [--target <${SKILL_INSTALL_TARGETS.join('|')}>]... [--yes] [--dry-run]`
2925
+
2926
+ interface SkillInstallChoice {
2927
+ label: string
2928
+ target: SkillInstallTarget
2929
+ defaultYes: boolean
2930
+ }
2931
+
2932
+ function describeSkillTarget(target: SkillInstallTarget): string {
2933
+ if (target === 'claude-user') return 'Claude Code, for you across all projects'
2934
+ if (target === 'claude-project') return 'Claude Code, only for this project'
2935
+ if (target === 'agent-user') return 'Codex / Open Agent Skills, for you across all projects'
2936
+ if (target === 'agents-project') return 'Codex / Open Agent Skills, only for this project'
2937
+ if (target === 'codex-project') return 'Codex legacy project folder (.codex/skills)'
2938
+ return 'Project instructions, managed Orizu section in AGENTS.md'
2939
+ }
2940
+
2941
+ async function promptSkillInstallTargets(scope: SkillInstallScope): Promise<SkillInstallTarget[]> {
2763
2942
  if (!isInteractiveTerminal()) {
2764
- throw new Error(
2765
- `Usage: orizu install-skill --target <${SKILL_INSTALL_TARGETS.join('|')}> [--yes] [--dry-run]`
2766
- )
2943
+ throw new Error(SKILL_INSTALL_USAGE)
2767
2944
  }
2768
2945
 
2769
- printLine('Where should the Orizu CLI skill be installed?')
2946
+ const choices: SkillInstallChoice[] = [
2947
+ { label: describeSkillTarget(getTargetForAgent('claude', scope)), target: getTargetForAgent('claude', scope), defaultYes: true },
2948
+ { label: describeSkillTarget(getTargetForAgent('codex', scope)), target: getTargetForAgent('codex', scope), defaultYes: true },
2949
+ { label: describeSkillTarget('agents-md'), target: 'agents-md', defaultYes: false },
2950
+ ]
2951
+
2952
+ printLine('Where should Orizu be available?')
2770
2953
  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)
2954
+ for (const choice of choices) {
2955
+ const installPath = getSkillInstallPath(choice.target)
2956
+ if (await askYesNo(` ${choice.label}\n ${installPath}?`, choice.defaultYes)) {
2957
+ targets.push(choice.target)
2958
+ }
2959
+ }
2960
+
2961
+ if (await askYesNo(' Show advanced targets?', false)) {
2962
+ for (const target of SKILL_INSTALL_TARGETS) {
2963
+ if (targets.includes(target)) {
2964
+ continue
2965
+ }
2966
+ const installPath = getSkillInstallPath(target)
2967
+ if (await askYesNo(` ${describeSkillTarget(target)} [${target}]\n ${installPath}?`, false)) {
2968
+ targets.push(target)
2969
+ }
2775
2970
  }
2776
2971
  }
2777
2972
 
2778
2973
  return targets
2779
2974
  }
2780
2975
 
2781
- function parseSkillInstallTargets(): SkillInstallTarget[] {
2782
- const rawTargets = getArgs('--target')
2976
+ function parseSkillInstallScope(): SkillInstallScope {
2977
+ const raw = getArg('--scope')
2978
+ if (raw === null || raw === 'global') {
2979
+ return 'global'
2980
+ }
2981
+ if (raw === 'project') {
2982
+ return 'local'
2983
+ }
2984
+ throw new Error(`Unknown --scope '${raw}'. Choices: global, project.`)
2985
+ }
2986
+
2987
+ function parseSkillInstallMode(): SkillInstallMode {
2988
+ const raw = getArg('--mode')
2989
+ if (raw === null) {
2990
+ return 'auto'
2991
+ }
2992
+ if (raw !== 'auto' && raw !== 'link' && raw !== 'copy') {
2993
+ throw new Error(`Unknown --mode '${raw}'. Choices: auto, link, copy.`)
2994
+ }
2995
+ return raw
2996
+ }
2997
+
2998
+ function parseSkillInstallTargets(scope: SkillInstallScope): SkillInstallTarget[] {
2783
2999
  const targets: SkillInstallTarget[] = []
2784
- for (const target of rawTargets) {
3000
+
3001
+ for (const agent of getArgs('--agent')) {
3002
+ if (!isSkillInstallAgent(agent)) {
3003
+ throw new Error(
3004
+ `Unknown agent '${agent}'. Available agents: ${SKILL_INSTALL_AGENTS.join(', ')}`
3005
+ )
3006
+ }
3007
+ const target = getTargetForAgent(agent, scope)
3008
+ if (!targets.includes(target)) {
3009
+ targets.push(target)
3010
+ }
3011
+ }
3012
+
3013
+ for (const target of getArgs('--target')) {
2785
3014
  if (!isSkillInstallTarget(target)) {
2786
3015
  throw new Error(
2787
3016
  `Unknown skill install target '${target}'. Available targets: ${SKILL_INSTALL_TARGETS.join(', ')}`
@@ -2791,6 +3020,7 @@ function parseSkillInstallTargets(): SkillInstallTarget[] {
2791
3020
  targets.push(target)
2792
3021
  }
2793
3022
  }
3023
+
2794
3024
  return targets
2795
3025
  }
2796
3026
 
@@ -2801,39 +3031,444 @@ function formatSkillInstallAction(action: string): string {
2801
3031
  return 'Would update'
2802
3032
  }
2803
3033
 
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()
3034
+ function describePlannedWrite(target: SkillInstallTarget, action: string, mode: string, path: string): string {
3035
+ const verb = action.includes('create') || action === 'created' ? 'create' : 'replace'
3036
+ if (target === 'agents-md') {
3037
+ return ` ${path} (${verb} managed Orizu section)`
2810
3038
  }
3039
+ if (mode === 'link') {
3040
+ return ` ${path} (${verb}, symlink to the CLI-managed skill)`
3041
+ }
3042
+ return ` ${path} (${verb}, full copy with sync metadata)`
3043
+ }
2811
3044
 
2812
- if (targets.length === 0) {
2813
- printLine('No targets selected; nothing to do.')
2814
- return
3045
+ interface SkillInstallOutcome {
3046
+ target: SkillInstallTarget
3047
+ path: string
3048
+ action: 'created' | 'updated' | 'skipped' | 'failed'
3049
+ mode: 'link' | 'copy' | 'section' | null
3050
+ error?: string
3051
+ }
3052
+
3053
+ async function applySkillInstallTargets(
3054
+ targets: SkillInstallTarget[],
3055
+ options: { mode: SkillInstallMode, skipConfirm: boolean, dryRun: boolean }
3056
+ ): Promise<SkillInstallOutcome[]> {
3057
+ const cliVersion = getCliVersion()
3058
+ const jsonMode = hasJsonFlag()
3059
+ const planned = targets.map(target =>
3060
+ installSkillTarget(target, { dryRun: true, mode: options.mode, cliVersion })
3061
+ )
3062
+
3063
+ if (!jsonMode) {
3064
+ printLine('Planned writes:')
3065
+ for (const plan of planned) {
3066
+ printLine(describePlannedWrite(plan.target, plan.action, plan.mode, plan.path))
3067
+ }
3068
+ printLine('')
3069
+ }
3070
+
3071
+ if (options.dryRun) {
3072
+ if (!jsonMode) {
3073
+ printLine('Dry run: no files were changed.')
3074
+ }
3075
+ return planned.map(plan => ({
3076
+ target: plan.target,
3077
+ path: plan.path,
3078
+ action: 'skipped',
3079
+ mode: plan.mode,
3080
+ }))
2815
3081
  }
2816
3082
 
3083
+ const outcomes: SkillInstallOutcome[] = []
2817
3084
  for (const target of targets) {
2818
- let overwrite = skipConfirm
3085
+ let overwrite = options.skipConfirm
2819
3086
  const installPath = getSkillInstallPath(target)
2820
- if (!dryRun && !overwrite && targetNeedsOverwrite(target)) {
3087
+ if (!overwrite && targetNeedsOverwrite(target)) {
2821
3088
  if (!isInteractiveTerminal()) {
2822
3089
  throw new Error(`${installPath} already exists. Pass --yes to replace it.`)
2823
3090
  }
2824
3091
  overwrite = await askYesNo(`Replace ${installPath}?`, true)
2825
3092
  if (!overwrite) {
2826
- printLine(`Skipped ${installPath}`)
3093
+ if (!jsonMode) {
3094
+ printLine(`Skipped ${installPath}`)
3095
+ }
3096
+ outcomes.push({ target, path: installPath, action: 'skipped', mode: null })
2827
3097
  continue
2828
3098
  }
2829
3099
  }
2830
3100
 
3101
+ try {
3102
+ const result = installSkillTarget(target, {
3103
+ overwrite,
3104
+ mode: options.mode,
3105
+ cliVersion,
3106
+ })
3107
+ if (!jsonMode) {
3108
+ const suffix = result.mode === 'link' ? ' (symlink)' : ''
3109
+ printLine(`${formatSkillInstallAction(result.action)} ${result.path}${suffix}`)
3110
+ }
3111
+ outcomes.push({
3112
+ target,
3113
+ path: result.path,
3114
+ action: result.action as 'created' | 'updated',
3115
+ mode: result.mode,
3116
+ })
3117
+ } catch (error: any) {
3118
+ const message = error?.message || String(error)
3119
+ if (!jsonMode) {
3120
+ printLine(`Failed ${installPath}: ${message}`)
3121
+ }
3122
+ outcomes.push({ target, path: installPath, action: 'failed', mode: null, error: message })
3123
+ }
3124
+ }
3125
+
3126
+ return outcomes
3127
+ }
3128
+
3129
+ async function installSkillCommand() {
3130
+ const skipConfirm = hasArg('--yes')
3131
+ const dryRun = hasArg('--dry-run')
3132
+ const scope = parseSkillInstallScope()
3133
+ const mode = parseSkillInstallMode()
3134
+ let targets = parseSkillInstallTargets(scope)
3135
+ if (targets.length === 0) {
3136
+ targets = await promptSkillInstallTargets(scope)
3137
+ }
3138
+
3139
+ if (targets.length === 0) {
3140
+ if (hasJsonFlag()) {
3141
+ printJson({ installs: [] })
3142
+ return
3143
+ }
3144
+ printLine('No targets selected; nothing to do.')
3145
+ return
3146
+ }
3147
+
3148
+ const outcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun })
3149
+ if (hasJsonFlag()) {
3150
+ printJson({ dryRun, installs: outcomes })
3151
+ }
3152
+ }
3153
+
3154
+ function describeSkillTargetState(status: SkillTargetStatus): string {
3155
+ if (status.state === 'current') return 'current'
3156
+ if (status.state === 'stale') return 'stale (run `orizu skills update`)'
3157
+ if (status.state === 'broken-link') return 'broken symlink (run `orizu skills update`)'
3158
+ if (status.state === 'unmanaged') return 'exists without a managed Orizu section'
3159
+ return 'not installed'
3160
+ }
3161
+
3162
+ function skillsStatusCommand() {
3163
+ const source = resolveSkillSource()
3164
+ const statuses = SKILL_INSTALL_TARGETS.map(target => getSkillTargetStatus(target))
3165
+
3166
+ if (hasJsonFlag()) {
3167
+ printJson({
3168
+ name: source.name,
3169
+ root: source.root,
3170
+ source: source.source,
3171
+ cliVersion: getCliVersion(),
3172
+ sourceHash: computeSkillContentHash(source.root),
3173
+ targets: statuses.map(status => ({
3174
+ target: status.target,
3175
+ path: status.path,
3176
+ state: status.state,
3177
+ mode: status.mode,
3178
+ linkTarget: status.linkTarget,
3179
+ installedHash: status.installedHash,
3180
+ meta: status.meta,
3181
+ })),
3182
+ })
3183
+ return
3184
+ }
3185
+
3186
+ printLine(`Skill source: ${source.root} (${source.source})`)
3187
+ printLine('')
3188
+ for (const status of statuses) {
3189
+ const mode = status.mode ? `, ${status.mode}` : ''
3190
+ printLine(` ${status.target.padEnd(16)} ${describeSkillTargetState(status)}${mode}`)
3191
+ printLine(` ${''.padEnd(16)} ${status.path}`)
3192
+ }
3193
+ }
3194
+
3195
+ async function skillsUpdateCommand() {
3196
+ const dryRun = hasArg('--dry-run')
3197
+ const cliVersion = getCliVersion()
3198
+ const updates: Array<{ target: SkillInstallTarget, path: string, action: string }> = []
3199
+
3200
+ for (const target of SKILL_INSTALL_TARGETS) {
3201
+ const status = getSkillTargetStatus(target)
3202
+ if (status.state === 'missing' || status.state === 'unmanaged') {
3203
+ continue
3204
+ }
3205
+
3206
+ if (status.state === 'current') {
3207
+ updates.push({ target, path: status.path, action: 'already-current' })
3208
+ continue
3209
+ }
3210
+
3211
+ if (dryRun) {
3212
+ updates.push({ target, path: status.path, action: 'would-update' })
3213
+ continue
3214
+ }
3215
+
3216
+ const refreshMode: SkillInstallMode = status.mode === 'link' ? 'auto' : 'copy'
2831
3217
  const result = installSkillTarget(target, {
2832
- overwrite,
2833
- dryRun,
3218
+ overwrite: true,
3219
+ mode: refreshMode,
3220
+ cliVersion,
3221
+ })
3222
+ updates.push({
3223
+ target,
3224
+ path: status.path,
3225
+ action: status.state === 'broken-link' ? 'relinked' : result.action,
3226
+ })
3227
+ }
3228
+
3229
+ if (hasJsonFlag()) {
3230
+ printJson({ updates })
3231
+ return
3232
+ }
3233
+
3234
+ if (updates.length === 0) {
3235
+ printLine('No Orizu skill installs found. Run `orizu install-skill` first.')
3236
+ return
3237
+ }
3238
+
3239
+ for (const update of updates) {
3240
+ if (update.action === 'already-current') {
3241
+ printLine(`Current ${update.path}`)
3242
+ } else if (update.action === 'would-update') {
3243
+ printLine(`Would update ${update.path}`)
3244
+ } else if (update.action === 'relinked') {
3245
+ printLine(`Relinked ${update.path}`)
3246
+ } else {
3247
+ printLine(`Updated ${update.path}`)
3248
+ }
3249
+ }
3250
+ }
3251
+
3252
+ function skillsPathCommand() {
3253
+ const source = resolveSkillSource()
3254
+
3255
+ if (hasJsonFlag()) {
3256
+ printJson({
3257
+ name: source.name,
3258
+ root: source.root,
3259
+ skillMd: source.skillMd,
3260
+ source: source.source,
3261
+ cliVersion: getCliVersion(),
3262
+ skillHash: computeSkillContentHash(source.root),
2834
3263
  })
2835
- printLine(`${formatSkillInstallAction(result.action)} ${result.path}`)
3264
+ return
3265
+ }
3266
+
3267
+ printLine(hasArg('--skill-md') ? source.skillMd : source.root)
3268
+ }
3269
+
3270
+ const LAUNCHABLE_AGENTS: Record<string, string> = {
3271
+ claude: 'claude',
3272
+ codex: 'codex',
3273
+ }
3274
+
3275
+ function findExecutable(name: string): string | null {
3276
+ const pathValue = process.env.PATH || ''
3277
+ for (const dir of pathValue.split(delimiter)) {
3278
+ if (!dir) {
3279
+ continue
3280
+ }
3281
+ const candidate = join(dir, name)
3282
+ if (existsSync(candidate)) {
3283
+ return candidate
3284
+ }
3285
+ }
3286
+ return null
3287
+ }
3288
+
3289
+ function describeSetupAuthState(): { state: 'signed-in' | 'signed-out', baseUrl: string } {
3290
+ const baseUrl = getBaseUrl()
3291
+ return {
3292
+ state: getServerCredentials(baseUrl) ? 'signed-in' : 'signed-out',
3293
+ baseUrl,
3294
+ }
3295
+ }
3296
+
3297
+ async function setupCommand() {
3298
+ const dryRun = hasArg('--dry-run')
3299
+ const skipConfirm = hasArg('--yes')
3300
+ const noInput = hasArg('--no-input') || !isInteractiveTerminal()
3301
+
3302
+ printBannerIfInteractive()
3303
+ printLine('Orizu setup')
3304
+ printLine('')
3305
+
3306
+ // Phase 1: login
3307
+ printLine('1. Authentication')
3308
+ let auth = describeSetupAuthState()
3309
+ if (hasArg('--skip-login')) {
3310
+ printLine(' Skipped (--skip-login).')
3311
+ } else if (auth.state === 'signed-in') {
3312
+ printLine(` Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`)
3313
+ } else if (noInput || dryRun) {
3314
+ printLine(' Not signed in. Run `orizu login` to authenticate.')
3315
+ } else if (await askYesNo(' Sign in to Orizu now?', true)) {
3316
+ await login()
3317
+ auth = describeSetupAuthState()
3318
+ } else {
3319
+ printLine(' Skipped. Run `orizu login` when ready.')
3320
+ }
3321
+ printLine('')
3322
+
3323
+ // Phase 2: agent integration
3324
+ printLine('2. Agent integration')
3325
+ const scope = parseSkillInstallScope()
3326
+ const mode = parseSkillInstallMode()
3327
+ let targets = hasArg('--no-install') ? [] : parseSkillInstallTargets(scope)
3328
+ if (targets.length === 0 && !hasArg('--no-install')) {
3329
+ if (noInput) {
3330
+ printLine(' No agents selected. Pass --agent claude and/or --agent codex, or rerun interactively.')
3331
+ } else {
3332
+ targets = await promptSkillInstallTargets(scope)
3333
+ }
3334
+ }
3335
+
3336
+ let installOutcomes: SkillInstallOutcome[] = []
3337
+ if (targets.length > 0) {
3338
+ installOutcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun })
3339
+ } else if (hasArg('--no-install')) {
3340
+ printLine(' Skipped (--no-install).')
3341
+ }
3342
+ printLine('')
3343
+
3344
+ // Phase 3: local workspace
3345
+ printLine('3. Local workspace')
3346
+ let workspaceState: 'created' | 'exists' | 'skipped' | 'would-create' = 'skipped'
3347
+ if (hasArg('--no-workspace')) {
3348
+ printLine(' Skipped (--no-workspace).')
3349
+ } else if (workspaceExists()) {
3350
+ workspaceState = 'exists'
3351
+ printLine(` ${WORKSPACE_DIR_NAME}/ already initialized.`)
3352
+ } else {
3353
+ const wantsWorkspace = hasArg('--workspace') ||
3354
+ (!noInput && await askYesNo(` Create a gitignored ${WORKSPACE_DIR_NAME}/ workspace for local Orizu artifacts?`, true))
3355
+ if (wantsWorkspace) {
3356
+ const result = initOrizuWorkspace({
3357
+ baseUrl: auth.baseUrl,
3358
+ cliVersion: getCliVersion(),
3359
+ dryRun,
3360
+ })
3361
+ workspaceState = result.state === 'would-create' ? 'would-create' : result.state
3362
+ for (const action of result.actions) {
3363
+ printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`)
3364
+ }
3365
+ } else {
3366
+ printLine(` Skipped. Rerun with --workspace to create ${WORKSPACE_DIR_NAME}/.`)
3367
+ }
3368
+ }
3369
+ printLine('')
3370
+
3371
+ // Phase 4: coding-agent handoff
3372
+ const handoffPrompt = renderAgentSetupPrompt({
3373
+ workspacePath: workspaceState === 'created' || workspaceState === 'exists'
3374
+ ? getWorkspaceRoot()
3375
+ : null,
3376
+ })
3377
+ if (!hasArg('--no-handoff')) {
3378
+ printLine('4. Coding-agent handoff')
3379
+ printLine(' Give your coding agent this prompt to plan repo-specific Orizu adoption:')
3380
+ printLine('')
3381
+ printLine('--- prompt start ---')
3382
+ printLine(handoffPrompt)
3383
+ printLine('--- prompt end ---')
3384
+ printLine('')
3385
+
3386
+ const detected = Object.keys(LAUNCHABLE_AGENTS).filter(agent => findExecutable(LAUNCHABLE_AGENTS[agent]))
3387
+ if (detected.length > 0) {
3388
+ printLine(` Detected agents: ${detected.join(', ')}. Launch one with the prompt, e.g.:`)
3389
+ for (const agent of detected) {
3390
+ printLine(` ${LAUNCHABLE_AGENTS[agent]} "$(orizu setup prompt)"`)
3391
+ }
3392
+ }
3393
+
3394
+ const launchAgent = getArg('--launch')
3395
+ if (launchAgent) {
3396
+ if (!(launchAgent in LAUNCHABLE_AGENTS)) {
3397
+ throw new Error(`Unknown --launch agent '${launchAgent}'. Choices: ${Object.keys(LAUNCHABLE_AGENTS).join(', ')}.`)
3398
+ }
3399
+ const binary = findExecutable(LAUNCHABLE_AGENTS[launchAgent])
3400
+ if (!isInteractiveTerminal()) {
3401
+ printLine(' Not launching: --launch requires an interactive terminal.')
3402
+ } else if (!binary) {
3403
+ printLine(` Not launching: '${LAUNCHABLE_AGENTS[launchAgent]}' was not found on PATH.`)
3404
+ } else if (dryRun) {
3405
+ printLine(` Would launch ${binary} with the setup prompt.`)
3406
+ } else if (skipConfirm || await askYesNo(` Launch ${LAUNCHABLE_AGENTS[launchAgent]} with the setup prompt now?`, true)) {
3407
+ printLine(` Launching ${binary}…`)
3408
+ spawnSync(binary, [handoffPrompt], { stdio: 'inherit' })
3409
+ }
3410
+ }
3411
+ printLine('')
3412
+ }
3413
+
3414
+ // Setup summary (success criteria checklist)
3415
+ const summaryTargets = installOutcomes.length > 0
3416
+ ? installOutcomes
3417
+ .filter(outcome => outcome.action !== 'failed')
3418
+ .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target) }))
3419
+ : []
3420
+ const source = resolveSkillSource()
3421
+
3422
+ if (hasJsonFlag()) {
3423
+ printJson({
3424
+ auth: { state: auth.state, server: auth.baseUrl },
3425
+ integrations: installOutcomes.map(outcome => ({
3426
+ target: outcome.target,
3427
+ path: outcome.path,
3428
+ action: outcome.action,
3429
+ mode: outcome.mode,
3430
+ error: outcome.error,
3431
+ })),
3432
+ skill: {
3433
+ root: source.root,
3434
+ source: source.source,
3435
+ cliVersion: getCliVersion(),
3436
+ skillHash: computeSkillContentHash(source.root),
3437
+ },
3438
+ workspace: { state: workspaceState, path: getWorkspaceRoot() },
3439
+ })
3440
+ return
3441
+ }
3442
+
3443
+ printLine('Setup summary')
3444
+ printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`)
3445
+ if (summaryTargets.length > 0) {
3446
+ printLine(' Integrations:')
3447
+ for (const { outcome, status } of summaryTargets) {
3448
+ const modeLabel = status.mode ? `, ${status.mode}` : ''
3449
+ printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`)
3450
+ }
3451
+ } else {
3452
+ printLine(' Integrations: none installed this run — `orizu install-skill` sets them up.')
3453
+ }
3454
+ const failed = installOutcomes.filter(outcome => outcome.action === 'failed')
3455
+ for (const failure of failed) {
3456
+ printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`)
3457
+ }
3458
+ printLine(` Skill source: ${source.root} (${source.source})`)
3459
+ printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : `${getWorkspaceRoot()} (${workspaceState})`}`)
3460
+ printLine(' Next: hand the prompt above to your coding agent to plan repo-specific Orizu adoption.')
3461
+ }
3462
+
3463
+ function setupPromptCommand() {
3464
+ const prompt = renderAgentSetupPrompt({
3465
+ workspacePath: workspaceExists() ? getWorkspaceRoot() : null,
3466
+ })
3467
+ if (hasJsonFlag()) {
3468
+ printJson({ prompt })
3469
+ return
2836
3470
  }
3471
+ printLine(prompt)
2837
3472
  }
2838
3473
 
2839
3474
  function readJsonFile(pathArg: string): Record<string, unknown> {
@@ -3100,6 +3735,10 @@ async function createAppFromFile() {
3100
3735
  app: { id: string; name: string; versionNum: number; componentName?: string; url?: string }
3101
3736
  warnings?: string[]
3102
3737
  }>(response, 'App create')
3738
+ if (hasJsonFlag()) {
3739
+ printJson({ app: data.app, warnings: data.warnings || [] })
3740
+ return
3741
+ }
3103
3742
  printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
3104
3743
  if (data.app.url) {
3105
3744
  printLine(`View app: ${formatTerminalLink(data.app.url)}`)
@@ -3140,6 +3779,14 @@ async function previewAppFromFile() {
3140
3779
  keepOpen,
3141
3780
  })
3142
3781
 
3782
+ if (hasJsonFlag()) {
3783
+ printJson({
3784
+ url: result.url,
3785
+ screenshotPath: result.screenshotPath || null,
3786
+ warnings: result.warnings,
3787
+ })
3788
+ return
3789
+ }
3143
3790
  printLine(`Preview rendered: ${formatTerminalLink(result.url)}`)
3144
3791
  if (result.screenshotPath) {
3145
3792
  printLine(`Screenshot: ${sanitizeTerminalText(result.screenshotPath)}`)
@@ -3191,6 +3838,10 @@ async function updateAppFromFile() {
3191
3838
  app: { id: string; name: string; versionNum: number; componentName?: string }
3192
3839
  warnings?: string[]
3193
3840
  }>(response, 'App update')
3841
+ if (hasJsonFlag()) {
3842
+ printJson({ app: data.app, warnings: data.warnings || [] })
3843
+ return
3844
+ }
3194
3845
  printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`)
3195
3846
  if (data.warnings?.length) {
3196
3847
  printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
@@ -3238,6 +3889,10 @@ async function linkAppDataset() {
3238
3889
  versionNum: number
3239
3890
  }>(response, 'App link dataset')
3240
3891
 
3892
+ if (hasJsonFlag()) {
3893
+ printJson({ app: data.app, linkedDataset: data.linkedDataset, versionNum: data.versionNum })
3894
+ return
3895
+ }
3241
3896
  printLine(
3242
3897
  `Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`
3243
3898
  )
@@ -3294,7 +3949,7 @@ async function createTask() {
3294
3949
 
3295
3950
  if (!response.ok) {
3296
3951
  const cliError = await formatTaskCreateError(response)
3297
- if (hasArg('--json')) {
3952
+ if (hasJsonFlag()) {
3298
3953
  const payload = cliError.structuredPayload ?? { error: cliError.message }
3299
3954
  printLine(JSON.stringify({
3300
3955
  ...payload,
@@ -3318,7 +3973,7 @@ async function createTask() {
3318
3973
  warning?: string
3319
3974
  }>(response, 'Task create')
3320
3975
 
3321
- if (hasArg('--json')) {
3976
+ if (hasJsonFlag()) {
3322
3977
  printLine(JSON.stringify({
3323
3978
  taskId: data.task.id,
3324
3979
  datasetId,
@@ -3390,7 +4045,7 @@ async function publishTask() {
3390
4045
  'Task publish'
3391
4046
  )
3392
4047
 
3393
- if (hasArg('--json')) {
4048
+ if (hasJsonFlag()) {
3394
4049
  printLine(JSON.stringify({
3395
4050
  taskId: data.task.id,
3396
4051
  status: data.task.status,
@@ -3436,6 +4091,10 @@ async function assignTask() {
3436
4091
  }
3437
4092
 
3438
4093
  const data = await parseJsonResponse<{ assignmentsCreated: number }>(response, 'Task assign')
4094
+ if (hasJsonFlag()) {
4095
+ printJson({ assignmentsCreated: data.assignmentsCreated })
4096
+ return
4097
+ }
3439
4098
  printLine(`Created ${data.assignmentsCreated} assignments.`)
3440
4099
  }
3441
4100
 
@@ -3448,7 +4107,7 @@ async function taskStatus() {
3448
4107
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`)
3449
4108
  if (!response.ok) {
3450
4109
  const rawBody = await response.text()
3451
- if (hasArg('--json')) {
4110
+ if (hasJsonFlag()) {
3452
4111
  let errorPayload: Record<string, unknown> = { error: rawBody }
3453
4112
  try {
3454
4113
  const parsed = JSON.parse(rawBody)
@@ -3475,7 +4134,7 @@ async function taskStatus() {
3475
4134
  }
3476
4135
 
3477
4136
  const data = await parseJsonResponse<TaskStatusPayload>(response, 'Task status')
3478
- if (hasArg('--json')) {
4137
+ if (hasJsonFlag()) {
3479
4138
  printLine(JSON.stringify(data, null, 2))
3480
4139
  return
3481
4140
  }
@@ -3502,6 +4161,10 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
3502
4161
  }
3503
4162
 
3504
4163
  const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
4164
+ if (hasJsonFlag()) {
4165
+ printJson({ task: data.task })
4166
+ return
4167
+ }
3505
4168
  const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
3506
4169
  printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
3507
4170
  }
@@ -3626,7 +4289,7 @@ async function appDetail() {
3626
4289
  throw new Error(`App '${appId}' not found in project '${projectSlug}'`)
3627
4290
  }
3628
4291
 
3629
- if (hasArg('--json')) {
4292
+ if (hasJsonFlag()) {
3630
4293
  printLine(JSON.stringify(detail, null, 2))
3631
4294
  return
3632
4295
  }
@@ -3687,6 +4350,14 @@ async function exportAppSource() {
3687
4350
  : defaultAppExportFilename(data.app.name, data.version.versionNum)
3688
4351
 
3689
4352
  writeTextFileEnsuringDir(filename, data.version.code)
4353
+ if (hasJsonFlag()) {
4354
+ printJson({
4355
+ app: { id: data.app.id, name: data.app.name },
4356
+ versionNum: data.version.versionNum,
4357
+ savedTo: filename,
4358
+ })
4359
+ return
4360
+ }
3690
4361
  printLine(
3691
4362
  `Saved app ${sanitizeTerminalText(data.app.name)} v${data.version.versionNum} source to ${sanitizeTerminalText(filename)}`
3692
4363
  )
@@ -3694,8 +4365,12 @@ async function exportAppSource() {
3694
4365
 
3695
4366
  async function listTeamMembers() {
3696
4367
  const teamSlug = await resolveTeamSlug(getArg('--team'))
3697
-
3698
- printTeamMembers(await fetchTeamMembers(teamSlug))
4368
+ const members = await fetchTeamMembers(teamSlug)
4369
+ if (hasJsonFlag()) {
4370
+ printJson({ team: teamSlug, members })
4371
+ return
4372
+ }
4373
+ printTeamMembers(members)
3699
4374
  }
3700
4375
 
3701
4376
  async function addTeamMember() {
@@ -3714,6 +4389,10 @@ async function addTeamMember() {
3714
4389
  throw new Error(`Failed to add team member: ${await response.text()}`)
3715
4390
  }
3716
4391
  const data = await parseJsonResponse<{ member: TeamMember }>(response, 'Team member add')
4392
+ if (hasJsonFlag()) {
4393
+ printJson({ member: data.member })
4394
+ return
4395
+ }
3717
4396
  printLine(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`)
3718
4397
  }
3719
4398
 
@@ -3737,6 +4416,10 @@ async function removeTeamMember() {
3737
4416
  if (!response.ok) {
3738
4417
  throw new Error(`Failed to remove team member: ${await response.text()}`)
3739
4418
  }
4419
+ if (hasJsonFlag()) {
4420
+ printJson({ removed: { id: member.id, email: member.email } })
4421
+ return
4422
+ }
3740
4423
  printLine(`Removed team member ${sanitizeTerminalText(member.email)}`)
3741
4424
  }
3742
4425
 
@@ -3771,6 +4454,10 @@ async function changeTeamMemberRole() {
3771
4454
  throw new Error(`Failed to update member role: ${await response.text()}`)
3772
4455
  }
3773
4456
 
4457
+ if (hasJsonFlag()) {
4458
+ printJson({ member: { id: member.id, email: member.email, role } })
4459
+ return
4460
+ }
3774
4461
  printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`)
3775
4462
  }
3776
4463
 
@@ -3873,6 +4560,10 @@ async function uploadJsonlDatasetInChunks(
3873
4560
  throw new Error('Dataset file contains no rows')
3874
4561
  }
3875
4562
 
4563
+ if (hasJsonFlag()) {
4564
+ printJson({ dataset })
4565
+ return
4566
+ }
3876
4567
  printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`)
3877
4568
  if (dataset.url) {
3878
4569
  printLine(`View dataset: ${formatTerminalLink(dataset.url)}`)
@@ -3902,6 +4593,10 @@ async function uploadDataset() {
3902
4593
  const { rows, sourceType } = parseDatasetFile(file)
3903
4594
  const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3904
4595
 
4596
+ if (hasJsonFlag()) {
4597
+ printJson({ dataset: data.dataset })
4598
+ return
4599
+ }
3905
4600
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
3906
4601
  if (data.dataset.url) {
3907
4602
  printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`)
@@ -4039,6 +4734,10 @@ async function downloadDataset() {
4039
4734
  const bytes = new Uint8Array(await response.arrayBuffer())
4040
4735
  writeFileSync(filename, bytes)
4041
4736
 
4737
+ if (hasJsonFlag()) {
4738
+ printJson({ datasetId, format, savedTo: filename })
4739
+ return
4740
+ }
4042
4741
  printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
4043
4742
  }
4044
4743
 
@@ -4204,6 +4903,10 @@ async function appendDatasetRows() {
4204
4903
  }
4205
4904
 
4206
4905
  if (lastResult) {
4906
+ if (hasJsonFlag()) {
4907
+ printJson({ dataset: lastResult.dataset, appendedCount: totalAppended })
4908
+ return
4909
+ }
4207
4910
  printLine(
4208
4911
  `Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
4209
4912
  )
@@ -4265,6 +4968,10 @@ async function editDatasetRows() {
4265
4968
  updatedCount: number
4266
4969
  }>(response, 'Dataset edit rows')
4267
4970
 
4971
+ if (hasJsonFlag()) {
4972
+ printJson({ dataset: data.dataset, updatedCount: data.updatedCount })
4973
+ return
4974
+ }
4268
4975
  printLine(
4269
4976
  `Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`
4270
4977
  )
@@ -4304,6 +5011,10 @@ async function deleteDatasetRows() {
4304
5011
  deletedCount: number
4305
5012
  }>(response, 'Dataset delete rows')
4306
5013
 
5014
+ if (hasJsonFlag()) {
5015
+ printJson({ dataset: data.dataset, deletedCount: data.deletedCount })
5016
+ return
5017
+ }
4307
5018
  printLine(
4308
5019
  `Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
4309
5020
  )
@@ -4358,6 +5069,10 @@ async function deleteDataset() {
4358
5069
  dataset: { id: string }
4359
5070
  }>(response, 'Dataset delete')
4360
5071
 
5072
+ if (hasJsonFlag()) {
5073
+ printJson({ dataset: data.dataset, deleted: true })
5074
+ return
5075
+ }
4361
5076
  printLine(`Deleted dataset ${sanitizeTerminalText(data.dataset.id)}.`)
4362
5077
  }
4363
5078
 
@@ -4394,6 +5109,10 @@ async function lockDataset() {
4394
5109
  }
4395
5110
  }>(response, 'Dataset lock')
4396
5111
 
5112
+ if (hasJsonFlag()) {
5113
+ printJson({ dataset: data.dataset })
5114
+ return
5115
+ }
4397
5116
  printLine(
4398
5117
  `Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`
4399
5118
  )
@@ -4431,6 +5150,10 @@ async function cloneDataset() {
4431
5150
  }
4432
5151
  }>(response, 'Dataset clone')
4433
5152
 
5153
+ if (hasJsonFlag()) {
5154
+ printJson({ dataset: data.dataset })
5155
+ return
5156
+ }
4434
5157
  printLine(
4435
5158
  `Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`
4436
5159
  )
@@ -4462,6 +5185,10 @@ async function downloadAnnotations() {
4462
5185
  const bytes = new Uint8Array(await response.arrayBuffer())
4463
5186
  writeFileSync(filename, bytes)
4464
5187
 
5188
+ if (hasJsonFlag()) {
5189
+ printJson({ taskId, format, savedTo: filename })
5190
+ return
5191
+ }
4465
5192
  printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
4466
5193
  }
4467
5194
 
@@ -4706,7 +5433,11 @@ async function runnersExec() {
4706
5433
  } else {
4707
5434
  writeFileSync(outPath, resultJsonl)
4708
5435
  }
4709
- printLine(`Wrote ${context.rows.length} runner results to ${sanitizeTerminalText(outPath)}`)
5436
+ if (hasJsonFlag()) {
5437
+ printJson({ rowCount: context.rows.length, out: outPath })
5438
+ } else {
5439
+ printLine(`Wrote ${context.rows.length} runner results to ${sanitizeTerminalText(outPath)}`)
5440
+ }
4710
5441
  } finally {
4711
5442
  materializedRunner.cleanup()
4712
5443
  }
@@ -4747,6 +5478,9 @@ export async function main(rawArgs = process.argv.slice(2)) {
4747
5478
 
4748
5479
  const helpTarget = extractHelpTarget(cliArgs)
4749
5480
  if (helpTarget !== null) {
5481
+ if (helpTarget.length === 0) {
5482
+ printBannerIfInteractive()
5483
+ }
4750
5484
  printLine(renderHelpForArgs(helpTarget))
4751
5485
  return
4752
5486
  }
@@ -4769,6 +5503,16 @@ export async function main(rawArgs = process.argv.slice(2)) {
4769
5503
  return
4770
5504
  }
4771
5505
 
5506
+ if (command === 'setup' && subcommand === 'prompt') {
5507
+ setupPromptCommand()
5508
+ return
5509
+ }
5510
+
5511
+ if (command === 'setup') {
5512
+ await setupCommand()
5513
+ return
5514
+ }
5515
+
4772
5516
  if (command === 'install-skill') {
4773
5517
  await installSkillCommand()
4774
5518
  return
@@ -4779,6 +5523,21 @@ export async function main(rawArgs = process.argv.slice(2)) {
4779
5523
  return
4780
5524
  }
4781
5525
 
5526
+ if (command === 'skills' && subcommand === 'path') {
5527
+ skillsPathCommand()
5528
+ return
5529
+ }
5530
+
5531
+ if (command === 'skills' && subcommand === 'status') {
5532
+ skillsStatusCommand()
5533
+ return
5534
+ }
5535
+
5536
+ if (command === 'skills' && subcommand === 'update') {
5537
+ await skillsUpdateCommand()
5538
+ return
5539
+ }
5540
+
4782
5541
  if (command === 'login') {
4783
5542
  await login()
4784
5543
  return
@@ -4862,6 +5621,16 @@ export async function main(rawArgs = process.argv.slice(2)) {
4862
5621
  return
4863
5622
  }
4864
5623
 
5624
+ if (command === 'prompts' && subcommand === 'archive') {
5625
+ await setPromptArchivedState(true)
5626
+ return
5627
+ }
5628
+
5629
+ if (command === 'prompts' && subcommand === 'restore') {
5630
+ await setPromptArchivedState(false)
5631
+ return
5632
+ }
5633
+
4865
5634
  if (command === 'prompts' && subcommand === 'labels' && cliArgs[2] === 'set') {
4866
5635
  await setPromptLabel()
4867
5636
  return