heyspex 0.2.0 → 0.2.1
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 +14 -0
- package/bin/hs.mjs +163 -11
- package/dist/cli/hs-check.mjs +88 -46
- package/dist/cli/hs-claim.mjs +84 -39
- package/dist/cli/hs-connect.mjs +583 -129
- package/dist/cli/hs-defer.mjs +251 -30
- package/dist/cli/hs-dev.mjs +20 -8
- package/dist/cli/hs-env.mjs +62 -34
- package/dist/cli/hs-index-rebuild.mjs +288 -83
- package/dist/cli/hs-index-reconcile.mjs +397 -111
- package/dist/cli/hs-init.mjs +2143 -91
- package/dist/cli/hs-install.mjs +628 -106
- package/dist/cli/hs-join.mjs +686 -236
- package/dist/cli/hs-vault-pull.mjs +3189 -179
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +396 -178
- package/dist/cli/hs-workspace.mjs +289 -151
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
- package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/package.json +20 -2
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/run-hook.mjs +20 -2
- package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
- package/plugins/heyspex/hooks/hooks.json +94 -37
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
- package/scripts/claude-hooks/_run.mjs +316 -23
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
- package/scripts/claude-hooks/session-end.mjs +2 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
- package/scripts/cursor-hooks/manifest.mjs +46 -0
- package/scripts/heyspex-mode.mjs +20 -0
- package/scripts/hs-mode.mjs +1 -1
- package/scripts/hs-view.mjs +180 -17
- package/scripts/install-claude-code-hooks.mjs +3 -1
- package/scripts/lib/console-url-resolver.mjs +3 -2
- package/scripts/lib/customer-kit.mjs +327 -10
- package/scripts/lib/dev-mode.mjs +7 -6
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-log.mjs +2 -1
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -356
- package/scripts/lib/window-placement.mjs +109 -0
- package/spexharness/src/adapters/claude-code.mjs +11 -3
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/intent-classifier.mjs +4 -2
- package/spexharness/src/intent-directive.mjs +626 -541
- package/spexharness/src/rule-table.mjs +31 -1
- package/vendor/heyspex-mcp/account-switch.mjs +6 -5
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
- package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
- package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
- package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
- package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
- package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
- package/vendor/heyspex-mcp/cli.mjs +7 -1
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
- package/vendor/heyspex-mcp/console-transport.mjs +7 -1
- package/vendor/heyspex-mcp/credentials.mjs +9 -8
- package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
- package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
- package/vendor/heyspex-mcp/http-transport.mjs +2 -0
- package/vendor/heyspex-mcp/list-tools.mjs +1 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
- package/vendor/heyspex-mcp/plan-push.mjs +3 -2
- package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
- package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
- package/vendor/heyspex-mcp/promote-research.mjs +6 -0
- package/vendor/heyspex-mcp/read-path.mjs +162 -6
- package/vendor/heyspex-mcp/request-context.mjs +25 -1
- package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/session-owner.mjs +3 -2
- package/vendor/heyspex-mcp/session-tools.mjs +10 -0
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
- package/vendor/heyspex-mcp/split-research.mjs +3 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
- package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
- package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
- package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
- package/vendor/heyspex-mcp/vault-io.mjs +720 -724
- package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
- package/vendor/heyspex-mcp/whoami.mjs +2 -1
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -50,8 +50,11 @@
|
|
|
50
50
|
import { createHash } from 'node:crypto';
|
|
51
51
|
import fs from 'node:fs';
|
|
52
52
|
import path from 'node:path';
|
|
53
|
+
import os from 'node:os';
|
|
53
54
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
54
55
|
|
|
56
|
+
import { dispositionFor } from './hook-dispositions.mjs';
|
|
57
|
+
|
|
55
58
|
/** @typedef {'customer' | 'platform'} KitScope */
|
|
56
59
|
|
|
57
60
|
/**
|
|
@@ -75,6 +78,7 @@ export const HOOK_SCOPES = Object.freeze({
|
|
|
75
78
|
'inject-communication-style': { scope: 'customer', why: 'Plain-language communication rules each turn.' },
|
|
76
79
|
'inject-vault-context': { scope: 'customer', why: 'Lexical vault retrieval for the prompt.' },
|
|
77
80
|
'git-identity-guard': { scope: 'customer', why: 'Agents never write git identity (owner ruling 2026-09-19).' },
|
|
81
|
+
'vault-write-tier-guard': { scope: 'customer', why: 'Cloud write mode: disk edits to vault specs are a mirror the console never reads back (HS-FCNH).' },
|
|
78
82
|
'settings-write-gate': { scope: 'customer', why: 'Approval before the vault\'s .heyspex/config.yaml changes.' },
|
|
79
83
|
'emit-claim-status': { scope: 'customer', why: 'Claim liveness heartbeat to the console.' },
|
|
80
84
|
'enforce-vault-naming': { scope: 'customer', why: 'Naming V3 filename guard (HS-RY2N).' },
|
|
@@ -82,7 +86,10 @@ export const HOOK_SCOPES = Object.freeze({
|
|
|
82
86
|
'plan-review-gate': { scope: 'customer', why: 'Plan review + QC markers before ExitPlanMode.' },
|
|
83
87
|
'plan-exit-ingest-gate': { scope: 'customer', why: 'Plan ingest on ExitPlanMode; arms claim-before-code.' },
|
|
84
88
|
'claim-before-code-gate': { scope: 'customer', why: 'First code edit after plan exit needs a claim.' },
|
|
89
|
+
'directive-obligation-gate': { scope: 'customer', why: 'A capture_on_idea directive leaves a durable obligation; an unmet one denies sub-agent spawns, product-code writes and builds, and re-drives Stop once. Also claim-before-build (HS-20SW).' },
|
|
85
90
|
'capture-workflow-gate': { scope: 'customer', why: 'No implementation spec straight out of an open capture.' },
|
|
91
|
+
'vault-mirror-catchup': { scope: 'customer', why: 'Cloud write mode: a SessionStart detached `hs pull --apply` closes a stale disk mirror before the first tool call (HS-XJFQ).' },
|
|
92
|
+
'diagnose-capture-reminder': { scope: 'customer', why: 'Warn when a diagnose is filed with no capture holding the owner\'s words (HS-IK2D).' },
|
|
86
93
|
'git-stash-gate': { scope: 'customer', why: 'Always ask before a mutating git stash.' },
|
|
87
94
|
'research-method-guard': { scope: 'customer', why: 'Research method guard (HS-EN4F).' },
|
|
88
95
|
'deep-research-cost-gate': { scope: 'customer', why: 'Cost confirmation before a paid deep-research launch.' },
|
|
@@ -102,6 +109,7 @@ export const HOOK_SCOPES = Object.freeze({
|
|
|
102
109
|
'coo-guard-check': { scope: 'platform', why: 'dev-factory breakdown-delegation guard.' },
|
|
103
110
|
'coo-denylist-gate': { scope: 'platform', why: 'dev-factory COO window deny list.' },
|
|
104
111
|
'agent-spawn-delegation-gate': { scope: 'platform', why: 'dev-factory seat routing for subagents.' },
|
|
112
|
+
'capture-execution-gate': { scope: 'platform', why: 'Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8).' },
|
|
105
113
|
'check-mcp-freshness-hook': { scope: 'platform', why: 'Freshness of the vendored MCP build — a platform-dev concern.' },
|
|
106
114
|
'dev-server-task-reminder': { scope: 'platform', why: 'spex-console dev server + Neon DB safety.' },
|
|
107
115
|
'env-vault-guard': { scope: 'platform', why: 'Guards heyspex-app\'s encrypted env vault paths.' },
|
|
@@ -138,6 +146,7 @@ export const CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
138
146
|
{ event: 'SessionStart', hook: 'check-claude-md-budget', timeout: 5 },
|
|
139
147
|
{ event: 'SessionStart', hook: 'check-mcp-freshness-hook', timeout: 5 },
|
|
140
148
|
{ event: 'SessionStart', hook: 'git-reflog-audit', timeout: 5 },
|
|
149
|
+
{ event: 'SessionStart', hook: 'vault-mirror-catchup', timeout: 5 },
|
|
141
150
|
{ event: 'PreCompact', hook: 'wip-checkpoint', timeout: 5 },
|
|
142
151
|
{ event: 'UserPromptSubmit', hook: 'preflight-reminder', timeout: 5 },
|
|
143
152
|
{ event: 'UserPromptSubmit', hook: 'intent-classifier-trigger', timeout: 5 },
|
|
@@ -145,10 +154,13 @@ export const CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
145
154
|
{ event: 'UserPromptSubmit', hook: 'inject-vault-context', timeout: 10 },
|
|
146
155
|
{ event: 'PreToolUse', matcher: 'Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell', hook: 'env-vault-guard', timeout: 5 },
|
|
147
156
|
{ event: 'PreToolUse', matcher: 'Bash|PowerShell', hook: 'git-identity-guard', timeout: 5 },
|
|
157
|
+
{ event: 'PreToolUse', matcher: 'Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell', hook: 'vault-write-tier-guard', timeout: 5 },
|
|
148
158
|
{ event: 'PreToolUse', matcher: 'Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set', hook: 'settings-write-gate', timeout: 5 },
|
|
149
159
|
{ event: 'PreToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PreToolUse'], timeout: 5 },
|
|
150
160
|
{ event: 'PreToolUse', matcher: '.*', hook: 'coo-denylist-gate', timeout: 5 },
|
|
151
161
|
{ event: 'PreToolUse', matcher: 'Agent|Task', hook: 'agent-spawn-delegation-gate', timeout: 5 },
|
|
162
|
+
{ event: 'PreToolUse', matcher: 'Bash|PowerShell|Agent|Task', hook: 'capture-execution-gate', timeout: 5 },
|
|
163
|
+
{ event: 'PreToolUse', matcher: 'Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell', hook: 'directive-obligation-gate', timeout: 5 },
|
|
152
164
|
{ event: 'PreToolUse', matcher: 'mcp__heyspex__heyspex_claim_spec', hook: 'coo-guard-check', timeout: 5 },
|
|
153
165
|
{ event: 'PreToolUse', matcher: 'Write|NotebookEdit|Bash|PowerShell', hook: 'enforce-vault-naming', timeout: 5 },
|
|
154
166
|
{ event: 'PreToolUse', matcher: 'Write|NotebookEdit|Bash|PowerShell', hook: 'guard-auto-memory-writes', timeout: 5 },
|
|
@@ -157,6 +169,7 @@ export const CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
157
169
|
{ event: 'PreToolUse', matcher: 'ExitPlanMode', hook: 'plan-exit-ingest-gate', timeout: 5 },
|
|
158
170
|
{ event: 'PreToolUse', matcher: 'Edit|MultiEdit|Write', hook: 'claim-before-code-gate', timeout: 5 },
|
|
159
171
|
{ event: 'PreToolUse', matcher: 'mcp__heyspex__heyspex_file_spec', hook: 'capture-workflow-gate', timeout: 8 },
|
|
172
|
+
{ event: 'PreToolUse', matcher: 'mcp__heyspex__heyspex_file_spec', hook: 'diagnose-capture-reminder', timeout: 5 },
|
|
160
173
|
{ event: 'PreToolUse', matcher: 'Bash|PowerShell|mcp__neon__prepare_database_migration|mcp__neon__complete_database_migration|mcp__neon__reset_from_parent|mcp__neon__delete_branch|mcp__Neon__prepare_database_migration|mcp__Neon__complete_database_migration|mcp__Neon__reset_from_parent|mcp__Neon__delete_branch', hook: 'dev-server-task-reminder', timeout: 10 },
|
|
161
174
|
{ event: 'PreToolUse', matcher: 'Bash|PowerShell', hook: 'git-stash-gate', timeout: 10 },
|
|
162
175
|
{ event: 'PreToolUse', matcher: 'Skill|Workflow', hook: 'research-method-guard', timeout: 5 },
|
|
@@ -169,7 +182,9 @@ export const CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
169
182
|
{ event: 'Stop', hook: 'epic-milestone-phrasing-gate', timeout: 5 },
|
|
170
183
|
{ event: 'Stop', hook: 'git-reflog-audit', timeout: 5 },
|
|
171
184
|
{ event: 'Stop', hook: 'enforce-session-sequence', timeout: 5 },
|
|
185
|
+
{ event: 'Stop', hook: 'directive-obligation-gate', timeout: 5 },
|
|
172
186
|
{ event: 'PostToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PostToolUse'], timeout: 5 },
|
|
187
|
+
{ event: 'PostToolUse', matcher: 'Skill|mcp__heyspex__heyspex_update_capture|mcp__heyspex__heyspex_promote_capture.*|mcp__heyspex__heyspex_file_spec|mcp__heyspex__heyspex_claim_spec|mcp__heyspex__heyspex_release_spec|mcp__heyspex__heyspex_defer_spec', hook: 'directive-obligation-gate', timeout: 5 },
|
|
173
188
|
{ event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', hook: 'notify-status-flip', timeout: 5 },
|
|
174
189
|
{ event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', hook: 'close-terminal-on-ship', timeout: 5 },
|
|
175
190
|
{ event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', hook: 'scripts/stamp-provenance-on-edit.mjs', timeout: 5 },
|
|
@@ -305,11 +320,73 @@ export const CUSTOMER_LAUNCHERS = Object.freeze({
|
|
|
305
320
|
*/
|
|
306
321
|
export function customerRunnerPrefix(_platformRoot, surface = 'claude') {
|
|
307
322
|
const l = CUSTOMER_LAUNCHERS[surface];
|
|
323
|
+
// Claude rows are self-locating (Copilot CLI loads .claude/settings.json and does not document CLAUDE_PROJECT_DIR).
|
|
324
|
+
if (surface === 'claude') return CUSTOMER_RUNNER_PREFIX;
|
|
308
325
|
return `node "$${l.projectDirEnv}/${l.rel.join('/')}"`;
|
|
309
326
|
}
|
|
310
327
|
|
|
311
328
|
/** Runner prefix inside the plugin: its own launcher, which finds the platform. */
|
|
312
|
-
|
|
329
|
+
const BOOTSTRAP_WALK =
|
|
330
|
+
"var d=process.cwd(),r=e.CLAUDE_PROJECT_DIR;if(!r||!f.existsSync(p.join(r,...n))){r='';for(;;){if(f.existsSync(p.join(d,...n))){r=d;break}var q=p.dirname(d);if(q===d)break;d=q}}";
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* SELF-LOCATING BOOTSTRAP ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]], Copilot compatibility).
|
|
334
|
+
*
|
|
335
|
+
* GitHub Copilot CLI runs the hooks in .claude/settings.json too, but it is not documented to set
|
|
336
|
+
* CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT, and a PowerShell-run command expands an unset $VAR to
|
|
337
|
+
* nothing - so \`node "$CLAUDE_PROJECT_DIR/scripts/claude-hooks/_run.mjs"\` became \`node /scripts/...\`,
|
|
338
|
+
* MODULE_NOT_FOUND, exit 1, and Copilot turns ANY non-zero exit into a DENY (on a \`.*\` matcher: every tool
|
|
339
|
+
* call). A registered command must therefore (a) work with no env var at all, under bash AND PowerShell, and
|
|
340
|
+
* (b) FAIL OPEN when it cannot find the runtime. It is an inline \`node -e\` script, single-quoted JS only
|
|
341
|
+
* (no double quote, backslash, dollar or backtick), so it survives JSON and every shell unchanged:
|
|
342
|
+
* root = CLAUDE_PROJECT_DIR if it holds the shim, else walk up from cwd looking for scripts/claude-hooks/_run.mjs;
|
|
343
|
+
* not found -> exit 0; found -> import the shim in-process with the same argv and stdin (the shim owns both).
|
|
344
|
+
* The plugin variant first tries <CLAUDE_PLUGIN_ROOT|PLUGIN_ROOT>/bin/run-hook.mjs. The trailing //comment is
|
|
345
|
+
* deliberate: it keeps the recognisable ...scripts/claude-hooks/_run.mjs" <hook> tail every drift check,
|
|
346
|
+
* ourHookTail and the doctor already parse.
|
|
347
|
+
* @param {{ plugin?: boolean }} [opts]
|
|
348
|
+
*/
|
|
349
|
+
export function buildBootstrap({ plugin = false } = {}) {
|
|
350
|
+
const walk = BOOTSTRAP_WALK;
|
|
351
|
+
const body = plugin
|
|
352
|
+
? "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='',m=e.CLAUDE_PLUGIN_ROOT||e.COPILOT_PLUGIN_ROOT||e.PLUGIN_ROOT;" +
|
|
353
|
+
"if(m&&f.existsSync(p.join(m,'bin','run-hook.mjs')))s=p.join(m,'bin','run-hook.mjs');" +
|
|
354
|
+
"if(!s){" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}}"
|
|
355
|
+
: "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='';" +
|
|
356
|
+
walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}";
|
|
357
|
+
return (
|
|
358
|
+
body +
|
|
359
|
+
"if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));" +
|
|
360
|
+
"import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});" +
|
|
361
|
+
(plugin ? '//bin/run-hook.mjs' : '//scripts/claude-hooks/_run.mjs')
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Platform checkout runner (.claude/settings.json): self-locating, env-independent, fail-open. */
|
|
366
|
+
export const PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
|
|
367
|
+
|
|
368
|
+
/** Runner prefix inside the plugin: self-locating too (plugin root env, then cwd walk-up), fail-open. */
|
|
369
|
+
export const PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Customer-repo bootstrap (same defect, same fix as {@link buildBootstrap}): the committed launcher
|
|
373
|
+
* `.claude/heyspex/run-hook.mjs` is resolved from CLAUDE_PROJECT_DIR if it holds the launcher, else by walking up
|
|
374
|
+
* from cwd, else exit 0 (fail open). Sets CLAUDE_PROJECT_DIR to the found root so the shim reads
|
|
375
|
+
* <project>/.heyspex/mode.json (`hs mode off`). Single-quoted JS only; survives JSON, bash and PowerShell.
|
|
376
|
+
*/
|
|
377
|
+
export function buildCustomerBootstrap() {
|
|
378
|
+
const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(',');
|
|
379
|
+
return (
|
|
380
|
+
"var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=[" + rel + "],s='';" +
|
|
381
|
+
BOOTSTRAP_WALK + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}" +
|
|
382
|
+
"if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));" +
|
|
383
|
+
"import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});" +
|
|
384
|
+
'//' + CUSTOMER_LAUNCHERS.claude.rel.join('/')
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Customer repo runner (.claude/settings.json): self-locating, env-independent, fail-open. */
|
|
389
|
+
export const CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
|
|
313
390
|
|
|
314
391
|
// The launcher is kept as ONE template string (not a sibling file) so the
|
|
315
392
|
// bundled CLI (`dist/cli/*.mjs`, built from install.ts) carries it by
|
|
@@ -392,7 +469,12 @@ const LAUNCHER_TEMPLATE = [
|
|
|
392
469
|
'// (the Cursor one) runs exactly as if Cursor had invoked it directly.',
|
|
393
470
|
'const shim = resolve(root, ...SHIM);',
|
|
394
471
|
'process.argv[1] = shim;',
|
|
395
|
-
'
|
|
472
|
+
'try {',
|
|
473
|
+
' await import(pathToFileURL(shim).href);',
|
|
474
|
+
'} catch (err) {',
|
|
475
|
+
" process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
|
|
476
|
+
' process.exit(0);',
|
|
477
|
+
'}',
|
|
396
478
|
'',
|
|
397
479
|
].join('\n');
|
|
398
480
|
|
|
@@ -576,15 +658,10 @@ export function renderPluginHooksDocument() {
|
|
|
576
658
|
hooks: [{ type: 'command', command: d.command, timeout: d.timeout }],
|
|
577
659
|
});
|
|
578
660
|
}
|
|
661
|
+
// No top-level `_comment`: GitHub Copilot loads this file too and a strict reader may reject unknown keys. The
|
|
662
|
+
// provenance note lives in plugins/heyspex/README.md (GENERATED from this file's customer-scoped rows; edit
|
|
663
|
+
// scripts/lib/customer-kit.mjs and run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`).
|
|
579
664
|
return {
|
|
580
|
-
_comment:
|
|
581
|
-
'GENERATED from scripts/lib/customer-kit.mjs (customer-scoped rows only) — do not edit by hand; ' +
|
|
582
|
-
'run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`. Every command goes through ' +
|
|
583
|
-
'bin/run-hook.mjs, which finds the hook runtime (HEYSPEX_PLATFORM_ROOT / HEYSPEX_MCP_ENTRY when set ' +
|
|
584
|
-
'in the machine\'s own env, then the plugin\'s own location, node_modules/heyspex, ' +
|
|
585
|
-
'$CLAUDE_PROJECT_DIR) — never $CLAUDE_PROJECT_DIR/scripts, which only exists in heyspex-app. ' +
|
|
586
|
-
'WARNING: a repo whose .claude/settings.json already carries the hs install kit hooks runs every ' +
|
|
587
|
-
'hook twice if this plugin is also enabled — use one or the other.',
|
|
588
665
|
hooks,
|
|
589
666
|
};
|
|
590
667
|
}
|
|
@@ -593,6 +670,230 @@ export function renderPluginHooksText() {
|
|
|
593
670
|
return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}\n`;
|
|
594
671
|
}
|
|
595
672
|
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
// GitHub Copilot cloud agent: .github/hooks/heyspex.json
|
|
675
|
+
// ---------------------------------------------------------------------------
|
|
676
|
+
// The cloud agent loads NO plugins and no user-level hook files; its only hook source is
|
|
677
|
+
// `.github/hooks/*.json` on the default branch. This is the one place a generated Copilot
|
|
678
|
+
// manifest is justified ([[HS-0GBS-CTF8-RES-copilot-plugin-parity-with-the-spexcode]], option 3).
|
|
679
|
+
// Generated from the SAME customer rows as hooks.json, restricted to the hooks whose Copilot
|
|
680
|
+
// disposition (scripts/lib/hook-dispositions.mjs) is 'mapped'. The cloud agent is Linux bash only,
|
|
681
|
+
// so every entry is `type: "command"` with a `bash` command; no PowerShell, no node -e bootstrap.
|
|
682
|
+
|
|
683
|
+
/** Claude hook event -> Copilot hooks event (camelCase, as documented for the CLI and the cloud agent). */
|
|
684
|
+
export const COPILOT_EVENT_NAMES = Object.freeze({
|
|
685
|
+
SessionStart: 'sessionStart',
|
|
686
|
+
SessionEnd: 'sessionEnd',
|
|
687
|
+
UserPromptSubmit: 'userPromptSubmitted',
|
|
688
|
+
PreToolUse: 'preToolUse',
|
|
689
|
+
PostToolUse: 'postToolUse',
|
|
690
|
+
Stop: 'agentStop',
|
|
691
|
+
PreCompact: 'preCompact',
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Hooks shipped to the cloud agent although their Copilot disposition is 'advisory-degraded': their
|
|
696
|
+
* prompt-time OUTPUT is dropped, but they still run and ARM state (the HS-20SW capture obligation), and
|
|
697
|
+
* the 'mapped' directive-obligation-gate is inert without that arming.
|
|
698
|
+
*/
|
|
699
|
+
export const GITHUB_HOOKS_STATE_ARMING = Object.freeze(['intent-classifier-trigger']);
|
|
700
|
+
|
|
701
|
+
/** Claude tool name -> Copilot tool name (documented: ask_user bash create edit glob grep powershell task view web_fetch). */
|
|
702
|
+
const COPILOT_TOOL_NAMES = Object.freeze({
|
|
703
|
+
Write: 'create',
|
|
704
|
+
Edit: 'edit',
|
|
705
|
+
MultiEdit: 'edit',
|
|
706
|
+
NotebookEdit: 'edit',
|
|
707
|
+
Bash: 'bash',
|
|
708
|
+
PowerShell: 'powershell',
|
|
709
|
+
Agent: 'task',
|
|
710
|
+
Task: 'task',
|
|
711
|
+
WebFetch: 'web_fetch',
|
|
712
|
+
WebSearch: 'web_search',
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Translate a Claude matcher to Copilot tool names. MCP tools become a suffix regex because the MCP
|
|
717
|
+
* tool-name spelling inside hook payloads is not documented (unverified live); tools with no Copilot
|
|
718
|
+
* counterpart (Skill, Workflow, ExitPlanMode) are dropped.
|
|
719
|
+
* @param {string | undefined} matcher
|
|
720
|
+
* @returns {string | null | undefined} undefined = no matcher, null = nothing translatable
|
|
721
|
+
*/
|
|
722
|
+
export function toCopilotMatcher(matcher) {
|
|
723
|
+
if (!matcher) return undefined;
|
|
724
|
+
const out = [];
|
|
725
|
+
for (const part of matcher.split('|')) {
|
|
726
|
+
let t = null;
|
|
727
|
+
if (part === '.*') t = '.*';
|
|
728
|
+
else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
|
|
729
|
+
else if (part.startsWith(MCP_PROJECT_PREFIX)) t = '.*' + part.slice(MCP_PROJECT_PREFIX.length);
|
|
730
|
+
else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = '.*' + part.slice(MCP_PLUGIN_PREFIX.length);
|
|
731
|
+
if (t && !out.includes(t)) out.push(t);
|
|
732
|
+
}
|
|
733
|
+
if (out.includes('.*')) return '.*';
|
|
734
|
+
return out.length ? out.join('|') : null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Hooks the cloud-agent manifest carries (see the module comment above). */
|
|
738
|
+
export function githubHookNames() {
|
|
739
|
+
const names = new Set();
|
|
740
|
+
for (const r of customerHookRows()) {
|
|
741
|
+
if (dispositionFor(r.hook, 'copilot')?.disposition === 'mapped' || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
|
|
742
|
+
}
|
|
743
|
+
return names;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* The bash command for one hook: resolve the runtime from node_modules/heyspex first, then the checkout's own
|
|
748
|
+
* scripts/, else exit 0 (fail open; Copilot turns any non-zero exit into a deny).
|
|
749
|
+
* @param {{ hook: string, args?: string[] }} row
|
|
750
|
+
*/
|
|
751
|
+
export function githubHookCommand(row) {
|
|
752
|
+
const tail = hookTail(row);
|
|
753
|
+
return (
|
|
754
|
+
'R=node_modules/heyspex/scripts/claude-hooks/_run.mjs; ' +
|
|
755
|
+
'[ -f "$R" ] || R=./scripts/claude-hooks/_run.mjs; ' +
|
|
756
|
+
'[ -f "$R" ] || exit 0; ' +
|
|
757
|
+
`exec node "$R" ${tail}`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** The cloud agent's .github/hooks/heyspex.json document, generated from the customer rows. */
|
|
762
|
+
export function renderGithubHooksDocument() {
|
|
763
|
+
const allowed = githubHookNames();
|
|
764
|
+
const hooks = {};
|
|
765
|
+
for (const row of customerHookRows()) {
|
|
766
|
+
if (!allowed.has(row.hook)) continue;
|
|
767
|
+
const event = COPILOT_EVENT_NAMES[row.event];
|
|
768
|
+
if (!event) continue;
|
|
769
|
+
const matcher = toCopilotMatcher(row.matcher);
|
|
770
|
+
if (matcher === null) continue; // a tool-scoped row with no Copilot tool to match
|
|
771
|
+
(hooks[event] ??= []).push({
|
|
772
|
+
type: 'command',
|
|
773
|
+
...(matcher ? { matcher } : {}),
|
|
774
|
+
bash: githubHookCommand(row),
|
|
775
|
+
timeoutSec: row.timeout,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return { version: 1, hooks };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export function renderGithubHooksText() {
|
|
782
|
+
return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}\n`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
786
|
+
// Double-firing detector (plugin AND repo hooks)
|
|
787
|
+
// ---------------------------------------------------------------------------
|
|
788
|
+
// Copilot CLI reads hooks from the plugin AND from .claude/settings.json / .github/hooks in the repo, and
|
|
789
|
+
// Claude Code does the same for plugin + .claude/settings.json: with both present every hook runs twice.
|
|
790
|
+
// Rule: pick one of plugin OR repo hooks. This detector is pure (injected fs), so the doctor and a test share it.
|
|
791
|
+
|
|
792
|
+
const readJsonFile = (deps, file) => {
|
|
793
|
+
try {
|
|
794
|
+
const text = deps.readFile(file);
|
|
795
|
+
return text == null ? undefined : JSON.parse(text);
|
|
796
|
+
} catch {
|
|
797
|
+
return null; // present but unparseable
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
/** @param {unknown} settings */
|
|
802
|
+
function countOurRows(settings) {
|
|
803
|
+
let n = 0;
|
|
804
|
+
const hooks = settings && typeof settings === 'object' && settings.hooks && typeof settings.hooks === 'object' ? settings.hooks : {};
|
|
805
|
+
for (const groups of Object.values(hooks)) {
|
|
806
|
+
for (const g of Array.isArray(groups) ? groups : []) {
|
|
807
|
+
// Claude shape: { matcher, hooks: [{ command }] }; Copilot .github/hooks shape: { bash } / { command }.
|
|
808
|
+
const cmds = Array.isArray(g?.hooks) ? g.hooks.map((h) => h?.command) : [g?.command, g?.bash];
|
|
809
|
+
for (const c of cmds) if (typeof c === 'string' && (ourHookTail(c) !== null || /claude-hooks\/_run\.mjs/.test(c))) n++;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return n;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Whether BOTH repo-level HeySpex hooks AND an installed heyspex plugin are present for a project.
|
|
817
|
+
*
|
|
818
|
+
* Repo hooks: HeySpex rows in <project>/.claude/settings.json, .claude/settings.local.json or
|
|
819
|
+
* <project>/.github/hooks/*.json. Plugin evidence (any of): ~/.claude/plugins/installed_plugins.json with a
|
|
820
|
+
* "heyspex@..." key; `enabledPlugins` naming heyspex in ~/.claude/settings.json or the project's Claude settings;
|
|
821
|
+
* a heyspex directory under ~/.copilot/installed-plugins or ~/.copilot/plugins (best effort: the Copilot plugin
|
|
822
|
+
* list file is not documented, `copilot plugin list` is the authority). If no plugin registry can be read at all
|
|
823
|
+
* and repo hooks exist, the answer is 'unknown' (never a failure).
|
|
824
|
+
* @param {{ targetRoot: string, home?: string, deps?: { readFile: (p: string) => string | null, listDir?: (p: string) => string[] } }} opts
|
|
825
|
+
* @returns {{ status: 'ok' | 'warn' | 'unknown', detail: string, repoHooks: number, plugin: boolean | null, evidence: string[] }}
|
|
826
|
+
*/
|
|
827
|
+
export function detectDoubleFire({ targetRoot, home = os.homedir(), deps = fsDeps }) {
|
|
828
|
+
const evidence = [];
|
|
829
|
+
const repoFiles = ['.claude/settings.json', '.claude/settings.local.json'].map((f) => path.join(targetRoot, f));
|
|
830
|
+
let repoHooks = 0;
|
|
831
|
+
for (const f of repoFiles) {
|
|
832
|
+
const n = countOurRows(readJsonFile(deps, f));
|
|
833
|
+
if (n) { repoHooks += n; evidence.push(`${n} HeySpex hook row(s) in ${f}`); }
|
|
834
|
+
}
|
|
835
|
+
const ghDir = path.join(targetRoot, '.github', 'hooks');
|
|
836
|
+
for (const name of (deps.listDir ? deps.listDir(ghDir) : []).filter((x) => x.endsWith('.json'))) {
|
|
837
|
+
const n = countOurRows(readJsonFile(deps, path.join(ghDir, name)));
|
|
838
|
+
if (n) { repoHooks += n; evidence.push(`${n} HeySpex hook row(s) in .github/hooks/${name}`); }
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
let readable = 0;
|
|
842
|
+
let plugin = false;
|
|
843
|
+
const installed = readJsonFile(deps, path.join(home, '.claude', 'plugins', 'installed_plugins.json'));
|
|
844
|
+
if (installed !== undefined) {
|
|
845
|
+
readable++;
|
|
846
|
+
const keys = Object.keys(installed?.plugins ?? installed ?? {});
|
|
847
|
+
if (keys.some((k) => /^heyspex(@|$)/i.test(k))) { plugin = true; evidence.push('heyspex in ~/.claude/plugins/installed_plugins.json'); }
|
|
848
|
+
}
|
|
849
|
+
const userSettings = path.join(home, '.claude', 'settings.json');
|
|
850
|
+
for (const f of [userSettings, ...repoFiles]) {
|
|
851
|
+
const st = readJsonFile(deps, f);
|
|
852
|
+
if (st === undefined) continue;
|
|
853
|
+
if (f === userSettings) readable++; // a project file proves nothing about what is installed elsewhere
|
|
854
|
+
const ep = st?.enabledPlugins;
|
|
855
|
+
if (ep && typeof ep === 'object' && Object.entries(ep).some(([k, v]) => /^heyspex(@|$)/i.test(k) && v !== false)) {
|
|
856
|
+
plugin = true;
|
|
857
|
+
evidence.push(`heyspex in enabledPlugins of ${f}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (deps.listDir) {
|
|
861
|
+
for (const sub of ['installed-plugins', 'plugins']) {
|
|
862
|
+
const dir = path.join(home, '.copilot', sub);
|
|
863
|
+
const names = deps.listDir(dir);
|
|
864
|
+
if (names.length) readable++;
|
|
865
|
+
if (names.some((n) => /heyspex/i.test(n))) {
|
|
866
|
+
plugin = true;
|
|
867
|
+
evidence.push(`heyspex under ${dir}`);
|
|
868
|
+
} else {
|
|
869
|
+
// one level deeper: <marketplace>/<plugin> layouts
|
|
870
|
+
for (const n of names) {
|
|
871
|
+
if (deps.listDir(path.join(dir, n)).some((m) => /heyspex/i.test(m))) {
|
|
872
|
+
plugin = true;
|
|
873
|
+
evidence.push(`heyspex under ${path.join(dir, n)}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (repoHooks === 0) return { status: 'ok', detail: 'no repo-level HeySpex hooks to double up with a plugin', repoHooks, plugin: plugin || null, evidence };
|
|
881
|
+
if (plugin) {
|
|
882
|
+
return {
|
|
883
|
+
status: 'warn',
|
|
884
|
+
detail: `HeySpex hooks are registered in the repo AND the heyspex plugin is installed, so every hook fires twice (${evidence.join('; ')})`,
|
|
885
|
+
repoHooks,
|
|
886
|
+
plugin: true,
|
|
887
|
+
evidence,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (readable === 0) {
|
|
891
|
+
return { status: 'unknown', detail: 'repo-level HeySpex hooks present, but no plugin registry could be read to rule out a second copy', repoHooks, plugin: null, evidence };
|
|
892
|
+
}
|
|
893
|
+
return { status: 'ok', detail: 'repo-level hooks only; no heyspex plugin found', repoHooks, plugin: false, evidence };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
|
|
596
897
|
// ---------------------------------------------------------------------------
|
|
597
898
|
// Harness commands (.claude/commands, .cursor/commands)
|
|
598
899
|
// ---------------------------------------------------------------------------
|
|
@@ -880,6 +1181,22 @@ if (invokedDirectly(import.meta.url, 'customer-kit.mjs')) {
|
|
|
880
1181
|
process.exit(1);
|
|
881
1182
|
}
|
|
882
1183
|
console.log('customer-kit: plugins/heyspex/hooks/hooks.json up to date');
|
|
1184
|
+
} else if (process.argv.includes('--write-github-hooks') || process.argv.includes('--check-github-hooks')) {
|
|
1185
|
+
const ghTarget = path.join(platformRoot, '.github', 'hooks', 'heyspex.json');
|
|
1186
|
+
const ghWant = renderGithubHooksText();
|
|
1187
|
+
const ghSame = (fsDeps.readFile(ghTarget) ?? '').replace(/\r\n/g, '\n') === ghWant;
|
|
1188
|
+
if (process.argv.includes('--write-github-hooks')) {
|
|
1189
|
+
if (!ghSame) {
|
|
1190
|
+
fs.mkdirSync(path.dirname(ghTarget), { recursive: true });
|
|
1191
|
+
fs.writeFileSync(ghTarget, ghWant, 'utf8');
|
|
1192
|
+
}
|
|
1193
|
+
console.log(`customer-kit: ${ghSame ? 'unchanged' : 'wrote'} ${path.relative(platformRoot, ghTarget).replace(/\\/g, '/')}`);
|
|
1194
|
+
} else if (!ghSame) {
|
|
1195
|
+
console.error('customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`');
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
} else {
|
|
1198
|
+
console.log('customer-kit: .github/hooks/heyspex.json up to date');
|
|
1199
|
+
}
|
|
883
1200
|
} else {
|
|
884
1201
|
console.log('usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks');
|
|
885
1202
|
}
|
package/scripts/lib/dev-mode.mjs
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
*
|
|
48
48
|
* Zero deps beyond node builtins (same rule as console-url-resolver.mjs).
|
|
49
49
|
*/
|
|
50
|
+
import { heyspexHome } from './heyspex-home.mjs';
|
|
50
51
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
51
52
|
import { homedir } from 'node:os';
|
|
52
53
|
import { join } from 'node:path';
|
|
@@ -58,7 +59,7 @@ export const LOCAL_CONSOLE_URL = 'http://localhost:3000';
|
|
|
58
59
|
export const DEV_MODE_FILENAME = 'dev-mode.json';
|
|
59
60
|
|
|
60
61
|
/** Absolute path of the machine-local record. */
|
|
61
|
-
export function devModeFilePath(homeDir =
|
|
62
|
+
export function devModeFilePath(homeDir = heyspexHome()) {
|
|
62
63
|
return join(homeDir, '.heyspex', DEV_MODE_FILENAME);
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -97,7 +98,7 @@ const defaultReadFile = (p) => readFileSync(p, 'utf8');
|
|
|
97
98
|
* `readFile` is an injectable seam (same shape as the TS twin's) so the
|
|
98
99
|
* parity test can feed both implementations the SAME in-memory fixture.
|
|
99
100
|
*/
|
|
100
|
-
export function readLocalDevModeRecord({ homeDir =
|
|
101
|
+
export function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
|
|
101
102
|
let raw;
|
|
102
103
|
try {
|
|
103
104
|
raw = readFile(devModeFilePath(homeDir));
|
|
@@ -135,7 +136,7 @@ export function readLocalDevModeRecord({ homeDir = homedir(), readFile = default
|
|
|
135
136
|
*/
|
|
136
137
|
export function resolveDevMode({
|
|
137
138
|
env = process.env,
|
|
138
|
-
homeDir =
|
|
139
|
+
homeDir = heyspexHome(),
|
|
139
140
|
readFile = defaultReadFile,
|
|
140
141
|
cloudSetting = undefined,
|
|
141
142
|
} = {}) {
|
|
@@ -382,7 +383,7 @@ export function upsertCredentialsYamlKeys(homeDir, updates) {
|
|
|
382
383
|
* its historical name for the hook scripts that import it; the precedence is
|
|
383
384
|
* the shared module's, never re-implemented here.
|
|
384
385
|
*/
|
|
385
|
-
export function readCredentialsMcpToken({ env = process.env, homeDir =
|
|
386
|
+
export function readCredentialsMcpToken({ env = process.env, homeDir = heyspexHome() } = {}) {
|
|
386
387
|
return readBearer({ env, homeDir }).token;
|
|
387
388
|
}
|
|
388
389
|
|
|
@@ -432,7 +433,7 @@ async function discardResponseBody(res) {
|
|
|
432
433
|
export async function fetchCloudDevMode({
|
|
433
434
|
consoleUrl,
|
|
434
435
|
fetchImpl = fetch,
|
|
435
|
-
homeDir =
|
|
436
|
+
homeDir = heyspexHome(),
|
|
436
437
|
timeoutMs = 4000,
|
|
437
438
|
} = {}) {
|
|
438
439
|
if (!consoleUrl) return { ok: false, setting: undefined, error: 'no console url' };
|
|
@@ -488,7 +489,7 @@ export class DevModeNotAdminError extends Error {
|
|
|
488
489
|
export async function applyDevMode(
|
|
489
490
|
target,
|
|
490
491
|
{
|
|
491
|
-
homeDir =
|
|
492
|
+
homeDir = heyspexHome(),
|
|
492
493
|
fetchImpl = fetch,
|
|
493
494
|
source = 'cli',
|
|
494
495
|
now = () => new Date().toISOString(),
|