@primitive.ai/prim 0.1.0-alpha.1 → 0.1.0-alpha.10

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,38 @@ 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
+ if (!process.env.VITEST) {
112
+ main().catch((error) => {
113
+ console.error("[prim] Pre-commit hook error:", error);
114
+ process.exit(0);
115
+ });
116
+ }
117
+ export {
118
+ HOOK_TIMEOUT_MS,
119
+ findAffectedContexts,
120
+ matchPattern,
121
+ syncAffectedSpecs
122
+ };
package/dist/index.js CHANGED
@@ -11,6 +11,9 @@ import {
11
11
  } from "./chunk-3APLWTLB.js";
12
12
 
13
13
  // src/index.ts
14
+ import { readFileSync as readFileSync5 } from "fs";
15
+ import { dirname as dirname2, resolve as resolve2 } from "path";
16
+ import { fileURLToPath } from "url";
14
17
  import { Command } from "commander";
15
18
 
16
19
  // src/commands/auth.ts
@@ -100,10 +103,10 @@ function registerAuthCommands(program2) {
100
103
  process.exit(1);
101
104
  });
102
105
  });
103
- const port = await new Promise((resolve2) => {
106
+ const port = await new Promise((resolve3) => {
104
107
  server.listen(CALLBACK_PORT, LOCALHOST, () => {
105
108
  const addr = server.address();
106
- resolve2(typeof addr === "object" && addr ? addr.port : 0);
109
+ resolve3(typeof addr === "object" && addr ? addr.port : 0);
107
110
  });
108
111
  });
109
112
  const redirectUri = `http://${LOCALHOST}:${port}/callback`;
@@ -234,14 +237,14 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
234
237
  import { readFileSync as readFileSync2 } from "fs";
235
238
  function registerContextCommands(program2) {
236
239
  const context = program2.command("context").description("Manage contexts");
237
- 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) => {
238
241
  const client = getClient();
239
242
  const params = new URLSearchParams();
240
- if (opts.taskId) {
241
- params.set("taskId", opts.taskId);
243
+ if (opts.projectId) {
244
+ params.set("taskId", opts.projectId);
242
245
  }
243
246
  if (opts.scope) {
244
- params.set("scope", opts.scope);
247
+ params.set("scope", opts.scope === "project" ? "task" : opts.scope);
245
248
  }
246
249
  const contexts = await client.get(`/api/cli/contexts?${params.toString()}`);
247
250
  printContextList(contexts);
@@ -251,16 +254,16 @@ function registerContextCommands(program2) {
251
254
  const ctx = await client.get(`/api/cli/contexts/${contextId}`);
252
255
  console.log(JSON.stringify(ctx, null, 2));
253
256
  });
254
- 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(
255
258
  async (opts) => {
256
259
  const client = getClient();
257
260
  let text = opts.text;
258
261
  if (opts.file) {
259
262
  text = readFileSync2(opts.file, "utf-8");
260
263
  }
261
- 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;
262
265
  const result = await client.post("/api/cli/contexts", {
263
- scope: opts.scope,
266
+ scope: opts.scope === "project" ? "task" : opts.scope,
264
267
  name: opts.name,
265
268
  text,
266
269
  taskIds,
@@ -286,19 +289,19 @@ function registerContextCommands(program2) {
286
289
  await client.delete(`/api/cli/contexts/${contextId}`);
287
290
  console.log(`Deleted context: ${contextId}`);
288
291
  });
289
- 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) => {
290
293
  const client = getClient();
291
294
  await client.post(`/api/cli/contexts/${contextId}/link`, {
292
- taskId: opts.task
295
+ taskId: opts.project
293
296
  });
294
- console.log(`Linked context ${contextId} to task ${opts.task}`);
297
+ console.log(`Linked context ${contextId} to project ${opts.project}`);
295
298
  });
296
- 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) => {
297
300
  const client = getClient();
298
301
  await client.post(`/api/cli/contexts/${contextId}/unlink`, {
299
- taskId: opts.task
302
+ taskId: opts.project
300
303
  });
301
- console.log(`Unlinked context ${contextId} from task ${opts.task}`);
304
+ console.log(`Unlinked context ${contextId} from project ${opts.project}`);
302
305
  });
303
306
  }
304
307
  function printContextList(contexts) {
@@ -307,7 +310,7 @@ function printContextList(contexts) {
307
310
  return;
308
311
  }
309
312
  for (const ctx of contexts) {
310
- const scope = ctx.scope ?? "task";
313
+ const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
311
314
  const spec = ctx.isSpecDocument ? " [SPEC]" : "";
312
315
  const name = ctx.name ?? ctx.title ?? "(unnamed)";
313
316
  console.log(`${ctx._id} ${scope.padEnd(8)} ${name}${spec}`);
@@ -330,7 +333,7 @@ if command -v prim-pre-commit >/dev/null 2>&1; then
330
333
  elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
331
334
  ./node_modules/.bin/prim-pre-commit
332
335
  else
333
- 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
334
337
  fi
335
338
  `;
336
339
  var PRIM_BLOCK_START = "# >>> prim pre-commit hook >>>";
@@ -341,7 +344,7 @@ if command -v prim-pre-commit >/dev/null 2>&1; then
341
344
  elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
342
345
  ./node_modules/.bin/prim-pre-commit
343
346
  else
344
- 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
345
348
  fi
346
349
  ${PRIM_BLOCK_END}`;
347
350
  function getGitRoot() {
@@ -357,8 +360,8 @@ function detectHusky(gitRoot) {
357
360
  const pkgPath = resolve(gitRoot, "package.json");
358
361
  if (existsSync2(pkgPath)) {
359
362
  try {
360
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
361
- const scripts = pkg.scripts ?? {};
363
+ const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
364
+ const scripts = pkg2.scripts ?? {};
362
365
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
363
366
  return true;
364
367
  }
@@ -453,16 +456,33 @@ function registerHooksCommands(program2) {
453
456
  });
454
457
  }
455
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
+
456
476
  // src/commands/spec.ts
457
477
  import { readFileSync as readFileSync4 } from "fs";
458
478
  function registerSpecCommands(program2) {
459
479
  const spec = program2.command("spec").description("Manage spec documents");
460
- 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) => {
461
481
  const client = getClient();
462
- if (opts.taskId) {
463
- 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}`);
464
484
  if (specs.length === 0) {
465
- console.log("No spec document found for this task.");
485
+ console.log("No spec document found for this project.");
466
486
  return;
467
487
  }
468
488
  printSpec(specs[0]);
@@ -474,7 +494,7 @@ function registerSpecCommands(program2) {
474
494
  return;
475
495
  }
476
496
  for (const ctx of contexts) {
477
- const scope = ctx.scope ?? "task";
497
+ const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
478
498
  const review = ctx.specReviewStatus ?? "\u2014";
479
499
  const name = ctx.name ?? "(unnamed)";
480
500
  console.log(`${ctx._id} ${scope.padEnd(8)} ${String(review).padEnd(10)} ${name}`);
@@ -511,7 +531,7 @@ ${contexts.length} spec(s)`);
511
531
  }
512
532
  console.log(`Updated spec: ${contextId}`);
513
533
  });
514
- 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) => {
515
535
  const client = getClient();
516
536
  const ctx = await client.get(`/api/cli/contexts/${contextId}`);
517
537
  if (!ctx.isSpecDocument) {
@@ -521,7 +541,7 @@ ${contexts.length} spec(s)`);
521
541
  await client.post(`/api/cli/contexts/${contextId}/sync`);
522
542
  console.log(`Triggered sync for spec: ${contextId}`);
523
543
  if (ctx.specRootTaskId) {
524
- console.log(`Root task: ${ctx.specRootTaskId}`);
544
+ console.log(`Root project: ${ctx.specRootTaskId}`);
525
545
  }
526
546
  });
527
547
  spec.command("map <contextId>").description("Map file patterns to a spec (used by pre-commit hook to detect affected specs)").requiredOption(
@@ -563,9 +583,9 @@ function printSpec(ctx) {
563
583
  const patterns = ctx.filePatterns;
564
584
  console.log(`ID: ${ctx._id}`);
565
585
  console.log(`Name: ${name}`);
566
- console.log(`Scope: ${ctx.scope ?? "task"}`);
586
+ console.log(`Scope: ${ctx.scope === "task" ? "project" : ctx.scope ?? "project"}`);
567
587
  console.log(`Review Status: ${review}`);
568
- console.log(`Root Task: ${ctx.specRootTaskId ?? "\u2014"}`);
588
+ console.log(`Root Project: ${ctx.specRootTaskId ?? "\u2014"}`);
569
589
  console.log(`Sync Version: ${ctx.syncVersion ?? 0}`);
570
590
  console.log(`Index Status: ${ctx.indexStatus ?? "\u2014"}`);
571
591
  console.log(`File Patterns: ${patterns?.length ? patterns.join(", ") : "\u2014"}`);
@@ -578,30 +598,15 @@ ${preview}`);
578
598
  }
