@stigmer/cli 3.18.0-dev.20260918103812 → 3.18.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.
Files changed (109) hide show
  1. package/commands/bootstrap.d.ts +3 -0
  2. package/commands/bootstrap.d.ts.map +1 -0
  3. package/commands/bootstrap.js +75 -0
  4. package/commands/bootstrap.js.map +1 -0
  5. package/commands/marketplace.d.ts.map +1 -1
  6. package/commands/marketplace.js +7 -7
  7. package/commands/marketplace.js.map +1 -1
  8. package/config/resolve.d.ts.map +1 -1
  9. package/config/resolve.js +8 -7
  10. package/config/resolve.js.map +1 -1
  11. package/local/bootstrap.d.ts +32 -13
  12. package/local/bootstrap.d.ts.map +1 -1
  13. package/local/bootstrap.js +105 -64
  14. package/local/bootstrap.js.map +1 -1
  15. package/local/daemon/launch.d.ts +1 -1
  16. package/local/daemon/launch.d.ts.map +1 -1
  17. package/local/paths.d.ts +1 -1
  18. package/local/paths.js +1 -1
  19. package/local/plugins/defaults.d.ts +27 -7
  20. package/local/plugins/defaults.d.ts.map +1 -1
  21. package/local/plugins/defaults.js +55 -28
  22. package/local/plugins/defaults.js.map +1 -1
  23. package/local/runtime/runtimes-install.d.ts.map +1 -1
  24. package/local/runtime/runtimes-install.js +7 -4
  25. package/local/runtime/runtimes-install.js.map +1 -1
  26. package/local/seedpack-retire.d.ts +63 -0
  27. package/local/seedpack-retire.d.ts.map +1 -0
  28. package/local/seedpack-retire.js +361 -0
  29. package/local/seedpack-retire.js.map +1 -0
  30. package/local/system-org.d.ts +15 -0
  31. package/local/system-org.d.ts.map +1 -0
  32. package/local/system-org.js +57 -0
  33. package/local/system-org.js.map +1 -0
  34. package/marketplace/config.d.ts +3 -1
  35. package/marketplace/config.d.ts.map +1 -1
  36. package/marketplace/config.js +24 -11
  37. package/marketplace/config.js.map +1 -1
  38. package/marketplace/github.d.ts +37 -0
  39. package/marketplace/github.d.ts.map +1 -1
  40. package/marketplace/github.js +70 -1
  41. package/marketplace/github.js.map +1 -1
  42. package/marketplace/install.d.ts +13 -6
  43. package/marketplace/install.d.ts.map +1 -1
  44. package/marketplace/install.js +63 -16
  45. package/marketplace/install.js.map +1 -1
  46. package/marketplace/official.js +4 -4
  47. package/marketplace/official.js.map +1 -1
  48. package/marketplace/render.d.ts.map +1 -1
  49. package/marketplace/render.js +10 -4
  50. package/marketplace/render.js.map +1 -1
  51. package/package.json +7 -7
  52. package/program.js +2 -2
  53. package/program.js.map +1 -1
  54. package/resources/apply/declarative.js +2 -2
  55. package/resources/apply/declarative.js.map +1 -1
  56. package/resources/plugin.d.ts.map +1 -1
  57. package/resources/plugin.js +12 -9
  58. package/resources/plugin.js.map +1 -1
  59. package/src/commands/bootstrap.test.ts +253 -0
  60. package/src/commands/bootstrap.ts +91 -0
  61. package/src/commands/install.test.ts +28 -9
  62. package/src/commands/marketplace.test.ts +28 -14
  63. package/src/commands/marketplace.ts +9 -7
  64. package/src/config/resolve.test.ts +2 -2
  65. package/src/config/resolve.ts +8 -7
  66. package/src/local/bootstrap.test.ts +110 -38
  67. package/src/local/bootstrap.ts +179 -89
  68. package/src/local/daemon/launch.ts +1 -1
  69. package/src/local/paths.ts +1 -1
  70. package/src/local/plugins/defaults.test.ts +46 -13
  71. package/src/local/plugins/defaults.ts +77 -34
  72. package/src/local/runtime/runtimes-install.test.ts +2 -0
  73. package/src/local/runtime/runtimes-install.ts +7 -4
  74. package/src/local/seedpack-retire.test.ts +499 -0
  75. package/src/local/seedpack-retire.ts +474 -0
  76. package/src/local/system-org.test.ts +124 -0
  77. package/src/local/system-org.ts +63 -0
  78. package/src/marketplace/__fixtures__/cursor-marketplace.ts +5 -2
  79. package/src/marketplace/config.test.ts +47 -22
  80. package/src/marketplace/config.ts +27 -12
  81. package/src/marketplace/github.test.ts +77 -1
  82. package/src/marketplace/github.ts +115 -1
  83. package/src/marketplace/install.test.ts +84 -17
  84. package/src/marketplace/install.ts +73 -15
  85. package/src/marketplace/official.ts +4 -4
  86. package/src/marketplace/read.test.ts +1 -1
  87. package/src/marketplace/render.ts +13 -4
  88. package/src/program.ts +2 -2
  89. package/src/resources/apply/declarative.integration.test.ts +2 -2
  90. package/src/resources/apply/declarative.ts +2 -2
  91. package/src/resources/connect/discover.test.ts +1 -1
  92. package/src/resources/plugin.ts +11 -10
  93. package/commands/seedpack.d.ts +0 -3
  94. package/commands/seedpack.d.ts.map +0 -1
  95. package/commands/seedpack.js +0 -98
  96. package/commands/seedpack.js.map +0 -1
  97. package/local/seedpack/apply.d.ts +0 -72
  98. package/local/seedpack/apply.d.ts.map +0 -1
  99. package/local/seedpack/apply.js +0 -145
  100. package/local/seedpack/apply.js.map +0 -1
  101. package/local/seedpack/content.d.ts +0 -61
  102. package/local/seedpack/content.d.ts.map +0 -1
  103. package/local/seedpack/content.js +0 -178
  104. package/local/seedpack/content.js.map +0 -1
  105. package/src/commands/seedpack.ts +0 -116
  106. package/src/local/seedpack/apply.test.ts +0 -287
  107. package/src/local/seedpack/apply.ts +0 -199
  108. package/src/local/seedpack/content.test.ts +0 -169
  109. package/src/local/seedpack/content.ts +0 -222
