@primitive.ai/prim 0.1.0-alpha.12 → 0.1.0-alpha.14

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.
Files changed (3) hide show
  1. package/SKILL.md +24 -11
  2. package/dist/index.js +207 -76
  3. package/package.json +1 -1
package/SKILL.md CHANGED
@@ -83,9 +83,13 @@ npx --yes @primitive.ai/prim project create -n "<name>" --spec <contextId> #
83
83
 
84
84
  ### Install the pre-commit hook
85
85
  ```
86
- npx --yes @primitive.ai/prim hooks install # auto-detects Husky and prompts
86
+ npx --yes @primitive.ai/prim hooks install # auto-detects Husky and prompts
87
+ npx --yes @primitive.ai/prim hooks install --yes # confirm Husky (non-interactive)
88
+ npx --yes @primitive.ai/prim hooks install --target=git-hooks # force .git/hooks (skip Husky detection)
87
89
  npx --yes @primitive.ai/prim hooks uninstall
88
90
  ```
91
+ Under `CI=1` (or with `--non-interactive`), `hooks install` fails fast in a Husky repo unless `--yes` or `--target` is set. The error message names both escapes.
92
+
89
93
  **Note:** `hooks uninstall` only removes `.git/hooks/pre-commit`. If the hook was installed into `.husky/pre-commit`, you must remove the prim block from that file manually.
90
94
 
91
95
  ## How the pre-commit hook behaves
@@ -106,17 +110,26 @@ What that means:
106
110
 
107
111
  ## Output formats
108
112
 
109
- | Command | Output | Where the ID is |
113
+ Every data-returning command accepts `--json`. With `--json` set, stdout is a single JSON document — pipe to `jq` instead of parsing text:
114
+
115
+ - `id=$(npx --yes @primitive.ai/prim context create -s global -n foo --text "x" --json | jq -r ._id)` — capture an ID
116
+ - `npx --yes @primitive.ai/prim spec list --json | jq -r '.[]._id'` — list every spec ID
117
+ - `npx --yes @primitive.ai/prim auth status --json | jq -r .authenticated` — boolean; the exit code remains the authoritative signal
118
+
119
+ Without `--json`, mutating commands (`context create/update/delete/link/unlink`, `spec update/sync/map/unmap/auto-map`, `project create`) emit the bare resource `_id` to **stdout** (one line, no prefix) and human-readable diagnostics to **stderr**. So this also works as a one-liner without `jq`:
120
+
121
+ - `id=$(npx --yes @primitive.ai/prim context create -s global -n foo --text "x")`
122
+
123
+ | Command | Without `--json` | With `--json` |
110
124
  |---|---|---|
111
- | `npx --yes @primitive.ai/prim context create` | `Created context: <id>` | Match `^Created context: (\S+)` |
112
- | `npx --yes @primitive.ai/prim project create` | `Created project: <id>` | Match `^Created project: (\S+)` |
113
- | `npx --yes @primitive.ai/prim spec update` | `Updated spec: <id>` | Match `^Updated spec: (\S+)` |
114
- | `npx --yes @primitive.ai/prim spec sync` | `Triggered sync for spec: <id>` | Match `^Triggered sync for spec: (\S+)` |
115
- | `npx --yes @primitive.ai/prim context list`, `npx --yes @primitive.ai/prim spec list` | Table with trailing count line | First token of each row (skip the final `N spec(s)` / `N context(s)` summary line) |
116
- | `npx --yes @primitive.ai/prim spec list --project-id <pid>` | Single-spec block (key:value) | `ID:` line |
117
- | `npx --yes @primitive.ai/prim context get <id>` | Pretty-printed JSON | `._id` field |
118
- | `npx --yes @primitive.ai/prim spec get <id>` | Human-readable key:value block | `ID:` line |
119
- | `npx --yes @primitive.ai/prim spec get <id> --text-only` | Raw spec markdown, nothing else | n/a |
125
+ | Mutators above | stdout: bare `_id`; stderr: `Created/Updated/...` prefix (plus secondary lines: `Root project:`, `Linked spec:`, pattern lists) | stdout: `{ "_id": "<id>", }` with extras where applicable (`spec sync` adds `specRootTaskId`; `context link/unlink` add `project`; `project create --spec` adds `spec`; `spec map/unmap` add `filePatterns`) |
126
+ | `context list`, `spec list` (non-empty) | stdout: rows (first token = `_id`); stderr: `N context(s)` / `N spec(s)` summary | stdout: JSON array |
127
+ | `context list`, `spec list` (empty) | stdout: (empty); stderr: `No contexts found.` / `No spec documents found.` | stdout: `[]` |
128
+ | `spec list --project-id <pid>` | stdout: key:value block (or stdout empty + stderr `No spec document found for this project.` if none) | stdout: single object or `null` |
129
+ | `context get <id>` | stdout: pretty-printed JSON (always JSON; `--json` accepted for symmetry) | stdout: pretty-printed JSON |
130
+ | `spec get <id>` | stdout: human-readable key:value block (`ID:` line first) | stdout: JSON object |
131
+ | `spec get <id> --text-only` | stdout: raw spec markdown, nothing else | stdout: JSON object (`--json` wins over `--text-only`) |
132
+ | `auth status` | stdout: human readout; **exit code is the authoritative signal** (0 = authed) | stdout: JSON; exit code unchanged |
120
133
 