579
599
  }
580
600
 
581
- // src/commands/task.ts
582
- function registerTaskCommands(program2) {
583
- const task = program2.command("task").description("Manage tasks");
584
- 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) => {
585
- const client = getClient();
586
- const result = await client.post("/api/cli/tasks", {
587
- name: opts.name,
588
- description: opts.description,
589
- specContextId: opts.spec
590
- });
591
- console.log(`Created task: ${result._id}`);
592
- if (opts.spec) {
593
- console.log(`Linked spec: ${opts.spec}`);
594
- }
595
- });
596
- }
597
-
598
601
  // src/index.ts
602
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
603
+ var pkg = JSON.parse(readFileSync5(resolve2(__dirname, "../package.json"), "utf-8"));
599
604
  var program = new Command();
600
- program.name("prim").description("CLI for managing Primitive specs and contexts").version("0.1.0-alpha.1");
605
+ program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version);
601
606
  registerAuthCommands(program);
602
607
  registerContextCommands(program);
603
608
  registerSpecCommands(program);
604
- registerTaskCommands(program);
609
+ registerProjectCommands(program);
605
610
  registerHooksCommands(program);
606
611
  process.on("unhandledRejection", (err) => {
607
612
  const msg = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.10",
4
4
  "description": "CLI for managing Primitive specs, contexts, and git hooks",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/campus-ai/prim.git"
9
+ "url": "git+https://github.com/campus-ai/prim.git"
10
10
  },
11
11
  "homepage": "https://github.com/campus-ai/prim#readme",
12
12
  "bugs": {
@@ -26,9 +26,10 @@
26
26
  "engines": {
27
27
  "node": ">=20.0.0"
28
28
  },
29
+ "packageManager": "pnpm@9.15.9",
29
30
  "bin": {
30
- "prim": "./dist/index.js",
31
- "prim-pre-commit": "./dist/hooks/pre-commit.js"
31
+ "prim": "dist/index.js",
32
+ "prim-pre-commit": "dist/hooks/pre-commit.js"
32
33
  },
33
34
  "main": "./dist/index.js",
34
35
  "files": [
@@ -36,17 +37,6 @@
36
37
  "LICENSE",
37
38
  "README.md"
38
39
  ],
39
- "dependencies": {
40
- "commander": "^12.1.0"
41
- },
42
- "devDependencies": {
43
- "@biomejs/biome": "^1.9.0",
44
- "@types/node": "^25.5.0",
45
- "@vitest/coverage-v8": "^3.1.0",
46
- "tsup": "^8.0.0",
47
- "typescript": "^5.5.0",
48
- "vitest": "^3.1.0"
49
- },
50
40
  "scripts": {
51
41
  "build": "tsup src/index.ts src/hooks/pre-commit.ts --format esm --clean",
52
42
  "postbuild": "chmod +x ./dist/index.js ./dist/hooks/pre-commit.js",
@@ -59,5 +49,16 @@
59
49
  "test": "vitest run",
60
50
  "test:watch": "vitest",
61
51
  "test:coverage": "vitest run --coverage"
52
+ },
53
+ "dependencies": {
54
+ "commander": "^12.1.0"
55
+ },
56
+ "devDependencies": {
57
+ "@biomejs/biome": "^1.9.0",
58
+ "@types/node": "^25.5.0",
59
+ "@vitest/coverage-v8": "^3.1.0",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^5.5.0",
62
+ "vitest": "^3.1.0"
62
63
  }
63
- }
64
+ }