heyspex 0.1.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 (104) hide show
  1. package/README.md +157 -0
  2. package/bin/hs.mjs +1028 -0
  3. package/bin/postinstall.mjs +88 -0
  4. package/dev-factory/lib/fleet-readiness.mjs +274 -0
  5. package/dev-factory/lib/fleet-state.mjs +250 -0
  6. package/dev-factory/lib/sufficiency-score.mjs +441 -0
  7. package/dist/cli/hs-check.mjs +747 -0
  8. package/dist/cli/hs-claim.mjs +541 -0
  9. package/dist/cli/hs-connect.mjs +3436 -0
  10. package/dist/cli/hs-defer.mjs +8455 -0
  11. package/dist/cli/hs-env.mjs +36845 -0
  12. package/dist/cli/hs-init.mjs +1145 -0
  13. package/dist/cli/hs-install.mjs +1390 -0
  14. package/dist/cli/hs-join.mjs +38553 -0
  15. package/dist/cli/hs-vault-pull.mjs +7782 -0
  16. package/dist/cli/hs-vault.mjs +4166 -0
  17. package/dist/cli/hs-workspace.mjs +2898 -0
  18. package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
  19. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
  20. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
  21. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
  22. package/docs/mcp-client-config.md +200 -0
  23. package/package.json +124 -0
  24. package/scripts/check-mcp-freshness.mjs +379 -0
  25. package/scripts/dev-server-status.mjs +722 -0
  26. package/scripts/gateway-skills-lib.mjs +421 -0
  27. package/scripts/lib/console-url-resolver.mjs +152 -0
  28. package/scripts/lib/dev-mode.mjs +369 -0
  29. package/scripts/lib/hs-link.mjs +154 -0
  30. package/scripts/lib/hs-source-commit.mjs +207 -0
  31. package/scripts/lib/hs-source-log.mjs +366 -0
  32. package/scripts/lib/hs-source-preflight.mjs +402 -0
  33. package/scripts/lib/hs-source-push.mjs +307 -0
  34. package/scripts/lib/hs-source-secret-scan.mjs +263 -0
  35. package/scripts/lib/standalone-doctor.mjs +89 -0
  36. package/scripts/vault-id-lib.mjs +173 -0
  37. package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
  38. package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
  39. package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
  40. package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
  41. package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
  42. package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
  43. package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
  44. package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
  45. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
  46. package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
  47. package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
  48. package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
  49. package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
  50. package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
  51. package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
  52. package/vendor/heyspex-mcp/cli.mjs +15754 -0
  53. package/vendor/heyspex-mcp/config-settings.mjs +419 -0
  54. package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
  55. package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
  56. package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
  57. package/vendor/heyspex-mcp/design-tools.mjs +449 -0
  58. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
  59. package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
  60. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
  61. package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
  62. package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
  63. package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
  64. package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
  65. package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
  66. package/vendor/heyspex-mcp/next-review.mjs +548 -0
  67. package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
  68. package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
  69. package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
  70. package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
  71. package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
  72. package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
  73. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  74. package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
  75. package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
  76. package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
  77. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
  78. package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
  79. package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
  80. package/vendor/heyspex-mcp/read-path.mjs +138 -0
  81. package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
  82. package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
  83. package/vendor/heyspex-mcp/register-spec.mjs +417 -0
  84. package/vendor/heyspex-mcp/research-focus.mjs +476 -0
  85. package/vendor/heyspex-mcp/research-tools.mjs +109 -0
  86. package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
  87. package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
  88. package/vendor/heyspex-mcp/review-focus.mjs +55 -0
  89. package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
  90. package/vendor/heyspex-mcp/run-front.mjs +60 -0
  91. package/vendor/heyspex-mcp/scheduler.mjs +813 -0
  92. package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
  93. package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
  94. package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
  95. package/vendor/heyspex-mcp/split-research.mjs +832 -0
  96. package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
  97. package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
  98. package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
  99. package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
  100. package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
  101. package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
  102. package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
  103. package/vendor/heyspex-mcp/why-now.mjs +64 -0
  104. package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
