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,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hs-source-commit.mjs — `hs source commit`: path-based staging wrapper
|
|
4
|
+
* around the existing owner-directed commit gate
|
|
5
|
+
* (HS-EZNJ-CWPX, epic [[HS-Q0K8-CWPX-EPIC-spexsource-local-git-preflight-commit]]).
|
|
6
|
+
*
|
|
7
|
+
* This module NEVER runs `git add -A` / `git add -u` and NEVER runs a bare
|
|
8
|
+
* `git commit` — it stages exactly the paths named in the most recent
|
|
9
|
+
* `hs source preflight` stage plan (one `git add <path>` per path) and then
|
|
10
|
+
* shells out to the existing owner-directed commit path
|
|
11
|
+
* (`scripts/owner-commit.mjs`, i.e. `npm run commit:owner`) so
|
|
12
|
+
* `guard-agent-commit` and the pre-commit hooks still run unmodified. See
|
|
13
|
+
* the colocated test for the enforcement proof.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
getTreeStatus,
|
|
21
|
+
readStagePlanFile,
|
|
22
|
+
computeDirtyFingerprint,
|
|
23
|
+
REPO_ROOT,
|
|
24
|
+
} from './hs-source-preflight.mjs';
|
|
25
|
+
import { createSessionBranch } from './session-branch.mjs';
|
|
26
|
+
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const OWNER_COMMIT_SCRIPT = path.join(REPO_ROOT, 'scripts', 'owner-commit.mjs');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default subprocess runner — injectable so tests can spy without ever
|
|
32
|
+
* mutating real git state. Never throws; callers check `.status`.
|
|
33
|
+
* @param {string} cmd
|
|
34
|
+
* @param {string[]} args
|
|
35
|
+
* @param {object} [opts]
|
|
36
|
+
* @returns {{ status: number, error?: Error }}
|
|
37
|
+
*/
|
|
38
|
+
export function defaultRun(cmd, args, opts = {}) {
|
|
39
|
+
const result = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
|
|
40
|
+
if (result.error) return { status: 1, error: result.error };
|
|
41
|
+
return { status: result.status ?? 1 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Stages exactly the paths in `plan.included` (path-based `git add <path>`,
|
|
46
|
+
* one invocation per path — never `-A`/`-u`), then commits via the existing
|
|
47
|
+
* owner-directed gate. Refuses (no staging calls at all) when no plan is on
|
|
48
|
+
* disk or it has drifted from the current working tree.
|
|
49
|
+
*
|
|
50
|
+
* @param {{
|
|
51
|
+
* message: string,
|
|
52
|
+
* cwd?: string,
|
|
53
|
+
* run?: typeof defaultRun,
|
|
54
|
+
* getStatus?: typeof getTreeStatus,
|
|
55
|
+
* readPlan?: typeof readStagePlanFile,
|
|
56
|
+
* }} opts
|
|
57
|
+
* @returns {{ ok: boolean, code?: string, message?: string, staged?: string[] }}
|
|
58
|
+
*/
|
|
59
|
+
export function commitFromPlan({
|
|
60
|
+
message,
|
|
61
|
+
cwd = process.cwd(),
|
|
62
|
+
run = defaultRun,
|
|
63
|
+
getStatus = getTreeStatus,
|
|
64
|
+
readPlan = readStagePlanFile,
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (!message || typeof message !== 'string' || message.trim() === '') {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
code: 'NO_MESSAGE',
|
|
70
|
+
message:
|
|
71
|
+
'hs source commit: a commit message is required (e.g. `hs source commit -m "feat: …"`).',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const plan = readPlan({ cwd });
|
|
76
|
+
if (!plan) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
code: 'NO_PLAN',
|
|
80
|
+
message:
|
|
81
|
+
'hs source commit: no stage plan found. Run `hs source preflight` first — it produces the plan this command consumes.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const currentStatus = getStatus({ cwd, run });
|
|
86
|
+
const currentFingerprint = computeDirtyFingerprint(currentStatus.dirty);
|
|
87
|
+
if (currentFingerprint !== plan.dirtyFingerprint) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
code: 'STALE_PLAN',
|
|
91
|
+
message:
|
|
92
|
+
'hs source commit: the working tree has changed since the last `hs source preflight` — the stage plan is stale. Re-run `hs source preflight` and try again.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(plan.included) || plan.included.length === 0) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
code: 'EMPTY_PLAN',
|
|
100
|
+
message:
|
|
101
|
+
'hs source commit: the stage plan has nothing to stage (empty or fully denylisted). Nothing to commit.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (plan.ok === false) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
code: 'PREFLIGHT_FAILED',
|
|
109
|
+
message:
|
|
110
|
+
'hs source commit: the last `hs source preflight` reported a failing lint/format check or a secret-scan finding. Fix it and re-run preflight before committing.',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const staged = [];
|
|
115
|
+
for (const p of plan.included) {
|
|
116
|
+
const result = run('git', ['add', p], { cwd });
|
|
117
|
+
if (result.status !== 0) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
code: 'STAGE_FAILED',
|
|
121
|
+
message: `hs source commit: \`git add ${p}\` failed — aborting before any commit was attempted.`,
|
|
122
|
+
staged,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
staged.push(p);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const commitResult = run(process.execPath, [OWNER_COMMIT_SCRIPT, '-m', message], { cwd });
|
|
129
|
+
if (commitResult.status !== 0) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
code: 'COMMIT_FAILED',
|
|
133
|
+
message: 'hs source commit: the owner-directed commit gate (owner-commit.mjs) failed — see its output above.',
|
|
134
|
+
staged,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { ok: true, staged };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* CLI entry for `hs source commit`. Parses `-m/--message <msg>` and an
|
|
143
|
+
* optional `--session <id>` from argv.
|
|
144
|
+
*
|
|
145
|
+
* When `--session <id>` is given, the session's branch is created/selected
|
|
146
|
+
* via `scripts/lib/session-branch.mjs` (HS-Z7VJ) BEFORE any staging or
|
|
147
|
+
* committing happens — session-branch.mjs is the only thing that ever runs
|
|
148
|
+
* `git checkout`/`git checkout -b`/`git config` on that path; this function
|
|
149
|
+
* never spawns git itself. Omitting `--session` leaves the existing
|
|
150
|
+
* no-flag path completely unchanged: `createSessionBranch` is never called,
|
|
151
|
+
* so there is no branch-creation/selection side effect at all (AC2).
|
|
152
|
+
*
|
|
153
|
+
* @param {string[]} argv (verb-stripped: just the flags/args after `commit`)
|
|
154
|
+
* @param {{
|
|
155
|
+
* cwd?: string,
|
|
156
|
+
* run?: (cmd: string, args: string[], opts?: object) => { status: number, stdout?: string, stderr?: string },
|
|
157
|
+
* createSessionBranchFn?: typeof createSessionBranch,
|
|
158
|
+
* }} [opts]
|
|
159
|
+
* @returns {number} process exit code
|
|
160
|
+
*/
|
|
161
|
+
export function runCommitCommand(argv, { cwd = process.cwd(), run, createSessionBranchFn = createSessionBranch } = {}) {
|
|
162
|
+
let message = '';
|
|
163
|
+
let sessionId;
|
|
164
|
+
for (let i = 0; i < argv.length; i++) {
|
|
165
|
+
if ((argv[i] === '-m' || argv[i] === '--message') && argv[i + 1] !== undefined) {
|
|
166
|
+
message = argv[i + 1];
|
|
167
|
+
i++;
|
|
168
|
+
} else if (argv[i] === '--session' && argv[i + 1] !== undefined) {
|
|
169
|
+
sessionId = argv[i + 1];
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (sessionId !== undefined) {
|
|
175
|
+
const branchResult = createSessionBranchFn({ sessionId, cwd, run });
|
|
176
|
+
if (!branchResult.ok) {
|
|
177
|
+
console.error(branchResult.message);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
console.log(
|
|
181
|
+
`hs source commit: ${branchResult.reused ? 'reusing' : 'created'} session branch ${branchResult.branch}.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result = commitFromPlan({ message, cwd, run });
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
console.error(result.message);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
console.log(`hs source commit: committed ${result.staged.length} path(s).`);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- direct-invocation entry --------------------------------------------
|
|
195
|
+
|
|
196
|
+
function invokedDirectly() {
|
|
197
|
+
try {
|
|
198
|
+
return path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1] ?? '');
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (invokedDirectly()) {
|
|
205
|
+
// argv: [node, script, "commit", ...flags] — drop the leading verb token.
|
|
206
|
+
process.exit(runCommitCommand(process.argv.slice(3)));
|
|
207
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hs-source-log.mjs — `hs source log <spec-id>`: commit-history retrieval
|
|
4
|
+
* for diagnosis (HS-I6AB, epic
|
|
5
|
+
* [[HS-VKD5-6JBX-EPIC-commit-history-retrieval-for-diagnosis]]).
|
|
6
|
+
*
|
|
7
|
+
* Reads the console's `GET /api/specs/:id/settlement` route
|
|
8
|
+
* (`spex-console/src/app/api/specs/[id]/settlement/route.ts`) to print the
|
|
9
|
+
* most recent commit-settlement row for a spec — repo, branch, commit sha,
|
|
10
|
+
* and when it settled — so diagnosis/triage can look up a spec's commit
|
|
11
|
+
* history from the console-known side, without a local git checkout
|
|
12
|
+
* (mirrors why `spec-settlement-info.tsx` exists in the console itself:
|
|
13
|
+
* under ADR-0022/ADR-0023 a browser-only cloud user has none).
|
|
14
|
+
*
|
|
15
|
+
* The console lookup is an injectable HTTP client (`fetchImpl`), never a
|
|
16
|
+
* hardcoded `fetch` call — tests inject a fake console and never touch the
|
|
17
|
+
* network. Console URL resolution reuses the ONE shared resolver
|
|
18
|
+
* (`console-url-resolver.mjs`) rather than a hand-rolled localhost guess.
|
|
19
|
+
*
|
|
20
|
+
* HS-CZ84 extends this module with `lookupCommitHistory` — a local
|
|
21
|
+
* `git -C <vaultRoot> log --grep <spec-id>` fallback (through the same
|
|
22
|
+
* injected-runner pattern `hs-source-commit.mjs`'s `defaultRun` uses) for
|
|
23
|
+
* when the console lookup finds nothing, so commit-history lookup still
|
|
24
|
+
* works with no console reachable. Provenance (`console` vs
|
|
25
|
+
* `local-git-log`) is always explicit on the result — see
|
|
26
|
+
* {@link formatSettlementResult}.
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
import { resolveConsoleUrl } from './console-url-resolver.mjs';
|
|
36
|
+
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
export const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
39
|
+
|
|
40
|
+
// ---- Bearer-header idiom -------------------------------------------------
|
|
41
|
+
// Mirrors `readPairedToken`/`bearerHeaderFor` in
|
|
42
|
+
// `spex-console/src/lib/cli/control-shared.ts` byte-for-byte in behavior:
|
|
43
|
+
// same paired `hsmcp_`/`hscmp_` token read from `~/.heyspex/credentials.yaml`,
|
|
44
|
+
// same loopback-skip rule (a local dev console never needed auth; a genuinely
|
|
45
|
+
// remote/paired console does — [[HS-PH7O-ILNC-DIAG-hs-check-report-cannot-authenticate]]).
|
|
46
|
+
// Duplicated here in plain Node ESM rather than imported from that TS module
|
|
47
|
+
// because `scripts/lib/*.mjs` runs standalone (bare `node`, no Next.js build
|
|
48
|
+
// step) — the same reasoning `console-url-resolver.mjs`'s own module doc
|
|
49
|
+
// gives for staying dependency-free.
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Raw paired Bearer token from `~/.heyspex/credentials.yaml`, or `undefined`
|
|
53
|
+
* if absent/unreadable.
|
|
54
|
+
* @returns {string | undefined}
|
|
55
|
+
*/
|
|
56
|
+
export function readPairedToken() {
|
|
57
|
+
try {
|
|
58
|
+
const raw = readFileSync(join(homedir(), '.heyspex', 'credentials.yaml'), 'utf-8');
|
|
59
|
+
const match = raw.match(/\b(hsmcp_[A-Za-z0-9_-]+|hscmp_[A-Za-z0-9_-]+)\b/);
|
|
60
|
+
return match?.[1];
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Bearer header for a non-loopback console; `{}` for loopback targets or
|
|
68
|
+
* when no token is paired.
|
|
69
|
+
* @param {string} consoleUrl
|
|
70
|
+
* @returns {Record<string, string>}
|
|
71
|
+
*/
|
|
72
|
+
export function bearerHeaderFor(consoleUrl) {
|
|
73
|
+
try {
|
|
74
|
+
const host = new URL(consoleUrl).hostname;
|
|
75
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1') {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
const token = readPairedToken();
|
|
79
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
80
|
+
} catch {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- console settlement lookup ------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The minimal HTTP-client surface this module needs — an injectable stand-in
|
|
89
|
+
* for `fetch`. Production uses the real global `fetch`; tests inject a fake
|
|
90
|
+
* console that never touches the network.
|
|
91
|
+
* @typedef {(url: string, init?: object) => Promise<{
|
|
92
|
+
* ok: boolean,
|
|
93
|
+
* status: number,
|
|
94
|
+
* json: () => Promise<any>,
|
|
95
|
+
* }>} HttpClient
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One settlement row, shaped exactly like `SettlementRow` in
|
|
100
|
+
* `spex-console/src/lib/git-export/settlement-store.ts` — never reimplemented
|
|
101
|
+
* ad hoc.
|
|
102
|
+
* @typedef {{
|
|
103
|
+
* found: true,
|
|
104
|
+
* provenance: 'console',
|
|
105
|
+
* repo: string,
|
|
106
|
+
* branch: string,
|
|
107
|
+
* commitSha: string,
|
|
108
|
+
* settledAt: string,
|
|
109
|
+
* }} ConsoleSettlementFound
|
|
110
|
+
* @typedef {{
|
|
111
|
+
* found: false,
|
|
112
|
+
* provenance: 'console',
|
|
113
|
+
* reason: string,
|
|
114
|
+
* }} ConsoleSettlementNotFound
|
|
115
|
+
* @typedef {ConsoleSettlementFound | ConsoleSettlementNotFound} ConsoleSettlementResult
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Look up the most recent settlement row for `specId` via the console's
|
|
120
|
+
* `GET /api/specs/:id/settlement` route. NEVER throws: a network error, a
|
|
121
|
+
* non-OK HTTP status, an unparsable body, or a `{ settled: false }` response
|
|
122
|
+
* all resolve to a typed `{ found: false, reason }` result rather than
|
|
123
|
+
* rejecting or crashing the caller.
|
|
124
|
+
*
|
|
125
|
+
* @param {{
|
|
126
|
+
* specId: string,
|
|
127
|
+
* consoleUrl?: string,
|
|
128
|
+
* fetchImpl?: HttpClient,
|
|
129
|
+
* repoRoot?: string,
|
|
130
|
+
* }} opts
|
|
131
|
+
* @returns {Promise<ConsoleSettlementResult>}
|
|
132
|
+
*/
|
|
133
|
+
export async function lookupConsoleSettlement({
|
|
134
|
+
specId,
|
|
135
|
+
consoleUrl,
|
|
136
|
+
fetchImpl = fetch,
|
|
137
|
+
repoRoot,
|
|
138
|
+
} = {}) {
|
|
139
|
+
const resolvedUrl = consoleUrl ?? resolveConsoleUrl({ repoRoot });
|
|
140
|
+
const url = `${resolvedUrl}/api/specs/${encodeURIComponent(specId)}/settlement`;
|
|
141
|
+
|
|
142
|
+
let res;
|
|
143
|
+
try {
|
|
144
|
+
res = await fetchImpl(url, { headers: { ...bearerHeaderFor(resolvedUrl) } });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return {
|
|
147
|
+
found: false,
|
|
148
|
+
provenance: 'console',
|
|
149
|
+
reason: `no console reachable at ${resolvedUrl} (${err instanceof Error ? err.message : String(err)})`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!res || !res.ok) {
|
|
154
|
+
return {
|
|
155
|
+
found: false,
|
|
156
|
+
provenance: 'console',
|
|
157
|
+
reason: `console returned ${res ? res.status : 'no response'} for ${url}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let body;
|
|
162
|
+
try {
|
|
163
|
+
body = await res.json();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return {
|
|
166
|
+
found: false,
|
|
167
|
+
provenance: 'console',
|
|
168
|
+
reason: `console response was not valid JSON (${err instanceof Error ? err.message : String(err)})`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!body || body.settled !== true) {
|
|
173
|
+
return {
|
|
174
|
+
found: false,
|
|
175
|
+
provenance: 'console',
|
|
176
|
+
reason:
|
|
177
|
+
body && typeof body.reason === 'string'
|
|
178
|
+
? `no settlement found (${body.reason})`
|
|
179
|
+
: 'no settlement found for this spec',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
found: true,
|
|
185
|
+
provenance: 'console',
|
|
186
|
+
repo: body.repo,
|
|
187
|
+
branch: body.branch,
|
|
188
|
+
commitSha: body.commit_sha,
|
|
189
|
+
settledAt: body.settled_at,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- local git-log fallback (HS-CZ84) ------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Default git-log runner — injectable so tests never touch a real repo,
|
|
197
|
+
* exactly like `defaultRun` in `hs-source-commit.mjs`. Unlike that sibling
|
|
198
|
+
* (which uses `stdio: 'inherit'` because it only cares about the exit code),
|
|
199
|
+
* this runner CAPTURES stdout — this module needs to parse and print the
|
|
200
|
+
* matched commits, not just observe whether the command succeeded.
|
|
201
|
+
* @param {string} cmd
|
|
202
|
+
* @param {string[]} args
|
|
203
|
+
* @param {object} [opts]
|
|
204
|
+
* @returns {{ status: number, stdout: string, error?: Error }}
|
|
205
|
+
*/
|
|
206
|
+
export function defaultGitLogRun(cmd, args, opts = {}) {
|
|
207
|
+
const result = spawnSync(cmd, args, {
|
|
208
|
+
encoding: 'utf8',
|
|
209
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
210
|
+
...opts,
|
|
211
|
+
});
|
|
212
|
+
if (result.error) return { status: 1, stdout: '', error: result.error };
|
|
213
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? '' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @typedef {{
|
|
218
|
+
* found: true,
|
|
219
|
+
* provenance: 'local-git-log',
|
|
220
|
+
* output: string,
|
|
221
|
+
* } | {
|
|
222
|
+
* found: false,
|
|
223
|
+
* provenance: 'local-git-log',
|
|
224
|
+
* reason: string,
|
|
225
|
+
* }} LocalGitLogResult
|
|
226
|
+
* @typedef {ConsoleSettlementResult | LocalGitLogResult} LookupResult
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Local fallback: `git -C <vaultRoot> log --grep <spec-id>`, run ONLY
|
|
231
|
+
* through the injected `run` — this module never shells out to git any
|
|
232
|
+
* other way. Never throws: a failed spawn or a non-zero exit resolves to a
|
|
233
|
+
* typed `{ found: false, reason }` result, same contract as
|
|
234
|
+
* {@link lookupConsoleSettlement}.
|
|
235
|
+
*
|
|
236
|
+
* @param {{ specId: string, vaultRoot?: string, run?: typeof defaultGitLogRun }} opts
|
|
237
|
+
* @returns {LocalGitLogResult}
|
|
238
|
+
*/
|
|
239
|
+
export function lookupLocalGitLog({ specId, vaultRoot = REPO_ROOT, run = defaultGitLogRun } = {}) {
|
|
240
|
+
const result = run('git', ['-C', vaultRoot, 'log', '--grep', specId]);
|
|
241
|
+
|
|
242
|
+
if (result.error) {
|
|
243
|
+
return {
|
|
244
|
+
found: false,
|
|
245
|
+
provenance: 'local-git-log',
|
|
246
|
+
reason: `git log failed to run (${result.error.message})`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (result.status !== 0) {
|
|
250
|
+
return {
|
|
251
|
+
found: false,
|
|
252
|
+
provenance: 'local-git-log',
|
|
253
|
+
reason: `git log exited ${result.status}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const output = (result.stdout ?? '').trim();
|
|
258
|
+
if (!output) {
|
|
259
|
+
return {
|
|
260
|
+
found: false,
|
|
261
|
+
provenance: 'local-git-log',
|
|
262
|
+
reason: `no local commits found matching --grep ${specId}`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { found: true, provenance: 'local-git-log', output };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Combined commit-history lookup (HS-CZ84): try the console settlement
|
|
271
|
+
* route first; when it finds nothing (unreachable OR no rows — both are
|
|
272
|
+
* "the console path came up empty"), fall back to the local
|
|
273
|
+
* `git log --grep <spec-id>` path. The returned result's `provenance` field
|
|
274
|
+
* always says which path actually produced it — the two result shapes are
|
|
275
|
+
* never silently merged (per this task's own do-not).
|
|
276
|
+
*
|
|
277
|
+
* @param {{
|
|
278
|
+
* specId: string,
|
|
279
|
+
* consoleUrl?: string,
|
|
280
|
+
* fetchImpl?: HttpClient,
|
|
281
|
+
* repoRoot?: string,
|
|
282
|
+
* vaultRoot?: string,
|
|
283
|
+
* gitRun?: typeof defaultGitLogRun,
|
|
284
|
+
* }} opts
|
|
285
|
+
* @returns {Promise<LookupResult>}
|
|
286
|
+
*/
|
|
287
|
+
export async function lookupCommitHistory({
|
|
288
|
+
specId,
|
|
289
|
+
consoleUrl,
|
|
290
|
+
fetchImpl = fetch,
|
|
291
|
+
repoRoot,
|
|
292
|
+
vaultRoot,
|
|
293
|
+
gitRun = defaultGitLogRun,
|
|
294
|
+
} = {}) {
|
|
295
|
+
const consoleResult = await lookupConsoleSettlement({ specId, consoleUrl, fetchImpl, repoRoot });
|
|
296
|
+
if (consoleResult.found) return consoleResult;
|
|
297
|
+
return lookupLocalGitLog({ specId, vaultRoot: vaultRoot ?? repoRoot ?? REPO_ROOT, run: gitRun });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Render a lookup result as the CLI's printed output. Always labels
|
|
302
|
+
* provenance explicitly (`console` vs `local-git-log`) so a diagnosis
|
|
303
|
+
* reading the output is never misled about where the answer came from
|
|
304
|
+
* (AC2).
|
|
305
|
+
* @param {LookupResult} result
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
export function formatSettlementResult(result) {
|
|
309
|
+
if (result.found && result.provenance === 'console') {
|
|
310
|
+
return [
|
|
311
|
+
`hs source log: settled (via ${result.provenance})`,
|
|
312
|
+
` repo: ${result.repo}`,
|
|
313
|
+
` branch: ${result.branch}`,
|
|
314
|
+
` commitSha: ${result.commitSha}`,
|
|
315
|
+
` settledAt: ${result.settledAt}`,
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
318
|
+
if (result.found && result.provenance === 'local-git-log') {
|
|
319
|
+
return [`hs source log: found (via ${result.provenance} fallback)`, result.output].join('\n');
|
|
320
|
+
}
|
|
321
|
+
return `hs source log: not found (via ${result.provenance}) — ${result.reason}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* CLI entry for `hs source log <spec-id>`. Tries the console settlement
|
|
326
|
+
* route first, then falls back to local `git log --grep` (HS-CZ84) when the
|
|
327
|
+
* console path comes up empty — see {@link lookupCommitHistory}.
|
|
328
|
+
* @param {string[]} argv (verb-stripped: args after `log`)
|
|
329
|
+
* @param {{
|
|
330
|
+
* cwd?: string,
|
|
331
|
+
* fetchImpl?: HttpClient,
|
|
332
|
+
* gitRun?: typeof defaultGitLogRun,
|
|
333
|
+
* }} [opts]
|
|
334
|
+
* @returns {Promise<number>} process exit code — 0 when found by either
|
|
335
|
+
* path, 1 when the command ran cleanly but found nothing (usage error
|
|
336
|
+
* included), never a thrown exception either way.
|
|
337
|
+
*/
|
|
338
|
+
export async function runLogCommand(
|
|
339
|
+
argv,
|
|
340
|
+
{ cwd = process.cwd(), fetchImpl = fetch, gitRun = defaultGitLogRun } = {},
|
|
341
|
+
) {
|
|
342
|
+
const specId = argv[0];
|
|
343
|
+
if (!specId) {
|
|
344
|
+
console.error('hs source log: a spec id is required (e.g. `hs source log HS-TASK-XA40`).');
|
|
345
|
+
return 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = await lookupCommitHistory({ specId, fetchImpl, repoRoot: cwd, gitRun });
|
|
349
|
+
console.log(formatSettlementResult(result));
|
|
350
|
+
return result.found ? 0 : 1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---- direct-invocation entry --------------------------------------------
|
|
354
|
+
|
|
355
|
+
function invokedDirectly() {
|
|
356
|
+
try {
|
|
357
|
+
return path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1] ?? '');
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (invokedDirectly()) {
|
|
364
|
+
// argv: [node, script, "log", ...flags] — drop the leading verb token.
|
|
365
|
+
runLogCommand(process.argv.slice(3)).then((code) => process.exit(code));
|
|
366
|
+
}
|