@primitive.ai/prim 0.1.0-alpha.7 → 0.1.0-alpha.9

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @primitive.ai/prim
2
2
 
3
- The official CLI for [Primitive](https://getprimitive.ai). Manage specs, contexts, tasks, and git hooks from the command line.
3
+ The official CLI for [Primitive](https://getprimitive.ai). Manage specs, contexts, projects, and git hooks from the command line.
4
4
 
5
5
  > [!WARNING]
6
6
  > This project is in **alpha**. Commands and APIs may change between releases.
@@ -45,16 +45,16 @@ prim auth status # Check authentication status
45
45
 
46
46
  ### Specs
47
47
 
48
- Specs are documents that drive implementation. They can be synced to a task DAG and mapped to file patterns for automatic pre-commit hook integration.
48
+ Specs are documents that drive implementation. They can be synced to a project DAG and mapped to file patterns for automatic pre-commit hook integration.
49
49
 
50
50
  ```bash
51
51
  prim spec list # List all specs
52
- prim spec list --task-id <id> # Find spec for a root task
52
+ prim spec list --project-id <id> # Find spec for a root project
53
53
  prim spec get <id> # Show spec details
54
54
  prim spec get <id> --text-only # Print raw spec text
55
55
  prim spec update <id> --file spec.md # Update spec from file
56
56
  prim spec update <id> --name "New" # Rename a spec
57
- prim spec sync <id> # Trigger spec-to-task sync
57
+ prim spec sync <id> # Trigger spec-to-project sync
58
58
  prim spec map <id> -p "src/auth/**" # Map file patterns to a spec
59
59
  prim spec unmap <id> # Clear all file patterns
60
60
  prim spec unmap <id> -p "src/auth/**" # Remove specific pattern
@@ -64,24 +64,24 @@ prim spec auto-map <id> # Auto-detect file patterns
64
64
  ### Contexts
65
65
 
66
66
  ```bash
67
- prim context list # List all contexts
68
- prim context list --scope task # Filter by scope
69
- prim context list --task-id <id> # List contexts for a task
70
- prim context get <id> # Get context details
71
- prim context create -s task -n "Name" # Create a context
72
- prim context create -s task -n "Name" --file path/to/file
73
- prim context update <id> --name "New" # Update a context
74
- prim context delete <id> # Delete a context
75
- prim context link <id> --task <tid> # Link context to task
76
- prim context unlink <id> --task <tid> # Unlink context from task
67
+ prim context list # List all contexts
68
+ prim context list --scope project # Filter by scope
69
+ prim context list --project-id <id> # List contexts for a project
70
+ prim context get <id> # Get context details
71
+ prim context create -s project -n "Name" # Create a context
72
+ prim context create -s project -n "Name" --file path/to/file
73
+ prim context update <id> --name "New" # Update a context
74
+ prim context delete <id> # Delete a context
75
+ prim context link <id> --project <pid> # Link context to project
76
+ prim context unlink <id> --project <pid> # Unlink context from project
77
77
  ```
78
78
 
79
- ### Tasks
79
+ ### Projects
80
80
 
81
81
  ```bash
82
- prim task create -n "Task name" # Create a task
83
- prim task create -n "Task name" -d "Description" # Create with description
84
- prim task create -n "Task name" --spec <contextId> # Create and link a spec
82
+ prim project create -n "Project name" # Create a project
83
+ prim project create -n "Project name" -d "Description" # Create with description
84
+ prim project create -n "Project name" --spec <contextId> # Create and link a spec
85
85
  ```
86
86
 
87
87
  ### Hooks
@@ -11,6 +11,11 @@ function getStagedFiles() {
11
11
  });
12
12
  return output.trim().split("\n").filter((f) => f.length > 0);
13
13
  }
14
+ function getStagedDiff(files) {
15
+ return execSync(`git diff --cached -- ${files.map((f) => `"${f}"`).join(" ")}`, {
16
+ encoding: "utf-8"
17
+ });
18
+ }
14
19
  function matchPattern(filePath, pattern) {
15
20
  const regexStr = pattern.replaceAll("**", "\xA7GLOBSTAR\xA7").replaceAll("*", "[^/]*").replaceAll("\xA7GLOBSTAR\xA7", ".*");
16
21
  const regex = new RegExp(`^${regexStr}$`);
@@ -39,29 +44,35 @@ function findAffectedContexts(stagedFiles, specs) {
39
44
  return affected;
40
45
  }
41
46
  var HOOK_TIMEOUT_MS = 1e4;
42
- async function main() {
43
- const stagedFiles = getStagedFiles();
47
+ var defaultDeps = {
48
+ getClient,
49
+ getStagedFiles,
50
+ getStagedDiff
51
+ };
52
+ async function syncAffectedSpecs(deps = defaultDeps) {
53
+ const stagedFiles = deps.getStagedFiles();
44
54
  if (stagedFiles.length === 0) {
45
- process.exit(0);
55
+ return [];
46
56
  }
47
- const client = getClient();
57
+ const client = deps.getClient();
48
58
  let mappings = [];
49
59
  try {
50
60
  mappings = await client.get("/api/cli/specs/mappings", {
51
61
  signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
52
62
  });
53
63
  } catch {
54
- process.exit(0);
64
+ return [];
55
65
  }
56
66
  if (mappings.length === 0) {
57
- process.exit(0);
67
+ return [];
58
68
  }
59
69
  const affectedContexts = findAffectedContexts(stagedFiles, mappings);
60
70
  if (affectedContexts.size === 0) {
61
- process.exit(0);
71
+ return [];
62
72
  }
63
73
  console.log(`[prim] ${String(affectedContexts.size)} spec(s) affected by staged changes:`);
64
- for (const [contextId] of affectedContexts) {
74
+ const synced = [];
75
+ for (const [contextId, affected] of affectedContexts) {
65
76
  try {
66
77
  const ctx = await client.get(`/api/cli/contexts/${contextId}`, {
67
78
  signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
@@ -74,18 +85,39 @@ async function main() {
74
85
  console.log(` [skip] ${contextId} \u2014 not a spec document`);
75
86
  continue;
76
87
  }
77
- await client.post(`/api/cli/contexts/${contextId}/sync`, void 0, {
78
- signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
79
- });
88
+ const diffContent = deps.getStagedDiff(affected.matchedFiles);
89
+ if (!diffContent) {
90
+ console.log(` [skip] ${contextId} \u2014 no diff content`);
91
+ continue;
92
+ }
93
+ await client.post(
94
+ `/api/cli/contexts/${contextId}/sync-diff`,
95
+ { diffContent, affectedFiles: affected.matchedFiles },
96
+ { signal: AbortSignal.timeout(HOOK_TIMEOUT_MS) }
97
+ );
80
98
  console.log(` [synced] ${contextId} \u2014 ${ctx.name ?? "(unnamed)"}`);
99
+ synced.push(contextId);
81
100
  } catch (error) {
82
101
  const message = error instanceof Error ? error.message : String(error);
83
102
  console.error(` [error] ${contextId} \u2014 ${message}`);
84
103
  }
85
104
  }
86
- process.exit(0);
105
+ return synced;
87
106
  }
88
- main().catch((error) => {
89
- console.error("[prim] Pre-commit hook error:", error);
107
+ async function main() {
108
+ await syncAffectedSpecs();
90
109
  process.exit(0);
91
- });
110
+ }
111
+ var isDirectExecution = typeof process !== "undefined" && process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"));
112
+ if (isDirectExecution) {
113
+ main().catch((error) => {
114
+ console.error("[prim] Pre-commit hook error:", error);
115
+ process.exit(0);
116
+ });
117
+ }
118
+ export {
119
+ HOOK_TIMEOUT_MS,
120
+ findAffectedContexts,
121
+ matchPattern,
122
+ syncAffectedSpecs
123
+ };
package/dist/index.js CHANGED
@@ -237,14 +237,14 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
237
237
  import { readFileSync as readFileSync2 } from "fs";
238
238
  function registerContextCommands(program2) {
239
239
  const context = program2.command("context").description("Manage contexts");
240
- context.command("list").description("List contexts").option("-s, --scope <scope>", "Filter by scope: task, global, external").option("-t, --task-id <taskId>", "List contexts linked to a specific task").action(async (opts) => {
240
+ 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) => {
241
241
  const client = getClient();
242
242
  const params = new URLSearchParams();
243
- if (opts.taskId) {
244
- params.set("taskId", opts.taskId);
243
+ if (opts.projectId) {
244
+ params.set("taskId", opts.projectId);
245
245
  }
246
246
  if (opts.scope) {
247
- params.set("scope", opts.scope);
247
+ params.set("scope", opts.scope === "project" ? "task" : opts.scope);
248
248
  }
249
249
  const contexts = await client.get(`/api/cli/contexts?${params.toString()}`);
250
250
  printContextList(contexts);
@@ -254,16 +254,16 @@ function registerContextCommands(program2) {
254
254
  const ctx = await client.get(`/api/cli/contexts/${contextId}`);
255
255
  console.log(JSON.stringify(ctx, null, 2));
256
256
  });
257
- context.command("create").description("Create a new context").requiredOption("-s, --scope <scope>", "Scope: task, 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("--task-id <taskId>", "Link to task(s), comma-separated").option("--spec", "Mark as a spec document").action(
257
+ 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(
258
258
  async (opts) => {
259
259
  const client = getClient();
260
260
  let text = opts.text;
261
261
  if (opts.file) {
262
262
  text = readFileSync2(opts.file, "utf-8");
263
263
  }
264
- const taskIds = opts.taskId ? opts.taskId.split(",").map((id) => id.trim()) : void 0;
264
+ const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
265
265
  const result = await client.post("/api/cli/contexts", {
266
- scope: opts.scope,
266
+ scope: opts.scope === "project" ? "task" : opts.scope,
267
267
  name: opts.name,
268
268
  text,
269
269
  taskIds,
@@ -289,19 +289,19 @@ function registerContextCommands(program2) {
289
289
  await client.delete(`/api/cli/contexts/${contextId}`);
290
290
  console.log(`Deleted context: ${contextId}`);
291
291
  });
292
- context.command("link <contextId>").description("Link a context to a task").requiredOption("--task <taskId>", "Task ID to link to").action(async (contextId, opts) => {
292
+ context.command("link <contextId>").description("Link a context to a project").requiredOption("--project <projectId>", "Project ID to link to").action(async (contextId, opts) => {
293
293
  const client = getClient();
294
294
  await client.post(`/api/cli/contexts/${contextId}/link`, {
295
- taskId: opts.task
295
+ taskId: opts.project
296
296
  });
297
- console.log(`Linked context ${contextId} to task ${opts.task}`);
297
+ console.log(`Linked context ${contextId} to project ${opts.project}`);
298
298
  });
299
- context.command("unlink <contextId>").description("Unlink a context from a task").requiredOption("--task <taskId>", "Task ID to unlink from").action(async (contextId, opts) => {
299
+ context.command("unlink <contextId>").description("Unlink a context from a project").requiredOption("--project <projectId>", "Project ID to unlink from").action(async (contextId, opts) => {
300
300
  const client = getClient();
301
301
  await client.post(`/api/cli/contexts/${contextId}/unlink`, {
302
- taskId: opts.task
302
+ taskId: opts.project
303
303
  });
304
- console.log(`Unlinked context ${contextId} from task ${opts.task}`);
304
+ console.log(`Unlinked context ${contextId} from project ${opts.project}`);
305
305
  });
306
306
  }
307
307
  function printContextList(contexts) {
@@ -310,7 +310,7 @@ function printContextList(contexts) {
310
310
  return;
311
311
  }
312
312
  for (const ctx of contexts) {
313
- const scope = ctx.scope ?? "task";
313
+ const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
314
314
  const spec = ctx.isSpecDocument ? " [SPEC]" : "";
315
315
  const name = ctx.name ?? ctx.title ?? "(unnamed)";
316
316
  console.log(`${ctx._id} ${scope.padEnd(8)} ${name}${spec}`);
@@ -333,7 +333,7 @@ if command -v prim-pre-commit >/dev/null 2>&1; then
333
333
  elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
334
334
  ./node_modules/.bin/prim-pre-commit
335
335
  else
336
- npx --yes @primitive.ai/prim pre-commit-hook 2>/dev/null || true
336
+ npx --yes -p @primitive.ai/prim prim-pre-commit 2>/dev/null || true
337
337
  fi
338
338
  `;
339
339
  var PRIM_BLOCK_START = "# >>> prim pre-commit hook >>>";
@@ -344,7 +344,7 @@ if command -v prim-pre-commit >/dev/null 2>&1; then
344
344
  elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
345
345
  ./node_modules/.bin/prim-pre-commit
346
346
  else
347
- npx --yes @primitive.ai/prim pre-commit-hook 2>/dev/null || true
347
+ npx --yes -p @primitive.ai/prim prim-pre-commit 2>/dev/null || true
348
348
  fi
349
349
  ${PRIM_BLOCK_END}`;
350
350
  function getGitRoot() {
@@ -456,16 +456,33 @@ function registerHooksCommands(program2) {
456
456
  });
457
457
  }
458
458
 
459
+ // src/commands/project.ts
460
+ function registerProjectCommands(program2) {
461
+ const project = program2.command("project").description("Manage projects");
462
+ 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) => {
463
+ const client = getClient();
464
+ const result = await client.post("/api/cli/tasks", {
465
+ name: opts.name,
466
+ description: opts.description,
467
+ specContextId: opts.spec
468
+ });
469
+ console.log(`Created project: ${result._id}`);
470
+ if (opts.spec) {
471
+ console.log(`Linked spec: ${opts.spec}`);
472
+ }
473
+ });
474
+ }
475
+
459
476
  // src/commands/spec.ts
460
477
  import { readFileSync as readFileSync4 } from "fs";
461
478
  function registerSpecCommands(program2) {
462
479
  const spec = program2.command("spec").description("Manage spec documents");
463
- spec.command("list").description("List spec documents").option("-t, --task-id <taskId>", "List spec for a specific root task").action(async (opts) => {
480
+ spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").action(async (opts) => {
464
481
  const client = getClient();
465
- if (opts.taskId) {
466
- const specs = await client.get(`/api/cli/specs?rootTaskId=${opts.taskId}`);
482
+ if (opts.projectId) {
483
+ const specs = await client.get(`/api/cli/specs?rootTaskId=${opts.projectId}`);
467
484
  if (specs.length === 0) {
468
- console.log("No spec document found for this task.");
485
+ console.log("No spec document found for this project.");
469
486
  return;
470
487
  }
471
488
  printSpec(specs[0]);
@@ -477,7 +494,7 @@ function registerSpecCommands(program2) {
477
494
  return;
478
495
  }
479
496
  for (const ctx of contexts) {
480
- const scope = ctx.scope ?? "task";
497
+ const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
481
498
  const review = ctx.specReviewStatus ?? "\u2014";
482
499
  const name = ctx.name ?? "(unnamed)";
483
500
  console.log(`${ctx._id} ${scope.padEnd(8)} ${String(review).padEnd(10)} ${name}`);
@@ -514,7 +531,7 @@ ${contexts.length} spec(s)`);
514
531
  }