@@ -0,0 +1,1390 @@
1
+ import { createRequire as __hsCreateRequire } from 'node:module';
2
+ const require = __hsCreateRequire(import.meta.url);
3
+
4
+ // spex-console/bin/hs-install.ts
5
+ import { spawnSync } from "node:child_process";
6
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
7
+ import { join as join4, resolve } from "node:path";
8
+ import process2 from "node:process";
9
+
10
+ // spex-console/src/lib/vault-registry/registry-file.ts
11
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+
15
+ // spex-console/src/lib/vault-registry/types.ts
16
+ var VAULT_REGISTRY_SCHEMA_VERSION = 1;
17
+ function emptyVaultRegistry() {
18
+ return {
19
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
20
+ workspaces: [],
21
+ vaults: [],
22
+ active_workspace_id: null,
23
+ active_vault_id: null
24
+ };
25
+ }
26
+
27
+ // spex-console/src/lib/vault-registry/registry-file.ts
28
+ function vaultRegistryDir(opts = {}) {
29
+ return opts.registryDir ?? join(homedir(), ".heyspex");
30
+ }
31
+ function vaultRegistryFilePath(opts = {}) {
32
+ return join(vaultRegistryDir(opts), "registry.json");
33
+ }
34
+ function isNonEmptyString(v) {
35
+ return typeof v === "string" && v.trim().length > 0;
36
+ }
37
+ function normalizeWorkspace(raw) {
38
+ if (!raw || typeof raw !== "object") return null;
39
+ const o = raw;
40
+ if (!isNonEmptyString(o.id)) return null;
41
+ if (!isNonEmptyString(o.display_name)) return null;
42
+ if (!isNonEmptyString(o.created_at)) return null;
43
+ const record = {
44
+ id: o.id,
45
+ display_name: o.display_name,
46
+ created_at: o.created_at,
47
+ archived: o.archived === true
48
+ };
49
+ if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
50
+ return record;
51
+ }
52
+ function normalizeVault(raw) {
53
+ if (!raw || typeof raw !== "object") return null;
54
+ const o = raw;
55
+ if (!isNonEmptyString(o.id)) return null;
56
+ if (!isNonEmptyString(o.workspace_id)) return null;
57
+ if (!isNonEmptyString(o.display_name)) return null;
58
+ if (!isNonEmptyString(o.repo_root_abs)) return null;
59
+ if (typeof o.schema_version !== "number") return null;
60
+ if (!isNonEmptyString(o.registered_at)) return null;
61
+ const record = {
62
+ id: o.id,
63
+ workspace_id: o.workspace_id,
64
+ display_name: o.display_name,
65
+ repo_root_abs: o.repo_root_abs,
66
+ schema_version: o.schema_version,
67
+ registered_at: o.registered_at,
68
+ archived: o.archived === true
69
+ };
70
+ if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
71
+ return record;
72
+ }
73
+ function parseVaultRegistryFile(json) {
74
+ const fallback = emptyVaultRegistry();
75
+ if (!json || typeof json !== "object") return fallback;
76
+ const o = json;
77
+ const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
78
+ const workspaces = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
79
+ const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
80
+ const vaults = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
81
+ const active_workspace_id = isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null;
82
+ const active_vault_id = isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null;
83
+ return {
84
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
85
+ workspaces,
86
+ vaults,
87
+ active_workspace_id,
88
+ active_vault_id
89
+ };
90
+ }
91
+ function loadVaultRegistry(opts = {}) {
92
+ const filePath = vaultRegistryFilePath(opts);
93
+ if (!existsSync(filePath)) return emptyVaultRegistry();
94
+ try {
95
+ const raw = readFileSync(filePath, "utf8");
96
+ return parseVaultRegistryFile(JSON.parse(raw));
97
+ } catch {
98
+ return emptyVaultRegistry();
99
+ }
100
+ }
101
+ function writeVaultRegistry(registry, opts = {}) {
102
+ const filePath = vaultRegistryFilePath(opts);
103
+ mkdirSync(dirname(filePath), { recursive: true });
104
+ const tmpPath = `${filePath}.tmp`;
105
+ writeFileSync(tmpPath, `${JSON.stringify(registry, null, 2)}
106
+ `, "utf8");
107
+ renameSync(tmpPath, filePath);
108
+ }
109
+ function isValidId(id) {
110
+ if (!isNonEmptyString(id)) return false;
111
+ if (id.includes("/") || id.includes("\\")) return false;
112
+ if (id === "." || id === "..") return false;
113
+ return true;
114
+ }
115
+ function registerWorkspace(record, opts = {}) {
116
+ if (!isValidId(record.id)) {
117
+ throw new Error(`registerWorkspace: invalid workspace id "${record.id}"`);
118
+ }
119
+ const registry = loadVaultRegistry(opts);
120
+ const idx = registry.workspaces.findIndex((w) => w.id === record.id);
121
+ if (idx >= 0) {
122
+ registry.workspaces[idx] = { ...record };
123
+ } else {
124
+ registry.workspaces.push({ ...record });
125
+ }
126
+ writeVaultRegistry(registry, opts);
127
+ return registry;
128
+ }
129
+ function registerVault(record, opts = {}) {
130
+ if (!isValidId(record.id)) {
131
+ throw new Error(`registerVault: invalid vault id "${record.id}"`);
132
+ }
133
+ if (!isValidId(record.workspace_id)) {
134
+ throw new Error(`registerVault: invalid workspace_id "${record.workspace_id}"`);
135
+ }
136
+ if (!isNonEmptyString(record.repo_root_abs)) {
137
+ throw new Error("registerVault: repo_root_abs is required");
138
+ }
139
+ const registry = loadVaultRegistry(opts);
140
+ const isSameVault = (v) => v.id === record.id || v.repo_root_abs === record.repo_root_abs;
141
+ const idx = registry.vaults.findIndex(isSameVault);
142
+ if (idx >= 0) {
143
+ registry.vaults[idx] = { ...record };
144
+ registry.vaults = registry.vaults.filter((v, i) => i === idx || !isSameVault(v));
145
+ } else {
146
+ registry.vaults.push({ ...record });
147
+ }
148
+ writeVaultRegistry(registry, opts);
149
+ return registry;
150
+ }
151
+ function setActiveWorkspaceId(workspaceId, opts = {}) {
152
+ const registry = loadVaultRegistry(opts);
153
+ registry.active_workspace_id = workspaceId;
154
+ writeVaultRegistry(registry, opts);
155
+ return registry;
156
+ }
157
+
158
+ // spex-console/src/lib/cli/control-shared.ts
159
+ import { readFileSync as readFileSync2 } from "node:fs";
160
+ import { homedir as homedir2 } from "node:os";
161
+ import { join as join2 } from "node:path";
162
+ var EXIT = {
163
+ /** Success. */
164
+ OK: 0,
165
+ /** Usage error — unknown subcommand/flag, or a missing required argument. */
166
+ USAGE: 1,
167
+ /** Claim race lost — the spec is already claimed by another agent (409). */
168
+ CONFLICT: 9,
169
+ /** Target not found — release of an unheld claim, etc. (404). */
170
+ NOT_FOUND: 10,
171
+ /** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
172
+ INVALID_ARG: 11,
173
+ /** SpexConsole unreachable (network error reaching the dev server). */
174
+ CONSOLE_UNREACHABLE: 12,
175
+ /** Server failure — a non-OK response that isn't 400/402/404/409. */
176
+ WRITE_FAILED: 13,
177
+ /** Neon control plane absent — the endpoint returned 402 (upgrade required). */
178
+ NEON_REQUIRED: 14,
179
+ /**
180
+ * A cross-account `--account <id>` override was refused — the caller is
181
+ * not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
182
+ * [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
183
+ * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
184
+ * malformed command.
185
+ */
186
+ FORBIDDEN: 15
187
+ };
188
+
189
+ // spex-console/src/lib/cli/customer-agent-contract.ts
190
+ var CUSTOMER_AGENT_CONTRACT = `# Agent Instructions
191
+
192
+ This project is managed with **HeySpex** \u2014 a spec vault under \`specs/\` is the system of record (the markdown files *are* the tracker; there is no separate database). The **HeySpex MCP server** is the source of truth for how to work here: procedures, project memory, and design patterns all load **on demand** through its tools, so this file stays small.
193
+
194
+ ## At session start
195
+
196
+ Recall project memory before doing anything: \`heyspex_recall_memories({ limit: 10 })\`. It surfaces lessons, decisions, and conventions specific to this project.
197
+
198
+ Project memory lives in the vault and loads on demand \u2014 **do not mirror it into your local auto-memory index** (e.g. Claude Code's \`MEMORY.md\`). Keep that index to your own working notes; durable project knowledge goes to the vault via \`heyspex_file_spec\`. This keeps HeySpex's footprint in your memory window to a single recall call.
199
+
200
+ ## Before any significant action \u2014 ask the procedures
201
+
202
+ Before filing work, a destructive change, or closing a session, consult the procedures via MCP (do not guess from examples):
203
+
204
+ - \`heyspex_preflight({ intent })\` \u2014 call first; returns the procedures to consult. Common intents: \`file_epic\`, \`file_task\`, \`destructive_action\`, \`close_session\`, \`claim_spec\`.
205
+ - \`heyspex_get_procedure({ name })\` \u2014 fetch one full procedure when preflight names it.
206
+ - \`heyspex_list_procedures({ query? })\` \u2014 browse what's available.
207
+
208
+ ## Working a task (the handshake)
209
+
210
+ 1. Get the next task: the on-deck prompt comes from HeySpex.
211
+ 2. Before starting, confirm with the user, then call \`heyspex_claim_spec({ spec_id })\` \u2014 this marks it in progress.
212
+ 3. As you complete each acceptance criterion, call \`heyspex_report_progress({ acProgress, currentStage })\` so progress shows live.
213
+ 4. When all criteria are met, call \`heyspex_submit_for_review({ spec_id })\`. **Never hand-edit \`status:\` in a spec's frontmatter** \u2014 the status API owns it.
214
+ 5. The agent does **not** commit or push \u2014 a human reviews and commits. A dirty working tree is the expected hand-off.
215
+
216
+ ## Capture knowledge as you go
217
+
218
+ After research or a notable decision, save it to the vault (\`heyspex_save_research\` for findings; file a memory spec for lessons/decisions) so the next session inherits it. Cross-link specs with \`[[wikilinks]]\`.
219
+
220
+ ## Where things live
221
+
222
+ \`specs/\` holds the work \u2014 \`specs/epics/\`, \`specs/tasks/\`, \`specs/research/\`, \`specs/memory/\`. Never set a spec \`id\` by hand; file via \`heyspex_file_spec\`.
223
+
224
+ ## Everything else
225
+
226
+ If you need to know *how* something works in this project, **ask the HeySpex MCP server** (\`heyspex_get_procedure\` / \`heyspex_list_procedures\`) rather than expecting it here. This file is intentionally a thin contract; the detail lives one fetch away.
227
+ `;
228
+ function renderCustomerAgentContract() {
229
+ return CUSTOMER_AGENT_CONTRACT;
230
+ }
231
+
232
+ // spex-console/src/lib/cli/init.ts
233
+ import { randomUUID } from "node:crypto";
234
+
235
+ // scripts/lib/console-url-resolver.mjs
236
+ import { readFileSync as readFileSync3 } from "node:fs";
237
+ import { homedir as homedir3 } from "node:os";
238
+ import { join as join3 } from "node:path";
239
+ var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
240
+ function trimTrailingSlash(url) {
241
+ return url.replace(/\/+$/, "");
242
+ }
243
+ function readCredentialsConsoleUrl({ homeDir = homedir3() } = {}) {
244
+ try {
245
+ const raw = readFileSync3(join3(homeDir, ".heyspex", "credentials.yaml"), "utf8");
246
+ const match = raw.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
247
+ const value = match?.[1]?.trim();
248
+ return value && value.length > 0 ? trimTrailingSlash(value) : null;
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+ function readDiscoveryFileConsoleUrl(repoRoot) {
254
+ if (!repoRoot) return null;
255
+ for (const name of [
256
+ "console-url",
257
+ "cockpit-url"
258
+ /* legacy-cockpit */
259
+ ]) {
260
+ try {
261
+ const discovery = readFileSync3(join3(repoRoot, ".heyspex", name), "utf8").trim();
262
+ if (discovery.length > 0) return trimTrailingSlash(discovery);
263
+ } catch {
264
+ }
265
+ }
266
+ return null;
267
+ }
268
+ var ConsoleUrlUnresolvedError = class extends Error {
269
+ constructor() {
270
+ super(
271
+ `resolveConsoleUrl: no HEYSPEX_CONSOLE_URL/HEYSPEX_COCKPIT_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the ${DEFAULT_CONSOLE_URL} default in strict mode.`
272
+ );
273
+ this.name = "ConsoleUrlUnresolvedError";
274
+ }
275
+ };
276
+ function resolveConsoleUrl({
277
+ repoRoot,
278
+ env = process.env,
279
+ homeDir = homedir3(),
280
+ strict = false
281
+ } = {}) {
282
+ const envUrl = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
283
+ if (envUrl) return trimTrailingSlash(envUrl);
284
+ const credentialsUrl = readCredentialsConsoleUrl({ homeDir });
285
+ if (credentialsUrl) return credentialsUrl;
286
+ const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot);
287
+ if (discoveryUrl) return discoveryUrl;
288
+ if (strict) throw new ConsoleUrlUnresolvedError();
289
+ return DEFAULT_CONSOLE_URL;
290
+ }
291
+
292
+ // spex-console/src/lib/cli/init.ts
293
+ var HEYSPEX_YAML_SCHEMA_VERSION = 2;
294
+ var DEFAULT_CONSOLE_URL2 = resolveConsoleUrl();
295
+ function mintVaultId() {
296
+ return randomUUID();
297
+ }
298
+ function parseVaultId(content) {
299
+ const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
300
+ if (!m) return null;
301
+ const raw = m[1].trim();
302
+ const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
303
+ return unquoted.length > 0 ? unquoted : null;
304
+ }
305
+ function appendVaultIdLine(content, vaultId) {
306
+ const trimmed = content.replace(/\n+$/, "");
307
+ return `${trimmed}
308
+ vault_id: ${vaultId}
309
+ `;
310
+ }
311
+ function renderHeyspexYaml(args) {
312
+ return [
313
+ `schema_version: ${HEYSPEX_YAML_SCHEMA_VERSION}`,
314
+ `console_url: ${args.consoleUrl}`,
315
+ `vault_root: ${args.vaultRoot}`,
316
+ `vault_id: ${args.vaultId}`,
317
+ ""
318
+ ].join("\n");
319
+ }
320
+ function readExistingContent(deps, markerPath) {
321
+ if (!deps.readFile) return null;
322
+ try {
323
+ return deps.readFile(markerPath);
324
+ } catch {
325
+ return null;
326
+ }
327
+ }
328
+ function runInit(args, deps) {
329
+ const markerPath = deps.joinPath(deps.repoRoot, "heyspex.yaml");
330
+ const exists = deps.fileExists(markerPath);
331
+ const existingContent = exists ? readExistingContent(deps, markerPath) : null;
332
+ const existingVaultId = existingContent ? parseVaultId(existingContent) : null;
333
+ if (exists && !args.force) {
334
+ if (existingVaultId) {
335
+ return {
336
+ code: EXIT.OK,
337
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
338
+ payload: {
339
+ ok: true,
340
+ created: false,
341
+ path: markerPath,
342
+ reason: "already_initialized",
343
+ vault_id: existingVaultId
344
+ },
345
+ isError: false
346
+ };
347
+ }
348
+ if (existingContent === null) {
349
+ return {
350
+ code: EXIT.OK,
351
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
352
+ payload: { ok: true, created: false, path: markerPath, reason: "already_initialized" },
353
+ isError: false
354
+ };
355
+ }
356
+ const mintedId = args.adoptedVaultId ?? mintVaultId();
357
+ const updatedContent = appendVaultIdLine(existingContent, mintedId);
358
+ try {
359
+ deps.writeFile(markerPath, updatedContent);
360
+ } catch (err) {
361
+ const message = err instanceof Error ? err.message : String(err);
362
+ return {
363
+ code: EXIT.WRITE_FAILED,
364
+ message: `failed to write heyspex.yaml: ${message}`,
365
+ payload: { ok: false, error: "write_failed", path: markerPath, message },
366
+ isError: true
367
+ };
368
+ }
369
+ return {
370
+ code: EXIT.OK,
371
+ message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).`,
372
+ payload: {
373
+ ok: true,
374
+ created: false,
375
+ backfilled: true,
376
+ path: markerPath,
377
+ vault_id: mintedId
378
+ },
379
+ isError: false
380
+ };
381
+ }
382
+ const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
383
+ const content = renderHeyspexYaml({ ...args, vaultId });
384
+ try {
385
+ deps.writeFile(markerPath, content);
386
+ } catch (err) {
387
+ const message = err instanceof Error ? err.message : String(err);
388
+ return {
389
+ code: EXIT.WRITE_FAILED,
390
+ message: `failed to write heyspex.yaml: ${message}`,
391
+ payload: { ok: false, error: "write_failed", path: markerPath, message },
392
+ isError: true
393
+ };
394
+ }
395
+ return {
396
+ code: EXIT.OK,
397
+ message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).`,
398
+ payload: {
399
+ ok: true,
400
+ created: true,
401
+ overwrote: exists,
402
+ path: markerPath,
403
+ schema_version: HEYSPEX_YAML_SCHEMA_VERSION,
404
+ console_url: args.consoleUrl,
405
+ vault_root: args.vaultRoot,
406
+ vault_id: vaultId
407
+ },
408
+ isError: false
409
+ };
410
+ }
411
+ var HELP = [
412
+ "Usage:",
413
+ " hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
414
+ " hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
415
+ " [--vault-name <name>] [--no-git] [--json]",
416
+ "",
417
+ "Write the top-level heyspex.yaml vault marker (schema_version, console_url,",
418
+ "vault_root, vault_id) \u2014 the committed file the console / web companion read",
419
+ "to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
420
+ "--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
421
+ "compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
422
+ "present; a fresh one is minted only when absent.",
423
+ "Does not modify .heyspex/config.yaml or its templates (ADR-0009).",
424
+ "",
425
+ "--customer-vault <dir> scaffolds a REAL customer vault at <dir> instead: the",
426
+ "specs/ folder tree, heyspex.yaml, and every row of the template shipping",
427
+ "manifest (spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
428
+ "copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
429
+ "plus a generated CLAUDE.md, the kept-principles index, and (unless --no-git)",
430
+ "one initial `git init` + commit pinning the template version. Refuses a",
431
+ "non-empty <dir> with a plain error. See init.customer-vault.ts.",
432
+ "",
433
+ "Flags:",
434
+ " --console-url <url> console_url value (default: $HEYSPEX_CONSOLE_URL or",
435
+ " https://console.heyspex.local).",
436
+ " --vault-root <path> vault_root value (default: specs/).",
437
+ " --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
438
+ " --principles <all|none|a,b> Which universal principle docs to ship",
439
+ " (--customer-vault only). Omitted + a TTY: interactive",
440
+ " keep/skip prompt per principle. Omitted + no TTY: all.",
441
+ " --vault-name <name> Display name for the scaffolded vault (--customer-vault",
442
+ " only; default: <dir>'s basename).",
443
+ " --no-git Skip git init + the initial commit (--customer-vault only).",
444
+ " --force Overwrite an existing heyspex.yaml.",
445
+ " --json Emit a machine-readable JSON result.",
446
+ " --help, -h Show this help.",
447
+ "",
448
+ "Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
449
+ ].join("\n");
450
+
451
+ // spex-console/src/lib/cli/install-conflicts.ts
452
+ var INSTALL_CONFLICT_REGISTRY = [
453
+ {
454
+ id: "beads-directory",
455
+ label: "Beads (`.beads/`)",
456
+ severity: "blocker",
457
+ why: "Beads and HeySpex both claim to be the agent queue; dual SSOT causes hook + status drift.",
458
+ remediation: "Remove `.beads/` and retire bd hooks (`hs install` documents manual steps; future: `hs install --remediate beads`)."
459
+ },
460
+ {
461
+ id: "beads-agents-md",
462
+ label: "Beads block in AGENTS.md",
463
+ severity: "warning",
464
+ why: "Agents still receive `bd prime` / `bd ready` instructions alongside HeySpex.",
465
+ remediation: "Replace the `<!-- BEGIN BEADS INTEGRATION -->` block with HeySpex agent rules."
466
+ },
467
+ {
468
+ id: "beads-claude-md",
469
+ label: "Beads block in CLAUDE.md",
470
+ severity: "warning",
471
+ why: "Claude Code session hooks and close-out still reference beads.",
472
+ remediation: "Replace beads integration with the HeySpex vault section."
473
+ },
474
+ {
475
+ id: "beads-claude-hooks",
476
+ label: "Claude Code `bd prime` hooks",
477
+ severity: "blocker",
478
+ why: "SessionStart runs `bd prime` every turn \u2014 fights HeySpex MCP recall/on-deck.",
479
+ remediation: "Clear `.claude/settings.json` hooks or remove `bd prime` entries."
480
+ },
481
+ {
482
+ id: "workflow-pm",
483
+ label: "workflow-pm / wfpm (beads PM dashboard)",
484
+ severity: "warning",
485
+ why: "PM scripts assume `bd` as queue truth.",
486
+ remediation: "Remove `workflow-pm` devDependency and `pm:*` npm scripts."
487
+ },
488
+ {
489
+ id: "parallel-issue-tracker",
490
+ label: "Alternate markdown issue tracker under `issues/` or `.github/ISSUE_TEMPLATE` only",
491
+ severity: "warning",
492
+ why: "Agents may file work outside `specs/` if instructions mention GitHub Issues as primary.",
493
+ remediation: "Point agent docs at `specs/` only; keep GitHub Issues optional for humans."
494
+ }
495
+ ];
496
+ function includesAny(haystack, needles) {
497
+ return needles.some((n) => haystack.includes(n));
498
+ }
499
+ function scanInstallConflicts(targetDir, deps) {
500
+ const hits = [];
501
+ const rule = (id) => INSTALL_CONFLICT_REGISTRY.find((r) => r.id === id);
502
+ const beadsDir = deps.joinPath(targetDir, ".beads");
503
+ if (deps.fileExists(beadsDir)) {
504
+ const r = rule("beads-directory");
505
+ hits.push({ ...r, signal: beadsDir });
506
+ }
507
+ const agentsMd = deps.joinPath(targetDir, "AGENTS.md");
508
+ const agentsText = deps.readFile(agentsMd);
509
+ if (agentsText && includesAny(agentsText, [
510
+ "BEGIN BEADS INTEGRATION",
511
+ "Use `bd` for ALL task tracking",
512
+ "Run `bd prime`"
513
+ ])) {
514
+ const r = rule("beads-agents-md");
515
+ hits.push({ ...r, signal: agentsMd });
516
+ }
517
+ const claudeMd = deps.joinPath(targetDir, "CLAUDE.md");
518
+ const claudeText = deps.readFile(claudeMd);
519
+ if (claudeText && includesAny(claudeText, ["BEGIN BEADS INTEGRATION", "bd prime", "bd dolt push"])) {
520
+ const r = rule("beads-claude-md");
521
+ hits.push({ ...r, signal: claudeMd });
522
+ }
523
+ const claudeSettings = deps.joinPath(targetDir, ".claude", "settings.json");
524
+ const settingsText = deps.readFile(claudeSettings);
525
+ if (settingsText && settingsText.includes("bd prime")) {
526
+ const r = rule("beads-claude-hooks");
527
+ hits.push({ ...r, signal: claudeSettings });
528
+ }
529
+ const pkgJson = deps.joinPath(targetDir, "package.json");
530
+ const pkgText = deps.readFile(pkgJson);
531
+ if (pkgText && includesAny(pkgText, ["workflow-pm", '"wfpm"'])) {
532
+ const r = rule("workflow-pm");
533
+ hits.push({ ...r, signal: pkgJson });
534
+ }
535
+ return hits;
536
+ }
537
+ function formatConflictReport(hits) {
538
+ if (hits.length === 0) {
539
+ return "Conflict scan: no known tooling conflicts detected.";
540
+ }
541
+ const lines = [
542
+ `Conflict scan: ${hits.length} known conflict(s) detected \u2014 resolve before relying on HeySpex as the only queue.`,
543
+ ""
544
+ ];
545
+ for (const hit of hits) {
546
+ lines.push(
547
+ `[${hit.severity.toUpperCase()}] ${hit.label} (${hit.id})`,
548
+ ` Signal: ${hit.signal}`,
549
+ ` Why: ${hit.why}`,
550
+ ` Fix: ${hit.remediation}`,
551
+ ""
552
+ );
553
+ }
554
+ lines.push(
555
+ "Registry SSOT: specs/protocols/customer-vault-local-install-protocol.md",
556
+ "Future: `hs install --remediate <id>` for safe auto-fix paths."
557
+ );
558
+ return lines.join("\n");
559
+ }
560
+
561
+ // spex-console/src/lib/cli/install.ts
562
+ var MCP_REL = ["vendor", "heyspex-mcp", "cli.mjs"];
563
+ function parseInstallArgs(argv) {
564
+ let targetDir;
565
+ let platformRoot;
566
+ let cursor = true;
567
+ let claude = true;
568
+ let claudeApply = false;
569
+ let plugin = false;
570
+ let pluginApply = false;
571
+ let pluginScope = "project";
572
+ let statusline = true;
573
+ let commandCode = false;
574
+ let force = false;
575
+ let json = false;
576
+ let dryRun = false;
577
+ let skipConflictCheck = false;
578
+ let registerVaultFlag = false;
579
+ let workspace;
580
+ for (let i = 0; i < argv.length; i++) {
581
+ const arg = argv[i];
582
+ if (arg === "--help" || arg === "-h") return { help: true };
583
+ else if (arg === "--json") json = true;
584
+ else if (arg === "--force") force = true;
585
+ else if (arg === "--dry-run") dryRun = true;
586
+ else if (arg === "--skip-conflict-check") skipConflictCheck = true;
587
+ else if (arg === "--register-vault") registerVaultFlag = true;
588
+ else if (arg === "--workspace" || arg.startsWith("--workspace=")) {
589
+ const v = arg === "--workspace" ? argv[++i] : arg.slice("--workspace=".length);
590
+ if (v === void 0 || v.trim() === "") {
591
+ return { error: { code: EXIT.INVALID_ARG, message: "--workspace requires an id" } };
592
+ }
593
+ workspace = v.trim();
594
+ } else if (arg === "--claude-apply") claudeApply = true;
595
+ else if (arg === "--plugin") plugin = true;
596
+ else if (arg === "--plugin-apply") {
597
+ plugin = true;
598
+ pluginApply = true;
599
+ } else if (arg === "--plugin-scope" || arg.startsWith("--plugin-scope=")) {
600
+ const v = arg === "--plugin-scope" ? argv[++i] : arg.slice("--plugin-scope=".length);
601
+ if (!v || !PLUGIN_SCOPES.includes(v.trim())) {
602
+ return {
603
+ error: {
604
+ code: EXIT.INVALID_ARG,
605
+ message: `--plugin-scope requires one of: ${PLUGIN_SCOPES.join(", ")}`
606
+ }
607
+ };
608
+ }
609
+ pluginScope = v.trim();
610
+ plugin = true;
611
+ } else if (arg === "--no-statusline") statusline = false;
612
+ else if (arg === "--cc" || arg === "--command-code") commandCode = true;
613
+ else if (arg === "--cursor-only") {
614
+ cursor = true;
615
+ claude = false;
616
+ } else if (arg === "--claude-only") {
617
+ cursor = false;
618
+ claude = true;
619
+ } else if (arg === "--no-cursor") cursor = false;
620
+ else if (arg === "--no-claude") claude = false;
621
+ else if (arg === "--target" || arg.startsWith("--target=")) {
622
+ const v = arg === "--target" ? argv[++i] : arg.slice("--target=".length);
623
+ if (v === void 0 || v.trim() === "") {
624
+ return { error: { code: EXIT.INVALID_ARG, message: "--target requires a path" } };
625
+ }
626
+ targetDir = v.trim();
627
+ } else if (arg === "--platform" || arg.startsWith("--platform=")) {
628
+ const v = arg === "--platform" ? argv[++i] : arg.slice("--platform=".length);
629
+ if (v === void 0 || v.trim() === "") {
630
+ return { error: { code: EXIT.INVALID_ARG, message: "--platform requires a path" } };
631
+ }
632
+ platformRoot = v.trim();
633
+ } else if (arg.startsWith("-")) {
634
+ return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
635
+ } else {
636
+ return { error: { code: EXIT.USAGE, message: `unexpected argument: ${arg}` } };
637
+ }
638
+ }
639
+ return {
640
+ args: {
641
+ targetDir: targetDir ?? "",
642
+ platformRoot: platformRoot ?? "",
643
+ cursor,
644
+ claude,
645
+ claudeApply,
646
+ plugin,
647
+ pluginApply,
648
+ pluginScope,
649
+ statusline,
650
+ commandCode,
651
+ force,
652
+ json,
653
+ dryRun,
654
+ skipConflictCheck,
655
+ registerVault: registerVaultFlag,
656
+ workspace
657
+ }
658
+ };
659
+ }
660
+ function mcpCliPath(platformRoot, joinPath) {
661
+ return joinPath(platformRoot, ...MCP_REL);
662
+ }
663
+ function resolveInstallTargetDir(argsTargetDir, env, fallbackCwd) {
664
+ return argsTargetDir && argsTargetDir.trim() || env.HS_VAULT_ROOT?.trim() || env.HS_INVOKED_CWD?.trim() || fallbackCwd;
665
+ }
666
+ function renderCursorMcpJson(cliAbsPath) {
667
+ return {
668
+ mcpServers: {
669
+ heyspex: {
670
+ type: "stdio",
671
+ command: "node",
672
+ args: [cliAbsPath],
673
+ env: {
674
+ HEYSPEX_VAULT_ROOT: "${workspaceFolder}"
675
+ }
676
+ }
677
+ }
678
+ };
679
+ }
680
+ function renderClaudeMcpJson(cliAbsPath) {
681
+ return {
682
+ mcpServers: {
683
+ heyspex: {
684
+ type: "stdio",
685
+ command: "node",
686
+ args: [cliAbsPath],
687
+ env: {
688
+ HEYSPEX_VAULT_ROOT: "${CLAUDE_PROJECT_DIR:-.}"
689
+ }
690
+ }
691
+ }
692
+ };
693
+ }
694
+ function buildClaudeMcpAddCommandPowerShell(targetDir, cliAbsPath) {
695
+ const escapedCli = cliAbsPath.replace(/'/g, "''");
696
+ const escapedTarget = targetDir.replace(/'/g, "''");
697
+ return [
698
+ `Set-Location -LiteralPath '${escapedTarget}'`,
699
+ `claude mcp add --transport stdio --scope project --env HEYSPEX_VAULT_ROOT="$PWD" heyspex -- node '${escapedCli}'`
700
+ ].join("\n");
701
+ }
702
+ var PLUGIN_MARKETPLACE_NAME = "heyspex-marketplace";
703
+ var PLUGIN_INSTALL_SPEC = `heyspex@${PLUGIN_MARKETPLACE_NAME}`;
704
+ var PLUGIN_SCOPES = ["user", "project", "local"];
705
+ function resolvePluginSpec(platformRoot, deps) {
706
+ const fallback = {
707
+ marketplaceName: PLUGIN_MARKETPLACE_NAME,
708
+ pluginName: "heyspex",
709
+ spec: PLUGIN_INSTALL_SPEC
710
+ };
711
+ const raw = deps.readFile(pluginMarketplaceManifestPath(platformRoot, deps.joinPath));
712
+ if (!raw) return fallback;
713
+ try {
714
+ const parsed = JSON.parse(raw);
715
+ const marketplaceName = typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : fallback.marketplaceName;
716
+ const firstNamed = Array.isArray(parsed.plugins) ? parsed.plugins.find((p) => typeof p?.name === "string" && p.name.trim()) : void 0;
717
+ const pluginName = firstNamed ? firstNamed.name.trim() : fallback.pluginName;
718
+ return { marketplaceName, pluginName, spec: `${pluginName}@${marketplaceName}` };
719
+ } catch {
720
+ return fallback;
721
+ }
722
+ }
723
+ function pluginMarketplaceManifestPath(platformRoot, joinPath) {
724
+ return joinPath(platformRoot, ".claude-plugin", "marketplace.json");
725
+ }
726
+ function buildClaudePluginInstallCommandPowerShell(targetDir, platformRoot, spec = PLUGIN_INSTALL_SPEC, scope = "project") {
727
+ const escapedTarget = targetDir.replace(/'/g, "''");
728
+ const escapedPlatform = platformRoot.replace(/'/g, "''");
729
+ return [
730
+ `Set-Location -LiteralPath '${escapedTarget}'`,
731
+ `claude plugin marketplace add '${escapedPlatform}'`,
732
+ `claude plugin install ${spec} --scope ${scope}`
733
+ ].join("\n");
734
+ }
735
+ var STATUSLINE_VENDOR_FILES = [
736
+ ["scripts", "install-statusline.mjs"],
737
+ ["scripts", "claude-statusline", "statusline-command.mjs"],
738
+ ["scripts", "claude-statusline", "statusline-command.ps1"],
739
+ ["scripts", "claude-statusline", "fonts", "HeySpexIcons.ttf"],
740
+ ["scripts", "claude-statusline", "fonts", "README.md"]
741
+ ];
742
+ function renderStatusLineEntry() {
743
+ return {
744
+ type: "command",
745
+ command: 'node "$CLAUDE_PROJECT_DIR/scripts/claude-statusline/statusline-command.mjs"',
746
+ padding: 2,
747
+ refreshInterval: 5
748
+ };
749
+ }
750
+ function buildCommandCodeMcpAddCommand(cliAbsPath) {
751
+ const escapedCli = cliAbsPath.replace(/'/g, "''");
752
+ return [
753
+ `cmdc mcp add --transport stdio heyspex -- node '${escapedCli}'`,
754
+ `cmdc mcp env heyspex HEYSPEX_VAULT_ROOT "$PWD"`
755
+ ].join("\n");
756
+ }
757
+ function shouldWrite(exists, force) {
758
+ return !exists || force;
759
+ }
760
+ function pathTail(p) {
761
+ return p.split(/[\\/]/).filter(Boolean).pop() ?? p;
762
+ }
763
+ function slugFromPath(p) {
764
+ return pathTail(p).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
765
+ }
766
+ function registerInstalledVault(args, ctx) {
767
+ const { targetDir, readFile, joinPath, registryOpts, now, basename } = ctx;
768
+ let vaultId;
769
+ let schemaVersion = HEYSPEX_YAML_SCHEMA_VERSION;
770
+ const yamlContent = readFile(joinPath(targetDir, "heyspex.yaml"));
771
+ if (yamlContent) {
772
+ const existing = parseVaultId(yamlContent);
773
+ if (existing) vaultId = existing;
774
+ const versionMatch = yamlContent.match(/^schema_version:\s*(\d+)\s*$/m);
775
+ if (versionMatch) schemaVersion = Number(versionMatch[1]);
776
+ }
777
+ if (!vaultId) vaultId = slugFromPath(targetDir);
778
+ const workspaceId = args.workspace ?? "default";
779
+ const displayName = basename(targetDir);
780
+ const before = loadVaultRegistry(registryOpts);
781
+ const workspaceExisted = before.workspaces.some((w) => w.id === workspaceId);
782
+ const result = {
783
+ registered: !args.dryRun,
784
+ vault_id: vaultId,
785
+ workspace_id: workspaceId,
786
+ workspace_created: !workspaceExisted && args.workspace !== void 0,
787
+ workspace_activated: false,
788
+ repo_root_abs: targetDir,
789
+ schema_version: schemaVersion
790
+ };
791
+ if (args.dryRun) return result;
792
+ if (args.workspace !== void 0) {
793
+ if (!workspaceExisted) {
794
+ registerWorkspace(
795
+ { id: workspaceId, display_name: workspaceId, created_at: now },
796
+ registryOpts
797
+ );
798
+ }
799
+ setActiveWorkspaceId(workspaceId, registryOpts);
800
+ result.workspace_activated = true;
801
+ }
802
+ registerVault(
803
+ {
804
+ id: vaultId,
805
+ workspace_id: workspaceId,
806
+ display_name: displayName,
807
+ repo_root_abs: targetDir,
808
+ schema_version: schemaVersion,
809
+ registered_at: now
810
+ },
811
+ registryOpts
812
+ );
813
+ return result;
814
+ }
815
+ function resolveInstallContract(platformRoot, deps) {
816
+ try {
817
+ const raw = deps.readFile(deps.joinPath(platformRoot, "spex-console", "settings.json"));
818
+ if (!raw) return true;
819
+ const parsed = JSON.parse(raw);
820
+ const v = parsed?.agentMemory?.installContract;
821
+ return typeof v === "boolean" ? v : true;
822
+ } catch {
823
+ return true;
824
+ }
825
+ }
826
+ function runInstall(args, deps) {
827
+ const targetDir = deps.resolvedTarget;
828
+ const platformRoot = deps.resolvedPlatform;
829
+ const cliPath = mcpCliPath(platformRoot, deps.joinPath);
830
+ if (!deps.fileExists(cliPath)) {
831
+ return {
832
+ code: EXIT.INVALID_ARG,
833
+ message: `HeySpex platform MCP not found at ${cliPath}
834
+ Pass --platform <path-to-workflow-protocol> or run hs install from the platform repo.`,
835
+ payload: { ok: false, error: "platform_mcp_missing", path: cliPath },
836
+ isError: true
837
+ };
838
+ }
839
+ const steps = [];
840
+ const nextSteps = [];
841
+ const conflicts = args.skipConflictCheck ? [] : scanInstallConflicts(targetDir, {
842
+ fileExists: deps.fileExists,
843
+ readFile: deps.readFile,
844
+ joinPath: deps.joinPath
845
+ });
846
+ const specsDir = deps.joinPath(targetDir, "specs");
847
+ const cursorPath = deps.joinPath(targetDir, ".cursor", "mcp.json");
848
+ const claudePath = deps.joinPath(targetDir, ".mcp.json");
849
+ const gitkeepPath = deps.joinPath(specsDir, ".gitkeep");
850
+ const planWrite = (kind, filePath, exists, detail) => {
851
+ if (!shouldWrite(exists, args.force)) {
852
+ steps.push({
853
+ kind,
854
+ path: filePath,
855
+ action: "skipped",
856
+ detail: "already exists (use --force)"
857
+ });
858
+ return false;
859
+ }
860
+ steps.push({
861
+ kind,
862
+ path: filePath,
863
+ action: args.dryRun ? "would_create" : "created",
864
+ detail
865
+ });
866
+ return !args.dryRun;
867
+ };
868
+ if (!deps.fileExists(specsDir)) {
869
+ if (args.dryRun) {
870
+ steps.push({ kind: "specs_dir", path: specsDir, action: "would_create" });
871
+ } else {
872
+ deps.mkdir(specsDir);
873
+ steps.push({ kind: "specs_dir", path: specsDir, action: "created" });
874
+ }
875
+ }
876
+ if (planWrite("specs_gitkeep", gitkeepPath, deps.fileExists(gitkeepPath))) {
877
+ deps.writeFile(gitkeepPath, "");
878
+ }
879
+ const markerPath = deps.joinPath(targetDir, "heyspex.yaml");
880
+ const markerExists = deps.fileExists(markerPath);
881
+ if (!markerExists || args.force) {
882
+ if (args.dryRun) {
883
+ steps.push({
884
+ kind: "heyspex_yaml",
885
+ path: markerPath,
886
+ action: "would_create",
887
+ detail: markerExists ? "overwrite with --force" : void 0
888
+ });
889
+ } else {
890
+ const initResult = runInit(
891
+ {
892
+ consoleUrl: process.env.HEYSPEX_CONSOLE_URL?.trim() || "https://console.heyspex.local",
893
+ vaultRoot: "specs/",
894
+ force: args.force,
895
+ json: false
896
+ },
897
+ { ...deps, repoRoot: targetDir }
898
+ );
899
+ if (initResult.isError) {
900
+ return initResult;
901
+ }
902
+ steps.push({
903
+ kind: "heyspex_yaml",
904
+ path: markerPath,
905
+ action: "created",
906
+ detail: markerExists ? "overwrote" : "initialized"
907
+ });
908
+ }
909
+ } else {
910
+ steps.push({
911
+ kind: "heyspex_yaml",
912
+ path: markerPath,
913
+ action: "skipped",
914
+ detail: "already initialized"
915
+ });
916
+ }
917
+ const cursorJson = `${JSON.stringify(renderCursorMcpJson(cliPath), null, 2)}
918
+ `;
919
+ if (args.cursor) {
920
+ if (planWrite("cursor_mcp", cursorPath, deps.fileExists(cursorPath))) {
921
+ deps.mkdir(deps.joinPath(targetDir, ".cursor"));
922
+ deps.writeFile(cursorPath, cursorJson);
923
+ }
924
+ nextSteps.push(
925
+ "Cursor: toggle heyspex MCP off/on in Settings \u2192 Tools & MCP (or restart Cursor)."
926
+ );
927
+ }
928
+ const claudeJson = `${JSON.stringify(renderClaudeMcpJson(cliPath), null, 2)}
929
+ `;
930
+ const claudePs = buildClaudeMcpAddCommandPowerShell(targetDir, cliPath);
931
+ if (args.claude) {
932
+ if (planWrite("claude_mcp", claudePath, deps.fileExists(claudePath))) {
933
+ deps.writeFile(claudePath, claudeJson);
934
+ }
935
+ if (args.claudeApply) {
936
+ steps.push({
937
+ kind: "claude_mcp_apply",
938
+ path: "claude mcp add",
939
+ action: args.dryRun ? "would_create" : "created",
940
+ detail: "see claude_powershell if apply fails"
941
+ });
942
+ } else {
943
+ nextSteps.push(
944
+ "Claude Code: run the printed PowerShell block, or re-run with --claude-apply if `claude` is on PATH."
945
+ );
946
+ }
947
+ }
948
+ const pluginSpec = resolvePluginSpec(platformRoot, deps);
949
+ const pluginScope = args.pluginScope ?? "project";
950
+ const pluginPs = buildClaudePluginInstallCommandPowerShell(
951
+ targetDir,
952
+ platformRoot,
953
+ pluginSpec.spec,
954
+ pluginScope
955
+ );
956
+ const marketplaceManifest = pluginMarketplaceManifestPath(platformRoot, deps.joinPath);
957
+ const pluginAvailable = deps.fileExists(marketplaceManifest);
958
+ if (args.plugin) {
959
+ if (!pluginAvailable) {
960
+ steps.push({
961
+ kind: "claude_plugin",
962
+ path: marketplaceManifest,
963
+ action: "skipped",
964
+ detail: "platform checkout has no .claude-plugin/marketplace.json"
965
+ });
966
+ } else if (args.pluginApply) {
967
+ steps.push({
968
+ kind: "claude_plugin_apply",
969
+ path: "claude plugin install",
970
+ action: args.dryRun ? "would_create" : "created",
971
+ detail: "see claude_plugin_powershell if apply fails"
972
+ });
973
+ } else {
974
+ steps.push({
975
+ kind: "claude_plugin",
976
+ path: pluginSpec.spec,
977
+ action: "skipped",
978
+ detail: "printed below \u2014 run in PowerShell, or re-run with --plugin-apply"
979
+ });
980
+ nextSteps.push(
981
+ "Claude Code plugin: run the printed PowerShell block, or re-run with --plugin-apply if `claude` is on PATH."
982
+ );
983
+ }
984
+ if (pluginAvailable && args.claude) {
985
+ nextSteps.push(
986
+ "Plugin AND project .mcp.json both register the heyspex MCP server \u2014 once the plugin validates, re-run with --no-claude for plugin-only wiring (HS-QTBK prototype)."
987
+ );
988
+ }
989
+ }
990
+ if ((args.statusline ?? true) && args.claude) {
991
+ const statuslineShipsInPlatform = deps.fileExists(
992
+ deps.joinPath(platformRoot, "scripts", "claude-statusline", "statusline-command.mjs")
993
+ );
994
+ if (!statuslineShipsInPlatform) {
995
+ steps.push({
996
+ kind: "claude_statusline",
997
+ path: deps.joinPath(targetDir, ".claude", "settings.json"),
998
+ action: "skipped",
999
+ detail: "not shipped in this package"
1000
+ });
1001
+ } else {
1002
+ if (targetDir !== platformRoot) {
1003
+ if (!deps.copyFile) {
1004
+ steps.push({
1005
+ kind: "statusline_vendor",
1006
+ path: deps.joinPath(targetDir, "scripts", "claude-statusline"),
1007
+ action: "skipped",
1008
+ detail: "no copyFile capability in this caller"
1009
+ });
1010
+ } else {
1011
+ let copied = 0;
1012
+ let missing = 0;
1013
+ for (const rel of STATUSLINE_VENDOR_FILES) {
1014
+ const src = deps.joinPath(platformRoot, ...rel);
1015
+ const dest = deps.joinPath(targetDir, ...rel);
1016
+ if (!deps.fileExists(src)) {
1017
+ missing += 1;
1018
+ continue;
1019
+ }
1020
+ if (deps.fileExists(dest) && !args.force) continue;
1021
+ copied += 1;
1022
+ if (!args.dryRun) {
1023
+ deps.mkdir(deps.joinPath(targetDir, ...rel.slice(0, -1)));
1024
+ deps.copyFile(src, dest);
1025
+ }
1026
+ }
1027
+ steps.push({
1028
+ kind: "statusline_vendor",
1029
+ path: deps.joinPath(targetDir, "scripts", "claude-statusline"),
1030
+ action: copied === 0 ? "skipped" : args.dryRun ? "would_create" : "created",
1031
+ detail: copied === 0 ? "already vendored" : `${copied} file${copied === 1 ? "" : "s"} (renderer + font + installer)` + (missing > 0 ? `; ${missing} missing from platform` : "")
1032
+ });
1033
+ }
1034
+ }
1035
+ const settingsPath = deps.joinPath(targetDir, ".claude", "settings.json");
1036
+ const entry = renderStatusLineEntry();
1037
+ const raw = deps.readFile(settingsPath);
1038
+ let settings = raw === null ? {} : null;
1039
+ if (raw !== null) {
1040
+ try {
1041
+ settings = JSON.parse(raw);
1042
+ } catch {
1043
+ settings = null;
1044
+ }
1045
+ }
1046
+ const existing = settings?.statusLine;
1047
+ const ours = typeof existing?.command === "string" && existing.command.includes("statusline-command.");
1048
+ if (settings === null) {
1049
+ steps.push({
1050
+ kind: "claude_statusline",
1051
+ path: settingsPath,
1052
+ action: "skipped",
1053
+ detail: "settings.json unparseable \u2014 wire the statusline manually"
1054
+ });
1055
+ } else if (existing && !ours) {
1056
+ steps.push({
1057
+ kind: "claude_statusline",
1058
+ path: settingsPath,
1059
+ action: "skipped",
1060
+ detail: "a foreign statusLine entry exists \u2014 left alone"
1061
+ });
1062
+ } else if (existing && JSON.stringify(existing) === JSON.stringify(entry)) {
1063
+ steps.push({
1064
+ kind: "claude_statusline",
1065
+ path: settingsPath,
1066
+ action: "skipped",
1067
+ detail: "already installed"
1068
+ });
1069
+ } else {
1070
+ steps.push({
1071
+ kind: "claude_statusline",
1072
+ path: settingsPath,
1073
+ action: args.dryRun ? "would_create" : "created",
1074
+ detail: existing ? "repaired to canonical entry" : "branded statusline (vendored renderer)"
1075
+ });
1076
+ if (!args.dryRun) {
1077
+ settings.statusLine = entry;
1078
+ deps.mkdir(deps.joinPath(targetDir, ".claude"));
1079
+ deps.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
1080
+ `);
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+ const commandCodePs = buildCommandCodeMcpAddCommand(cliPath);
1086
+ if (args.commandCode) {
1087
+ steps.push({
1088
+ kind: "commandcode_mcp",
1089
+ path: "cmdc mcp add",
1090
+ action: args.dryRun ? "would_create" : "created",
1091
+ detail: "printed below \u2014 run in PowerShell to register heyspex MCP server"
1092
+ });
1093
+ nextSteps.push(
1094
+ "Command Code: run the printed PowerShell block to register the heyspex MCP server."
1095
+ );
1096
+ }
1097
+ const installContract = resolveInstallContract(platformRoot, deps);
1098
+ if (installContract) {
1099
+ const agentContract = renderCustomerAgentContract();
1100
+ const claudeMdPath = deps.joinPath(targetDir, "CLAUDE.md");
1101
+ const agentsMdPath = deps.joinPath(targetDir, "AGENTS.md");
1102
+ if (args.claude) {
1103
+ if (planWrite("claude_md", claudeMdPath, deps.fileExists(claudeMdPath), "thin agent contract")) {
1104
+ deps.writeFile(claudeMdPath, agentContract);
1105
+ }
1106
+ }
1107
+ if (planWrite(
1108
+ "agents_md",
1109
+ agentsMdPath,
1110
+ deps.fileExists(agentsMdPath),
1111
+ "cross-tool agent contract"
1112
+ )) {
1113
+ deps.writeFile(agentsMdPath, agentContract);
1114
+ }
1115
+ } else {
1116
+ steps.push({
1117
+ kind: "agent_contract",
1118
+ path: "CLAUDE.md / AGENTS.md",
1119
+ action: "skipped",
1120
+ detail: "disabled in Settings \u2192 Config \u2192 Memory"
1121
+ });
1122
+ }
1123
+ let registration;
1124
+ if (args.registerVault) {
1125
+ registration = registerInstalledVault(args, {
1126
+ targetDir,
1127
+ readFile: deps.readFile,
1128
+ joinPath: deps.joinPath,
1129
+ registryOpts: deps.registryOpts,
1130
+ now: deps.now ? deps.now() : (/* @__PURE__ */ new Date()).toISOString(),
1131
+ basename: deps.basename ?? pathTail
1132
+ });
1133
+ steps.push({
1134
+ kind: "vault_register",
1135
+ path: registration.repo_root_abs,
1136
+ action: args.dryRun ? "would_create" : "created",
1137
+ detail: `vault "${registration.vault_id}" \u2192 workspace "${registration.workspace_id}"` + (registration.workspace_activated ? " (active)" : "")
1138
+ });
1139
+ }
1140
+ if (conflicts.length > 0) {
1141
+ nextSteps.unshift(
1142
+ "Resolve conflict scan hits above before treating HeySpex as the only agent queue."
1143
+ );
1144
+ }
1145
+ nextSteps.push(`Open the project in Cursor, Claude Code, or Command Code: ${targetDir}`);
1146
+ nextSteps.push("Start capturing work with /hs-capture or file specs via heyspex MCP.");
1147
+ const payload = {
1148
+ ok: true,
1149
+ target_dir: targetDir,
1150
+ platform_root: platformRoot,
1151
+ mcp_cli: cliPath,
1152
+ steps,
1153
+ conflicts,
1154
+ claude_powershell: args.claude ? claudePs : void 0,
1155
+ claude_plugin_powershell: args.plugin && pluginAvailable ? pluginPs : void 0,
1156
+ claude_plugin_spec: args.plugin && pluginAvailable ? pluginSpec.spec : void 0,
1157
+ claude_plugin_scope: args.plugin && pluginAvailable ? pluginScope : void 0,
1158
+ command_code_powershell: args.commandCode ? commandCodePs : void 0,
1159
+ next_steps: nextSteps,
1160
+ dry_run: args.dryRun,
1161
+ registration
1162
+ };
1163
+ const lines = [
1164
+ args.dryRun ? "hs install (dry run) \u2014 no files written." : "hs install \u2014 vault bootstrap complete.",
1165
+ "",
1166
+ `Target: ${targetDir}`,
1167
+ `Platform: ${platformRoot}`,
1168
+ `MCP: ${cliPath}`,
1169
+ ""
1170
+ ];
1171
+ if (!args.skipConflictCheck) {
1172
+ lines.push(formatConflictReport(conflicts), "");
1173
+ }
1174
+ lines.push(
1175
+ "Steps:",
1176
+ ...steps.map(
1177
+ (s) => ` \u2022 ${s.kind}: ${s.action} \u2192 ${s.path}${s.detail ? ` (${s.detail})` : ""}`
1178
+ )
1179
+ );
1180
+ if (args.claude && claudePs) {
1181
+ lines.push("", "Claude Code (PowerShell):", claudePs);
1182
+ }
1183
+ if (args.plugin && pluginAvailable && pluginPs) {
1184
+ lines.push("", "Claude Code plugin (PowerShell):", pluginPs);
1185
+ }
1186
+ if (args.commandCode && commandCodePs) {
1187
+ lines.push("", "Command Code (PowerShell):", commandCodePs);
1188
+ }
1189
+ if (nextSteps.length) {
1190
+ lines.push("", "Next:", ...nextSteps.map((s) => ` \u2022 ${s}`));
1191
+ }
1192
+ return {
1193
+ code: EXIT.OK,
1194
+ message: lines.join("\n"),
1195
+ payload,
1196
+ isError: false
1197
+ };
1198
+ }
1199
+ var INSTALL_HELP = [
1200
+ "Usage:",
1201
+ " hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
1202
+ " [--no-cursor] [--no-claude] [--claude-apply]",
1203
+ " [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
1204
+ " [--no-statusline]",
1205
+ " [--cc | --command-code] [--register-vault [--workspace <id>]]",
1206
+ "",
1207
+ "Bootstrap a project vault for HeySpex agent tooling:",
1208
+ " \u2022 specs/ + heyspex.yaml (via hs init)",
1209
+ " \u2022 .cursor/mcp.json \u2014 Cursor MCP (gitignored, machine-local path to platform MCP)",
1210
+ " \u2022 .mcp.json \u2014 Claude Code project MCP (absolute platform path)",
1211
+ " \u2022 CLAUDE.md / AGENTS.md \u2014 thin agent contract pointing at the HeySpex MCP server",
1212
+ "",
1213
+ "Conflict scan: every install checks the target repo against the known-conflict",
1214
+ "registry (beads, workflow-pm, bd hooks, \u2026). See:",
1215
+ " specs/protocols/customer-vault-local-install-protocol.md",
1216
+ "",
1217
+ "Run from the HeySpex platform repo, or pass --platform to its checkout.",
1218
+ "Target defaults to the current directory.",
1219
+ "",
1220
+ "Flags:",
1221
+ " --target <dir> Project repo to bootstrap (default: cwd).",
1222
+ " --platform <dir> HeySpex platform checkout (default: HS_INSTALL_PLATFORM or dispatcher root).",
1223
+ " --claude-apply Run `claude mcp add --scope project` after writing files.",
1224
+ " --plugin Print the Claude Code plugin registration block (self-hosted",
1225
+ " marketplace at the platform root \u2014 HS-QTBK prototype).",
1226
+ " --plugin-apply Run `claude plugin marketplace add` + `claude plugin install`",
1227
+ " after writing files. Marketplace + plugin names are read from",
1228
+ ` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
1229
+ " --plugin-scope <s> Plugin install scope: user, project (default), or local.",
1230
+ " Implies --plugin.",
1231
+ " --no-statusline Skip the branded statusline (statusLine entry in the target's",
1232
+ " .claude/settings.json + machine-level glyph install).",
1233
+ " --cc, --command-code Print `cmdc mcp add` PowerShell one-liner for Command Code.",
1234
+ " --register-vault Register this repo's vault into ~/.heyspex/registry.json during install.",
1235
+ " --workspace <id> With --register-vault: ensure this workspace exists, make it active,",
1236
+ ' and associate the vault with it (default workspace: "default").',
1237
+ " --no-cursor Skip .cursor/mcp.json.",
1238
+ " --no-claude Skip .mcp.json and Claude instructions.",
1239
+ " --force Overwrite existing MCP manifests and heyspex.yaml.",
1240
+ " --dry-run Print the plan without writing.",
1241
+ " --skip-conflict-check Skip the known-conflict registry scan.",
1242
+ " --json Machine-readable result.",
1243
+ " --help, -h Show this help.",
1244
+ "",
1245
+ "Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
1246
+ ].join("\n");
1247
+
1248
+ // spex-console/bin/hs-install.ts
1249
+ function main() {
1250
+ const platformDefault = process2.env.HS_INSTALL_PLATFORM?.trim() || process2.env.HEYSPEX_PLATFORM_ROOT?.trim() || process2.env.HS_PKG_ROOT?.trim() || "";
1251
+ const parsed = parseInstallArgs(process2.argv.slice(3));
1252
+ if (parsed.help) {
1253
+ process2.stdout.write(`${INSTALL_HELP}
1254
+ `);
1255
+ process2.exit(EXIT.OK);
1256
+ }
1257
+ if (parsed.error) {
1258
+ process2.stderr.write(`hs install: ${parsed.error.message}
1259
+ `);
1260
+ process2.exit(parsed.error.code);
1261
+ }
1262
+ const args = parsed.args;
1263
+ const targetDir = resolve(resolveInstallTargetDir(args.targetDir, process2.env, process2.cwd()));
1264
+ const platformRoot = resolve(args.platformRoot || platformDefault || process2.cwd());
1265
+ const deps = {
1266
+ repoRoot: targetDir,
1267
+ resolvedTarget: targetDir,
1268
+ resolvedPlatform: platformRoot,
1269
+ fileExists: (p) => existsSync2(p),
1270
+ readFile: (p) => {
1271
+ try {
1272
+ return readFileSync4(p, "utf8");
1273
+ } catch {
1274
+ return null;
1275
+ }
1276
+ },
1277
+ writeFile: (p, c) => writeFileSync2(p, c, "utf8"),
1278
+ mkdir: (p) => mkdirSync2(p, { recursive: true }),
1279
+ copyFile: (src, dest) => copyFileSync(src, dest),
1280
+ joinPath: (...s) => join4(...s)
1281
+ };
1282
+ const result = runInstall(args, deps);
1283
+ const sink = result.isError ? process2.stderr : process2.stdout;
1284
+ sink.write(`${args.json ? JSON.stringify(result.payload, null, 2) : result.message}
1285
+ `);
1286
+ if (!result.isError && args.claudeApply && !args.dryRun && args.claude) {
1287
+ const cliPath = mcpCliPath(platformRoot, join4);
1288
+ const claude = spawnSync(
1289
+ "claude",
1290
+ [
1291
+ "mcp",
1292
+ "add",
1293
+ "--transport",
1294
+ "stdio",
1295
+ "--scope",
1296
+ "project",
1297
+ "--env",
1298
+ `HEYSPEX_VAULT_ROOT=${targetDir}`,
1299
+ "heyspex",
1300
+ "--",
1301
+ process2.execPath,
1302
+ cliPath
1303
+ ],
1304
+ { cwd: targetDir, encoding: "utf8", shell: process2.platform === "win32" }
1305
+ );
1306
+ if (claude.status !== 0) {
1307
+ process2.stderr.write(
1308
+ `hs install: claude mcp add failed (exit ${claude.status ?? "?"}).
1309
+ ${claude.stderr || claude.stdout || ""}
1310
+ Run manually:
1311
+ ${buildClaudeMcpAddCommandPowerShell(targetDir, cliPath)}
1312
+ `
1313
+ );
1314
+ } else {
1315
+ process2.stdout.write("Claude Code: registered heyspex MCP (--scope project).\n");
1316
+ }
1317
+ }
1318
+ if (!result.isError && args.pluginApply && !args.dryRun && existsSync2(pluginMarketplaceManifestPath(platformRoot, join4))) {
1319
+ const shell = process2.platform === "win32";
1320
+ const spec = resolvePluginSpec(platformRoot, {
1321
+ readFile: (p) => {
1322
+ try {
1323
+ return readFileSync4(p, "utf8");
1324
+ } catch {
1325
+ return null;
1326
+ }
1327
+ },
1328
+ joinPath: join4
1329
+ });
1330
+ const scope = args.pluginScope ?? "project";
1331
+ const manualBlock = buildClaudePluginInstallCommandPowerShell(
1332
+ targetDir,
1333
+ platformRoot,
1334
+ spec.spec,
1335
+ scope
1336
+ );
1337
+ const claudeRun = (cliArgs) => spawnSync("claude", cliArgs, { cwd: targetDir, encoding: "utf8", shell });
1338
+ let marketplace = claudeRun(["plugin", "marketplace", "add", platformRoot]);
1339
+ if (marketplace.status !== 0 && /already/i.test(marketplace.stderr + marketplace.stdout)) {
1340
+ marketplace = claudeRun(["plugin", "marketplace", "update", spec.marketplaceName]);
1341
+ }
1342
+ if (marketplace.status !== 0) {
1343
+ process2.stderr.write(
1344
+ `hs install: claude plugin marketplace add failed (exit ${marketplace.status ?? "?"}).
1345
+ ${marketplace.stderr || marketplace.stdout || ""}
1346
+ Run manually:
1347
+ ${manualBlock}
1348
+ `
1349
+ );
1350
+ } else {
1351
+ const install = claudeRun(["plugin", "install", spec.spec, "--scope", scope]);
1352
+ if (install.status !== 0 && !/already/i.test(install.stderr + install.stdout)) {
1353
+ process2.stderr.write(
1354
+ `hs install: claude plugin install failed (exit ${install.status ?? "?"}).
1355
+ ${install.stderr || install.stdout || ""}
1356
+ Run manually:
1357
+ ${manualBlock}
1358
+ `
1359
+ );
1360
+ } else {
1361
+ process2.stdout.write(
1362
+ `Claude Code: installed plugin ${spec.spec} (--scope ${scope}). Restart the Claude Code session to load it.
1363
+ `
1364
+ );
1365
+ }
1366
+ }
1367
+ }
1368
+ if (!result.isError && !args.dryRun && args.claude && (args.statusline ?? true)) {
1369
+ const glyphScript = [
1370
+ join4(targetDir, "scripts", "install-statusline.mjs"),
1371
+ join4(platformRoot, "scripts", "install-statusline.mjs")
1372
+ ].find((p) => existsSync2(p));
1373
+ const glyph = glyphScript ? spawnSync(process2.execPath, [glyphScript, "--glyph-only"], {
1374
+ cwd: targetDir,
1375
+ encoding: "utf8"
1376
+ }) : { status: 1, stdout: "", stderr: "install-statusline.mjs not found" };
1377
+ if (glyph.status === 0) {
1378
+ const out = (glyph.stdout || "").trim();
1379
+ if (out) process2.stdout.write(`${out}
1380
+ `);
1381
+ } else {
1382
+ process2.stderr.write(
1383
+ `hs install: glyph install skipped (${(glyph.stderr || glyph.stdout || "unknown error").trim()}) \u2014 see scripts/claude-statusline/fonts/README.md.
1384
+ `
1385
+ );
1386
+ }
1387
+ }
1388
+ process2.exit(result.code);
1389
+ }
1390
+ main();