121
134
  ## Pitfalls
122
135
 
package/dist/index.js CHANGED
@@ -24,6 +24,13 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
24
24
  import { createServer } from "http";
25
25
  import { platform } from "os";
26
26
  import { dirname } from "path";
27
+
28
+ // src/output.ts
29
+ function printJson(data) {
30
+ console.log(JSON.stringify(data, null, 2));
31
+ }
32
+
33
+ // src/commands/auth.ts
27
34
  var FILE_MODE = 384;
28
35
  var LOCALHOST = "127.0.0.1";
29
36
  var CALLBACK_PORT = 19876;
@@ -174,8 +181,22 @@ ${authUrl.toString()}
174
181
  console.log("No saved tokens found.");
175
182
  }
176
183
  });
177
- auth.command("status").description("Check authentication status and token expiry").action(() => {
184
+ auth.command("status").description("Check authentication status and token expiry").option("--json", "Output as JSON").action((opts) => {
178
185
  const token = getAuthToken();
186
+ if (opts.json) {
187
+ const expiresAt2 = getTokenExpiresAt();
188
+ const expiresInMs = expiresAt2 ? expiresAt2 - Date.now() : null;
189
+ const refreshPresent = existsSync(REFRESH_TOKEN_PATH);
190
+ printJson({
191
+ authenticated: !!token,
192
+ tokenFile: token ? TOKEN_FILE_PATH : null,
193
+ accessTokenExpiresInMs: expiresInMs,
194
+ accessTokenExpired: expiresInMs !== null && expiresInMs <= 0,
195
+ refreshTokenPresent: refreshPresent,
196
+ warnings: !token || refreshPresent ? [] : ["no refresh token"]
197
+ });
198
+ process.exit(token ? 0 : 1);
199
+ }
179
200
  if (!token) {
180
201
  console.log("Not authenticated. Run `prim auth login` to authenticate.");
181
202
  process.exit(1);
@@ -238,7 +259,7 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
238
259
  import { readFileSync as readFileSync2 } from "fs";
239
260
  function registerContextCommands(program2) {
240
261
  const context = program2.command("context").description("Manage contexts");
241
- context.command("list").description("List contexts").option("-s, --scope <scope>", "Filter by scope: project, global, external").option("-t, --project-id <projectId>", "List contexts linked to a specific project").action(async (opts) => {
262
+ context.command("list").description("List contexts").option("-s, --scope <scope>", "Filter by scope: project, global, external").option("-t, --project-id <projectId>", "List contexts linked to a specific project").option("--json", "Output as JSON").action(async (opts) => {
242
263
  const client = getClient();
243
264
  const params = new URLSearchParams();
244
265
  if (opts.projectId) {
@@ -248,14 +269,18 @@ function registerContextCommands(program2) {
248
269
  params.set("scope", opts.scope === "project" ? "task" : opts.scope);
249
270
  }
250
271
  const contexts = await client.get(`/api/cli/contexts?${params.toString()}`);
272
+ if (opts.json) {
273
+ printJson(contexts);
274
+ return;
275
+ }
251
276
  printContextList(contexts);
252
277
  });
253
- context.command("get <contextId>").description("Get a context by ID").action(async (contextId) => {
278
+ context.command("get <contextId>").description("Get a context by ID").option("--json", "Output as JSON (default behavior; accepted for symmetry)").action(async (contextId) => {
254
279
  const client = getClient();
255
280
  const ctx = await client.get(`/api/cli/contexts/${contextId}`);
256
- console.log(JSON.stringify(ctx, null, 2));
281
+ printJson(ctx);
257
282
  });
258
- context.command("create").description("Create a new context").requiredOption("-s, --scope <scope>", "Scope: project, global, external").requiredOption("-n, --name <name>", "Context name").option("-t, --text <text>", "Context text content").option("-f, --file <path>", "Read text content from file").option("--project-id <projectId>", "Link to project(s), comma-separated").option("--spec", "Mark as a spec document").action(
283
+ context.command("create").description("Create a new context").requiredOption("-s, --scope <scope>", "Scope: project, global, external").requiredOption("-n, --name <name>", "Context name").option("-t, --text <text>", "Context text content").option("-f, --file <path>", "Read text content from file").option("--project-id <projectId>", "Link to project(s), comma-separated").option("--spec", "Mark as a spec document").option("--json", "Output as JSON").action(
259
284
  async (opts) => {
260
285
  const client = getClient();
261
286
  let text = opts.text;
@@ -270,44 +295,71 @@ function registerContextCommands(program2) {
270
295
  taskIds,
271
296
  isSpecDocument: opts.spec ?? false
272
297
  });
273
- console.log(`Created context: ${result._id}`);
298
+ if (opts.json) {
299
+ printJson({ _id: result._id });
300
+ return;
301
+ }
302
+ console.error(`Created context: ${result._id}`);
303
+ console.log(result._id);
274
304
  }
275
305
  );
276
- context.command("update <contextId>").description("Update a context").option("-n, --name <name>", "New name").option("-t, --text <text>", "New text content").option("-f, --file <path>", "Read text content from file").action(async (contextId, opts) => {
277
- const client = getClient();
278
- let text = opts.text;
279
- if (opts.file) {
280
- text = readFileSync2(opts.file, "utf-8");
306
+ context.command("update <contextId>").description("Update a context").option("-n, --name <name>", "New name").option("-t, --text <text>", "New text content").option("-f, --file <path>", "Read text content from file").option("--json", "Output as JSON").action(
307
+ async (contextId, opts) => {
308
+ const client = getClient();
309
+ let text = opts.text;
310
+ if (opts.file) {
311
+ text = readFileSync2(opts.file, "utf-8");
312
+ }
313
+ await client.patch(`/api/cli/contexts/${contextId}`, {
314
+ name: opts.name,
315
+ text
316
+ });
317
+ if (opts.json) {
318
+ printJson({ _id: contextId });
319
+ return;
320
+ }
321
+ console.error(`Updated context: ${contextId}`);
322
+ console.log(contextId);
281
323
  }
282
- await client.patch(`/api/cli/contexts/${contextId}`, {
283
- name: opts.name,
284
- text
285
- });
286
- console.log(`Updated context: ${contextId}`);
287
- });
288
- context.command("delete <contextId>").description("Delete a context").action(async (contextId) => {
324
+ );
325
+ context.command("delete <contextId>").description("Delete a context").option("--json", "Output as JSON").action(async (contextId, opts) => {
289
326
  const client = getClient();
290
327
  await client.delete(`/api/cli/contexts/${contextId}`);
291
- console.log(`Deleted context: ${contextId}`);
328
+ if (opts.json) {
329
+ printJson({ _id: contextId });
330
+ return;
331
+ }
332
+ console.error(`Deleted context: ${contextId}`);
333
+ console.log(contextId);
292
334
  });
293
- context.command("link <contextId>").description("Link a context to a project").requiredOption("--project <projectId>", "Project ID to link to").action(async (contextId, opts) => {
335
+ context.command("link <contextId>").description("Link a context to a project").requiredOption("--project <projectId>", "Project ID to link to").option("--json", "Output as JSON").action(async (contextId, opts) => {
294
336
  const client = getClient();
295
337
  await client.post(`/api/cli/contexts/${contextId}/link`, {
296
338
  taskId: opts.project
297
339
  });
298
- console.log(`Linked context ${contextId} to project ${opts.project}`);
340
+ if (opts.json) {
341
+ printJson({ _id: contextId, project: opts.project });
342
+ return;
343
+ }
344
+ console.error(`Linked context ${contextId} to project ${opts.project}`);
345
+ console.log(contextId);
299
346
  });
300
- context.command("unlink <contextId>").description("Unlink a context from a project").requiredOption("--project <projectId>", "Project ID to unlink from").action(async (contextId, opts) => {
347
+ context.command("unlink <contextId>").description("Unlink a context from a project").requiredOption("--project <projectId>", "Project ID to unlink from").option("--json", "Output as JSON").action(async (contextId, opts) => {
301
348
  const client = getClient();
302
349
  await client.post(`/api/cli/contexts/${contextId}/unlink`, {
303
350
  taskId: opts.project
304
351
  });
305
- console.log(`Unlinked context ${contextId} from project ${opts.project}`);
352
+ if (opts.json) {
353
+ printJson({ _id: contextId, project: opts.project });
354
+ return;
355
+ }
356
+ console.error(`Unlinked context ${contextId} from project ${opts.project}`);
357
+ console.log(contextId);
306
358
  });
307
359
  }
308
360
  function printContextList(contexts) {
309
361
  if (contexts.length === 0) {
310
- console.log("No contexts found.");
362
+ console.error("No contexts found.");
311
363
  return;
312
364
  }
313
365
  for (const ctx of contexts) {
@@ -316,7 +368,7 @@ function printContextList(contexts) {
316
368
  const name = ctx.name ?? ctx.title ?? "(unnamed)";
317
369
  console.log(`${ctx._id} ${scope.padEnd(8)} ${name}${spec}`);
318
370
  }
319
- console.log(`
371
+ console.error(`
320
372
  ${contexts.length} context(s)`);
321
373
  }
322
374
 
@@ -324,6 +376,7 @@ ${contexts.length} context(s)`);
324
376
  import { execSync } from "child_process";
325
377
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
326
378
  import { resolve } from "path";
379
+ import { Option } from "commander";
327
380
  var HOOK_SCRIPT = `#!/bin/sh
328
381
  # prim pre-commit hook \u2014 auto-syncs affected specs on commit
329
382
  # Installed by: prim hooks install
@@ -431,17 +484,37 @@ function installToDotGit(gitRoot) {
431
484
  }
432
485
  function registerHooksCommands(program2) {
433
486
  const hooks = program2.command("hooks").description("Manage git hooks");
434
- hooks.command("install").description("Install the prim pre-commit hook").action(async () => {
487
+ hooks.command("install").description("Install the prim pre-commit hook (auto-detects Husky; use --target to override)").addOption(
488
+ new Option("--target <where>", "install destination; bypasses Husky detection").choices([
489
+ "husky",
490
+ "git-hooks"
491
+ ])
492
+ ).action(async (opts, command) => {
493
+ const globals = command.optsWithGlobals();
494
+ const nonInteractive = Boolean(
495
+ globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
496
+ );
435
497
  const gitRoot = getGitRoot();
498
+ if (opts.target === "husky") return installToHusky(gitRoot);
499
+ if (opts.target === "git-hooks") return installToDotGit(gitRoot);
436
500
  if (detectHusky(gitRoot)) {
437
- const confirmed = await askConfirmation(
501
+ if (globals.yes) return installToHusky(gitRoot);
502
+ if (nonInteractive) {
503
+ throw new Error(
504
+ "--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
505
+ );
506
+ }
507
+ if (!process.stdin.isTTY) {
508
+ console.error(
509
+ "Note: Husky detected but stdin is not a TTY \u2014 falling back to .git/hooks. Pass --yes for Husky or --non-interactive to fail fast."
510
+ );
511
+ } else if (await askConfirmation(
438
512
  "Husky detected. Install prim hook into .husky/pre-commit instead of .git/hooks/pre-commit?"
439
- );
440
- if (confirmed) {
441
- installToHusky(gitRoot);
442
- return;
513
+ )) {
514
+ return installToHusky(gitRoot);
515
+ } else {
516
+ console.log("Falling back to .git/hooks/pre-commit install.");
443
517
  }
444
- console.log("Falling back to .git/hooks/pre-commit install.");
445
518
  }
446
519
  installToDotGit(gitRoot);
447
520
  });
@@ -460,17 +533,22 @@ function registerHooksCommands(program2) {
460
533
  // src/commands/project.ts
461
534
  function registerProjectCommands(program2) {
462
535
  const project = program2.command("project").description("Manage projects");
463
- project.command("create").description("Create a new project").requiredOption("-n, --name <name>", "Project name").option("-d, --description <description>", "Project description").option("--spec <contextId>", "Link an existing spec as this project's spec").action(async (opts) => {
536
+ project.command("create").description("Create a new project").requiredOption("-n, --name <name>", "Project name").option("-d, --description <description>", "Project description").option("--spec <contextId>", "Link an existing spec as this project's spec").option("--json", "Output as JSON").action(async (opts) => {
464
537
  const client = getClient();
465
538
  const result = await client.post("/api/cli/tasks", {
466
539
  name: opts.name,
467
540
  description: opts.description,
468
541
  specContextId: opts.spec
469
542
  });
470
- console.log(`Created project: ${result._id}`);
543
+ if (opts.json) {
544
+ printJson(opts.spec ? { _id: result._id, spec: opts.spec } : { _id: result._id });
545
+ return;
546
+ }
547
+ console.error(`Created project: ${result._id}`);
471
548
  if (opts.spec) {
472
- console.log(`Linked spec: ${opts.spec}`);
549
+ console.error(`Linked spec: ${opts.spec}`);
473
550
  }
551
+ console.log(result._id);
474
552
  });
475
553
  }
476
554
 
@@ -592,12 +670,21 @@ function runUninstall(cwd, opts) {
592
670
  function runStatus(cwd, opts) {
593
671
  const target = resolveTarget(cwd, opts.target);
594
672
  if (target === null) return 1;
595
- if (!existsSync3(target)) {
673
+ const fileExists = existsSync3(target);
674
+ let installed = false;
675
+ if (fileExists) {
676
+ const content = readFileSync4(target, "utf-8");
677
+ installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
678
+ }
679
+ if (opts.json) {
680
+ printJson({ installed, target });
681
+ return installed ? 0 : 1;
682
+ }
683
+ if (!fileExists) {
596
684
  console.log(`No rules file at ${target}`);
597
685
  return 1;
598
686
  }
599
- const content = readFileSync4(target, "utf-8");
600
- if (content.includes(SKILL_BEGIN) && content.includes(SKILL_END)) {
687
+ if (installed) {
601
688
  console.log(`PRIM SKILL v1 installed at ${target}`);
602
689
  return 0;
603
690
  }
@@ -617,7 +704,7 @@ function registerSkillCommands(program2) {
617
704
  skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").action((opts) => {
618
705
  process.exit(runUninstall(process.cwd(), opts));
619
706
  });
620
- skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").action((opts) => {
707
+ skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--json", "Output as JSON").action((opts) => {
621
708
  process.exit(runStatus(process.cwd(), opts));
622
709
  });
623
710
  }
@@ -626,20 +713,28 @@ function registerSkillCommands(program2) {
626
713
  import { readFileSync as readFileSync5 } from "fs";
627
714
  function registerSpecCommands(program2) {
628
715
  const spec = program2.command("spec").description("Manage spec documents");
629
- spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").action(async (opts) => {
716
+ spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").option("--json", "Output as JSON").action(async (opts) => {
630
717
  const client = getClient();
631
718
  if (opts.projectId) {
632
719
  const specs = await client.get(`/api/cli/specs?rootTaskId=${opts.projectId}`);
720
+ if (opts.json) {
721
+ printJson(specs[0] ?? null);
722
+ return;
723
+ }
633
724
  if (specs.length === 0) {
634
- console.log("No spec document found for this project.");
725
+ console.error("No spec document found for this project.");
635
726
  return;
636
727
  }
637
728
  printSpec(specs[0]);
638
729
  return;
639
730
  }
640
731
  const contexts = await client.get("/api/cli/specs");
732
+ if (opts.json) {
733
+ printJson(contexts);
734
+ return;
735
+ }
641
736
  if (contexts.length === 0) {
642
- console.log("No spec documents found.");
737
+ console.error("No spec documents found.");
643
738
  return;
644
739
  }
645
740
  for (const ctx of contexts) {
@@ -648,39 +743,50 @@ function registerSpecCommands(program2) {
648
743
  const name = ctx.name ?? "(unnamed)";
649
744
  console.log(`${ctx._id} ${scope.padEnd(8)} ${String(review).padEnd(10)} ${name}`);
650
745
  }
651
- console.log(`
746
+ console.error(`
652
747
  ${contexts.length} spec(s)`);
653
748
  });
654
- spec.command("get <contextId>").description("Get a spec document by ID").option("--text-only", "Print only the text content (no metadata)").action(async (contextId, opts) => {
749
+ spec.command("get <contextId>").description("Get a spec document by ID").option("--text-only", "Print only the text content (no metadata)").option("--json", "Output as JSON (overrides --text-only)").action(async (contextId, opts) => {
655
750
  const client = getClient();
656
751
  const ctx = await client.get(`/api/cli/contexts/${contextId}`);
752
+ if (opts.json) {
753
+ printJson(ctx);
754
+ return;
755
+ }
657
756
  if (opts.textOnly) {
658
757
  console.log(ctx.text ?? "");
659
758
  return;
660
759
  }
661
760
  printSpec(ctx);
662
761
  });
663
- spec.command("update <contextId>").description("Update a spec document's text content").option("-t, --text <text>", "New text content").option("-f, --file <path>", "Read text content from file").option("-n, --name <name>", "New name").action(async (contextId, opts) => {
664
- const client = getClient();
665
- let text = opts.text;
666
- if (opts.file) {
667
- text = readFileSync5(opts.file, "utf-8");
668
- }
669
- if (!(text || opts.name)) {
670
- console.error("Provide --text, --file, or --name to update.");
671
- process.exit(1);
672
- }
673
- await client.patch(`/api/cli/contexts/${contextId}`, {
674
- name: opts.name,
675
- text,
676
- skipTiptapLifecycle: !!text
677
- });
678
- if (text) {
679
- await client.post(`/api/cli/contexts/${contextId}/inject`);
762
+ spec.command("update <contextId>").description("Update a spec document's text content").option("-t, --text <text>", "New text content").option("-f, --file <path>", "Read text content from file").option("-n, --name <name>", "New name").option("--json", "Output as JSON").action(
763
+ async (contextId, opts) => {
764
+ const client = getClient();
765
+ let text = opts.text;
766
+ if (opts.file) {
767
+ text = readFileSync5(opts.file, "utf-8");
768
+ }
769
+ if (!(text || opts.name)) {
770
+ console.error("Provide --text, --file, or --name to update.");
771
+ process.exit(1);
772
+ }
773
+ await client.patch(`/api/cli/contexts/${contextId}`, {
774
+ name: opts.name,
775
+ text,
776
+ skipTiptapLifecycle: !!text
777
+ });
778
+ if (text) {
779
+ await client.post(`/api/cli/contexts/${contextId}/inject`);
780
+ }
781
+ if (opts.json) {
782
+ printJson({ _id: contextId });
783
+ return;
784
+ }
785
+ console.error(`Updated spec: ${contextId}`);
786
+ console.log(contextId);
680
787
  }
681
- console.log(`Updated spec: ${contextId}`);
682
- });
683
- spec.command("sync <contextId>").description("Trigger spec \u2194 project DAG synchronization").action(async (contextId) => {
788
+ );
789
+ spec.command("sync <contextId>").description("Trigger spec \u2194 project DAG synchronization").option("--json", "Output as JSON").action(async (contextId, opts) => {
684
790
  const client = getClient();
685
791
  const ctx = await client.get(`/api/cli/contexts/${contextId}`);
686
792
  if (!ctx.isSpecDocument) {
@@ -688,42 +794,64 @@ ${contexts.length} spec(s)`);
688
794
  process.exit(1);
689
795
  }
690
796
  await client.post(`/api/cli/contexts/${contextId}/sync`);
691
- console.log(`Triggered sync for spec: ${contextId}`);
797
+ if (opts.json) {
798
+ printJson(
799
+ ctx.specRootTaskId ? { _id: contextId, specRootTaskId: ctx.specRootTaskId } : { _id: contextId }
800
+ );
801
+ return;
802
+ }
803
+ console.error(`Triggered sync for spec: ${contextId}`);
692
804
  if (ctx.specRootTaskId) {
693
- console.log(`Root project: ${ctx.specRootTaskId}`);
805
+ console.error(`Root project: ${ctx.specRootTaskId}`);
694
806
  }
807
+ console.log(contextId);
695
808
  });
696
809
  spec.command("map <contextId>").description("Map file patterns to a spec (used by pre-commit hook to detect affected specs)").requiredOption(
697
810
  "-p, --pattern <patterns...>",
698
811
  'Glob pattern(s) to associate, e.g. "src/auth/**"'
699
- ).action(async (contextId, opts) => {
812
+ ).option("--json", "Output as JSON").action(async (contextId, opts) => {
700
813
  const client = getClient();
701
814
  const result = await client.post(`/api/cli/contexts/${contextId}/map`, {
702
815
  patterns: opts.pattern
703
816
  });
704
- console.log(`Mapped patterns to spec ${contextId}:`);
817
+ if (opts.json) {
818
+ printJson({ _id: contextId, filePatterns: result.filePatterns });
819
+ return;
820
+ }
821
+ console.error(`Mapped patterns to spec ${contextId}:`);
705
822
  for (const p of result.filePatterns) {
706
- console.log(` ${p}`);
823
+ console.error(` ${p}`);
707
824
  }
825
+ console.log(contextId);
708
826
  });
709
- spec.command("unmap <contextId>").description("Remove file pattern mappings from a spec (omit --pattern to clear all)").option("-p, --pattern <patterns...>", "Specific pattern(s) to remove (omit to clear all)").action(async (contextId, opts) => {
827
+ spec.command("unmap <contextId>").description("Remove file pattern mappings from a spec (omit --pattern to clear all)").option("-p, --pattern <patterns...>", "Specific pattern(s) to remove (omit to clear all)").option("--json", "Output as JSON").action(async (contextId, opts) => {
710
828
  const client = getClient();
711
829
  const result = await client.post(`/api/cli/contexts/${contextId}/unmap`, {
712
830
  patterns: opts.pattern
713
831
  });
832
+ if (opts.json) {
833
+ printJson({ _id: contextId, filePatterns: result.filePatterns });
834
+ return;
835
+ }
714
836
  if (result.filePatterns.length === 0) {
715
- console.log(`Cleared all file patterns from spec ${contextId}`);
837
+ console.error(`Cleared all file patterns from spec ${contextId}`);
716
838
  } else {
717
- console.log(`Updated patterns for spec ${contextId}:`);
839
+ console.error(`Updated patterns for spec ${contextId}:`);
718
840
  for (const p of result.filePatterns) {
719
- console.log(` ${p}`);
841
+ console.error(` ${p}`);
720
842
  }
721
843
  }
844
+ console.log(contextId);
722
845
  });
723
- spec.command("auto-map <contextId>").description("Trigger auto-mapping of file patterns for a spec").action(async (contextId) => {
846
+ spec.command("auto-map <contextId>").description("Trigger auto-mapping of file patterns for a spec").option("--json", "Output as JSON").action(async (contextId, opts) => {
724
847
  const client = getClient();
725
848
  await client.post(`/api/cli/contexts/${contextId}/auto-map`);
726
- console.log(`Auto-mapping triggered for spec: ${contextId}`);
849
+ if (opts.json) {
850
+ printJson({ _id: contextId });
851
+ return;
852
+ }
853
+ console.error(`Auto-mapping triggered for spec: ${contextId}`);
854
+ console.log(contextId);
727
855
  });
728
856
  }
729
857
  function printSpec(ctx) {
@@ -752,7 +880,10 @@ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
752
880
  var pkg = JSON.parse(readFileSync6(resolve3(__dirname2, "../package.json"), "utf-8"));
753
881
  updateNotifier({ pkg }).notify();
754
882
  var program = new Command();
755
- program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version);
883
+ program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
884
+ "--non-interactive",
885
+ "fail fast instead of prompting (also: CI=1, PRIM_NON_INTERACTIVE=1)"
886
+ );
756
887
  registerAuthCommands(program);
757
888
  registerContextCommands(program);
758
889
  registerSpecCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "description": "CLI for managing Primitive specs, contexts, and git hooks",
5
5
  "type": "module",
6
6
  "license": "MIT",