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.
- package/README.md +157 -0
- package/bin/hs.mjs +1028 -0
- package/bin/postinstall.mjs +88 -0
- package/dev-factory/lib/fleet-readiness.mjs +274 -0
- package/dev-factory/lib/fleet-state.mjs +250 -0
- package/dev-factory/lib/sufficiency-score.mjs +441 -0
- package/dist/cli/hs-check.mjs +747 -0
- package/dist/cli/hs-claim.mjs +541 -0
- package/dist/cli/hs-connect.mjs +3436 -0
- package/dist/cli/hs-defer.mjs +8455 -0
- package/dist/cli/hs-env.mjs +36845 -0
- package/dist/cli/hs-init.mjs +1145 -0
- package/dist/cli/hs-install.mjs +1390 -0
- package/dist/cli/hs-join.mjs +38553 -0
- package/dist/cli/hs-vault-pull.mjs +7782 -0
- package/dist/cli/hs-vault.mjs +4166 -0
- package/dist/cli/hs-workspace.mjs +2898 -0
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
- package/docs/mcp-client-config.md +200 -0
- package/package.json +124 -0
- package/scripts/check-mcp-freshness.mjs +379 -0
- package/scripts/dev-server-status.mjs +722 -0
- package/scripts/gateway-skills-lib.mjs +421 -0
- package/scripts/lib/console-url-resolver.mjs +152 -0
- package/scripts/lib/dev-mode.mjs +369 -0
- package/scripts/lib/hs-link.mjs +154 -0
- package/scripts/lib/hs-source-commit.mjs +207 -0
- package/scripts/lib/hs-source-log.mjs +366 -0
- package/scripts/lib/hs-source-preflight.mjs +402 -0
- package/scripts/lib/hs-source-push.mjs +307 -0
- package/scripts/lib/hs-source-secret-scan.mjs +263 -0
- package/scripts/lib/standalone-doctor.mjs +89 -0
- package/scripts/vault-id-lib.mjs +173 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
- package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
- package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
- package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
- package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
- package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
- package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
- package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
- package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
- package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
- package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
- package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
- package/vendor/heyspex-mcp/cli.mjs +15754 -0
- package/vendor/heyspex-mcp/config-settings.mjs +419 -0
- package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
- package/vendor/heyspex-mcp/design-tools.mjs +449 -0
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
- package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
- package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
- package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
- package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
- package/vendor/heyspex-mcp/next-review.mjs +548 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
- package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
- package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
- package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
- package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
- package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
- package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
- package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
- package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
- package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
- package/vendor/heyspex-mcp/read-path.mjs +138 -0
- package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
- package/vendor/heyspex-mcp/register-spec.mjs +417 -0
- package/vendor/heyspex-mcp/research-focus.mjs +476 -0
- package/vendor/heyspex-mcp/research-tools.mjs +109 -0
- package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
- package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
- package/vendor/heyspex-mcp/review-focus.mjs +55 -0
- package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
- package/vendor/heyspex-mcp/run-front.mjs +60 -0
- package/vendor/heyspex-mcp/scheduler.mjs +813 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
- package/vendor/heyspex-mcp/split-research.mjs +832 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
- package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
- package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
- package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
- package/vendor/heyspex-mcp/why-now.mjs +64 -0
- package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
|
+
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// spex-console/bin/hs-init.ts
|
|
5
|
+
import {
|
|
6
|
+
copyFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync as readFileSync4,
|
|
11
|
+
writeFileSync
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, join as join3, resolve } from "node:path";
|
|
14
|
+
import process2 from "node:process";
|
|
15
|
+
import { createInterface } from "node:readline/promises";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
// spex-console/src/lib/cli/customer-claude-md.ts
|
|
19
|
+
function renderCustomerClaudeMd({
|
|
20
|
+
vaultName,
|
|
21
|
+
templateVersion
|
|
22
|
+
}) {
|
|
23
|
+
return `# Agent Instructions \u2014 ${vaultName}
|
|
24
|
+
|
|
25
|
+
This project's work is tracked in a git-tracked markdown vault under \`specs/\` \u2014 the files *are* the system of record (no separate database or issue tracker). The **HeySpex MCP server** is the source of truth for how to work here: procedures, project memory, and design patterns load **on demand** through its tools, so this file stays small and rarely needs editing.
|
|
26
|
+
|
|
27
|
+
## Session start
|
|
28
|
+
|
|
29
|
+
Recall project memory first: \`heyspex_recall_memories({ limit: 10 })\` \u2014 surfaces lessons, decisions, and conventions specific to this vault before you touch anything.
|
|
30
|
+
|
|
31
|
+
## Procedure discovery \u2014 before any high-stakes action
|
|
32
|
+
|
|
33
|
+
Before filing work, a destructive action, closing a session, or claiming a spec, consult the procedures via MCP (never guess from an example file):
|
|
34
|
+
|
|
35
|
+
- \`heyspex_preflight({ intent })\` \u2014 call first; returns which procedures to consult. Intents include \`file_task\`, \`file_epic\`, \`destructive_action\`, \`close_session\`, \`claim_spec\`.
|
|
36
|
+
- \`heyspex_get_procedure({ name })\` \u2014 fetch one full procedure body.
|
|
37
|
+
- \`heyspex_list_procedures({ query? })\` \u2014 browse by tag or keyword.
|
|
38
|
+
|
|
39
|
+
## The claim handshake
|
|
40
|
+
|
|
41
|
+
1. \`/hs-next\` returns the on-deck task as a \`prompt_markdown\`.
|
|
42
|
+
2. Confirm with the user, then call \`heyspex_claim_spec({ spec_id })\` \u2014 never start work unclaimed.
|
|
43
|
+
3. After each acceptance criterion, call \`heyspex_report_progress({ acProgress, currentStage })\` so progress shows live.
|
|
44
|
+
4. When every criterion is met, call \`heyspex_submit_for_review({ spec_id })\`. **Never hand-edit \`status:\` in frontmatter** \u2014 the status API flips it and rolls parents up.
|
|
45
|
+
5. Release the claim (\`heyspex_release_spec\`) on hand-off. The agent does not commit or push \u2014 a dirty working tree is the expected hand-off shape; a human reviews and commits.
|
|
46
|
+
|
|
47
|
+
## Filing new work
|
|
48
|
+
|
|
49
|
+
The vault under \`specs/\` is the only persistent queue. File new work via \`heyspex_file_spec\` (or \`heyspex_push_plan\` for a batch of epics/tasks) \u2014 **never set an \`id\` by hand**, the server mints it. Cross-link specs with \`[[wikilinks]]\`; every link must resolve to a real file under \`specs/\`.
|
|
50
|
+
|
|
51
|
+
## Capture knowledge as you go
|
|
52
|
+
|
|
53
|
+
- Use \`/hs-capture\` to fast-offload an idea before it slips; it promotes to research when ready.
|
|
54
|
+
- After any research or web search, save it via \`heyspex_save_research\` (Executive Summary + at least one cited source).
|
|
55
|
+
- Notable decisions and lessons become memory specs under \`specs/memory/{lessons,decisions,references}\` \u2014 link them to the specs they bear on.
|
|
56
|
+
|
|
57
|
+
## Conventions
|
|
58
|
+
|
|
59
|
+
- New entities are always filed via \`heyspex_file_spec\` \u2014 never hand-write frontmatter \`id:\`.
|
|
60
|
+
- The vault is the only durable queue \u2014 track real work as spec files, not ad-hoc TODOs.
|
|
61
|
+
- Agents do not commit or push. A dirty tree at session end is normal hand-off, not an error.
|
|
62
|
+
|
|
63
|
+
## This file vs. your vault
|
|
64
|
+
|
|
65
|
+
This CLAUDE.md was generated for **${vaultName}** from HeySpex template version **${templateVersion}** \u2014 it is yours to edit. Unlike HeySpex's own platform contract (which carries HeySpex's internal build tooling), everything above applies to *your* vault and *your* agents. Edit it freely as your project's conventions evolve; a later template update will not overwrite your changes.
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function customerClaudeMdHook() {
|
|
69
|
+
return (ctx) => {
|
|
70
|
+
const content = renderCustomerClaudeMd({
|
|
71
|
+
vaultName: ctx.vaultName,
|
|
72
|
+
templateVersion: ctx.templateVersion
|
|
73
|
+
});
|
|
74
|
+
const destPath = ctx.deps.joinPath(ctx.targetDir, "CLAUDE.md");
|
|
75
|
+
ctx.deps.writeFile(destPath, content);
|
|
76
|
+
ctx.recordWrite(destPath);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// spex-console/src/lib/cli/init.ts
|
|
81
|
+
import { randomUUID } from "node:crypto";
|
|
82
|
+
|
|
83
|
+
// scripts/lib/console-url-resolver.mjs
|
|
84
|
+
import { readFileSync } from "node:fs";
|
|
85
|
+
import { homedir } from "node:os";
|
|
86
|
+
import { join } from "node:path";
|
|
87
|
+
var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
|
|
88
|
+
function trimTrailingSlash(url) {
|
|
89
|
+
return url.replace(/\/+$/, "");
|
|
90
|
+
}
|
|
91
|
+
function readCredentialsConsoleUrl({ homeDir = homedir() } = {}) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = readFileSync(join(homeDir, ".heyspex", "credentials.yaml"), "utf8");
|
|
94
|
+
const match = raw.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
95
|
+
const value = match?.[1]?.trim();
|
|
96
|
+
return value && value.length > 0 ? trimTrailingSlash(value) : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function readDiscoveryFileConsoleUrl(repoRoot) {
|
|
102
|
+
if (!repoRoot) return null;
|
|
103
|
+
for (const name of [
|
|
104
|
+
"console-url",
|
|
105
|
+
"cockpit-url"
|
|
106
|
+
/* legacy-cockpit */
|
|
107
|
+
]) {
|
|
108
|
+
try {
|
|
109
|
+
const discovery = readFileSync(join(repoRoot, ".heyspex", name), "utf8").trim();
|
|
110
|
+
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
var ConsoleUrlUnresolvedError = class extends Error {
|
|
117
|
+
constructor() {
|
|
118
|
+
super(
|
|
119
|
+
`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.`
|
|
120
|
+
);
|
|
121
|
+
this.name = "ConsoleUrlUnresolvedError";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function resolveConsoleUrl({
|
|
125
|
+
repoRoot,
|
|
126
|
+
env = process.env,
|
|
127
|
+
homeDir = homedir(),
|
|
128
|
+
strict = false
|
|
129
|
+
} = {}) {
|
|
130
|
+
const envUrl = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
|
|
131
|
+
if (envUrl) return trimTrailingSlash(envUrl);
|
|
132
|
+
const credentialsUrl = readCredentialsConsoleUrl({ homeDir });
|
|
133
|
+
if (credentialsUrl) return credentialsUrl;
|
|
134
|
+
const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot);
|
|
135
|
+
if (discoveryUrl) return discoveryUrl;
|
|
136
|
+
if (strict) throw new ConsoleUrlUnresolvedError();
|
|
137
|
+
return DEFAULT_CONSOLE_URL;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
141
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
142
|
+
import { homedir as homedir2 } from "node:os";
|
|
143
|
+
import { join as join2 } from "node:path";
|
|
144
|
+
var EXIT = {
|
|
145
|
+
/** Success. */
|
|
146
|
+
OK: 0,
|
|
147
|
+
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
148
|
+
USAGE: 1,
|
|
149
|
+
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
150
|
+
CONFLICT: 9,
|
|
151
|
+
/** Target not found — release of an unheld claim, etc. (404). */
|
|
152
|
+
NOT_FOUND: 10,
|
|
153
|
+
/** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
|
|
154
|
+
INVALID_ARG: 11,
|
|
155
|
+
/** SpexConsole unreachable (network error reaching the dev server). */
|
|
156
|
+
CONSOLE_UNREACHABLE: 12,
|
|
157
|
+
/** Server failure — a non-OK response that isn't 400/402/404/409. */
|
|
158
|
+
WRITE_FAILED: 13,
|
|
159
|
+
/** Neon control plane absent — the endpoint returned 402 (upgrade required). */
|
|
160
|
+
NEON_REQUIRED: 14,
|
|
161
|
+
/**
|
|
162
|
+
* A cross-account `--account <id>` override was refused — the caller is
|
|
163
|
+
* not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
|
|
164
|
+
* [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
|
|
165
|
+
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
166
|
+
* malformed command.
|
|
167
|
+
*/
|
|
168
|
+
FORBIDDEN: 15
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// spex-console/src/lib/cli/init.ts
|
|
172
|
+
var HEYSPEX_YAML_SCHEMA_VERSION = 2;
|
|
173
|
+
var DEFAULT_CONSOLE_URL2 = resolveConsoleUrl();
|
|
174
|
+
function mintVaultId() {
|
|
175
|
+
return randomUUID();
|
|
176
|
+
}
|
|
177
|
+
function parseVaultId(content) {
|
|
178
|
+
const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
|
|
179
|
+
if (!m) return null;
|
|
180
|
+
const raw = m[1].trim();
|
|
181
|
+
const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
182
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
183
|
+
}
|
|
184
|
+
function appendVaultIdLine(content, vaultId) {
|
|
185
|
+
const trimmed = content.replace(/\n+$/, "");
|
|
186
|
+
return `${trimmed}
|
|
187
|
+
vault_id: ${vaultId}
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
function parseInitArgs(argv) {
|
|
191
|
+
let consoleUrl = (process.env.HEYSPEX_CONSOLE_URL ?? process.env.HEYSPEX_COCKPIT_URL)?.trim() || // legacy-cockpit env
|
|
192
|
+
DEFAULT_CONSOLE_URL2;
|
|
193
|
+
let vaultRoot = "specs/";
|
|
194
|
+
let force = false;
|
|
195
|
+
let json = false;
|
|
196
|
+
let customerVaultDir;
|
|
197
|
+
let principles;
|
|
198
|
+
let vaultName;
|
|
199
|
+
let noGit = false;
|
|
200
|
+
for (let i = 0; i < argv.length; i++) {
|
|
201
|
+
const arg = argv[i];
|
|
202
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
203
|
+
else if (arg === "--json") json = true;
|
|
204
|
+
else if (arg === "--force") force = true;
|
|
205
|
+
else if (arg === "--no-git") noGit = true;
|
|
206
|
+
else if (arg === "--principles" || arg.startsWith("--principles=")) {
|
|
207
|
+
const v = arg === "--principles" ? argv[++i] : arg.slice("--principles=".length);
|
|
208
|
+
if (v === void 0 || v.trim() === "") {
|
|
209
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--principles requires a value" } };
|
|
210
|
+
}
|
|
211
|
+
principles = v.trim();
|
|
212
|
+
} else if (arg === "--vault-name" || arg.startsWith("--vault-name=")) {
|
|
213
|
+
const v = arg === "--vault-name" ? argv[++i] : arg.slice("--vault-name=".length);
|
|
214
|
+
if (v === void 0 || v.trim() === "") {
|
|
215
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--vault-name requires a value" } };
|
|
216
|
+
}
|
|
217
|
+
vaultName = v.trim();
|
|
218
|
+
} else if (arg === "--console-url" || arg.startsWith("--console-url=") || arg === "--cockpit-url" || // legacy-cockpit alias
|
|
219
|
+
arg.startsWith("--cockpit-url=")) {
|
|
220
|
+
const eq = arg.indexOf("=");
|
|
221
|
+
const v = eq === -1 ? argv[++i] : arg.slice(eq + 1);
|
|
222
|
+
if (v === void 0 || v.trim() === "") {
|
|
223
|
+
return {
|
|
224
|
+
error: {
|
|
225
|
+
code: EXIT.INVALID_ARG,
|
|
226
|
+
message: `${eq === -1 ? arg : arg.slice(0, eq)} requires a value`
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
consoleUrl = v.trim();
|
|
231
|
+
} else if (arg === "--vault-root" || arg.startsWith("--vault-root=")) {
|
|
232
|
+
const v = arg === "--vault-root" ? argv[++i] : arg.slice("--vault-root=".length);
|
|
233
|
+
if (v === void 0 || v.trim() === "") {
|
|
234
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--vault-root requires a value" } };
|
|
235
|
+
}
|
|
236
|
+
vaultRoot = v.trim();
|
|
237
|
+
} else if (arg === "--customer-vault" || arg.startsWith("--customer-vault=")) {
|
|
238
|
+
const v = arg === "--customer-vault" ? argv[++i] : arg.slice("--customer-vault=".length);
|
|
239
|
+
if (v === void 0 || v.trim() === "") {
|
|
240
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--customer-vault requires a value" } };
|
|
241
|
+
}
|
|
242
|
+
customerVaultDir = v.trim();
|
|
243
|
+
} else if (arg.startsWith("-")) {
|
|
244
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
245
|
+
} else {
|
|
246
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${arg}` } };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName, noGit }
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function renderHeyspexYaml(args) {
|
|
254
|
+
return [
|
|
255
|
+
`schema_version: ${HEYSPEX_YAML_SCHEMA_VERSION}`,
|
|
256
|
+
`console_url: ${args.consoleUrl}`,
|
|
257
|
+
`vault_root: ${args.vaultRoot}`,
|
|
258
|
+
`vault_id: ${args.vaultId}`,
|
|
259
|
+
""
|
|
260
|
+
].join("\n");
|
|
261
|
+
}
|
|
262
|
+
function readExistingContent(deps, markerPath) {
|
|
263
|
+
if (!deps.readFile) return null;
|
|
264
|
+
try {
|
|
265
|
+
return deps.readFile(markerPath);
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function runInit(args, deps) {
|
|
271
|
+
const markerPath = deps.joinPath(deps.repoRoot, "heyspex.yaml");
|
|
272
|
+
const exists = deps.fileExists(markerPath);
|
|
273
|
+
const existingContent = exists ? readExistingContent(deps, markerPath) : null;
|
|
274
|
+
const existingVaultId = existingContent ? parseVaultId(existingContent) : null;
|
|
275
|
+
if (exists && !args.force) {
|
|
276
|
+
if (existingVaultId) {
|
|
277
|
+
return {
|
|
278
|
+
code: EXIT.OK,
|
|
279
|
+
message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
|
|
280
|
+
payload: {
|
|
281
|
+
ok: true,
|
|
282
|
+
created: false,
|
|
283
|
+
path: markerPath,
|
|
284
|
+
reason: "already_initialized",
|
|
285
|
+
vault_id: existingVaultId
|
|
286
|
+
},
|
|
287
|
+
isError: false
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (existingContent === null) {
|
|
291
|
+
return {
|
|
292
|
+
code: EXIT.OK,
|
|
293
|
+
message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
|
|
294
|
+
payload: { ok: true, created: false, path: markerPath, reason: "already_initialized" },
|
|
295
|
+
isError: false
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const mintedId = args.adoptedVaultId ?? mintVaultId();
|
|
299
|
+
const updatedContent = appendVaultIdLine(existingContent, mintedId);
|
|
300
|
+
try {
|
|
301
|
+
deps.writeFile(markerPath, updatedContent);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
+
return {
|
|
305
|
+
code: EXIT.WRITE_FAILED,
|
|
306
|
+
message: `failed to write heyspex.yaml: ${message}`,
|
|
307
|
+
payload: { ok: false, error: "write_failed", path: markerPath, message },
|
|
308
|
+
isError: true
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
code: EXIT.OK,
|
|
313
|
+
message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).`,
|
|
314
|
+
payload: {
|
|
315
|
+
ok: true,
|
|
316
|
+
created: false,
|
|
317
|
+
backfilled: true,
|
|
318
|
+
path: markerPath,
|
|
319
|
+
vault_id: mintedId
|
|
320
|
+
},
|
|
321
|
+
isError: false
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
|
|
325
|
+
const content = renderHeyspexYaml({ ...args, vaultId });
|
|
326
|
+
try {
|
|
327
|
+
deps.writeFile(markerPath, content);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
330
|
+
return {
|
|
331
|
+
code: EXIT.WRITE_FAILED,
|
|
332
|
+
message: `failed to write heyspex.yaml: ${message}`,
|
|
333
|
+
payload: { ok: false, error: "write_failed", path: markerPath, message },
|
|
334
|
+
isError: true
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
code: EXIT.OK,
|
|
339
|
+
message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).`,
|
|
340
|
+
payload: {
|
|
341
|
+
ok: true,
|
|
342
|
+
created: true,
|
|
343
|
+
overwrote: exists,
|
|
344
|
+
path: markerPath,
|
|
345
|
+
schema_version: HEYSPEX_YAML_SCHEMA_VERSION,
|
|
346
|
+
console_url: args.consoleUrl,
|
|
347
|
+
vault_root: args.vaultRoot,
|
|
348
|
+
vault_id: vaultId
|
|
349
|
+
},
|
|
350
|
+
isError: false
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
var HELP = [
|
|
354
|
+
"Usage:",
|
|
355
|
+
" hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
|
|
356
|
+
" hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
|
|
357
|
+
" [--vault-name <name>] [--no-git] [--json]",
|
|
358
|
+
"",
|
|
359
|
+
"Write the top-level heyspex.yaml vault marker (schema_version, console_url,",
|
|
360
|
+
"vault_root, vault_id) \u2014 the committed file the console / web companion read",
|
|
361
|
+
"to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
|
|
362
|
+
"--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
|
|
363
|
+
"compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
|
|
364
|
+
"present; a fresh one is minted only when absent.",
|
|
365
|
+
"Does not modify .heyspex/config.yaml or its templates (ADR-0009).",
|
|
366
|
+
"",
|
|
367
|
+
"--customer-vault <dir> scaffolds a REAL customer vault at <dir> instead: the",
|
|
368
|
+
"specs/ folder tree, heyspex.yaml, and every row of the template shipping",
|
|
369
|
+
"manifest (spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
|
|
370
|
+
"copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
|
|
371
|
+
"plus a generated CLAUDE.md, the kept-principles index, and (unless --no-git)",
|
|
372
|
+
"one initial `git init` + commit pinning the template version. Refuses a",
|
|
373
|
+
"non-empty <dir> with a plain error. See init.customer-vault.ts.",
|
|
374
|
+
"",
|
|
375
|
+
"Flags:",
|
|
376
|
+
" --console-url <url> console_url value (default: $HEYSPEX_CONSOLE_URL or",
|
|
377
|
+
" https://console.heyspex.local).",
|
|
378
|
+
" --vault-root <path> vault_root value (default: specs/).",
|
|
379
|
+
" --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
|
|
380
|
+
" --principles <all|none|a,b> Which universal principle docs to ship",
|
|
381
|
+
" (--customer-vault only). Omitted + a TTY: interactive",
|
|
382
|
+
" keep/skip prompt per principle. Omitted + no TTY: all.",
|
|
383
|
+
" --vault-name <name> Display name for the scaffolded vault (--customer-vault",
|
|
384
|
+
" only; default: <dir>'s basename).",
|
|
385
|
+
" --no-git Skip git init + the initial commit (--customer-vault only).",
|
|
386
|
+
" --force Overwrite an existing heyspex.yaml.",
|
|
387
|
+
" --json Emit a machine-readable JSON result.",
|
|
388
|
+
" --help, -h Show this help.",
|
|
389
|
+
"",
|
|
390
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
|
|
391
|
+
].join("\n");
|
|
392
|
+
|
|
393
|
+
// spex-console/src/lib/cli/init.customer-vault.ts
|
|
394
|
+
import { basename } from "node:path";
|
|
395
|
+
|
|
396
|
+
// spex-console/src/lib/cli/template-manifest.sources.mjs
|
|
397
|
+
var UNIVERSAL_TEMPLATE_ROWS = [
|
|
398
|
+
{ platformSource: "specs/entity-model.md", dest: "specs/entity-model.md" },
|
|
399
|
+
{
|
|
400
|
+
platformSource: "specs/protocols/canonical-frontmatter-templates.md",
|
|
401
|
+
dest: "specs/protocols/canonical-frontmatter-templates.md"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
platformSource: "specs/protocols/spec-hierarchy.md",
|
|
405
|
+
dest: "specs/protocols/spec-hierarchy.md"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
platformSource: "specs/protocols/memory-protocol.md",
|
|
409
|
+
dest: "specs/protocols/memory-protocol.md"
|
|
410
|
+
}
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
// dist/cli/._template-manifest-sources.cli-shim.mjs
|
|
414
|
+
var CURATED_TEMPLATE_ROOT = "dist/cli-templates/customer-vault";
|
|
415
|
+
|
|
416
|
+
// spex-console/src/lib/cli/template-manifest.ts
|
|
417
|
+
var TEMPLATE_VERSION = "1.0.0";
|
|
418
|
+
var TODO_NORTH_STAR = `---
|
|
419
|
+
id: HS-L1
|
|
420
|
+
name: TODO \u2014 your product name
|
|
421
|
+
type: l1
|
|
422
|
+
status: open
|
|
423
|
+
priority: 0
|
|
424
|
+
owner: TODO \u2014 your name/handle
|
|
425
|
+
created: TODO \u2014 YYYY-MM-DD
|
|
426
|
+
updated: TODO \u2014 YYYY-MM-DD
|
|
427
|
+
tags:
|
|
428
|
+
- north-star
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
# TODO \u2014 your product name \u2014 North Star
|
|
432
|
+
|
|
433
|
+
<!-- TODO: replace this whole file. This is a skeleton, not HeySpex's own vision doc \u2014 write
|
|
434
|
+
YOUR product's constitutional vision here: the problem, the customer, and the small number
|
|
435
|
+
of non-negotiable principles that every future architecture / entity-model / roadmap
|
|
436
|
+
decision must trace back to. -->
|
|
437
|
+
|
|
438
|
+
## Problem
|
|
439
|
+
|
|
440
|
+
TODO: what problem does this product solve, and for whom?
|
|
441
|
+
|
|
442
|
+
## Vision
|
|
443
|
+
|
|
444
|
+
TODO: what does "done" look like at the highest altitude?
|
|
445
|
+
|
|
446
|
+
## Principles
|
|
447
|
+
|
|
448
|
+
TODO: the small number of non-negotiable rules every feature decision must satisfy. See
|
|
449
|
+
\`specs/principles-index.md\` for the index pattern this vault ships with.
|
|
450
|
+
`;
|
|
451
|
+
var TODO_PRINCIPLES_INDEX = `---
|
|
452
|
+
id: HS-L2-PRINCIPLES-INDEX
|
|
453
|
+
name: Design Principles Index
|
|
454
|
+
type: l2
|
|
455
|
+
status: open
|
|
456
|
+
priority: 0
|
|
457
|
+
owner: TODO \u2014 your name/handle
|
|
458
|
+
created: TODO \u2014 YYYY-MM-DD
|
|
459
|
+
updated: TODO \u2014 YYYY-MM-DD
|
|
460
|
+
tags:
|
|
461
|
+
- l2
|
|
462
|
+
- principles
|
|
463
|
+
- index
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
# Design Principles Index
|
|
467
|
+
|
|
468
|
+
<!-- TODO: this is a skeleton index, not content. Populate the table below with YOUR product's
|
|
469
|
+
own numbered design principles (P1, P2, ...) \u2014 each principle should be its own
|
|
470
|
+
\`memory-decision\` spec under specs/memory/decisions/, linked from this table. A few
|
|
471
|
+
HeySpex-originated architecture commitments, generalized, are pre-seeded there as a
|
|
472
|
+
starting point \u2014 keep, edit, or delete each one. -->
|
|
473
|
+
|
|
474
|
+
One-page reference for the design principles that govern every feature decision in this vault.
|
|
475
|
+
|
|
476
|
+
## The principles at a glance
|
|
477
|
+
|
|
478
|
+
| # | One-line rule | When to cite | Refusal trigger | Memory |
|
|
479
|
+
| --- | --- | --- | --- | --- |
|
|
480
|
+
| P1 | TODO | TODO | TODO | TODO: link a real \`[[memory-decision]]\` spec here |
|
|
481
|
+
|
|
482
|
+
## How to use this index
|
|
483
|
+
|
|
484
|
+
TODO: describe how code review / feature design / roadmap decisions consult this index.
|
|
485
|
+
`;
|
|
486
|
+
var TODO_STARTER_TASK = `---
|
|
487
|
+
id: HS-START-0001
|
|
488
|
+
name: Your first task
|
|
489
|
+
summary: TODO \u2014 one-line summary of this task.
|
|
490
|
+
type: task
|
|
491
|
+
status: open
|
|
492
|
+
priority: 2
|
|
493
|
+
kind: feature
|
|
494
|
+
effort: 1
|
|
495
|
+
owner: TODO \u2014 your name/handle
|
|
496
|
+
created: TODO \u2014 YYYY-MM-DD
|
|
497
|
+
updated: TODO \u2014 YYYY-MM-DD
|
|
498
|
+
parent: '[[HS-START-0000-EPIC-your-first-epic]]'
|
|
499
|
+
tags:
|
|
500
|
+
- near-term
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
# Your first task
|
|
504
|
+
|
|
505
|
+
<!-- TODO: replace this with a real first task for your vault. This file exists so there is
|
|
506
|
+
something claimable the moment your vault is initialized \u2014 see
|
|
507
|
+
specs/protocols/canonical-frontmatter-templates.md for the full task template. Its
|
|
508
|
+
parent: field points at the starter epic (HS-START-0000) shipped alongside it, so the
|
|
509
|
+
vault lints clean (a task with no epic ancestor is an orphan) \u2014 see that epic's own file
|
|
510
|
+
for what it in turn required (a starter milestone + roadmap). -->
|
|
511
|
+
|
|
512
|
+
## Intended outcome
|
|
513
|
+
|
|
514
|
+
TODO: what observable condition does this task produce when done?
|
|
515
|
+
|
|
516
|
+
## Scope
|
|
517
|
+
|
|
518
|
+
TODO: what's in scope, what's explicitly out.
|
|
519
|
+
|
|
520
|
+
## Acceptance criteria
|
|
521
|
+
|
|
522
|
+
1. TODO: first checkable condition.
|
|
523
|
+
2. TODO: second checkable condition.
|
|
524
|
+
`;
|
|
525
|
+
var TODO_STARTER_EPIC = `---
|
|
526
|
+
id: HS-START-0000
|
|
527
|
+
name: Your first epic
|
|
528
|
+
summary: TODO \u2014 one-line summary of this epic.
|
|
529
|
+
type: epic
|
|
530
|
+
status: open
|
|
531
|
+
kind: feature
|
|
532
|
+
effort: 3
|
|
533
|
+
owner: TODO \u2014 your name/handle
|
|
534
|
+
created: TODO \u2014 YYYY-MM-DD
|
|
535
|
+
updated: TODO \u2014 YYYY-MM-DD
|
|
536
|
+
milestone: '[[HS-START-0002-MIL-your-first-milestone]]'
|
|
537
|
+
children:
|
|
538
|
+
- '[[HS-START-0001-TASK-your-first-task]]'
|
|
539
|
+
tags: []
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
# Your first epic
|
|
543
|
+
|
|
544
|
+
<!-- TODO: replace this with a real first epic for your vault. This file (plus its starter
|
|
545
|
+
milestone and roadmap, shipped alongside it) exists so the starter task has a home the
|
|
546
|
+
moment your vault is initialized \u2014 see specs/protocols/canonical-frontmatter-templates.md
|
|
547
|
+
for the full epic template. -->
|
|
548
|
+
|
|
549
|
+
## Intended outcome
|
|
550
|
+
|
|
551
|
+
**User-visible outcome of this epic (required):** TODO \u2014 what changes for the user when this
|
|
552
|
+
epic ships?
|
|
553
|
+
|
|
554
|
+
**Moves (\u2191 link, required):** TODO \u2014 which milestone outcome does it move, and how?
|
|
555
|
+
|
|
556
|
+
## Why this epic exists
|
|
557
|
+
|
|
558
|
+
**Problem right now (required):** TODO \u2014 one sentence: what specific problem does this solve,
|
|
559
|
+
and why is now the right time?
|
|
560
|
+
|
|
561
|
+
## Tasks
|
|
562
|
+
|
|
563
|
+
- [[HS-START-0001-TASK-your-first-task]]
|
|
564
|
+
|
|
565
|
+
## Acceptance gate \u2014 epic close
|
|
566
|
+
|
|
567
|
+
TODO: what must be true of every child task for this epic to be done?
|
|
568
|
+
|
|
569
|
+
## Change log
|
|
570
|
+
`;
|
|
571
|
+
var TODO_STARTER_MILESTONE = `---
|
|
572
|
+
id: HS-START-0002
|
|
573
|
+
name: Your first milestone
|
|
574
|
+
summary: TODO \u2014 one-line summary of this milestone.
|
|
575
|
+
type: milestone
|
|
576
|
+
status: open
|
|
577
|
+
owner: TODO \u2014 your name/handle
|
|
578
|
+
created: TODO \u2014 YYYY-MM-DD
|
|
579
|
+
updated: TODO \u2014 YYYY-MM-DD
|
|
580
|
+
epics:
|
|
581
|
+
- '[[HS-START-0000-EPIC-your-first-epic]]'
|
|
582
|
+
target_date: ''
|
|
583
|
+
tags:
|
|
584
|
+
- milestone
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
# Your first milestone
|
|
588
|
+
|
|
589
|
+
<!-- TODO: replace this with a real first milestone for your vault. It exists so the starter
|
|
590
|
+
epic has a home \u2014 see specs/protocols/canonical-frontmatter-templates.md for the full
|
|
591
|
+
milestone template. -->
|
|
592
|
+
|
|
593
|
+
## Intended outcome
|
|
594
|
+
|
|
595
|
+
**Outcome at the milestone altitude (required):** TODO \u2014 what outcome does this milestone
|
|
596
|
+
deliver?
|
|
597
|
+
|
|
598
|
+
## Why this is a milestone, not loose epics
|
|
599
|
+
|
|
600
|
+
TODO.
|
|
601
|
+
|
|
602
|
+
## Suggested landing order
|
|
603
|
+
|
|
604
|
+
- [[HS-START-0000-EPIC-your-first-epic]]
|
|
605
|
+
|
|
606
|
+
## Out of scope
|
|
607
|
+
|
|
608
|
+
TODO.
|
|
609
|
+
`;
|
|
610
|
+
var TODO_STARTER_ROADMAP = `---
|
|
611
|
+
id: HS-START-ROADMAP
|
|
612
|
+
name: Your roadmap
|
|
613
|
+
type: roadmap
|
|
614
|
+
status: open
|
|
615
|
+
owner: TODO \u2014 your name/handle
|
|
616
|
+
created: TODO \u2014 YYYY-MM-DD
|
|
617
|
+
updated: TODO \u2014 YYYY-MM-DD
|
|
618
|
+
roadmap_manifest:
|
|
619
|
+
version: 1
|
|
620
|
+
milestones:
|
|
621
|
+
- HS-START-0002-MIL-your-first-milestone
|
|
622
|
+
tags:
|
|
623
|
+
- roadmap
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
# Your roadmap
|
|
627
|
+
|
|
628
|
+
<!-- TODO: this is a skeleton roadmap, not content. \`roadmap_manifest.milestones\` is what makes
|
|
629
|
+
a milestone reachable from the roadmap (specs/entity-model.md \xA7roadmap) \u2014 list every
|
|
630
|
+
milestone you file here, in the order you intend to land them. -->
|
|
631
|
+
|
|
632
|
+
## Now
|
|
633
|
+
|
|
634
|
+
- [[HS-START-0000-EPIC-your-first-epic]] \u2014 TODO: one-line status.
|
|
635
|
+
|
|
636
|
+
## Next
|
|
637
|
+
|
|
638
|
+
TODO.
|
|
639
|
+
|
|
640
|
+
## Later
|
|
641
|
+
|
|
642
|
+
TODO.
|
|
643
|
+
`;
|
|
644
|
+
function principleSkeleton(args) {
|
|
645
|
+
return `---
|
|
646
|
+
id: HS-DEC-${args.id}
|
|
647
|
+
name: "${args.name}"
|
|
648
|
+
type: memory-decision
|
|
649
|
+
status: open
|
|
650
|
+
priority: 0
|
|
651
|
+
owner: TODO \u2014 your name/handle
|
|
652
|
+
created: TODO \u2014 YYYY-MM-DD
|
|
653
|
+
updated: TODO \u2014 YYYY-MM-DD
|
|
654
|
+
topic: ${args.topic}
|
|
655
|
+
decided: TODO \u2014 YYYY-MM-DD
|
|
656
|
+
deciders:
|
|
657
|
+
- TODO \u2014 your name/handle
|
|
658
|
+
tags:
|
|
659
|
+
- decision
|
|
660
|
+
- principle
|
|
661
|
+
- architecture
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
# ${args.name}
|
|
665
|
+
|
|
666
|
+
<!-- TODO: this is one of HeySpex's own architecture commitments, generalized as a starting
|
|
667
|
+
point. Confirm it fits your product as-is, edit it, or delete this file and drop it from
|
|
668
|
+
specs/principles-index.md. -->
|
|
669
|
+
|
|
670
|
+
## Decision
|
|
671
|
+
|
|
672
|
+
${args.decision}
|
|
673
|
+
|
|
674
|
+
## Rationale
|
|
675
|
+
|
|
676
|
+
${args.rationalePrompt}
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
var TEMPLATE_MANIFEST = [
|
|
680
|
+
// --- Universal rows: copied byte-identical — but from the CURATED copy
|
|
681
|
+
// (`${CURATED_TEMPLATE_ROOT}/${dest}`), not the raw platform doc. The raw
|
|
682
|
+
// platform doc carries ~449 wikilinks to platform-only specs that would
|
|
683
|
+
// never resolve in a customer vault (HS-I2U9); `scripts/regenerate-
|
|
684
|
+
// customer-templates.mjs` rewrites those into plain text once, ahead of
|
|
685
|
+
// time, from this SAME `UNIVERSAL_TEMPLATE_ROWS` list (one list, shared —
|
|
686
|
+
// see that module's + `template-manifest.sources.mjs`'s doc comments).
|
|
687
|
+
...UNIVERSAL_TEMPLATE_ROWS.map(
|
|
688
|
+
(row) => ({
|
|
689
|
+
source: `${CURATED_TEMPLATE_ROOT}/${row.dest}`,
|
|
690
|
+
classification: "universal-template",
|
|
691
|
+
dest: row.dest
|
|
692
|
+
})
|
|
693
|
+
),
|
|
694
|
+
// --- Shape rows: inline skeleton content with a TODO author prompt. ---
|
|
695
|
+
{
|
|
696
|
+
source: "specs/heyspex-north-star.md",
|
|
697
|
+
classification: "template-shape",
|
|
698
|
+
dest: "specs/north-star.md",
|
|
699
|
+
content: TODO_NORTH_STAR
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
source: "specs/principles-index.md",
|
|
703
|
+
classification: "template-shape",
|
|
704
|
+
dest: "specs/principles-index.md",
|
|
705
|
+
content: TODO_PRINCIPLES_INDEX
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
// No single platform analog — authored against the task-entity schema so
|
|
709
|
+
// there is something claimable the moment a customer vault exists.
|
|
710
|
+
source: "specs/protocols/canonical-frontmatter-templates.md",
|
|
711
|
+
classification: "template-shape",
|
|
712
|
+
dest: "specs/tasks/HS-START-0001-TASK-your-first-task.md",
|
|
713
|
+
content: TODO_STARTER_TASK
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
// No single platform analog — authored against the epic-entity schema so
|
|
717
|
+
// the starter task (above) has a `parent:` and the vault lints clean
|
|
718
|
+
// (HS-I2U9). See `TODO_STARTER_EPIC`'s doc comment for the full chain
|
|
719
|
+
// this requires (epic -> milestone -> roadmap).
|
|
720
|
+
source: "specs/protocols/canonical-frontmatter-templates.md",
|
|
721
|
+
classification: "template-shape",
|
|
722
|
+
dest: "specs/epics/HS-START-0000-EPIC-your-first-epic.md",
|
|
723
|
+
content: TODO_STARTER_EPIC
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
// No single platform analog — the starter epic's `milestone:` target.
|
|
727
|
+
source: "specs/protocols/canonical-frontmatter-templates.md",
|
|
728
|
+
classification: "template-shape",
|
|
729
|
+
dest: "specs/milestones/HS-START-0002-MIL-your-first-milestone.md",
|
|
730
|
+
content: TODO_STARTER_MILESTONE
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
// No single platform analog — schema per specs/entity-model.md §roadmap.
|
|
734
|
+
// Makes the starter milestone reachable ("not an orphan" per
|
|
735
|
+
// validateLinkage): it must appear in a roadmap_manifest.milestones list.
|
|
736
|
+
source: "specs/entity-model.md",
|
|
737
|
+
classification: "template-shape",
|
|
738
|
+
dest: "specs/roadmap.md",
|
|
739
|
+
content: TODO_STARTER_ROADMAP
|
|
740
|
+
},
|
|
741
|
+
// --- Universal principle docs (shape rows, group: "principle") ---
|
|
742
|
+
// Generalized from HeySpex's own P3/P4/P6/P7/P8 design-principle decisions
|
|
743
|
+
// (see specs/principles-index.md). Of those five, P6 (JTBD-parity) was
|
|
744
|
+
// excluded from this set of four: its decision body is inseparably about
|
|
745
|
+
// HeySpex's own bd/SuperClaude migration inventory (specific JTBD counts,
|
|
746
|
+
// named predecessor tools), not a portable architecture commitment the way
|
|
747
|
+
// the other four are. P3/P4/P7/P8 generalize cleanly.
|
|
748
|
+
{
|
|
749
|
+
source: "specs/memory/decisions/vault-as-format-primacy-validated-by-bd-retreat.md",
|
|
750
|
+
classification: "template-shape",
|
|
751
|
+
dest: "specs/memory/decisions/principle-vault-is-canonical.md",
|
|
752
|
+
group: "principle",
|
|
753
|
+
summary: "Markdown-in-git is the canonical store; nothing else drifts.",
|
|
754
|
+
content: principleSkeleton({
|
|
755
|
+
id: "principle-vault-is-canonical",
|
|
756
|
+
name: "Vault-as-Format Primacy \u2014 Markdown Files in Git Are Canonical",
|
|
757
|
+
topic: "principle-vault-is-canonical",
|
|
758
|
+
decision: "Markdown files in the git-tracked spec vault are the canonical store. Any derived index (cache, search index, dashboard) follows the vault \u2014 never the other way around.",
|
|
759
|
+
rationalePrompt: "TODO: why does this decision hold for your product? What alternative (a database, a JSON export) did you reject, and why?"
|
|
760
|
+
})
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
source: "specs/memory/decisions/composability-via-git-commit-boundary.md",
|
|
764
|
+
classification: "template-shape",
|
|
765
|
+
dest: "specs/memory/decisions/principle-git-commit-is-the-atomic-batch.md",
|
|
766
|
+
group: "principle",
|
|
767
|
+
summary: "A git commit is the atomic multi-file batch; no bespoke orchestration tier.",
|
|
768
|
+
content: principleSkeleton({
|
|
769
|
+
id: "principle-git-commit-is-the-atomic-batch",
|
|
770
|
+
name: "Composability via Git Commit Boundary \u2014 No Orchestration Tier",
|
|
771
|
+
topic: "principle-git-commit-is-the-atomic-batch",
|
|
772
|
+
decision: "A git commit is the atomic batch for multi-file operations. There is no custom orchestration verb (batch/transaction/pipeline command) layered above the git commit boundary.",
|
|
773
|
+
rationalePrompt: "TODO: why does this decision hold for your product?"
|
|
774
|
+
})
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
source: "specs/memory/decisions/orthogonal-asset-composition-p7.md",
|
|
778
|
+
classification: "template-shape",
|
|
779
|
+
dest: "specs/memory/decisions/principle-orthogonal-asset-composition.md",
|
|
780
|
+
group: "principle",
|
|
781
|
+
summary: "Independent behavior axes (command/persona/mode/workflow) compose, not bundle.",
|
|
782
|
+
content: principleSkeleton({
|
|
783
|
+
id: "principle-orthogonal-asset-composition",
|
|
784
|
+
name: "Orthogonal Asset Composition",
|
|
785
|
+
topic: "principle-orthogonal-asset-composition",
|
|
786
|
+
decision: "Independent axes of configurable behavior (e.g. command, persona, mode, workflow) compose orthogonally rather than being hard-coded together \u2014 each axis is a separate, combinable asset.",
|
|
787
|
+
rationalePrompt: "TODO: what are the independent axes in your product, and why keep them orthogonal instead of bundling them?"
|
|
788
|
+
})
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
source: "specs/memory/decisions/tool-agnostic-mcp-first-cli-second.md",
|
|
792
|
+
classification: "template-shape",
|
|
793
|
+
dest: "specs/memory/decisions/principle-tool-agnostic-by-construction.md",
|
|
794
|
+
group: "principle",
|
|
795
|
+
summary: "The product works across host tools/IDEs, not locked to one.",
|
|
796
|
+
content: principleSkeleton({
|
|
797
|
+
id: "principle-tool-agnostic-by-construction",
|
|
798
|
+
name: "Tool-Agnostic by Construction",
|
|
799
|
+
topic: "principle-tool-agnostic-by-construction",
|
|
800
|
+
decision: "The product does not depend on one specific host tool/IDE/agent. Every feature is built across compatibility layers so it works wherever at least one integration point is supported.",
|
|
801
|
+
rationalePrompt: "TODO: which compatibility layers does your product need (CLI? API? plugin? an MCP-equivalent)?"
|
|
802
|
+
})
|
|
803
|
+
}
|
|
804
|
+
];
|
|
805
|
+
|
|
806
|
+
// spex-console/src/lib/cli/init.customer-vault.ts
|
|
807
|
+
var DEFAULT_CONSOLE_URL3 = "https://console.heyspex.local";
|
|
808
|
+
var SCAFFOLD_FOLDERS = [
|
|
809
|
+
"tasks",
|
|
810
|
+
"epics",
|
|
811
|
+
"milestones",
|
|
812
|
+
"research",
|
|
813
|
+
"memory/lessons",
|
|
814
|
+
"memory/decisions",
|
|
815
|
+
"memory/references",
|
|
816
|
+
"memory/ui-patterns",
|
|
817
|
+
"protocols",
|
|
818
|
+
"reviews",
|
|
819
|
+
"captures",
|
|
820
|
+
"adr",
|
|
821
|
+
"checks",
|
|
822
|
+
"events"
|
|
823
|
+
];
|
|
824
|
+
function defaultVaultName(targetDir) {
|
|
825
|
+
return basename(targetDir) || targetDir;
|
|
826
|
+
}
|
|
827
|
+
function runCustomerVaultInit(args, deps, options = {}) {
|
|
828
|
+
const { targetDir } = args;
|
|
829
|
+
const written = [];
|
|
830
|
+
if (deps.exists(targetDir)) {
|
|
831
|
+
const entries = deps.readdir(targetDir);
|
|
832
|
+
if (entries.length > 0) {
|
|
833
|
+
return {
|
|
834
|
+
code: EXIT.INVALID_ARG,
|
|
835
|
+
message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory.`,
|
|
836
|
+
payload: { ok: false, error: "target_not_empty", path: targetDir },
|
|
837
|
+
isError: true,
|
|
838
|
+
written: []
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
deps.mkdirRecursive(targetDir);
|
|
843
|
+
}
|
|
844
|
+
const specsRoot = deps.joinPath(targetDir, "specs");
|
|
845
|
+
deps.mkdirRecursive(specsRoot);
|
|
846
|
+
for (const folder of SCAFFOLD_FOLDERS) {
|
|
847
|
+
deps.mkdirRecursive(deps.joinPath(specsRoot, folder));
|
|
848
|
+
}
|
|
849
|
+
const vaultId = mintVaultId();
|
|
850
|
+
const yamlContent = renderHeyspexYaml({
|
|
851
|
+
consoleUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL3,
|
|
852
|
+
vaultRoot: "specs/",
|
|
853
|
+
vaultId
|
|
854
|
+
});
|
|
855
|
+
const heyspexYamlPath = deps.joinPath(targetDir, "heyspex.yaml");
|
|
856
|
+
deps.writeFile(heyspexYamlPath, yamlContent);
|
|
857
|
+
written.push(heyspexYamlPath);
|
|
858
|
+
const filter = options.manifestFilter ?? (() => true);
|
|
859
|
+
const rows = TEMPLATE_MANIFEST.filter(filter);
|
|
860
|
+
for (const row of rows) {
|
|
861
|
+
const destAbs = deps.joinPath(targetDir, ...row.dest.split("/"));
|
|
862
|
+
if (row.classification === "universal-template") {
|
|
863
|
+
const srcAbs = deps.joinPath(deps.platformRoot, ...row.source.split("/"));
|
|
864
|
+
deps.copyFile(srcAbs, destAbs);
|
|
865
|
+
} else {
|
|
866
|
+
if (row.content === void 0) {
|
|
867
|
+
throw new Error(
|
|
868
|
+
`template-manifest row for dest "${row.dest}" is classified template-shape but has no inline content`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
deps.writeFile(destAbs, row.content);
|
|
872
|
+
}
|
|
873
|
+
written.push(destAbs);
|
|
874
|
+
}
|
|
875
|
+
if (options.afterScaffold && options.afterScaffold.length > 0) {
|
|
876
|
+
const ctx = {
|
|
877
|
+
targetDir,
|
|
878
|
+
vaultName: options.vaultName ?? defaultVaultName(targetDir),
|
|
879
|
+
templateVersion: TEMPLATE_VERSION,
|
|
880
|
+
manifestRows: rows,
|
|
881
|
+
deps,
|
|
882
|
+
recordWrite: (path) => {
|
|
883
|
+
written.push(path);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
for (const hook of options.afterScaffold) hook(ctx);
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
code: EXIT.OK,
|
|
890
|
+
message: `Initialized customer vault at ${targetDir} (${written.length} files written, template v${TEMPLATE_VERSION}).`,
|
|
891
|
+
payload: {
|
|
892
|
+
ok: true,
|
|
893
|
+
targetDir,
|
|
894
|
+
templateVersion: TEMPLATE_VERSION,
|
|
895
|
+
vaultId,
|
|
896
|
+
written
|
|
897
|
+
},
|
|
898
|
+
isError: false,
|
|
899
|
+
written
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// spex-console/src/lib/cli/init-git-finalize.ts
|
|
904
|
+
import { spawnSync } from "node:child_process";
|
|
905
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
906
|
+
var nodeGitRunner = {
|
|
907
|
+
run(args, cwd) {
|
|
908
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
909
|
+
return {
|
|
910
|
+
ok: result.error === void 0 && result.status === 0,
|
|
911
|
+
stdout: result.stdout ?? "",
|
|
912
|
+
stderr: result.stderr ?? ""
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
function readTemplateVersion(yamlText) {
|
|
917
|
+
const m = yamlText.match(/^template_version:\s*(.+?)\s*$/m);
|
|
918
|
+
if (!m) return null;
|
|
919
|
+
const raw = m[1].trim();
|
|
920
|
+
const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
921
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
922
|
+
}
|
|
923
|
+
function ensureTemplateVersionLine(content, version) {
|
|
924
|
+
if (readTemplateVersion(content) !== null) {
|
|
925
|
+
return content.replace(/^template_version:.*$/m, `template_version: ${version}`);
|
|
926
|
+
}
|
|
927
|
+
const trimmed = content.replace(/\n+$/, "");
|
|
928
|
+
return `${trimmed}
|
|
929
|
+
template_version: ${version}
|
|
930
|
+
`;
|
|
931
|
+
}
|
|
932
|
+
function gitFinalizeHook(runner, notify) {
|
|
933
|
+
return (ctx) => {
|
|
934
|
+
const target = ctx.targetDir;
|
|
935
|
+
const yamlPath = ctx.deps.joinPath(target, "heyspex.yaml");
|
|
936
|
+
const existingYaml = readFileSync3(yamlPath, "utf8");
|
|
937
|
+
const updatedYaml = ensureTemplateVersionLine(existingYaml, ctx.templateVersion);
|
|
938
|
+
ctx.deps.writeFile(yamlPath, updatedYaml);
|
|
939
|
+
ctx.recordWrite(yamlPath);
|
|
940
|
+
const probe = runner.run(["--version"], target);
|
|
941
|
+
if (!probe.ok) {
|
|
942
|
+
notify("git not found - vault left uninitialized, run git init yourself");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
runner.run(["-C", target, "init"], target);
|
|
946
|
+
runner.run(["-C", target, "add", "-A"], target);
|
|
947
|
+
const commitMessage = `hs init --customer-vault - initial scaffold from HeySpex templates v${ctx.templateVersion}`;
|
|
948
|
+
runner.run(
|
|
949
|
+
[
|
|
950
|
+
"-C",
|
|
951
|
+
target,
|
|
952
|
+
"-c",
|
|
953
|
+
"user.name=HeySpex",
|
|
954
|
+
"-c",
|
|
955
|
+
"user.email=init@heyspex.local",
|
|
956
|
+
"commit",
|
|
957
|
+
"-m",
|
|
958
|
+
commitMessage
|
|
959
|
+
],
|
|
960
|
+
target
|
|
961
|
+
);
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// spex-console/src/lib/cli/principle-selection.ts
|
|
966
|
+
import { basename as basename2 } from "node:path";
|
|
967
|
+
var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
|
|
968
|
+
function principleKey(row) {
|
|
969
|
+
const base = basename2(row.dest, ".md");
|
|
970
|
+
return base.startsWith("principle-") ? base.slice("principle-".length) : base;
|
|
971
|
+
}
|
|
972
|
+
function principleWikilink(row) {
|
|
973
|
+
return basename2(row.dest, ".md");
|
|
974
|
+
}
|
|
975
|
+
function parsePrinciplesFlag(value, rows) {
|
|
976
|
+
const principleRows = rows.filter((row) => row.group === "principle");
|
|
977
|
+
const validKeys = principleRows.map(principleKey);
|
|
978
|
+
const trimmed = value?.trim();
|
|
979
|
+
if (trimmed === void 0 || trimmed === "" || trimmed === "all") {
|
|
980
|
+
return { ok: true, selected: new Set(validKeys) };
|
|
981
|
+
}
|
|
982
|
+
if (trimmed === "none") {
|
|
983
|
+
return { ok: true, selected: /* @__PURE__ */ new Set() };
|
|
984
|
+
}
|
|
985
|
+
const requested = trimmed.split(",").map((key) => key.trim()).filter((key) => key.length > 0);
|
|
986
|
+
const unknown = requested.filter((key) => !validKeys.includes(key));
|
|
987
|
+
if (unknown.length > 0) {
|
|
988
|
+
return {
|
|
989
|
+
ok: false,
|
|
990
|
+
message: `Unknown principle key(s): ${unknown.join(", ")}. Valid keys are: ${validKeys.join(", ")} (or "all" / "none").`
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
return { ok: true, selected: new Set(requested) };
|
|
994
|
+
}
|
|
995
|
+
function listPrincipleChoices(rows) {
|
|
996
|
+
return rows.filter((row) => row.group === "principle").map((row) => ({ key: principleKey(row), summary: row.summary ?? "", dest: row.dest }));
|
|
997
|
+
}
|
|
998
|
+
async function promptPrincipleSelection(ask, rows) {
|
|
999
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1000
|
+
for (const choice of listPrincipleChoices(rows)) {
|
|
1001
|
+
const keep = await ask(`Keep principle "${choice.key}" \u2014 ${choice.summary}? (y/n)`);
|
|
1002
|
+
if (keep) {
|
|
1003
|
+
selected.add(choice.key);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return selected;
|
|
1007
|
+
}
|
|
1008
|
+
function principleManifestFilter(selected) {
|
|
1009
|
+
return (row) => {
|
|
1010
|
+
if (row.group !== "principle") {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
return selected.has(principleKey(row));
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function principlesIndexHook(selected, rows) {
|
|
1017
|
+
return (ctx) => {
|
|
1018
|
+
const indexRow = rows.find((row) => row.dest === PRINCIPLES_INDEX_DEST);
|
|
1019
|
+
if (!indexRow || indexRow.content === void 0) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`principlesIndexHook: no template-manifest row with inline content found for dest "${PRINCIPLES_INDEX_DEST}"`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
const principleRows = rows.filter((row) => row.group === "principle");
|
|
1025
|
+
const kept = principleRows.filter((row) => selected.has(principleKey(row)));
|
|
1026
|
+
const keptSection = kept.length === 0 ? "" : "\n## Universal principles kept\n\n" + kept.map((row) => `- [[${principleWikilink(row)}]] \u2014 ${row.summary ?? ""}`).join("\n") + "\n";
|
|
1027
|
+
const rendered = indexRow.content + keptSection;
|
|
1028
|
+
const destAbs = ctx.deps.joinPath(ctx.targetDir, ...PRINCIPLES_INDEX_DEST.split("/"));
|
|
1029
|
+
ctx.deps.writeFile(destAbs, rendered);
|
|
1030
|
+
ctx.recordWrite(destAbs);
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// spex-console/bin/hs-init.ts
|
|
1035
|
+
var INTERACTIVE = process2.stdin.isTTY === true;
|
|
1036
|
+
async function askYesNo(question) {
|
|
1037
|
+
const rl = createInterface({ input: process2.stdin, output: process2.stdout });
|
|
1038
|
+
try {
|
|
1039
|
+
const answer = (await rl.question(`${question} `)).trim().toLowerCase();
|
|
1040
|
+
return answer === "y" || answer === "yes" || answer === "";
|
|
1041
|
+
} finally {
|
|
1042
|
+
rl.close();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async function resolvePrincipleSelection(principlesFlag) {
|
|
1046
|
+
if (principlesFlag !== void 0) {
|
|
1047
|
+
return parsePrinciplesFlag(principlesFlag, TEMPLATE_MANIFEST);
|
|
1048
|
+
}
|
|
1049
|
+
if (INTERACTIVE) {
|
|
1050
|
+
process2.stdout.write("Select which universal principle docs to ship with this vault:\n");
|
|
1051
|
+
for (const choice of listPrincipleChoices(TEMPLATE_MANIFEST)) {
|
|
1052
|
+
process2.stdout.write(` - ${choice.key}: ${choice.summary}
|
|
1053
|
+
`);
|
|
1054
|
+
}
|
|
1055
|
+
const selected = await promptPrincipleSelection(askYesNo, TEMPLATE_MANIFEST);
|
|
1056
|
+
return { ok: true, selected };
|
|
1057
|
+
}
|
|
1058
|
+
return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
|
|
1059
|
+
}
|
|
1060
|
+
async function main() {
|
|
1061
|
+
const repoRoot = process2.env.HS_VAULT_ROOT ? resolve(process2.env.HS_VAULT_ROOT) : process2.cwd();
|
|
1062
|
+
const parsed = parseInitArgs(process2.argv.slice(3));
|
|
1063
|
+
if (parsed.help) {
|
|
1064
|
+
process2.stdout.write(`${HELP}
|
|
1065
|
+
`);
|
|
1066
|
+
process2.exit(EXIT.OK);
|
|
1067
|
+
}
|
|
1068
|
+
if (parsed.error) {
|
|
1069
|
+
process2.stderr.write(`hs init: ${parsed.error.message}
|
|
1070
|
+
`);
|
|
1071
|
+
process2.exit(parsed.error.code);
|
|
1072
|
+
}
|
|
1073
|
+
const args = parsed.args;
|
|
1074
|
+
if (args.customerVaultDir) {
|
|
1075
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1076
|
+
const platformRoot = resolve(here, "..", "..");
|
|
1077
|
+
const targetDir = resolve(args.customerVaultDir);
|
|
1078
|
+
const deps = {
|
|
1079
|
+
platformRoot,
|
|
1080
|
+
exists: (p) => existsSync(p),
|
|
1081
|
+
readdir: (p) => readdirSync(p),
|
|
1082
|
+
mkdirRecursive: (p) => {
|
|
1083
|
+
mkdirSync(p, { recursive: true });
|
|
1084
|
+
},
|
|
1085
|
+
writeFile: (p, c) => writeFileSync(p, c, "utf8"),
|
|
1086
|
+
copyFile: (src, dest) => copyFileSync(src, dest),
|
|
1087
|
+
joinPath: (...s) => join3(...s)
|
|
1088
|
+
};
|
|
1089
|
+
const principlesResult = await resolvePrincipleSelection(args.principles);
|
|
1090
|
+
if (!principlesResult.ok) {
|
|
1091
|
+
process2.stderr.write(`hs init: ${principlesResult.message}
|
|
1092
|
+
`);
|
|
1093
|
+
process2.exit(EXIT.INVALID_ARG);
|
|
1094
|
+
}
|
|
1095
|
+
const selected = principlesResult.selected;
|
|
1096
|
+
const notices = [];
|
|
1097
|
+
const afterScaffold = [
|
|
1098
|
+
customerClaudeMdHook(),
|
|
1099
|
+
principlesIndexHook(selected, TEMPLATE_MANIFEST)
|
|
1100
|
+
];
|
|
1101
|
+
if (!args.noGit) {
|
|
1102
|
+
afterScaffold.push(gitFinalizeHook(nodeGitRunner, (msg) => notices.push(msg)));
|
|
1103
|
+
}
|
|
1104
|
+
const result2 = runCustomerVaultInit({ targetDir }, deps, {
|
|
1105
|
+
consoleUrl: args.consoleUrl,
|
|
1106
|
+
vaultName: args.vaultName,
|
|
1107
|
+
manifestFilter: principleManifestFilter(selected),
|
|
1108
|
+
afterScaffold
|
|
1109
|
+
});
|
|
1110
|
+
const sink2 = result2.isError ? process2.stderr : process2.stdout;
|
|
1111
|
+
if (args.json) {
|
|
1112
|
+
sink2.write(`${JSON.stringify({ ...result2.payload, notices })}
|
|
1113
|
+
`);
|
|
1114
|
+
} else {
|
|
1115
|
+
sink2.write(`${result2.message}
|
|
1116
|
+
`);
|
|
1117
|
+
for (const notice of notices) sink2.write(`${notice}
|
|
1118
|
+
`);
|
|
1119
|
+
}
|
|
1120
|
+
process2.exit(result2.code);
|
|
1121
|
+
}
|
|
1122
|
+
const result = runInit(args, {
|
|
1123
|
+
repoRoot,
|
|
1124
|
+
fileExists: (p) => existsSync(p),
|
|
1125
|
+
readFile: (p) => {
|
|
1126
|
+
try {
|
|
1127
|
+
return readFileSync4(p, "utf8");
|
|
1128
|
+
} catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
writeFile: (p, c) => writeFileSync(p, c, "utf8"),
|
|
1133
|
+
joinPath: (...s) => join3(...s)
|
|
1134
|
+
});
|
|
1135
|
+
const sink = result.isError ? process2.stderr : process2.stdout;
|
|
1136
|
+
sink.write(`${args.json ? JSON.stringify(result.payload) : result.message}
|
|
1137
|
+
`);
|
|
1138
|
+
process2.exit(result.code);
|
|
1139
|
+
}
|
|
1140
|
+
main().catch((err) => {
|
|
1141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1142
|
+
process2.stderr.write(`hs init: unhandled: ${message}
|
|
1143
|
+
`);
|
|
1144
|
+
process2.exit(EXIT.WRITE_FAILED);
|
|
1145
|
+
});
|