heyspex 0.2.4 → 0.2.6
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/.claude-plugin/marketplace.json +1 -1
- package/bin/hs.mjs +281 -30
- package/dev-factory/lib/dispatch-guard.mjs +75 -0
- package/dev-factory/lib/runner-path.mjs +51 -0
- package/dev-factory/spexcrawl/spexcrawl-cli.mjs +734 -0
- package/dist/cli/hs-connect.mjs +20 -1
- package/dist/cli/hs-defer.mjs +3802 -3744
- package/dist/cli/hs-events.mjs +1296 -0
- package/dist/cli/hs-export-events.mjs +1252 -0
- package/dist/cli/hs-handoff.mjs +2576 -0
- package/dist/cli/hs-index-rebuild.mjs +209 -36
- package/dist/cli/hs-index-reconcile.mjs +212 -39
- package/dist/cli/hs-init.mjs +20 -1
- package/dist/cli/hs-install.mjs +20 -1
- package/dist/cli/hs-join.mjs +20 -1
- package/dist/cli/hs-next-launch.mjs +2698 -0
- package/dist/cli/hs-pin.mjs +1336 -0
- package/dist/cli/hs-prereq.mjs +1380 -0
- package/dist/cli/hs-revalidate.mjs +3257 -0
- package/dist/cli/hs-spawn.mjs +12377 -0
- package/dist/cli/hs-test.mjs +1046 -0
- package/dist/cli/hs-trash.mjs +362 -0
- package/dist/cli/hs-trust.mjs +197 -0
- package/dist/cli/hs-vault-pull.mjs +214 -41
- package/dist/cli/hs-vault-push.mjs +212 -39
- package/dist/cli-templates/harness-commands/claude/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/claude/hs-res.md +71 -63
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +66 -58
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +18 -18
- package/docs/copilot-plugin-install.md +2 -2
- package/package.json +9 -5
- package/plugins/heyspex/.claude-plugin/plugin.json +1 -1
- package/plugins/heyspex/.cursor-plugin/plugin.json +1 -1
- package/scripts/claude-hooks/wip-checkpoint.mjs +17 -2
- package/scripts/claude-statusline/statusline-command.mjs +287 -27
- package/scripts/hs-companion.mjs +8 -0
- package/scripts/install-claude-code-hooks.mjs +104 -3
- package/scripts/install-copilot-hooks.mjs +21 -1
- package/scripts/install-cursor-hooks.mjs +4 -3
- package/scripts/lib/capture-execution-guard.mjs +221 -0
- package/scripts/lib/customer-kit.mjs +48 -0
- package/scripts/lib/hs-companion-settings.mjs +25 -0
- package/scripts/record-grounding.mjs +38 -0
- package/vendor/heyspex-mcp/http-transport.mjs +1 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +37 -23
- package/bin/postinstall.mjs +0 -88
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
|
+
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// scripts/lib/heyspex-home.mjs
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
function heyspexHome(env = process.env) {
|
|
11
|
+
const override = env?.HEYSPEX_HOME;
|
|
12
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
13
|
+
return override.trim();
|
|
14
|
+
}
|
|
15
|
+
return homedir();
|
|
16
|
+
}
|
|
17
|
+
var init_heyspex_home = __esm({
|
|
18
|
+
"scripts/lib/heyspex-home.mjs"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// vendor/heyspex-mcp/credentials.mjs
|
|
24
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
25
|
+
import os, { homedir as homedir2 } from "node:os";
|
|
26
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
27
|
+
function credentialsFilePath(homeDir = heyspexHome()) {
|
|
28
|
+
return join2(homeDir, ".heyspex", "credentials.yaml");
|
|
29
|
+
}
|
|
30
|
+
function parseMcpToken(raw) {
|
|
31
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
32
|
+
const token = raw.match(MCP_TOKEN_LINE_RE)?.[1]?.trim();
|
|
33
|
+
return token ? token : null;
|
|
34
|
+
}
|
|
35
|
+
function isValidAccountId(id) {
|
|
36
|
+
return typeof id === "string" && ACCOUNT_ID_RE.test(id);
|
|
37
|
+
}
|
|
38
|
+
function topLevelScalar(raw, key) {
|
|
39
|
+
const re = new RegExp(`^${key}:[ \\t]*["']?([^"'\\r\\n#]+)`, "m");
|
|
40
|
+
const value = raw.match(re)?.[1]?.trim();
|
|
41
|
+
return value ? value : null;
|
|
42
|
+
}
|
|
43
|
+
function decodeScalar(text) {
|
|
44
|
+
const t = text.trim();
|
|
45
|
+
if (t === "" || t === "null" || t === "~") return null;
|
|
46
|
+
if (t.startsWith('"')) {
|
|
47
|
+
try {
|
|
48
|
+
const v = JSON.parse(t);
|
|
49
|
+
return typeof v === "string" && v ? v : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return t.replace(/^"|"$/g, "") || null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (t.startsWith("'")) return t.replace(/^'|'$/g, "") || null;
|
|
55
|
+
const bare = t.replace(/\s+#.*$/, "").trim();
|
|
56
|
+
return bare || null;
|
|
57
|
+
}
|
|
58
|
+
function emptyAccount() {
|
|
59
|
+
return { mcp_token: null, console_url: null, email: null, paired_at: null };
|
|
60
|
+
}
|
|
61
|
+
function findAccountsBlock(lines) {
|
|
62
|
+
const start = lines.findIndex((l) => /^accounts:[ \t]*(#.*)?$/.test(l));
|
|
63
|
+
if (start < 0) return null;
|
|
64
|
+
let end = start + 1;
|
|
65
|
+
while (end < lines.length && (lines[end].trim() === "" || /^[ \t]/.test(lines[end]))) end += 1;
|
|
66
|
+
while (end > start + 1 && lines[end - 1].trim() === "") end -= 1;
|
|
67
|
+
return { start, end };
|
|
68
|
+
}
|
|
69
|
+
function parseAccountsBlock(lines) {
|
|
70
|
+
const accounts = {};
|
|
71
|
+
const block = findAccountsBlock(lines);
|
|
72
|
+
if (!block) return accounts;
|
|
73
|
+
let current = null;
|
|
74
|
+
for (const line of lines.slice(block.start + 1, block.end)) {
|
|
75
|
+
const header = line.match(/^ {2}([^\s:#][^:]*):[ \t]*(#.*)?$/);
|
|
76
|
+
if (header) {
|
|
77
|
+
const id = decodeScalar(header[1]);
|
|
78
|
+
current = isValidAccountId(id) ? id : null;
|
|
79
|
+
if (current) accounts[current] = emptyAccount();
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const field = line.match(/^ {4}([a-z_]+):(.*)$/);
|
|
83
|
+
if (field && current && ACCOUNT_FIELDS.includes(field[1])) {
|
|
84
|
+
accounts[current][field[1]] = decodeScalar(field[2]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return accounts;
|
|
88
|
+
}
|
|
89
|
+
function parseCredentialsStore(raw) {
|
|
90
|
+
const text = typeof raw === "string" ? raw : "";
|
|
91
|
+
if (topLevelScalar(text, "schema_version") !== String(CREDENTIALS_SCHEMA_VERSION)) {
|
|
92
|
+
const token = parseMcpToken(text);
|
|
93
|
+
if (!token) return { schemaVersion: 1, currentAccountId: null, accounts: {} };
|
|
94
|
+
return {
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
currentAccountId: LEGACY_ACCOUNT_ID,
|
|
97
|
+
accounts: {
|
|
98
|
+
[LEGACY_ACCOUNT_ID]: { ...emptyAccount(), mcp_token: token, console_url: topLevelScalar(text, "console_url") }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const accounts = parseAccountsBlock(text.split(/\r?\n/));
|
|
103
|
+
const current = topLevelScalar(text, "current_account_id");
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 2,
|
|
106
|
+
currentAccountId: current && accounts[current] ? current : null,
|
|
107
|
+
accounts
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function resolveBearer({ envToken, credentialsText, accountId, legacyAccountId } = {}) {
|
|
111
|
+
const fromEnv = typeof envToken === "string" ? envToken.trim() : "";
|
|
112
|
+
if (fromEnv) return { token: fromEnv, source: "env" };
|
|
113
|
+
const store = parseCredentialsStore(credentialsText);
|
|
114
|
+
let entry;
|
|
115
|
+
if (typeof accountId === "string" && accountId.trim()) {
|
|
116
|
+
const wanted = accountId.trim();
|
|
117
|
+
entry = store.accounts[wanted];
|
|
118
|
+
if (!entry && store.schemaVersion === 1 && legacyAccountId && wanted === legacyAccountId) {
|
|
119
|
+
entry = store.accounts[LEGACY_ACCOUNT_ID];
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
entry = store.currentAccountId ? store.accounts[store.currentAccountId] : void 0;
|
|
123
|
+
}
|
|
124
|
+
if (entry?.mcp_token) return { token: entry.mcp_token, source: "credentials-file" };
|
|
125
|
+
return { token: null, source: null };
|
|
126
|
+
}
|
|
127
|
+
function readTextOrNull(read, path3) {
|
|
128
|
+
try {
|
|
129
|
+
const raw = read(path3);
|
|
130
|
+
return typeof raw === "string" ? raw : null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function readLegacyConfigAccountId(read, homeDir) {
|
|
136
|
+
const raw = readTextOrNull(read, join2(homeDir, ".heyspex", "config.yaml"));
|
|
137
|
+
const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
|
|
138
|
+
return value || null;
|
|
139
|
+
}
|
|
140
|
+
function readBearer({ env = process.env, homeDir = heyspexHome(), readFile, accountId } = {}) {
|
|
141
|
+
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
142
|
+
const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
|
|
143
|
+
const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
|
|
144
|
+
const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
|
|
145
|
+
return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
|
|
146
|
+
}
|
|
147
|
+
function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
|
|
148
|
+
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
149
|
+
const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
|
|
150
|
+
if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
|
|
151
|
+
return store.currentAccountId;
|
|
152
|
+
}
|
|
153
|
+
if (store.schemaVersion === 2 && store.currentAccountId === null) return null;
|
|
154
|
+
return readLegacyConfigAccountId(read, homeDir);
|
|
155
|
+
}
|
|
156
|
+
var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FIELDS, ACCOUNT_ID_RE, CONFIG_ACCOUNT_ID_RE;
|
|
157
|
+
var init_credentials = __esm({
|
|
158
|
+
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
159
|
+
"use strict";
|
|
160
|
+
init_heyspex_home();
|
|
161
|
+
MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
162
|
+
MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
163
|
+
CREDENTIALS_SCHEMA_VERSION = 2;
|
|
164
|
+
LEGACY_ACCOUNT_ID = "legacy";
|
|
165
|
+
ACCOUNT_FIELDS = ["mcp_token", "console_url", "email", "paired_at"];
|
|
166
|
+
ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
|
|
167
|
+
CONFIG_ACCOUNT_ID_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// scripts/lib/dev-mode.mjs
|
|
172
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
173
|
+
import { homedir as homedir8 } from "node:os";
|
|
174
|
+
import { join as join8 } from "node:path";
|
|
175
|
+
function devModeFilePath(homeDir = heyspexHome()) {
|
|
176
|
+
return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
177
|
+
}
|
|
178
|
+
function normalizeMode(raw) {
|
|
179
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
180
|
+
if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
|
|
181
|
+
if (v === "dev") return { mode: "dev", reliance: null };
|
|
182
|
+
if (v === "local") return { mode: "dev", reliance: "local" };
|
|
183
|
+
if (v === "cloud") return { mode: "dev", reliance: "cloud" };
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function normalizeReliance(raw) {
|
|
187
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
188
|
+
return v === "local" || v === "cloud" ? v : null;
|
|
189
|
+
}
|
|
190
|
+
function deriveSemantics(mode, reliance) {
|
|
191
|
+
const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
|
|
192
|
+
const local = mode === "dev" && effectiveReliance === "local";
|
|
193
|
+
return {
|
|
194
|
+
reliance: effectiveReliance,
|
|
195
|
+
consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
|
|
196
|
+
readTier: local ? "disk" : "neon",
|
|
197
|
+
vaultWriteMode: local ? "local" : "cloud"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
|
|
201
|
+
let raw;
|
|
202
|
+
try {
|
|
203
|
+
raw = readFile(devModeFilePath(homeDir));
|
|
204
|
+
} catch {
|
|
205
|
+
return { record: null, parseError: null };
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(raw);
|
|
209
|
+
const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
|
|
210
|
+
const reliance = normalizeReliance(parsed?.reliance);
|
|
211
|
+
if (!mode || mode === "dev" && !reliance) {
|
|
212
|
+
return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
record: {
|
|
216
|
+
mode,
|
|
217
|
+
reliance: mode === "prod" ? "cloud" : reliance,
|
|
218
|
+
console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
|
|
219
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
|
|
220
|
+
source: typeof parsed.source === "string" ? parsed.source : null
|
|
221
|
+
},
|
|
222
|
+
parseError: null
|
|
223
|
+
};
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function overrideCloudCheck(source, cloudKnown, staleAgainstCloud) {
|
|
229
|
+
if (source !== "local-override") return "not-applicable";
|
|
230
|
+
if (!cloudKnown) return "unverified";
|
|
231
|
+
return staleAgainstCloud ? "stale" : "in-sync";
|
|
232
|
+
}
|
|
233
|
+
function resolveDevMode({
|
|
234
|
+
env = process.env,
|
|
235
|
+
homeDir = heyspexHome(),
|
|
236
|
+
readFile = defaultReadFile,
|
|
237
|
+
cloudSetting = void 0
|
|
238
|
+
} = {}) {
|
|
239
|
+
const local = readLocalDevModeRecord({ homeDir, readFile });
|
|
240
|
+
const cloudKnown = cloudSetting !== void 0;
|
|
241
|
+
const cloudValue = cloudKnown && cloudSetting ? {
|
|
242
|
+
mode: cloudSetting.mode === "dev" ? "dev" : "prod",
|
|
243
|
+
reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
|
|
244
|
+
} : null;
|
|
245
|
+
let mode;
|
|
246
|
+
let reliance;
|
|
247
|
+
let source;
|
|
248
|
+
let relianceSource;
|
|
249
|
+
const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
|
|
250
|
+
if (envParsed) {
|
|
251
|
+
mode = envParsed.mode;
|
|
252
|
+
const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
|
|
253
|
+
reliance = envReliance ?? "local";
|
|
254
|
+
source = "env";
|
|
255
|
+
relianceSource = envReliance ? "env" : "assumed-local";
|
|
256
|
+
} else if (local.record) {
|
|
257
|
+
mode = local.record.mode;
|
|
258
|
+
reliance = local.record.reliance;
|
|
259
|
+
source = "local-override";
|
|
260
|
+
} else if (cloudValue) {
|
|
261
|
+
mode = cloudValue.mode;
|
|
262
|
+
reliance = cloudValue.reliance;
|
|
263
|
+
source = "cloud-setting";
|
|
264
|
+
} else {
|
|
265
|
+
mode = "prod";
|
|
266
|
+
reliance = "cloud";
|
|
267
|
+
source = "default";
|
|
268
|
+
}
|
|
269
|
+
const semantics = deriveSemantics(mode, reliance);
|
|
270
|
+
const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
|
|
271
|
+
const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
|
|
272
|
+
const cloudCheck = overrideCloudCheck(source, cloudKnown, staleAgainstCloud);
|
|
273
|
+
return {
|
|
274
|
+
mode,
|
|
275
|
+
reliance: semantics.reliance,
|
|
276
|
+
consoleUrl,
|
|
277
|
+
readTier: semantics.readTier,
|
|
278
|
+
vaultWriteMode: semantics.vaultWriteMode,
|
|
279
|
+
sources: {
|
|
280
|
+
mode: source,
|
|
281
|
+
reliance: relianceSource ?? source,
|
|
282
|
+
consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
|
|
283
|
+
readTier: "derived",
|
|
284
|
+
vaultWriteMode: "derived"
|
|
285
|
+
},
|
|
286
|
+
localOverride: {
|
|
287
|
+
present: local.record !== null,
|
|
288
|
+
record: local.record,
|
|
289
|
+
parseError: local.parseError,
|
|
290
|
+
staleAgainstCloud,
|
|
291
|
+
cloudCheck
|
|
292
|
+
},
|
|
293
|
+
cloudSetting: { known: cloudKnown, value: cloudValue }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
297
|
+
var init_dev_mode = __esm({
|
|
298
|
+
"scripts/lib/dev-mode.mjs"() {
|
|
299
|
+
"use strict";
|
|
300
|
+
init_heyspex_home();
|
|
301
|
+
init_credentials();
|
|
302
|
+
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
303
|
+
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
304
|
+
DEV_MODE_FILENAME = "dev-mode.json";
|
|
305
|
+
defaultReadFile = (p) => readFileSync8(p, "utf8");
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// spex-console/bin/hs-prereq.ts
|
|
310
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
311
|
+
import process3 from "node:process";
|
|
312
|
+
|
|
313
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
314
|
+
import path2 from "node:path";
|
|
315
|
+
|
|
316
|
+
// scripts/lib/hs-command.mjs
|
|
317
|
+
import { existsSync } from "node:fs";
|
|
318
|
+
import { delimiter, dirname, join } from "node:path";
|
|
319
|
+
import { fileURLToPath } from "node:url";
|
|
320
|
+
var HS_BIN_NAME = "hs";
|
|
321
|
+
var HS_DISPATCHER_PATH = (() => {
|
|
322
|
+
try {
|
|
323
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
324
|
+
return join(dirname(dirname(here)), "bin", "hs.mjs");
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
})();
|
|
329
|
+
function shellQuote(p) {
|
|
330
|
+
return /[\\\s]/.test(p) ? `"${p}"` : p;
|
|
331
|
+
}
|
|
332
|
+
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
333
|
+
function candidateNames(name, env) {
|
|
334
|
+
if (process.platform !== "win32") return [name];
|
|
335
|
+
const pathext = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
336
|
+
const extensions = [.../* @__PURE__ */ new Set([...pathext, ".ps1"])];
|
|
337
|
+
return [name, ...extensions.map((ext) => name + ext)];
|
|
338
|
+
}
|
|
339
|
+
function findOnPath(name, { env = process.env, exists = existsSync } = {}) {
|
|
340
|
+
try {
|
|
341
|
+
const raw = env.PATH ?? env.Path ?? env.path ?? "";
|
|
342
|
+
for (const dir of String(raw).split(delimiter)) {
|
|
343
|
+
const trimmed = dir.trim().replace(/^"(.*)"$/, "$1");
|
|
344
|
+
if (!trimmed) continue;
|
|
345
|
+
for (const candidate of candidateNames(name, env)) {
|
|
346
|
+
const full = join(trimmed, candidate);
|
|
347
|
+
try {
|
|
348
|
+
if (exists(full)) return full;
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
var cached = null;
|
|
358
|
+
var HS_ON_PATH_OVERRIDE_ENV = "HEYSPEX_HS_ON_PATH";
|
|
359
|
+
function onPathOverride(env) {
|
|
360
|
+
const raw = String(env?.[HS_ON_PATH_OVERRIDE_ENV] ?? "");
|
|
361
|
+
if (raw === "1") return true;
|
|
362
|
+
if (raw === "0") return false;
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
function describeHsCommand({ env, exists, useCache = true } = {}) {
|
|
366
|
+
const injected = env !== void 0 || exists !== void 0;
|
|
367
|
+
const effectiveEnv = env ?? process.env;
|
|
368
|
+
const override = onPathOverride(effectiveEnv);
|
|
369
|
+
if (override !== void 0) {
|
|
370
|
+
return {
|
|
371
|
+
onPath: override,
|
|
372
|
+
prefix: override ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
|
|
373
|
+
resolvedPath: null,
|
|
374
|
+
dispatcherPath: HS_DISPATCHER_PATH
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (useCache && !injected && cached) return cached;
|
|
378
|
+
const resolvedPath = findOnPath(HS_BIN_NAME, {
|
|
379
|
+
env: env ?? process.env,
|
|
380
|
+
exists: exists ?? existsSync
|
|
381
|
+
});
|
|
382
|
+
const answer = {
|
|
383
|
+
onPath: resolvedPath !== null,
|
|
384
|
+
prefix: resolvedPath !== null ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
|
|
385
|
+
resolvedPath,
|
|
386
|
+
dispatcherPath: HS_DISPATCHER_PATH
|
|
387
|
+
};
|
|
388
|
+
if (useCache && !injected) cached = answer;
|
|
389
|
+
return answer;
|
|
390
|
+
}
|
|
391
|
+
function hsCommand(args = "", options = {}) {
|
|
392
|
+
let prefix = HS_DISPATCHER_PREFIX;
|
|
393
|
+
try {
|
|
394
|
+
prefix = describeHsCommand(options).prefix;
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
const tail = String(args ?? "").trim();
|
|
398
|
+
return tail ? `${prefix} ${tail}` : prefix;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
402
|
+
init_heyspex_home();
|
|
403
|
+
init_credentials();
|
|
404
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
405
|
+
import { homedir as homedir6 } from "node:os";
|
|
406
|
+
import { join as join6 } from "node:path";
|
|
407
|
+
|
|
408
|
+
// vendor/heyspex-mcp/session-owner.mjs
|
|
409
|
+
init_heyspex_home();
|
|
410
|
+
init_credentials();
|
|
411
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
412
|
+
import { homedir as homedir3 } from "node:os";
|
|
413
|
+
import { join as join3 } from "node:path";
|
|
414
|
+
function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
|
|
415
|
+
const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
|
|
416
|
+
if (fromEnv) return fromEnv;
|
|
417
|
+
return readCurrentAccountId({ homeDir, readFile });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// vendor/heyspex-mcp/vault-less-endpoints.mjs
|
|
421
|
+
var VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
422
|
+
"* /api/mcp/session",
|
|
423
|
+
"* /api/mcp/pairing-codes",
|
|
424
|
+
"POST /api/vaults",
|
|
425
|
+
"GET /api/vaults",
|
|
426
|
+
"POST /api/workspaces",
|
|
427
|
+
"GET /api/workspaces",
|
|
428
|
+
"* /api/admin/dev-mode"
|
|
429
|
+
]);
|
|
430
|
+
function pathnameOf(path3) {
|
|
431
|
+
const raw = String(path3 ?? "").split("?")[0].split("#")[0];
|
|
432
|
+
const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
|
|
433
|
+
return noTrailing || "/";
|
|
434
|
+
}
|
|
435
|
+
function isVaultLessEndpoint(method, path3) {
|
|
436
|
+
const m = String(method || "GET").toUpperCase();
|
|
437
|
+
const pathname = pathnameOf(path3);
|
|
438
|
+
return VAULT_LESS_ENDPOINTS.some((row) => {
|
|
439
|
+
const [rowMethod, rowPath] = row.split(" ");
|
|
440
|
+
return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// vendor/heyspex-mcp/write-outbox.mjs
|
|
445
|
+
import {
|
|
446
|
+
readdirSync,
|
|
447
|
+
readFileSync as readFileSync3,
|
|
448
|
+
writeFileSync as writeFileSync2,
|
|
449
|
+
mkdirSync as mkdirSync2,
|
|
450
|
+
existsSync as existsSync2,
|
|
451
|
+
renameSync as renameSync2,
|
|
452
|
+
rmSync
|
|
453
|
+
} from "node:fs";
|
|
454
|
+
import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
|
|
455
|
+
|
|
456
|
+
// scripts/lib/shared-path-helpers.mjs
|
|
457
|
+
import { realpathSync } from "node:fs";
|
|
458
|
+
import { isAbsolute, resolve as resolve2, sep } from "node:path";
|
|
459
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
460
|
+
|
|
461
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
462
|
+
init_heyspex_home();
|
|
463
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
|
|
464
|
+
import { homedir as homedir5, tmpdir } from "node:os";
|
|
465
|
+
|
|
466
|
+
// vendor/heyspex-mcp/usable-dir-env.mjs
|
|
467
|
+
import { statSync } from "node:fs";
|
|
468
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
469
|
+
|
|
470
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
471
|
+
import { basename as basename2, dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
|
|
472
|
+
|
|
473
|
+
// vendor/heyspex-mcp/vault-binding.mjs
|
|
474
|
+
import {
|
|
475
|
+
copyFileSync,
|
|
476
|
+
existsSync as existsSync3,
|
|
477
|
+
mkdirSync as mkdirSync3,
|
|
478
|
+
readFileSync as readFileSync4,
|
|
479
|
+
realpathSync as realpathSync2,
|
|
480
|
+
renameSync as renameSync3,
|
|
481
|
+
statSync as statSync2,
|
|
482
|
+
unlinkSync,
|
|
483
|
+
writeFileSync as writeFileSync3
|
|
484
|
+
} from "node:fs";
|
|
485
|
+
import { homedir as homedir4 } from "node:os";
|
|
486
|
+
import path from "node:path";
|
|
487
|
+
|
|
488
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
489
|
+
var VAULT_NOT_REGISTERED = "VAULT_NOT_REGISTERED";
|
|
490
|
+
var VaultNotRegisteredError = class extends Error {
|
|
491
|
+
/**
|
|
492
|
+
* @param {string | null | undefined} repoRootAbs the root the resolver settled on
|
|
493
|
+
* @param {string | null | undefined} source the resolver tier that produced it
|
|
494
|
+
* @param {{ reason?: 'unregistered' | 'legacy-id', legacyId?: string | null }} [detail]
|
|
495
|
+
*/
|
|
496
|
+
constructor(repoRootAbs, source, { reason = "unregistered", legacyId = null } = {}) {
|
|
497
|
+
const root = typeof repoRootAbs === "string" && repoRootAbs.trim() ? repoRootAbs.trim() : null;
|
|
498
|
+
const where = root ? ` (${root})` : "";
|
|
499
|
+
const why = reason === "legacy-id" ? `This repo${where} is registered on this machine under a legacy id (\`${legacyId}\`), not the vault id the console knows, and its heyspex.yaml carries no \`vault_id\` to stand in for it, ` : `This repo${where} is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), `;
|
|
500
|
+
super(
|
|
501
|
+
why + `so the console call was refused before it was sent. Run \`${hsCommand("connect")}\`${root ? ` in ${root}` : ""} to ${reason === "legacy-id" ? "re-register" : "connect"} it, then try again. Nothing was sent.`
|
|
502
|
+
);
|
|
503
|
+
this.name = "VaultNotRegisteredError";
|
|
504
|
+
this.code = VAULT_NOT_REGISTERED;
|
|
505
|
+
this.reason = reason;
|
|
506
|
+
this.repoRootAbs = root;
|
|
507
|
+
this.source = typeof source === "string" ? source : null;
|
|
508
|
+
this.legacyId = legacyId;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
var CLOUD_VAULT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
512
|
+
function isCloudVaultId(value) {
|
|
513
|
+
return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
|
|
514
|
+
}
|
|
515
|
+
var MARKER_VAULT_ID_RE = /^vault_id:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
516
|
+
function readMarkerVaultId(repoRootAbs, { readFile } = {}) {
|
|
517
|
+
if (typeof repoRootAbs !== "string" || !repoRootAbs.trim()) return null;
|
|
518
|
+
const read = readFile ?? ((p) => readFileSync6(p, "utf8"));
|
|
519
|
+
try {
|
|
520
|
+
const raw = read(join6(repoRootAbs, "heyspex.yaml"));
|
|
521
|
+
const value = String(raw ?? "").match(MARKER_VAULT_ID_RE)?.[1]?.trim() ?? "";
|
|
522
|
+
return value || null;
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function cloudVaultIdFor(resolved, { readMarkerVaultId: readMarker = readMarkerVaultId } = {}) {
|
|
528
|
+
const rowId = resolved.vaultId.trim();
|
|
529
|
+
if (isCloudVaultId(rowId)) return rowId;
|
|
530
|
+
const marker = readMarker(resolved.repoRootAbs);
|
|
531
|
+
if (isCloudVaultId(marker)) return marker.trim();
|
|
532
|
+
throw new VaultNotRegisteredError(resolved.repoRootAbs, resolved.source, { reason: "legacy-id", legacyId: rowId });
|
|
533
|
+
}
|
|
534
|
+
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
535
|
+
|
|
536
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
537
|
+
init_credentials();
|
|
538
|
+
|
|
539
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
540
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
541
|
+
import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
|
|
542
|
+
|
|
543
|
+
// spex-console/src/lib/heyspex-home.ts
|
|
544
|
+
import { homedir as homedir7 } from "node:os";
|
|
545
|
+
function heyspexHome2(env = process.env) {
|
|
546
|
+
const override = env?.HEYSPEX_HOME;
|
|
547
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
548
|
+
return override.trim();
|
|
549
|
+
}
|
|
550
|
+
return homedir7();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// spex-console/src/lib/vault-registry/types.ts
|
|
554
|
+
var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
|
|
555
|
+
var VAULT_REGISTRY_SCHEMA_VERSION = 2;
|
|
556
|
+
function emptyVaultRegistry() {
|
|
557
|
+
return {
|
|
558
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
559
|
+
current_account_id: null,
|
|
560
|
+
accounts: {},
|
|
561
|
+
workspaces: [],
|
|
562
|
+
vaults: []
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
567
|
+
function vaultRegistryDir(opts = {}) {
|
|
568
|
+
return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
|
|
569
|
+
}
|
|
570
|
+
function vaultRegistryFilePath(opts = {}) {
|
|
571
|
+
return join7(vaultRegistryDir(opts), "registry.json");
|
|
572
|
+
}
|
|
573
|
+
function isNonEmptyString(v) {
|
|
574
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
575
|
+
}
|
|
576
|
+
function accountOf(o) {
|
|
577
|
+
return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
|
|
578
|
+
}
|
|
579
|
+
function normalizeWorkspace(raw) {
|
|
580
|
+
if (!raw || typeof raw !== "object") return null;
|
|
581
|
+
const o = raw;
|
|
582
|
+
if (!isNonEmptyString(o.id)) return null;
|
|
583
|
+
if (!isNonEmptyString(o.display_name)) return null;
|
|
584
|
+
if (!isNonEmptyString(o.created_at)) return null;
|
|
585
|
+
const record = {
|
|
586
|
+
id: o.id,
|
|
587
|
+
display_name: o.display_name,
|
|
588
|
+
created_at: o.created_at,
|
|
589
|
+
archived: o.archived === true,
|
|
590
|
+
account_id: accountOf(o)
|
|
591
|
+
};
|
|
592
|
+
if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
|
|
593
|
+
return record;
|
|
594
|
+
}
|
|
595
|
+
function normalizeVault(raw) {
|
|
596
|
+
if (!raw || typeof raw !== "object") return null;
|
|
597
|
+
const o = raw;
|
|
598
|
+
if (!isNonEmptyString(o.id)) return null;
|
|
599
|
+
if (!isNonEmptyString(o.workspace_id)) return null;
|
|
600
|
+
if (!isNonEmptyString(o.display_name)) return null;
|
|
601
|
+
if (!isNonEmptyString(o.repo_root_abs)) return null;
|
|
602
|
+
if (typeof o.schema_version !== "number") return null;
|
|
603
|
+
if (!isNonEmptyString(o.registered_at)) return null;
|
|
604
|
+
const record = {
|
|
605
|
+
id: o.id,
|
|
606
|
+
workspace_id: o.workspace_id,
|
|
607
|
+
display_name: o.display_name,
|
|
608
|
+
repo_root_abs: o.repo_root_abs,
|
|
609
|
+
schema_version: o.schema_version,
|
|
610
|
+
registered_at: o.registered_at,
|
|
611
|
+
archived: o.archived === true,
|
|
612
|
+
account_id: accountOf(o)
|
|
613
|
+
};
|
|
614
|
+
if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
|
|
615
|
+
return record;
|
|
616
|
+
}
|
|
617
|
+
function normalizeStoredPointers(raw) {
|
|
618
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
619
|
+
const out = {};
|
|
620
|
+
for (const field of ["active_vault_id", "active_workspace_id"]) {
|
|
621
|
+
const v = o[field];
|
|
622
|
+
if (v === null || isNonEmptyString(v)) out[field] = v;
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
function normalizePointers(raw) {
|
|
627
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
628
|
+
return {
|
|
629
|
+
active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
|
|
630
|
+
active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function registryFileSchemaOf(json) {
|
|
634
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) return null;
|
|
635
|
+
return json.schema_version === 2 ? 2 : 1;
|
|
636
|
+
}
|
|
637
|
+
function parseVaultRegistryFile(json) {
|
|
638
|
+
const schema = registryFileSchemaOf(json);
|
|
639
|
+
if (schema === null) return emptyVaultRegistry();
|
|
640
|
+
const o = json;
|
|
641
|
+
const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
|
|
642
|
+
const workspaces = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
|
|
643
|
+
const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
|
|
644
|
+
const vaults = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
|
|
645
|
+
if (schema === 1) {
|
|
646
|
+
const legacy = normalizePointers(o);
|
|
647
|
+
const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces.length > 0 || vaults.length > 0;
|
|
648
|
+
return {
|
|
649
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
650
|
+
current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
|
|
651
|
+
accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
|
|
652
|
+
workspaces,
|
|
653
|
+
vaults
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const accounts = {};
|
|
657
|
+
if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
|
|
658
|
+
for (const [id, pointers] of Object.entries(o.accounts)) {
|
|
659
|
+
if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
664
|
+
current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
|
|
665
|
+
accounts,
|
|
666
|
+
workspaces,
|
|
667
|
+
vaults
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function readRegistryJson(opts) {
|
|
671
|
+
const filePath = vaultRegistryFilePath(opts);
|
|
672
|
+
if (!existsSync5(filePath)) return { kind: "missing" };
|
|
673
|
+
try {
|
|
674
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
675
|
+
const json = JSON.parse(raw);
|
|
676
|
+
if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
|
|
677
|
+
return { kind: "ok", raw, json };
|
|
678
|
+
} catch {
|
|
679
|
+
return { kind: "unreadable" };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function inspectVaultRegistry(opts = {}) {
|
|
683
|
+
const read = readRegistryJson(opts);
|
|
684
|
+
if (read.kind !== "ok") {
|
|
685
|
+
return {
|
|
686
|
+
registry: emptyVaultRegistry(),
|
|
687
|
+
file_schema_version: null,
|
|
688
|
+
registry_unreadable: read.kind === "unreadable"
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
registry: parseVaultRegistryFile(read.json),
|
|
693
|
+
file_schema_version: registryFileSchemaOf(read.json),
|
|
694
|
+
registry_unreadable: false
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function loadVaultRegistry(opts = {}) {
|
|
698
|
+
return inspectVaultRegistry(opts).registry;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
702
|
+
var EXIT = {
|
|
703
|
+
/** Success. */
|
|
704
|
+
OK: 0,
|
|
705
|
+
/**
|
|
706
|
+
* A read-only CHECK ran correctly and its answer is "no" — `hs connect
|
|
707
|
+
* --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
|
|
708
|
+
* script can tell "you typed the command wrong" from "the command worked,
|
|
709
|
+
* the repo is not connected". Both were `1` until 2026-09-18, which made
|
|
710
|
+
* `--check` unusable in a conditional: every caller had to parse stdout to
|
|
711
|
+
* find out which of the two had happened.
|
|
712
|
+
*/
|
|
713
|
+
CHECK_FAILED: 2,
|
|
714
|
+
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
715
|
+
USAGE: 1,
|
|
716
|
+
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
717
|
+
CONFLICT: 9,
|
|
718
|
+
/** Target not found — release of an unheld claim, etc. (404). */
|
|
719
|
+
NOT_FOUND: 10,
|
|
720
|
+
/** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
|
|
721
|
+
INVALID_ARG: 11,
|
|
722
|
+
/** SpexConsole unreachable (network error reaching the dev server). */
|
|
723
|
+
CONSOLE_UNREACHABLE: 12,
|
|
724
|
+
/** Server failure — a non-OK response that isn't 400/402/404/409. */
|
|
725
|
+
WRITE_FAILED: 13,
|
|
726
|
+
/** Neon control plane absent — the endpoint returned 402 (upgrade required). */
|
|
727
|
+
NEON_REQUIRED: 14,
|
|
728
|
+
/**
|
|
729
|
+
* A cross-account `--account <id>` override was refused — the caller is
|
|
730
|
+
* not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
|
|
731
|
+
* [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
|
|
732
|
+
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
733
|
+
* malformed command.
|
|
734
|
+
*/
|
|
735
|
+
FORBIDDEN: 15,
|
|
736
|
+
/**
|
|
737
|
+
* A CLOUD write refused because no account id could be resolved (no
|
|
738
|
+
* `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
|
|
739
|
+
* `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
|
|
740
|
+
* ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
|
|
741
|
+
* `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
|
|
742
|
+
* authorized for this override".
|
|
743
|
+
*/
|
|
744
|
+
SIGN_IN_REQUIRED: 16,
|
|
745
|
+
/**
|
|
746
|
+
* Signed in, but the account a repo would be attached to was not confirmed:
|
|
747
|
+
* a non-interactive install/connect with no matching `--account` / `--yes`,
|
|
748
|
+
* an `account_id` that disagrees with the bearer's user, or the human
|
|
749
|
+
* aborting the identity prompt (`install-identity.ts`,
|
|
750
|
+
* [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
|
|
751
|
+
*/
|
|
752
|
+
IDENTITY_UNCONFIRMED: 17,
|
|
753
|
+
/**
|
|
754
|
+
* The repo this verb runs in is not registered on this machine (no
|
|
755
|
+
* `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
|
|
756
|
+
* cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
|
|
757
|
+
* nothing was sent. Names `hs connect`
|
|
758
|
+
* ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
|
|
759
|
+
*/
|
|
760
|
+
VAULT_NOT_REGISTERED: 18,
|
|
761
|
+
/**
|
|
762
|
+
* The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
|
|
763
|
+
* *`) was written but the CLOUD half did not land -- the console refused
|
|
764
|
+
* it or was never reached. The verb's receipt names the failed half and the
|
|
765
|
+
* exact command to re-run
|
|
766
|
+
* ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
|
|
767
|
+
* Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
|
|
768
|
+
* sync the cloud" from a generic server failure.
|
|
769
|
+
*/
|
|
770
|
+
CLOUD_WRITE_FAILED: 19,
|
|
771
|
+
/**
|
|
772
|
+
* The repo's committed `heyspex.yaml` records `account: <A>` but the machine
|
|
773
|
+
* is signed in as another account, so `hs connect` refused and wrote
|
|
774
|
+
* nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
|
|
775
|
+
* ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
|
|
776
|
+
*/
|
|
777
|
+
ACCOUNT_MISMATCH: 20,
|
|
778
|
+
/**
|
|
779
|
+
* Both sides of a setup verb's receipt were read back and still DISAGREE
|
|
780
|
+
* after a short bounded re-read of the cloud side (a briefly stale read is
|
|
781
|
+
* absorbed first). The write may well have landed; what the verb cannot
|
|
782
|
+
* claim is that this machine and the console now say the same thing. The
|
|
783
|
+
* receipt prints the diffs and the command to re-run
|
|
784
|
+
* ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
|
|
785
|
+
* Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
|
|
786
|
+
*/
|
|
787
|
+
RECEIPT_DISAGREE: 21
|
|
788
|
+
};
|
|
789
|
+
var VAULT_ID_HEADER = "x-heyspex-vault-id";
|
|
790
|
+
var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
|
|
791
|
+
function readPairedBearer(opts = {}) {
|
|
792
|
+
return readBearer(opts);
|
|
793
|
+
}
|
|
794
|
+
function readPairedToken(opts = {}) {
|
|
795
|
+
return readPairedBearer(opts).token ?? void 0;
|
|
796
|
+
}
|
|
797
|
+
function bearerHeaderFor(consoleUrl, opts = {}) {
|
|
798
|
+
try {
|
|
799
|
+
const host = new URL(consoleUrl).hostname;
|
|
800
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") {
|
|
801
|
+
return {};
|
|
802
|
+
}
|
|
803
|
+
const token = readPairedToken(opts);
|
|
804
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
805
|
+
} catch {
|
|
806
|
+
return {};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function scopeHeadersFor(scope, method, path3) {
|
|
810
|
+
if (scope === "none") {
|
|
811
|
+
if (!isVaultLessEndpoint(method, path3)) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path3} is vault-scoped and must name { accountId, vaultId }.`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
return {};
|
|
817
|
+
}
|
|
818
|
+
return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
|
|
819
|
+
}
|
|
820
|
+
async function controlRequest(deps, req) {
|
|
821
|
+
const url = `${deps.consoleUrl}${req.path}`;
|
|
822
|
+
const scopeHeaders = scopeHeadersFor(deps.scope, req.method, req.path);
|
|
823
|
+
let res;
|
|
824
|
+
try {
|
|
825
|
+
res = await deps.fetchImpl(url, {
|
|
826
|
+
method: req.method,
|
|
827
|
+
headers: {
|
|
828
|
+
"content-type": "application/json",
|
|
829
|
+
// A remote console (cloud vault write mode, [[HS-PH7O-ILNC-DIAG-hs-check-report-cannot-authenticate]])
|
|
830
|
+
// authenticates every control call; loopback never asked, so the header
|
|
831
|
+
// is attached only when the target is non-loopback and a paired token
|
|
832
|
+
// exists. Same `hsmcp_` bearer the MCP server presents.
|
|
833
|
+
...bearerHeaderFor(deps.consoleUrl),
|
|
834
|
+
// Invariant 2: name the vault and the account on every call, or the
|
|
835
|
+
// console serves whatever its leftover pointer says.
|
|
836
|
+
...scopeHeaders
|
|
837
|
+
},
|
|
838
|
+
...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
|
|
839
|
+
});
|
|
840
|
+
} catch (err) {
|
|
841
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
842
|
+
return {
|
|
843
|
+
ok: false,
|
|
844
|
+
error: {
|
|
845
|
+
code: EXIT.CONSOLE_UNREACHABLE,
|
|
846
|
+
message: `could not reach console at ${deps.consoleUrl}: ${message}
|
|
847
|
+
Start the dev server or set HEYSPEX_CONSOLE_URL.`,
|
|
848
|
+
payload: {
|
|
849
|
+
ok: false,
|
|
850
|
+
error: "console_unreachable",
|
|
851
|
+
url: deps.consoleUrl,
|
|
852
|
+
message
|
|
853
|
+
},
|
|
854
|
+
isError: true
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
let parsed = null;
|
|
859
|
+
try {
|
|
860
|
+
parsed = await res.json();
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
if (res.status === 402) {
|
|
864
|
+
return {
|
|
865
|
+
ok: false,
|
|
866
|
+
error: {
|
|
867
|
+
code: EXIT.NEON_REQUIRED,
|
|
868
|
+
message: "this command requires the Neon control plane, which is not configured (console returned 402).",
|
|
869
|
+
payload: {
|
|
870
|
+
ok: false,
|
|
871
|
+
error: "neon_required",
|
|
872
|
+
status: 402,
|
|
873
|
+
console_response: parsed
|
|
874
|
+
},
|
|
875
|
+
isError: true
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return { ok: true, status: res.status, body: parsed };
|
|
880
|
+
}
|
|
881
|
+
function envelopeMessage(body) {
|
|
882
|
+
if (body && typeof body === "object" && "message" in body) {
|
|
883
|
+
const m = body.message;
|
|
884
|
+
if (typeof m === "string") return m;
|
|
885
|
+
}
|
|
886
|
+
return "";
|
|
887
|
+
}
|
|
888
|
+
function writeFailed(status, body) {
|
|
889
|
+
const detail = envelopeMessage(body);
|
|
890
|
+
return {
|
|
891
|
+
code: EXIT.WRITE_FAILED,
|
|
892
|
+
message: `console returned ${status}${detail ? `: ${detail}` : ""}`,
|
|
893
|
+
payload: { ok: false, error: "write_failed", status, console_response: body },
|
|
894
|
+
isError: true
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function normalizeRoot(p) {
|
|
898
|
+
return path2.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
899
|
+
}
|
|
900
|
+
function resolveControlScope(input) {
|
|
901
|
+
const target = normalizeRoot(input.vaultRoot);
|
|
902
|
+
const row = input.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
|
|
903
|
+
if (!row) {
|
|
904
|
+
return {
|
|
905
|
+
ok: false,
|
|
906
|
+
error: {
|
|
907
|
+
code: EXIT.VAULT_NOT_REGISTERED,
|
|
908
|
+
message: `this repo (${input.vaultRoot}) is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), so the call was refused before it was sent. Run \`${hsCommand("connect")}\` in ${input.vaultRoot}, then try again.`,
|
|
909
|
+
payload: {
|
|
910
|
+
ok: false,
|
|
911
|
+
error: "vault_not_registered",
|
|
912
|
+
reason: "unregistered",
|
|
913
|
+
vault_root: input.vaultRoot
|
|
914
|
+
},
|
|
915
|
+
isError: true
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
let vaultId;
|
|
920
|
+
try {
|
|
921
|
+
vaultId = cloudVaultIdFor(
|
|
922
|
+
{ vaultId: row.id, repoRootAbs: row.repo_root_abs, source: "registry" },
|
|
923
|
+
input.readMarkerVaultId ? { readMarkerVaultId: input.readMarkerVaultId } : {}
|
|
924
|
+
);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
if (!(err instanceof VaultNotRegisteredError)) throw err;
|
|
927
|
+
const refused = err;
|
|
928
|
+
return {
|
|
929
|
+
ok: false,
|
|
930
|
+
error: {
|
|
931
|
+
code: EXIT.VAULT_NOT_REGISTERED,
|
|
932
|
+
message: refused.message,
|
|
933
|
+
payload: {
|
|
934
|
+
ok: false,
|
|
935
|
+
error: "vault_not_registered",
|
|
936
|
+
reason: refused.reason ?? "legacy-id",
|
|
937
|
+
vault_root: input.vaultRoot,
|
|
938
|
+
legacy_id: refused.legacyId ?? row.id
|
|
939
|
+
},
|
|
940
|
+
isError: true
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const accountId = input.accountId?.trim() ?? "";
|
|
945
|
+
if (!accountId) {
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
error: {
|
|
949
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
950
|
+
message: `no HeySpex account is configured on this machine (\`hs login\` has not been run), so the call was refused before it was sent. Run \`${hsCommand("login")}\` on this machine, then try again.`,
|
|
951
|
+
payload: { ok: false, error: "sign_in_required", vault_root: input.vaultRoot },
|
|
952
|
+
isError: true
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return { ok: true, scope: { accountId, vaultId } };
|
|
957
|
+
}
|
|
958
|
+
function resolveControlScopeFromMachine(vaultRoot, opts = {}) {
|
|
959
|
+
const registry = loadVaultRegistry(opts.registry);
|
|
960
|
+
const accountId = resolveSessionOwnerUncached({ env: opts.env, homeDir: opts.homeDir });
|
|
961
|
+
return resolveControlScope({ vaultRoot, accountId, vaults: registry.vaults });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// scripts/lib/console-url-resolver.mjs
|
|
965
|
+
init_heyspex_home();
|
|
966
|
+
init_dev_mode();
|
|
967
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
968
|
+
import { homedir as homedir9 } from "node:os";
|
|
969
|
+
import { join as join9 } from "node:path";
|
|
970
|
+
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
971
|
+
function trimTrailingSlash(url) {
|
|
972
|
+
return url.replace(/\/+$/, "");
|
|
973
|
+
}
|
|
974
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
975
|
+
function isLoopbackConsoleUrl(url) {
|
|
976
|
+
if (typeof url !== "string" || !url) return false;
|
|
977
|
+
try {
|
|
978
|
+
return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
979
|
+
} catch {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
var staleLoopbackCredentialsWarned = false;
|
|
984
|
+
function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
985
|
+
if (staleLoopbackCredentialsWarned) return;
|
|
986
|
+
staleLoopbackCredentialsWarned = true;
|
|
987
|
+
try {
|
|
988
|
+
log(
|
|
989
|
+
`[heyspex] ignoring console_url ${credentialsUrl} from ~/.heyspex/credentials.yaml: the resolved reliance is cloud, and a loopback console cannot serve it \u2014 using ${derivedUrl} (set HEYSPEX_CONSOLE_URL to override explicitly, or run \`hs dev mode:local\` to go back to a local console).
|
|
990
|
+
`
|
|
991
|
+
);
|
|
992
|
+
} catch {
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
|
|
996
|
+
try {
|
|
997
|
+
const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
|
|
998
|
+
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
999
|
+
const value = match?.[1]?.trim();
|
|
1000
|
+
return value && value.length > 0 ? trimTrailingSlash(value) : null;
|
|
1001
|
+
} catch {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function defaultReadFile2(p) {
|
|
1006
|
+
return readFileSync9(p, "utf8");
|
|
1007
|
+
}
|
|
1008
|
+
function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery: readDiscovery2 = defaultReadFile2 } = {}) {
|
|
1009
|
+
if (!repoRoot) return null;
|
|
1010
|
+
try {
|
|
1011
|
+
const discovery = String(readDiscovery2(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
|
|
1012
|
+
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
var ConsoleUrlUnresolvedError = class extends Error {
|
|
1018
|
+
constructor() {
|
|
1019
|
+
super(
|
|
1020
|
+
"resolveConsoleUrl: local reliance with no HEYSPEX_CONSOLE_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the localhost:3000 default in strict mode."
|
|
1021
|
+
);
|
|
1022
|
+
this.name = "ConsoleUrlUnresolvedError";
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
function resolveModeEnvelope({ env, homeDir }) {
|
|
1026
|
+
try {
|
|
1027
|
+
const resolved = resolveDevMode({ env, homeDir });
|
|
1028
|
+
return {
|
|
1029
|
+
reliance: resolved.reliance === "cloud" ? "cloud" : "local",
|
|
1030
|
+
consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
|
|
1031
|
+
};
|
|
1032
|
+
} catch {
|
|
1033
|
+
return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function resolveConsoleUrl({
|
|
1037
|
+
repoRoot,
|
|
1038
|
+
env = process.env,
|
|
1039
|
+
homeDir = heyspexHome(),
|
|
1040
|
+
strict = false,
|
|
1041
|
+
devMode,
|
|
1042
|
+
log = (line) => process.stderr.write(line),
|
|
1043
|
+
readFile = defaultReadFile2,
|
|
1044
|
+
readDiscovery: readDiscovery2 = defaultReadFile2
|
|
1045
|
+
} = {}) {
|
|
1046
|
+
const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
|
|
1047
|
+
if (envUrl) return trimTrailingSlash(envUrl);
|
|
1048
|
+
const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
|
|
1049
|
+
const cloud = mode.reliance === "cloud";
|
|
1050
|
+
const derivedUrl = trimTrailingSlash(
|
|
1051
|
+
cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
|
|
1052
|
+
);
|
|
1053
|
+
const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
|
|
1054
|
+
if (credentialsUrl) {
|
|
1055
|
+
if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
|
|
1056
|
+
warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
|
|
1057
|
+
} else {
|
|
1058
|
+
return credentialsUrl;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (!cloud) {
|
|
1062
|
+
const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery: readDiscovery2 });
|
|
1063
|
+
if (discoveryUrl) return discoveryUrl;
|
|
1064
|
+
if (strict) throw new ConsoleUrlUnresolvedError();
|
|
1065
|
+
}
|
|
1066
|
+
return derivedUrl;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// spex-console/src/lib/cli/defer.ts
|
|
1070
|
+
function resolveSpexConsoleUrl(vaultRoot, readDiscovery2, opts = {}) {
|
|
1071
|
+
return resolveConsoleUrl({
|
|
1072
|
+
repoRoot: vaultRoot,
|
|
1073
|
+
env: opts.env,
|
|
1074
|
+
homeDir: opts.homeDir,
|
|
1075
|
+
readFile: opts.readFile,
|
|
1076
|
+
readDiscovery: readDiscovery2,
|
|
1077
|
+
log: opts.log
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
var HELP = [
|
|
1081
|
+
"Usage:",
|
|
1082
|
+
" hs defer <spec-id> [--until <when>] [--json] [--no-daemon]",
|
|
1083
|
+
" hs undefer <spec-id> [--json] [--no-daemon]",
|
|
1084
|
+
"",
|
|
1085
|
+
"Time-shift a spec via its `defer_until` frontmatter field. A deferred spec is",
|
|
1086
|
+
"scheduler-invisible until the date passes, then auto-revives.",
|
|
1087
|
+
"",
|
|
1088
|
+
"Arguments:",
|
|
1089
|
+
" <spec-id> The spec to defer/undefer (e.g. HS-TASK-EWL5).",
|
|
1090
|
+
"",
|
|
1091
|
+
"Flags:",
|
|
1092
|
+
" --until <when> ISO date, or relative offset +Nd / +Nw / +Nm / +Ny.",
|
|
1093
|
+
" Defaults to +1y (matches the console Defer button).",
|
|
1094
|
+
" Ignored by `undefer` (which always clears the field).",
|
|
1095
|
+
" --json Emit a machine-readable JSON result.",
|
|
1096
|
+
" --no-daemon Write straight to the vault (no console needed; CI path).",
|
|
1097
|
+
" --help, -h Show this help.",
|
|
1098
|
+
"",
|
|
1099
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
|
|
1100
|
+
].join("\n");
|
|
1101
|
+
|
|
1102
|
+
// spex-console/src/lib/cli/prereq.ts
|
|
1103
|
+
var PATH_LABELS = {
|
|
1104
|
+
do_first: "Do the prerequisite first (then return)",
|
|
1105
|
+
switch_milestone: "Switch focus to the prerequisite's milestone",
|
|
1106
|
+
move_in: "Move the prerequisite into this milestone (mutates roadmap structure)",
|
|
1107
|
+
dispatch: "Dispatch the prerequisite to another agent",
|
|
1108
|
+
defer: "Defer the prerequisite (treat the blocker as resolved for now)"
|
|
1109
|
+
};
|
|
1110
|
+
var ACTION_SUBS = /* @__PURE__ */ new Set(["do-first", "switch", "move-in", "defer"]);
|
|
1111
|
+
function parsePrereqArgs(argv) {
|
|
1112
|
+
const first = argv[0];
|
|
1113
|
+
if (first === "--help" || first === "-h" || first === void 0) {
|
|
1114
|
+
return { help: true };
|
|
1115
|
+
}
|
|
1116
|
+
const isAction = ACTION_SUBS.has(first);
|
|
1117
|
+
const sub = isAction ? first : "list";
|
|
1118
|
+
const positionals = [];
|
|
1119
|
+
let noReturn = false;
|
|
1120
|
+
let dispatch = false;
|
|
1121
|
+
let confirm = false;
|
|
1122
|
+
let json = false;
|
|
1123
|
+
for (let i = isAction ? 1 : 0; i < argv.length; i++) {
|
|
1124
|
+
const arg = argv[i];
|
|
1125
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
1126
|
+
else if (arg === "--json") json = true;
|
|
1127
|
+
else if (arg === "--no-return") noReturn = true;
|
|
1128
|
+
else if (arg === "--dispatch") dispatch = true;
|
|
1129
|
+
else if (arg === "--confirm") confirm = true;
|
|
1130
|
+
else if (arg.startsWith("-")) {
|
|
1131
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
1132
|
+
} else {
|
|
1133
|
+
positionals.push(arg);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (sub === "list") {
|
|
1137
|
+
const blockedId2 = positionals[0];
|
|
1138
|
+
if (!blockedId2) {
|
|
1139
|
+
return { error: { code: EXIT.USAGE, message: "prereq requires a blocked spec id" } };
|
|
1140
|
+
}
|
|
1141
|
+
if (positionals.length > 1) {
|
|
1142
|
+
return {
|
|
1143
|
+
error: { code: EXIT.USAGE, message: `unexpected argument: ${positionals[1]}` }
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
if (noReturn || dispatch || confirm) {
|
|
1147
|
+
return {
|
|
1148
|
+
error: {
|
|
1149
|
+
code: EXIT.USAGE,
|
|
1150
|
+
message: "--no-return / --dispatch / --confirm only apply to an action subcommand"
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
return { args: { sub, blockedId: blockedId2, json } };
|
|
1155
|
+
}
|
|
1156
|
+
const blockedId = positionals[0];
|
|
1157
|
+
const prereqId = positionals[1];
|
|
1158
|
+
if (!blockedId || !prereqId) {
|
|
1159
|
+
return {
|
|
1160
|
+
error: {
|
|
1161
|
+
code: EXIT.USAGE,
|
|
1162
|
+
message: `prereq ${sub} requires <blocked-id> <prereq-id>`
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
if (positionals.length > 2) {
|
|
1167
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${positionals[2]}` } };
|
|
1168
|
+
}
|
|
1169
|
+
if ((noReturn || dispatch) && sub !== "do-first") {
|
|
1170
|
+
return {
|
|
1171
|
+
error: { code: EXIT.USAGE, message: `--no-return / --dispatch only apply to do-first` }
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
if (confirm && sub !== "move-in") {
|
|
1175
|
+
return { error: { code: EXIT.USAGE, message: `--confirm only applies to move-in` } };
|
|
1176
|
+
}
|
|
1177
|
+
return { args: { sub, blockedId, prereqId, noReturn, dispatch, confirm, json } };
|
|
1178
|
+
}
|
|
1179
|
+
function describePaths(paths) {
|
|
1180
|
+
if (!paths || paths.length === 0) return " (no resolution paths available)";
|
|
1181
|
+
return paths.map((p) => ` \u2022 ${p} \u2014 ${PATH_LABELS[p] ?? p}`).join("\n");
|
|
1182
|
+
}
|
|
1183
|
+
function describeCrossMilestone(b) {
|
|
1184
|
+
const head = ` ${b.id ?? "(unknown id)"}${b.name ? ` \u2014 ${b.name}` : ""}`;
|
|
1185
|
+
const milestones = b.prereqMilestoneId || b.blockedMilestoneId ? `
|
|
1186
|
+
prereq milestone: ${b.prereqMilestoneId ?? "(unknown)"} \xB7 blocked milestone: ${b.blockedMilestoneId ?? "(unknown)"}` : "";
|
|
1187
|
+
return `${head}${milestones}
|
|
1188
|
+
${describePaths(b.availablePaths)}`;
|
|
1189
|
+
}
|
|
1190
|
+
function pathForSub(args) {
|
|
1191
|
+
switch (args.sub) {
|
|
1192
|
+
case "do-first":
|
|
1193
|
+
return args.dispatch ? "dispatch" : "do_first";
|
|
1194
|
+
case "switch":
|
|
1195
|
+
return "switch_milestone";
|
|
1196
|
+
case "move-in":
|
|
1197
|
+
return "move_in";
|
|
1198
|
+
case "defer":
|
|
1199
|
+
return "defer";
|
|
1200
|
+
default:
|
|
1201
|
+
return "do_first";
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async function runPrereq(args, deps) {
|
|
1205
|
+
if (args.sub === "list") {
|
|
1206
|
+
const res2 = await controlRequest(deps, {
|
|
1207
|
+
method: "GET",
|
|
1208
|
+
path: `/api/specs/${encodeURIComponent(args.blockedId)}/blockers`
|
|
1209
|
+
});
|
|
1210
|
+
if (!res2.ok) return res2.error;
|
|
1211
|
+
if (res2.status !== 200) return writeFailed(res2.status, res2.body);
|
|
1212
|
+
const blockers = (res2.body?.blockers ?? []).filter(
|
|
1213
|
+
(b) => Boolean(b)
|
|
1214
|
+
);
|
|
1215
|
+
const cross = blockers.filter((b) => b.crossMilestone);
|
|
1216
|
+
const message = cross.length === 0 ? `No cross-milestone prerequisites block ${args.blockedId}.` : [
|
|
1217
|
+
`Cross-milestone prerequisites blocking ${args.blockedId}:`,
|
|
1218
|
+
"",
|
|
1219
|
+
...cross.map(describeCrossMilestone)
|
|
1220
|
+
].join("\n");
|
|
1221
|
+
return {
|
|
1222
|
+
code: EXIT.OK,
|
|
1223
|
+
message,
|
|
1224
|
+
payload: { ok: true, blockedId: args.blockedId, crossMilestone: cross },
|
|
1225
|
+
isError: false
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
if (args.sub === "move-in" && !args.confirm) {
|
|
1229
|
+
return {
|
|
1230
|
+
code: EXIT.USAGE,
|
|
1231
|
+
message: "move-in mutates roadmap structure (it relocates the prerequisite into this milestone). Re-run with --confirm to proceed.",
|
|
1232
|
+
payload: {
|
|
1233
|
+
ok: false,
|
|
1234
|
+
error: "confirmation_required",
|
|
1235
|
+
path: "move_in"
|
|
1236
|
+
},
|
|
1237
|
+
isError: true
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
const path3 = pathForSub(args);
|
|
1241
|
+
const body = {
|
|
1242
|
+
blocked_id: args.blockedId,
|
|
1243
|
+
prereq_id: args.prereqId,
|
|
1244
|
+
path: path3,
|
|
1245
|
+
actor: "agent"
|
|
1246
|
+
};
|
|
1247
|
+
if (args.sub === "do-first") {
|
|
1248
|
+
body.auto_return = !args.noReturn;
|
|
1249
|
+
}
|
|
1250
|
+
if (args.sub === "move-in") {
|
|
1251
|
+
body.confirm = true;
|
|
1252
|
+
}
|
|
1253
|
+
const res = await controlRequest(deps, { method: "POST", path: "/api/focus/prereq", body });
|
|
1254
|
+
if (!res.ok) return res.error;
|
|
1255
|
+
if (res.status === 409) {
|
|
1256
|
+
const detail = envelopeMessage(res.body);
|
|
1257
|
+
return {
|
|
1258
|
+
code: EXIT.CONFLICT,
|
|
1259
|
+
message: `console requires confirmation${detail ? `: ${detail}` : ""}`,
|
|
1260
|
+
payload: { ok: false, error: "confirmation_required", path: path3, console_response: res.body },
|
|
1261
|
+
isError: true
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
if (res.status === 400) {
|
|
1265
|
+
const detail = envelopeMessage(res.body);
|
|
1266
|
+
return {
|
|
1267
|
+
code: EXIT.INVALID_ARG,
|
|
1268
|
+
message: `console rejected the ${path3} path${detail ? `: ${detail}` : ""}`,
|
|
1269
|
+
payload: { ok: false, error: "invalid_request", path: path3, message: detail },
|
|
1270
|
+
isError: true
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
if (res.status !== 200) return writeFailed(res.status, res.body);
|
|
1274
|
+
const result = res.body?.result ?? null;
|
|
1275
|
+
return {
|
|
1276
|
+
code: EXIT.OK,
|
|
1277
|
+
message: `Prerequisite path \`${path3}\` executed for ${args.prereqId} \u2192 ${args.blockedId}.`,
|
|
1278
|
+
payload: { ok: true, path: path3, result },
|
|
1279
|
+
isError: false
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
var HELP2 = [
|
|
1283
|
+
"Usage:",
|
|
1284
|
+
" hs prereq <blocked-id> List cross-milestone prerequisites + paths",
|
|
1285
|
+
" hs prereq do-first <blocked-id> <prereq-id> Do the prerequisite first, then return",
|
|
1286
|
+
" hs prereq switch <blocked-id> <prereq-id> Switch focus to the prerequisite's milestone",
|
|
1287
|
+
" hs prereq move-in <blocked-id> <prereq-id> --confirm",
|
|
1288
|
+
" Move the prerequisite into this milestone",
|
|
1289
|
+
" hs prereq defer <blocked-id> <prereq-id> Defer the prerequisite",
|
|
1290
|
+
"",
|
|
1291
|
+
"Wraps GET /api/specs/:id/blockers (list) and POST /api/focus/prereq (actions)",
|
|
1292
|
+
"([[HS-EPIC-QQBD]]). Requires a reachable console.",
|
|
1293
|
+
"",
|
|
1294
|
+
"Flags:",
|
|
1295
|
+
" --no-return do-first only: do not auto-return to the blocked spec.",
|
|
1296
|
+
" --dispatch do-first only: dispatch the prerequisite instead of doing it first.",
|
|
1297
|
+
" --confirm move-in only: acknowledge that the roadmap structure will change.",
|
|
1298
|
+
" --json Emit a machine-readable JSON result.",
|
|
1299
|
+
" --help, -h Show this help.",
|
|
1300
|
+
"",
|
|
1301
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 9 confirmation-required \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 server-error \xB7 14 neon-required"
|
|
1302
|
+
].join("\n");
|
|
1303
|
+
|
|
1304
|
+
// spex-console/bin/run-entry.ts
|
|
1305
|
+
import process2 from "node:process";
|
|
1306
|
+
var BinExit = class extends Error {
|
|
1307
|
+
code;
|
|
1308
|
+
constructor(code) {
|
|
1309
|
+
super(`exit ${code}`);
|
|
1310
|
+
this.name = "BinExit";
|
|
1311
|
+
this.code = code;
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
function runEntry(name, main2, options = {}) {
|
|
1315
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
|
|
1316
|
+
void (async () => {
|
|
1317
|
+
try {
|
|
1318
|
+
await main2();
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
if (err instanceof BinExit) {
|
|
1321
|
+
process2.exitCode = err.code;
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1325
|
+
process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
|
|
1326
|
+
`);
|
|
1327
|
+
process2.exitCode = fallbackCode;
|
|
1328
|
+
}
|
|
1329
|
+
})();
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// spex-console/bin/hs-prereq.ts
|
|
1333
|
+
function readDiscovery(p) {
|
|
1334
|
+
try {
|
|
1335
|
+
return readFileSync10(p, "utf-8");
|
|
1336
|
+
} catch {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
async function main() {
|
|
1341
|
+
const vaultRoot = process3.env.HS_VAULT_ROOT;
|
|
1342
|
+
if (!vaultRoot) {
|
|
1343
|
+
process3.stderr.write(
|
|
1344
|
+
"hs prereq: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
|
|
1345
|
+
);
|
|
1346
|
+
process3.exitCode = EXIT.USAGE;
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const parsed = parsePrereqArgs(process3.argv.slice(3));
|
|
1350
|
+
if (parsed.help) {
|
|
1351
|
+
process3.stdout.write(`${HELP2}
|
|
1352
|
+
`);
|
|
1353
|
+
process3.exitCode = EXIT.OK;
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (parsed.error) {
|
|
1357
|
+
process3.stderr.write(`hs prereq: ${parsed.error.message}
|
|
1358
|
+
`);
|
|
1359
|
+
process3.exitCode = parsed.error.code;
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const consoleUrl = resolveSpexConsoleUrl(vaultRoot, readDiscovery);
|
|
1363
|
+
const scoped = resolveControlScopeFromMachine(vaultRoot);
|
|
1364
|
+
if (!scoped.ok) {
|
|
1365
|
+
process3.stderr.write(`hs prereq: ${scoped.error.message}
|
|
1366
|
+
`);
|
|
1367
|
+
process3.exitCode = scoped.error.code;
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const result = await runPrereq(parsed.args, {
|
|
1371
|
+
fetchImpl: fetch,
|
|
1372
|
+
consoleUrl,
|
|
1373
|
+
scope: scoped.scope
|
|
1374
|
+
});
|
|
1375
|
+
const sink = result.isError ? process3.stderr : process3.stdout;
|
|
1376
|
+
sink.write(`${parsed.args.json ? JSON.stringify(result.payload) : result.message}
|
|
1377
|
+
`);
|
|
1378
|
+
process3.exitCode = result.code;
|
|
1379
|
+
}
|
|
1380
|
+
runEntry("hs prereq", main, { fallbackCode: EXIT.WRITE_FAILED });
|