@@ -1,199 +0,0 @@
1
- // In-process seedpack apply.
2
- //
3
- // The TS equivalent of Go's seedpackbootstrap.Apply, with one deliberate
4
- // improvement: instead of shelling out to `stigmer apply` as a subprocess, this
5
- // drives the same in-process declarative-apply path that `stigmer apply` uses —
6
- // one code path, no recursion guard, no second process.
7
- //
8
- // Two phases respect the resource hierarchy (mirrors the Go bootstrap):
9
- // 1. organizations/ via file mode — the org must exist before its members.
10
- // 2. the project (stigmer.yaml + agents/skills/mcp-servers/workflows) via the
11
- // declarative reconciler, under the target org.
12
- //
13
- // Idempotent, with the applied-hash truth living with the backend it describes
14
- // (cloud#429): every apply stamps the content hash as a reserved label on the
15
- // seedpack Project, and cloud-mode runs skip by reading that label back — so
16
- // any machine (operator laptop, CI) sees the same applied state. Local mode
17
- // keeps the original marker file (the local data dir lives and dies with the
18
- // local backend, so a local file IS backend-scoped state there).
19
-
20
- import { mkdtempSync, rmSync } from "node:fs";
21
- import { tmpdir } from "node:os";
22
- import { join } from "node:path";
23
- import { create } from "@bufbuild/protobuf";
24
- import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
25
- import type { Stigmer } from "@stigmer/sdk";
26
- import { applyItem, resolveApplyItems } from "../../resources/apply/apply.js";
27
- import { applyDeclarative, detectTrack } from "../../resources/apply/declarative.js";
28
- import type { ControllerFn } from "../../resources/apply/handlers.js";
29
- import {
30
- extractSeedpack,
31
- hashSeedpackContent,
32
- readMarker,
33
- resolveSeedpackContent,
34
- type SeedpackContent,
35
- writeMarker,
36
- } from "./content.js";
37
-
38
- const DEFAULT_ORG = "stigmer";
39
- const ORG_ENV_VAR = "STIGMER_SEEDPACK_ORG";
40
-
41
- /**
42
- * Reserved label on the seedpack Project recording the last applied content
43
- * hash (cloud#429). Writing a NEW value requires `can_write_reserved_labels`
44
- * (the seeding identity's capability); re-sending an unchanged value is an
45
- * echo the cloud guard passes for any caller — so re-applies of identical
46
- * content are never treated as reserved-label writes.
47
- */
48
- export const SEEDPACK_HASH_LABEL = "stigmer.ai/seedpack-hash";
49
-
50
- /**
51
- * The seedpack Project's slug. The server derives it from the manifest's
52
- * `metadata.name` in `seedpack/stigmer.yaml` — "stigmer-seedpack" is already
53
- * slug-shaped, so name and slug coincide. Pinned by a test against the real
54
- * content so a rename there fails loudly here.
55
- */
56
- export const SEEDPACK_PROJECT_SLUG = "stigmer-seedpack";
57
-
58
- export interface SeedpackApplyDeps {
59
- /** Raw command-controller accessor (full-proto apply, preserves metadata.id). */
60
- readonly controller: ControllerFn;
61
- /** High-level client for skill push + post-apply MCP discovery. */
62
- readonly stigmer: Stigmer;
63
- /** Human progress lines (stderr). */
64
- readonly info: (line: string) => void;
65
- /** Warnings (stderr). */
66
- readonly warn: (line: string) => void;
67
- }
68
-
69
- export interface SeedpackApplyOptions {
70
- /** Directory holding the idempotency marker (local mode only). */
71
- readonly markerDir: string;
72
- /** Target org slug. Defaults to STIGMER_SEEDPACK_ORG, then "stigmer". */
73
- readonly org?: string;
74
- /** Re-apply even when the content hash is unchanged. */
75
- readonly force?: boolean;
76
- /** Pre-resolved content (injectable for tests); resolved on demand otherwise. */
77
- readonly content?: SeedpackContent;
78
- readonly home?: string;
79
- /**
80
- * Cloud mode: the idempotency truth is the {@link SEEDPACK_HASH_LABEL} on the
81
- * server's seedpack Project, read via {@link readServerSeedpackHash} — the
82
- * local marker is neither read nor written, so a stateless machine (CI, a
83
- * second operator laptop) still skips unchanged content correctly.
84
- */
85
- readonly useServerHash?: boolean;
86
- }
87
-
88
- export interface SeedpackApplyResult {
89
- /** True when the seedpack was applied; false when skipped as up to date. */
90
- readonly applied: boolean;
91
- /** The current content hash (stored in the marker on a successful apply). */
92
- readonly hash: string;
93
- /** The resolved target org. */
94
- readonly org: string;
95
- }
96
-
97
- /** Resolve the target org: explicit option > env var > "stigmer". */
98
- export function resolveSeedpackOrg(explicit?: string): string {
99
- if (explicit !== undefined && explicit !== "") return explicit;
100
- const env = process.env[ORG_ENV_VAR];
101
- if (env !== undefined && env !== "") return env;
102
- return DEFAULT_ORG;
103
- }
104
-
105
- /** Compute the current seedpack content hash without applying (drives `status`). */
106
- export function seedpackContentHash(opts: Pick<SeedpackApplyOptions, "content" | "home"> = {}): string {
107
- const content = opts.content ?? resolveSeedpackContent({ home: opts.home });
108
- return hashSeedpackContent(content.dir);
109
- }
110
-
111
- /**
112
- * Read the applied content hash recorded on the backend's seedpack Project,
113
- * or null when it cannot be proven (project absent — never applied — or the
114
- * read failed). Null means "apply": the apply itself is idempotent server-side,
115
- * so an unprovable state costs one redundant pass, never a skipped update.
116
- */
117
- export async function readServerSeedpackHash(stigmer: Stigmer, org: string): Promise<string | null> {
118
- try {
119
- const project = await stigmer.project.getByReference({ org, slug: SEEDPACK_PROJECT_SLUG });
120
- return project.metadata?.labels?.[SEEDPACK_HASH_LABEL] ?? null;
121
- } catch {
122
- return null;
123
- }
124
- }
125
-
126
- /**
127
- * Apply the seedpack to the backend the deps connect to. Returns `applied:false`
128
- * (without touching the backend beyond the cloud-mode hash read) when the
129
- * recorded hash already matches the content hash and `force` is not set.
130
- */
131
- export async function applySeedpack(deps: SeedpackApplyDeps, opts: SeedpackApplyOptions): Promise<SeedpackApplyResult> {
132
- const org = resolveSeedpackOrg(opts.org);
133
- const content = opts.content ?? resolveSeedpackContent({ home: opts.home });
134
- const hash = hashSeedpackContent(content.dir);
135
-
136
- const appliedHash = opts.useServerHash === true
137
- ? await readServerSeedpackHash(deps.stigmer, org)
138
- : readMarker(opts.markerDir);
139
- if (opts.force !== true && appliedHash === hash) {
140
- return { applied: false, hash, org };
141
- }
142
-
143
- deps.info("Applying system resources (seedpack)…");
144
- const stage = mkdtempSync(join(tmpdir(), "stigmer-seedpack-"));
145
- try {
146
- extractSeedpack(content.dir, stage);
147
- await applyOrganizations(deps, stage);
148
- await applyProject(deps, stage, org, hash);
149
- // The Project label written above is the cloud-mode record; the marker
150
- // stays the local-mode one. Never both — two records of one fact drift.
151
- if (opts.useServerHash !== true) {
152
- writeMarker(opts.markerDir, hash);
153
- }
154
- } finally {
155
- rmSync(stage, { recursive: true, force: true });
156
- }
157
-
158
- deps.info("System resources applied successfully");
159
- return { applied: true, hash, org };
160
- }
161
-
162
- // Phase 1: organizations via file mode. Organizations sit above the project and
163
- // carry no org context themselves, so they apply with an empty org.
164
- async function applyOrganizations(deps: SeedpackApplyDeps, stageDir: string): Promise<void> {
165
- const orgDir = join(stageDir, "organizations");
166
- let items: ReturnType<typeof resolveApplyItems>;
167
- try {
168
- items = resolveApplyItems(orgDir);
169
- } catch {
170
- return; // No organizations to apply.
171
- }
172
- for (const item of items) {
173
- const outcome = await applyItem(deps.controller, item, "", false);
174
- if (outcome.warning !== undefined) deps.warn(outcome.warning);
175
- }
176
- }
177
-
178
- // Phase 2: the project (agents, skills, MCP servers, workflows) via the shared
179
- // declarative reconciler — the same path `stigmer apply` runs for a user project.
180
- //
181
- // The content hash is stamped as a reserved label on the Project HERE, in the
182
- // seedpack path only — general `stigmer apply` never writes reserved labels.
183
- // The stamp rides the project apply that happens anyway, so recording the hash
184
- // costs no extra RPC and can never succeed while the apply failed.
185
- async function applyProject(deps: SeedpackApplyDeps, stageDir: string, org: string, contentHash: string): Promise<void> {
186
- const detect = detectTrack(stageDir);
187
- if (detect.track !== "declarative" || detect.project === undefined) {
188
- throw new Error(`seedpack is not a declarative project (detected '${detect.track}')`);
189
- }
190
- detect.project.metadata ??= create(ApiResourceMetadataSchema, {});
191
- detect.project.metadata.labels[SEEDPACK_HASH_LABEL] = contentHash;
192
- await applyDeclarative(detect, {
193
- controller: deps.controller,
194
- stigmer: deps.stigmer,
195
- org,
196
- info: deps.info,
197
- warn: deps.warn,
198
- });
199
- }
@@ -1,169 +0,0 @@
1
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { classify, ExitCode } from "../../errors/index.js";
6
- import {
7
- acquireSeedpack,
8
- extractSeedpack,
9
- hashSeedpackContent,
10
- listContentFiles,
11
- readMarker,
12
- resolveSeedpackContent,
13
- SEEDPACK_ENTRIES,
14
- writeMarker,
15
- } from "./content.js";
16
-
17
- // Build a minimal seedpack content tree with one non-canonical sibling (tools/)
18
- // to prove the canonical-entry filtering.
19
- function makeFixture(): string {
20
- const dir = mkdtempSync(join(tmpdir(), "seedpack-content-"));
21
- writeFileSync(join(dir, "stigmer.yaml"), "kind: Project\n");
22
- mkdirSync(join(dir, "organizations"), { recursive: true });
23
- writeFileSync(join(dir, "organizations", "stigmer.yaml"), "kind: Organization\n");
24
- mkdirSync(join(dir, "skills", "demo"), { recursive: true });
25
- writeFileSync(join(dir, "skills", "demo", "SKILL.md"), "---\nname: demo\n---\n");
26
- mkdirSync(join(dir, "agents"), { recursive: true });
27
- writeFileSync(join(dir, "agents", "a.yaml"), "kind: Agent\n");
28
- // Non-canonical siblings that must never be hashed/extracted.
29
- mkdirSync(join(dir, "tools"), { recursive: true });
30
- writeFileSync(join(dir, "tools", "gen.sh"), "echo hi\n");
31
- mkdirSync(join(dir, "icons"), { recursive: true });
32
- writeFileSync(join(dir, "icons", "logo.svg"), "<svg/>\n");
33
- return dir;
34
- }
35
-
36
- let fixture: string;
37
- beforeEach(() => {
38
- fixture = makeFixture();
39
- });
40
- afterEach(() => {
41
- rmSync(fixture, { recursive: true, force: true });
42
- });
43
-
44
- describe("listContentFiles", () => {
45
- it("lists only canonical entries, lexically sorted", () => {
46
- const files = listContentFiles(fixture);
47
- expect(files).toEqual([
48
- "agents/a.yaml",
49
- "organizations/stigmer.yaml",
50
- "skills/demo/SKILL.md",
51
- "stigmer.yaml",
52
- ]);
53
- expect(files.some((f) => f.startsWith("tools/") || f.startsWith("icons/"))).toBe(false);
54
- });
55
- });
56
-
57
- describe("hashSeedpackContent", () => {
58
- it("is deterministic and sha256-prefixed", () => {
59
- expect(hashSeedpackContent(fixture)).toBe(hashSeedpackContent(fixture));
60
- expect(hashSeedpackContent(fixture)).toMatch(/^sha256:[0-9a-f]{16}$/);
61
- });
62
-
63
- it("changes when content changes but ignores non-canonical files", () => {
64
- const before = hashSeedpackContent(fixture);
65
- writeFileSync(join(fixture, "tools", "extra.sh"), "noise\n");
66
- expect(hashSeedpackContent(fixture)).toBe(before);
67
- writeFileSync(join(fixture, "agents", "b.yaml"), "kind: Agent\n");
68
- expect(hashSeedpackContent(fixture)).not.toBe(before);
69
- });
70
- });
71
-
72
- describe("extractSeedpack", () => {
73
- it("copies only canonical entries and hashes identically to the source", () => {
74
- const dest = mkdtempSync(join(tmpdir(), "seedpack-extract-"));
75
- try {
76
- extractSeedpack(fixture, dest);
77
- for (const entry of readdirSync(dest)) {
78
- expect(SEEDPACK_ENTRIES).toContain(entry as (typeof SEEDPACK_ENTRIES)[number]);
79
- }
80
- expect(existsSync(join(dest, "tools"))).toBe(false);
81
- expect(hashSeedpackContent(dest)).toBe(hashSeedpackContent(fixture));
82
- } finally {
83
- rmSync(dest, { recursive: true, force: true });
84
- }
85
- });
86
- });
87
-
88
- describe("marker", () => {
89
- it("round-trips and returns null when absent", () => {
90
- const dir = mkdtempSync(join(tmpdir(), "seedpack-marker-"));
91
- try {
92
- expect(readMarker(dir)).toBeNull();
93
- writeMarker(dir, "sha256:abc123");
94
- expect(readMarker(dir)).toBe("sha256:abc123");
95
- } finally {
96
- rmSync(dir, { recursive: true, force: true });
97
- }
98
- });
99
- });
100
-
101
- describe("resolveSeedpackContent", () => {
102
- it("uses the repo seedpack tree in dev", () => {
103
- const content = resolveSeedpackContent();
104
- expect(content.source).toBe("repo");
105
- expect(existsSync(join(content.dir, "stigmer.yaml"))).toBe(true);
106
- });
107
-
108
- it("excludes the non-resource canary manifest from the content set", () => {
109
- // The CI canary manifest has no `kind`; if it were staged it would break the
110
- // declarative-apply bootstrap. It lives in canary/ (outside SEEDPACK_ENTRIES).
111
- const files = listContentFiles(resolveSeedpackContent().dir);
112
- expect(files.some((f) => f.startsWith("canary/"))).toBe(false);
113
- expect(files.some((f) => f.endsWith("credential-manifest.yaml"))).toBe(false);
114
- });
115
- });
116
-
117
- describe("acquireSeedpack", () => {
118
- it("refuses a non-release (dev) build", () => {
119
- const err = (() => {
120
- try {
121
- acquireSeedpack({ home: fixture, version: "0.0.0-dev" });
122
- } catch (e) {
123
- return e;
124
- }
125
- })();
126
- expect(classify(err)?.exitCode).toBe(ExitCode.General);
127
- });
128
-
129
- // Presence beats acquirability (the server acquirer's rule; the all-in-one
130
- // image bakes dev-stamped content npm never published).
131
- it("uses already-installed content even for a non-release (dev) build", () => {
132
- const home = mkdtempSync(join(tmpdir(), "seedpack-home-"));
133
- const pkgDir = join(home, ".stigmer", "runtimes", "0.0.0-dev.abc123", "node_modules", "@stigmer", "seedpack");
134
- mkdirSync(pkgDir, { recursive: true });
135
- writeFileSync(join(pkgDir, "stigmer.yaml"), "kind: Organization\n");
136
-
137
- let installs = 0;
138
- const dir = acquireSeedpack({
139
- home,
140
- version: "0.0.0-dev.abc123",
141
- install: () => {
142
- installs += 1;
143
- },
144
- });
145
-
146
- expect(dir).toBe(pkgDir);
147
- expect(installs).toBe(0);
148
- });
149
-
150
- it("installs on demand and is idempotent on the second call", () => {
151
- const home = mkdtempSync(join(tmpdir(), "seedpack-home-"));
152
- let installs = 0;
153
- const install = (installDir: string) => {
154
- installs += 1;
155
- const pkgDir = join(installDir, "node_modules", "@stigmer", "seedpack");
156
- mkdirSync(pkgDir, { recursive: true });
157
- writeFileSync(join(pkgDir, "stigmer.yaml"), "kind: Project\n");
158
- };
159
- try {
160
- const dir1 = acquireSeedpack({ home, version: "1.2.3", install });
161
- const dir2 = acquireSeedpack({ home, version: "1.2.3", install });
162
- expect(dir1).toBe(dir2);
163
- expect(existsSync(join(dir1, "stigmer.yaml"))).toBe(true);
164
- expect(installs).toBe(1);
165
- } finally {
166
- rmSync(home, { recursive: true, force: true });
167
- }
168
- });
169
- });
@@ -1,222 +0,0 @@
1
- // Resolution and on-demand acquisition of the seedpack content.
2
- //
3
- // The seedpack (system agents, skills, MCP servers, workflows under the
4
- // "stigmer" org) is the system content every backend is bootstrapped with.
5
- // A lean `npx @stigmer/cli` must not carry ~300 content files (DD-002), so the
6
- // content ships as `@stigmer/seedpack` and is acquired on demand — the same
7
- // pattern as `@stigmer/runner-slim` and the managed Temporal binary.
8
- //
9
- // Two sources, tried in order (mirrors runtime/runner.ts):
10
- // 1. A repo-tree checkout (dev): `<repo>/seedpack`.
11
- // 2. The published `@stigmer/seedpack@<cli-version>`, installed on demand into
12
- // ~/.stigmer/runtimes/<version>/ and read from
13
- // node_modules/@stigmer/seedpack.
14
- //
15
- // The content-hash and extract logic mirror `@stigmer/seedpack` exactly; the CLI
16
- // reimplements them over a resolved directory so it stays decoupled from the
17
- // package in the lean install (where the package is not a dependency, only an
18
- // on-demand artifact). `seedpack/src/index.ts` is the one definition both follow.
19
-
20
- import { createHash } from "node:crypto";
21
- import {
22
- cpSync,
23
- existsSync,
24
- mkdirSync,
25
- readdirSync,
26
- readFileSync,
27
- statSync,
28
- writeFileSync,
29
- } from "node:fs";
30
- import { homedir } from "node:os";
31
- import { dirname, join, posix, relative, sep } from "node:path";
32
- import { fileURLToPath } from "node:url";
33
- import { CliExitError } from "../../errors/cli-exit-error.js";
34
- import { ExitCode } from "../../errors/exit-codes.js";
35
- import { log } from "../../logger.js";
36
- import { VERSION } from "../../version.js";
37
- import { runtimesDir } from "../paths.js";
38
- import {
39
- ensureRuntimesRoot,
40
- isAcquirableRelease,
41
- npmInstallIntoRuntimes,
42
- type NpmInstall,
43
- } from "../runtime/runtimes-install.js";
44
-
45
- const SEEDPACK_PACKAGE = "@stigmer/seedpack";
46
-
47
- /**
48
- * The canonical seedpack entries, in apply-safe order. Kept in sync with
49
- * `SEEDPACK_ENTRIES` in `@stigmer/seedpack` (`seedpack/src/index.ts`, the one
50
- * definition) — `tools/`, `icons/`, and `canary/` (CI canary metadata, not a
51
- * resource) are deliberately excluded so the content set (and thus the
52
- * idempotency hash) is identical across delivery paths.
53
- */
54
- export const SEEDPACK_ENTRIES = [
55
- "stigmer.yaml",
56
- "organizations",
57
- "skills",
58
- "agents",
59
- "workflows",
60
- "mcp-servers",
61
- ] as const;
62
-
63
- /** Marker file recording the last successfully-applied content hash. */
64
- export const MARKER_FILE = ".seedpack-bootstrapped";
65
-
66
- export interface SeedpackContent {
67
- /** Absolute path to the content root (holds stigmer.yaml). */
68
- readonly dir: string;
69
- /** Where the content came from — surfaced in diagnostics. */
70
- readonly source: "repo" | "package";
71
- }
72
-
73
- export interface ResolveSeedpackOptions {
74
- home?: string;
75
- /** Release version of @stigmer/seedpack to acquire (defaults to the CLI version). */
76
- version?: string;
77
- /** npm install implementation (injectable for tests). */
78
- install?: NpmInstall;
79
- }
80
-
81
- /**
82
- * Resolve the seedpack content directory: a repo-tree checkout in dev, else the
83
- * on-demand-acquired `@stigmer/seedpack` package. Throws actionable guidance when
84
- * acquisition is not possible (non-release build, or a failed/incomplete install).
85
- */
86
- export function resolveSeedpackContent(
87
- opts: ResolveSeedpackOptions = {},
88
- ): SeedpackContent {
89
- const repo = repoSeedpackDir();
90
- if (repo !== null) return { dir: repo, source: "repo" };
91
- return { dir: acquireSeedpack(opts), source: "package" };
92
- }
93
-
94
- /**
95
- * Acquire the published `@stigmer/seedpack@<version>` into
96
- * ~/.stigmer/runtimes/<version>/ (idempotent) and return its package directory.
97
- * The version is pinned to the CLI's own version so system content stays in
98
- * lockstep with the control plane it bootstraps.
99
- *
100
- * Presence beats acquirability (the server acquirer's rule, stated in
101
- * runtime/server.ts): installed content is used whatever its version string;
102
- * only an install we would have to perform is refused for a non-release build.
103
- */
104
- export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
105
- const home = opts.home ?? homedir();
106
- const version = opts.version ?? VERSION;
107
-
108
- const installDir = join(runtimesDir(home), version);
109
- const pkgDir = join(installDir, "node_modules", "@stigmer", "seedpack");
110
- const marker = join(pkgDir, "stigmer.yaml");
111
-
112
- if (!existsSync(marker)) {
113
- if (!isAcquirableRelease(version)) {
114
- throw new CliExitError(
115
- `cannot acquire ${SEEDPACK_PACKAGE} for a non-release build (${version})`,
116
- ExitCode.General,
117
- [
118
- "Run from the repo (the seedpack/ tree is used directly in dev).",
119
- "On-demand acquisition is only available for published releases.",
120
- ],
121
- );
122
- }
123
- log.info(`acquiring ${SEEDPACK_PACKAGE}`, { version, dir: installDir });
124
- // The same per-version root the server and runner packages install into;
125
- // npm install is additive and non-pruning, so the prefix is safely shared.
126
- ensureRuntimesRoot(installDir);
127
- const install = opts.install ?? npmInstallIntoRuntimes;
128
- install(installDir, `${SEEDPACK_PACKAGE}@${version}`);
129
- }
130
-
131
- if (!existsSync(marker)) {
132
- throw new CliExitError(
133
- `${SEEDPACK_PACKAGE} install did not produce ${marker}`,
134
- ExitCode.General,
135
- [`Remove ${installDir} and retry.`],
136
- );
137
- }
138
- return pkgDir;
139
- }
140
-
141
- /**
142
- * Deterministic SHA-256 over the canonical content, identical to
143
- * `@stigmer/seedpack`'s `contentHash` (and the Go algorithm): files walked in
144
- * lexical order, each contributing its forward-slash relative path, a NUL, and
145
- * its bytes. Used as the idempotency marker.
146
- */
147
- export function hashSeedpackContent(dir: string): string {
148
- const hash = createHash("sha256");
149
- for (const rel of listContentFiles(dir)) {
150
- hash.update(rel);
151
- hash.update(NUL);
152
- hash.update(readFileSync(join(dir, rel)));
153
- }
154
- return `sha256:${hash.digest("hex").slice(0, 16)}`;
155
- }
156
-
157
- /**
158
- * Copy the canonical entries from `srcDir` into `destDir`, producing a clean
159
- * Stigmer project the declarative-apply path can process directly (never
160
- * carrying build tooling or assets that happen to sit beside the content).
161
- */
162
- export function extractSeedpack(srcDir: string, destDir: string): void {
163
- mkdirSync(destDir, { recursive: true });
164
- for (const entry of SEEDPACK_ENTRIES) {
165
- const src = join(srcDir, entry);
166
- if (!existsSync(src)) continue;
167
- cpSync(src, join(destDir, entry), { recursive: true });
168
- }
169
- }
170
-
171
- const NUL = Uint8Array.of(0);
172
-
173
- /** Lexically-sorted, forward-slash relative paths of every file under the canonical entries. */
174
- export function listContentFiles(root: string): string[] {
175
- const files: string[] = [];
176
- const walk = (abs: string): void => {
177
- for (const dirent of readdirSync(abs, { withFileTypes: true })) {
178
- const full = join(abs, dirent.name);
179
- if (dirent.isDirectory()) walk(full);
180
- else if (dirent.isFile()) files.push(toPosix(relative(root, full)));
181
- }
182
- };
183
- for (const entry of SEEDPACK_ENTRIES) {
184
- const abs = join(root, entry);
185
- if (!existsSync(abs)) continue;
186
- if (statSync(abs).isDirectory()) walk(abs);
187
- else files.push(entry);
188
- }
189
- return files.sort();
190
- }
191
-
192
- /** The stored marker hash for `markerDir`, or null when never applied. */
193
- export function readMarker(markerDir: string): string | null {
194
- try {
195
- return readFileSync(join(markerDir, MARKER_FILE), "utf8").trim() || null;
196
- } catch {
197
- return null;
198
- }
199
- }
200
-
201
- /** Persist the applied content hash so a future run can skip unchanged content. */
202
- export function writeMarker(markerDir: string, hash: string): void {
203
- mkdirSync(markerDir, { recursive: true });
204
- writeFileSync(join(markerDir, MARKER_FILE), `${hash}\n`);
205
- }
206
-
207
- function toPosix(p: string): string {
208
- return sep === posix.sep ? p : p.split(sep).join(posix.sep);
209
- }
210
-
211
- // Walk up from this module to a repo root containing seedpack/stigmer.yaml.
212
- function repoSeedpackDir(): string | null {
213
- let dir = dirname(fileURLToPath(import.meta.url));
214
- for (let i = 0; i < 10; i += 1) {
215
- const candidate = join(dir, "seedpack");
216
- if (existsSync(join(candidate, "stigmer.yaml"))) return candidate;
217
- const parent = dirname(dir);
218
- if (parent === dir) break;
219
- dir = parent;
220
- }
221
- return null;
222
- }