515
532
  console.log(`Updated spec: ${contextId}`);
516
533
  });
517
- spec.command("sync <contextId>").description("Trigger spec \u2194 task DAG synchronization").action(async (contextId) => {
534
+ spec.command("sync <contextId>").description("Trigger spec \u2194 project DAG synchronization").action(async (contextId) => {
518
535
  const client = getClient();
519
536
  const ctx = await client.get(`/api/cli/contexts/${contextId}`);
520
537
  if (!ctx.isSpecDocument) {
@@ -524,7 +541,7 @@ ${contexts.length} spec(s)`);
524
541
  await client.post(`/api/cli/contexts/${contextId}/sync`);
525
542
  console.log(`Triggered sync for spec: ${contextId}`);
526
543
  if (ctx.specRootTaskId) {
527
- console.log(`Root task: ${ctx.specRootTaskId}`);
544
+ console.log(`Root project: ${ctx.specRootTaskId}`);
528
545
  }
529
546
  });
530
547
  spec.command("map <contextId>").description("Map file patterns to a spec (used by pre-commit hook to detect affected specs)").requiredOption(
@@ -566,9 +583,9 @@ function printSpec(ctx) {
566
583
  const patterns = ctx.filePatterns;
567
584
  console.log(`ID: ${ctx._id}`);
568
585
  console.log(`Name: ${name}`);
569
- console.log(`Scope: ${ctx.scope ?? "task"}`);
586
+ console.log(`Scope: ${ctx.scope === "task" ? "project" : ctx.scope ?? "project"}`);
570
587
  console.log(`Review Status: ${review}`);
571
- console.log(`Root Task: ${ctx.specRootTaskId ?? "\u2014"}`);
588
+ console.log(`Root Project: ${ctx.specRootTaskId ?? "\u2014"}`);
572
589
  console.log(`Sync Version: ${ctx.syncVersion ?? 0}`);
573
590
  console.log(`Index Status: ${ctx.indexStatus ?? "\u2014"}`);
574
591
  console.log(`File Patterns: ${patterns?.length ? patterns.join(", ") : "\u2014"}`);
@@ -581,23 +598,6 @@ ${preview}`);
581
598
  }
582
599
  }
583
600
 
584
- // src/commands/task.ts
585
- function registerTaskCommands(program2) {
586
- const task = program2.command("task").description("Manage tasks");
587
- task.command("create").description("Create a new task").requiredOption("-n, --name <name>", "Task name").option("-d, --description <description>", "Task description").option("--spec <contextId>", "Link an existing spec as this task's spec").action(async (opts) => {
588
- const client = getClient();
589
- const result = await client.post("/api/cli/tasks", {
590
- name: opts.name,
591
- description: opts.description,
592
- specContextId: opts.spec
593
- });
594
- console.log(`Created task: ${result._id}`);
595
- if (opts.spec) {
596
- console.log(`Linked spec: ${opts.spec}`);
597
- }
598
- });
599
- }
600
-
601
601
  // src/index.ts
602
602
  var __dirname = dirname2(fileURLToPath(import.meta.url));
603
603
  var pkg = JSON.parse(readFileSync5(resolve2(__dirname, "../package.json"), "utf-8"));
@@ -606,7 +606,7 @@ program.name("prim").description("CLI for managing Primitive specs and contexts"
606
606
  registerAuthCommands(program);
607
607
  registerContextCommands(program);
608
608
  registerSpecCommands(program);
609
- registerTaskCommands(program);
609
+ registerProjectCommands(program);
610
610
  registerHooksCommands(program);
611
611
  process.on("unhandledRejection", (err) => {
612
612
  const msg = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.9",
4
4
  "description": "CLI for managing Primitive specs, contexts, and git hooks",
5
5
  "type": "module",
6
6
  "license": "MIT",