@hegemonart/get-design-done 1.57.1 → 1.57.3
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 +26 -41
- package/.claude-plugin/plugin.json +23 -48
- package/CHANGELOG.md +139 -0
- package/README.md +166 -511
- package/SKILL.md +4 -6
- package/agents/README.md +33 -36
- package/agents/a11y-mapper.md +3 -3
- package/agents/component-benchmark-harvester.md +6 -6
- package/agents/component-benchmark-synthesizer.md +3 -3
- package/agents/compose-executor.md +3 -3
- package/agents/cost-forecaster.md +2 -2
- package/agents/design-auditor.md +7 -7
- package/agents/design-authority-watcher.md +15 -15
- package/agents/design-context-builder.md +4 -4
- package/agents/design-context-checker-gate.md +1 -1
- package/agents/design-discussant.md +2 -2
- package/agents/design-doc-writer.md +1 -1
- package/agents/design-executor.md +2 -2
- package/agents/design-figma-writer.md +2 -2
- package/agents/design-fixer.md +7 -7
- package/agents/design-integration-checker-gate.md +1 -1
- package/agents/design-integration-checker.md +1 -1
- package/agents/design-paper-writer.md +3 -3
- package/agents/design-pencil-writer.md +1 -1
- package/agents/design-planner.md +21 -0
- package/agents/design-reflector.md +39 -39
- package/agents/design-research-synthesizer.md +1 -0
- package/agents/design-start-writer.md +1 -1
- package/agents/design-update-checker.md +5 -5
- package/agents/design-verifier-gate.md +1 -1
- package/agents/design-verifier.md +52 -48
- package/agents/ds-generator.md +2 -2
- package/agents/ds-migration-planner.md +4 -4
- package/agents/email-executor.md +9 -9
- package/agents/experiment-result-ingester.md +3 -3
- package/agents/flutter-executor.md +5 -5
- package/agents/gdd-graph-refresh.md +3 -3
- package/agents/gdd-intel-updater.md +2 -2
- package/agents/motion-mapper.md +2 -2
- package/agents/motion-verifier.md +4 -4
- package/agents/pdf-executor.md +8 -8
- package/agents/perf-analyzer.md +17 -17
- package/agents/pr-commenter.md +9 -9
- package/agents/prototype-gate.md +2 -2
- package/agents/quality-gate-runner.md +1 -1
- package/agents/rollout-coordinator.md +3 -3
- package/agents/swift-executor.md +4 -4
- package/agents/ticket-sync-agent.md +6 -6
- package/agents/user-research-synthesizer.md +2 -2
- package/connections/connections.md +44 -45
- package/connections/cursor.md +72 -0
- package/connections/preview.md +3 -3
- package/hooks/first-run-nudge.cjs +171 -0
- package/hooks/gdd-intel-trigger.js +243 -0
- package/hooks/gdd-mcp-circuit-breaker.js +62 -7
- package/hooks/gdd-precompact-snapshot.js +50 -29
- package/hooks/gdd-protected-paths.js +150 -18
- package/hooks/gdd-risk-gate.js +93 -1
- package/hooks/gdd-sessionstart-recap.js +59 -24
- package/hooks/hooks.json +13 -4
- package/hooks/inject-using-gdd.cjs +188 -0
- package/hooks/update-check.cjs +511 -0
- package/package.json +9 -3
- package/reference/STATE-TEMPLATE.md +10 -13
- package/reference/audit-scoring.md +1 -1
- package/reference/cache-tier-doctrine.md +46 -0
- package/reference/config-schema.md +9 -9
- package/reference/i18n.md +1 -1
- package/reference/intel-schema.md +37 -2
- package/reference/meta-rules.md +4 -4
- package/reference/model-tiers.md +2 -2
- package/reference/registry.json +101 -94
- package/reference/runtime-models.md +11 -1
- package/reference/shared-preamble.md +13 -14
- package/reference/skill-graph.md +22 -3
- package/scripts/bootstrap.cjs +373 -0
- package/scripts/injection-patterns.cjs +58 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +57 -26
- package/scripts/lib/install/converters/codex-plugin.cjs +5 -2
- package/scripts/lib/install/converters/cursor.cjs +20 -0
- package/scripts/lib/issue-reporter/report-flow.cjs +1 -1
- package/scripts/lib/manifest/skills.json +75 -28
- package/scripts/lib/state/query-surface.cjs +67 -9
- package/scripts/lib/state/state-store.cjs +68 -26
- package/scripts/lib/worktree-resolve.cjs +4 -16
- package/sdk/cli/commands/stage.ts +17 -0
- package/sdk/cli/index.js +14 -0
- package/skills/README.md +46 -0
- package/skills/bootstrap-ds/SKILL.md +1 -1
- package/skills/cache-manager/SKILL.md +3 -3
- package/skills/cache-manager/cache-policy.md +1 -1
- package/skills/compare/SKILL.md +1 -1
- package/skills/design/SKILL.md +19 -0
- package/skills/explore/SKILL.md +11 -0
- package/skills/figma-write/SKILL.md +13 -2
- package/skills/new-cycle/SKILL.md +1 -1
- package/skills/paper-write/SKILL.md +54 -0
- package/skills/peer-cli-customize/SKILL.md +0 -1
- package/skills/peers/SKILL.md +1 -1
- package/skills/pencil-write/SKILL.md +54 -0
- package/skills/reflect/procedures/capability-gap-scan.md +0 -1
- package/skills/report-issue/SKILL.md +2 -2
- package/skills/report-issue/report-issue-procedure.md +0 -1
- package/skills/router/SKILL.md +2 -2
- package/skills/synthesize/SKILL.md +1 -1
- package/skills/turn-closeout/SKILL.md +1 -1
- package/skills/verify/verify-procedure.md +10 -11
- package/skills/warm-cache/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/add-backlog/SKILL.md +0 -48
- package/dist/claude-code/.claude/skills/analyze-dependencies/SKILL.md +0 -95
- package/dist/claude-code/.claude/skills/apply-reflections/SKILL.md +0 -109
- package/dist/claude-code/.claude/skills/apply-reflections/apply-reflections-procedure.md +0 -170
- package/dist/claude-code/.claude/skills/audit/SKILL.md +0 -79
- package/dist/claude-code/.claude/skills/bandit-status/SKILL.md +0 -94
- package/dist/claude-code/.claude/skills/benchmark/SKILL.md +0 -65
- package/dist/claude-code/.claude/skills/bootstrap-ds/SKILL.md +0 -43
- package/dist/claude-code/.claude/skills/brief/SKILL.md +0 -145
- package/dist/claude-code/.claude/skills/budget/SKILL.md +0 -45
- package/dist/claude-code/.claude/skills/cache-manager/SKILL.md +0 -66
- package/dist/claude-code/.claude/skills/cache-manager/cache-policy.md +0 -126
- package/dist/claude-code/.claude/skills/check-update/SKILL.md +0 -98
- package/dist/claude-code/.claude/skills/compare/SKILL.md +0 -82
- package/dist/claude-code/.claude/skills/compare/compare-rubric.md +0 -171
- package/dist/claude-code/.claude/skills/complete-cycle/SKILL.md +0 -81
- package/dist/claude-code/.claude/skills/connections/SKILL.md +0 -71
- package/dist/claude-code/.claude/skills/connections/connections-onboarding.md +0 -608
- package/dist/claude-code/.claude/skills/context/SKILL.md +0 -137
- package/dist/claude-code/.claude/skills/continue/SKILL.md +0 -24
- package/dist/claude-code/.claude/skills/darkmode/SKILL.md +0 -76
- package/dist/claude-code/.claude/skills/darkmode/darkmode-audit-procedure.md +0 -258
- package/dist/claude-code/.claude/skills/debug/SKILL.md +0 -41
- package/dist/claude-code/.claude/skills/debug/debug-feedback-loops.md +0 -119
- package/dist/claude-code/.claude/skills/design/SKILL.md +0 -99
- package/dist/claude-code/.claude/skills/design/design-procedure.md +0 -304
- package/dist/claude-code/.claude/skills/discover/SKILL.md +0 -78
- package/dist/claude-code/.claude/skills/discover/discover-procedure.md +0 -222
- package/dist/claude-code/.claude/skills/discuss/SKILL.md +0 -96
- package/dist/claude-code/.claude/skills/do/SKILL.md +0 -45
- package/dist/claude-code/.claude/skills/explore/SKILL.md +0 -107
- package/dist/claude-code/.claude/skills/explore/explore-procedure.md +0 -267
- package/dist/claude-code/.claude/skills/export/SKILL.md +0 -30
- package/dist/claude-code/.claude/skills/extract-learnings/SKILL.md +0 -114
- package/dist/claude-code/.claude/skills/fast/SKILL.md +0 -91
- package/dist/claude-code/.claude/skills/figma-extract/SKILL.md +0 -64
- package/dist/claude-code/.claude/skills/figma-write/SKILL.md +0 -39
- package/dist/claude-code/.claude/skills/graphify/SKILL.md +0 -49
- package/dist/claude-code/.claude/skills/health/SKILL.md +0 -99
- package/dist/claude-code/.claude/skills/health/health-mcp-detection.md +0 -44
- package/dist/claude-code/.claude/skills/health/health-skill-length-report.md +0 -69
- package/dist/claude-code/.claude/skills/help/SKILL.md +0 -87
- package/dist/claude-code/.claude/skills/instinct/SKILL.md +0 -111
- package/dist/claude-code/.claude/skills/list-assumptions/SKILL.md +0 -61
- package/dist/claude-code/.claude/skills/list-pins/SKILL.md +0 -27
- package/dist/claude-code/.claude/skills/live/SKILL.md +0 -98
- package/dist/claude-code/.claude/skills/locale/SKILL.md +0 -51
- package/dist/claude-code/.claude/skills/map/SKILL.md +0 -89
- package/dist/claude-code/.claude/skills/migrate/SKILL.md +0 -70
- package/dist/claude-code/.claude/skills/migrate-context/SKILL.md +0 -123
- package/dist/claude-code/.claude/skills/new-addendum/SKILL.md +0 -81
- package/dist/claude-code/.claude/skills/new-cycle/SKILL.md +0 -37
- package/dist/claude-code/.claude/skills/new-cycle/milestone-completeness-rubric.md +0 -87
- package/dist/claude-code/.claude/skills/new-project/SKILL.md +0 -53
- package/dist/claude-code/.claude/skills/new-skill/SKILL.md +0 -90
- package/dist/claude-code/.claude/skills/next/SKILL.md +0 -68
- package/dist/claude-code/.claude/skills/note/SKILL.md +0 -48
- package/dist/claude-code/.claude/skills/openrouter-status/SKILL.md +0 -86
- package/dist/claude-code/.claude/skills/optimize/SKILL.md +0 -97
- package/dist/claude-code/.claude/skills/override/SKILL.md +0 -86
- package/dist/claude-code/.claude/skills/pause/SKILL.md +0 -77
- package/dist/claude-code/.claude/skills/peer-cli-add/SKILL.md +0 -88
- package/dist/claude-code/.claude/skills/peer-cli-add/peer-cli-protocol.md +0 -161
- package/dist/claude-code/.claude/skills/peer-cli-customize/SKILL.md +0 -90
- package/dist/claude-code/.claude/skills/peers/SKILL.md +0 -96
- package/dist/claude-code/.claude/skills/pin/SKILL.md +0 -37
- package/dist/claude-code/.claude/skills/plan/SKILL.md +0 -105
- package/dist/claude-code/.claude/skills/plan/plan-procedure.md +0 -278
- package/dist/claude-code/.claude/skills/plant-seed/SKILL.md +0 -48
- package/dist/claude-code/.claude/skills/pr-branch/SKILL.md +0 -32
- package/dist/claude-code/.claude/skills/progress/SKILL.md +0 -107
- package/dist/claude-code/.claude/skills/quality-gate/SKILL.md +0 -90
- package/dist/claude-code/.claude/skills/quality-gate/threat-modeling.md +0 -101
- package/dist/claude-code/.claude/skills/quick/SKILL.md +0 -44
- package/dist/claude-code/.claude/skills/reapply-patches/SKILL.md +0 -32
- package/dist/claude-code/.claude/skills/recall/SKILL.md +0 -75
- package/dist/claude-code/.claude/skills/reflect/SKILL.md +0 -85
- package/dist/claude-code/.claude/skills/reflect/procedures/capability-gap-scan.md +0 -120
- package/dist/claude-code/.claude/skills/report-issue/SKILL.md +0 -53
- package/dist/claude-code/.claude/skills/report-issue/report-issue-procedure.md +0 -120
- package/dist/claude-code/.claude/skills/resume/SKILL.md +0 -93
- package/dist/claude-code/.claude/skills/review-backlog/SKILL.md +0 -46
- package/dist/claude-code/.claude/skills/review-decisions/SKILL.md +0 -42
- package/dist/claude-code/.claude/skills/roi/SKILL.md +0 -54
- package/dist/claude-code/.claude/skills/rollout-status/SKILL.md +0 -35
- package/dist/claude-code/.claude/skills/router/SKILL.md +0 -89
- package/dist/claude-code/.claude/skills/router/capability-gap-emitter.md +0 -65
- package/dist/claude-code/.claude/skills/router/router-pick-emitter.md +0 -78
- package/dist/claude-code/.claude/skills/router/router-rules.md +0 -84
- package/dist/claude-code/.claude/skills/scan/SKILL.md +0 -92
- package/dist/claude-code/.claude/skills/scan/scan-procedure.md +0 -732
- package/dist/claude-code/.claude/skills/settings/SKILL.md +0 -87
- package/dist/claude-code/.claude/skills/ship/SKILL.md +0 -48
- package/dist/claude-code/.claude/skills/sketch/SKILL.md +0 -78
- package/dist/claude-code/.claude/skills/sketch-wrap-up/SKILL.md +0 -92
- package/dist/claude-code/.claude/skills/skill-manifest/SKILL.md +0 -79
- package/dist/claude-code/.claude/skills/spike/SKILL.md +0 -67
- package/dist/claude-code/.claude/skills/spike-wrap-up/SKILL.md +0 -86
- package/dist/claude-code/.claude/skills/start/SKILL.md +0 -67
- package/dist/claude-code/.claude/skills/start/start-procedure.md +0 -115
- package/dist/claude-code/.claude/skills/state/SKILL.md +0 -106
- package/dist/claude-code/.claude/skills/stats/SKILL.md +0 -51
- package/dist/claude-code/.claude/skills/style/SKILL.md +0 -71
- package/dist/claude-code/.claude/skills/style/style-doc-procedure.md +0 -150
- package/dist/claude-code/.claude/skills/synthesize/SKILL.md +0 -94
- package/dist/claude-code/.claude/skills/timeline/SKILL.md +0 -66
- package/dist/claude-code/.claude/skills/todo/SKILL.md +0 -64
- package/dist/claude-code/.claude/skills/turn-closeout/SKILL.md +0 -95
- package/dist/claude-code/.claude/skills/undo/SKILL.md +0 -31
- package/dist/claude-code/.claude/skills/unlock-decision/SKILL.md +0 -54
- package/dist/claude-code/.claude/skills/unpin/SKILL.md +0 -31
- package/dist/claude-code/.claude/skills/update/SKILL.md +0 -56
- package/dist/claude-code/.claude/skills/using-gdd/SKILL.md +0 -78
- package/dist/claude-code/.claude/skills/verify/SKILL.md +0 -113
- package/dist/claude-code/.claude/skills/verify/verify-procedure.md +0 -512
- package/dist/claude-code/.claude/skills/warm-cache/SKILL.md +0 -81
- package/dist/claude-code/.claude/skills/watch-authorities/SKILL.md +0 -82
- package/dist/claude-code/.claude/skills/zoom-out/SKILL.md +0 -26
- package/hooks/first-run-nudge.sh +0 -82
- package/hooks/inject-using-gdd.sh +0 -72
- package/hooks/run-hook.cmd +0 -35
- package/hooks/update-check.sh +0 -251
- package/scripts/lib/audit-aggregator/index.cjs +0 -219
- package/scripts/lib/hedge-ensemble.cjs +0 -217
- package/skills/discover/SKILL.md +0 -78
- package/skills/discover/discover-procedure.md +0 -222
- package/skills/new-cycle/milestone-completeness-rubric.md +0 -87
- package/skills/scan/SKILL.md +0 -92
- package/skills/scan/scan-procedure.md +0 -732
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hooks/inject-using-gdd.cjs — SessionStart per-harness context injector (D-07).
|
|
4
|
+
*
|
|
5
|
+
* Node CommonJS port of hooks/inject-using-gdd.sh. Written so it runs natively on
|
|
6
|
+
* Windows (where bash is often absent or stubbed) without spawning a subshell.
|
|
7
|
+
*
|
|
8
|
+
* The forcing function GDD lacked: on every session start / /clear / compact this
|
|
9
|
+
* reads skills/using-gdd/SKILL.md (the bootstrap discipline contract) and emits
|
|
10
|
+
* it as the host harness's SessionStart "additionalContext" shape so the agent is
|
|
11
|
+
* primed with the 1%-rule + red-flags + skill-priority before it acts.
|
|
12
|
+
*
|
|
13
|
+
* Three emitted shapes (ONE JSON object on stdout, terminated by "\n"):
|
|
14
|
+
* Cursor (CURSOR_PLUGIN_ROOT set) -> {"additional_context": "<escaped>"}
|
|
15
|
+
* Claude Code (CLAUDE_PLUGIN_ROOT set, no Cursor)
|
|
16
|
+
* -> {"hookSpecificOutput":
|
|
17
|
+
* {"hookEventName": "SessionStart",
|
|
18
|
+
* "additionalContext": "<escaped>"}}
|
|
19
|
+
* SDK-standard (neither; e.g. COPILOT_CLI) -> {"additionalContext": "<escaped>"}
|
|
20
|
+
*
|
|
21
|
+
* Branch order: check Cursor BEFORE Claude Code — a Cursor session may also export
|
|
22
|
+
* CLAUDE_PLUGIN_ROOT, and Cursor's own var must win.
|
|
23
|
+
*
|
|
24
|
+
* Byte-format preserved: the original bash uses `printf '{"key": %s}\n' "$ESCAPED"`,
|
|
25
|
+
* which yields a single space after each colon and a trailing newline. We build the
|
|
26
|
+
* envelope by hand (using JSON.stringify only for the escaped string value) so the
|
|
27
|
+
* stdout bytes match the bash original. JSON.parse is whitespace-insensitive, so
|
|
28
|
+
* tests pass either way, but matching the wire format keeps any downstream
|
|
29
|
+
* byte-sensitive consumer happy.
|
|
30
|
+
*
|
|
31
|
+
* Silent-on-failure: every error path exits 0 (matching the bash defensive contract
|
|
32
|
+
* — a partial install or missing SKILL.md must still produce a syntactically valid
|
|
33
|
+
* JSON envelope). The bash never had non-zero exits, so neither do we.
|
|
34
|
+
*
|
|
35
|
+
* Sourcing guard: helpers are exported via module.exports; main() runs only when
|
|
36
|
+
* this file is the entry point (require.main === module). Mirrors the bash
|
|
37
|
+
* `[ "${BASH_SOURCE[0]}" = "$0" ]` pattern so tests can require this module and
|
|
38
|
+
* exercise helpers without firing the emit side-effect.
|
|
39
|
+
*
|
|
40
|
+
* NO-CASCADE (D-06): wired ONLY under SessionStart in hooks/hooks.json. Subagent
|
|
41
|
+
* spawns do not fire SessionStart, so the inject cannot cascade into a subagent's
|
|
42
|
+
* context. (Structural guarantee; behavioral proof = P33.)
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const fs = require('node:fs');
|
|
48
|
+
const path = require('node:path');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the plugin root the same way the bash original does:
|
|
52
|
+
* CURSOR_PLUGIN_ROOT -> CLAUDE_PLUGIN_ROOT -> dirname(__file__)/..
|
|
53
|
+
* Then normalize Windows backslashes to forward slashes (matching the bash
|
|
54
|
+
* `"${ROOT//\\//}"` parameter substitution) so downstream path joins are stable.
|
|
55
|
+
*
|
|
56
|
+
* @param {NodeJS.ProcessEnv} env Environment (defaults to process.env).
|
|
57
|
+
* @param {string} selfDir Directory of this script (defaults to __dirname).
|
|
58
|
+
* @returns {string} Plugin root, forward-slash normalized.
|
|
59
|
+
*/
|
|
60
|
+
function resolveRoot(env, selfDir) {
|
|
61
|
+
const e = env || process.env;
|
|
62
|
+
const sd = selfDir || __dirname;
|
|
63
|
+
// Bash: ROOT="${CURSOR_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-${SELF_DIR}/..}}"
|
|
64
|
+
// The :- operator treats both unset AND empty as "use the fallback". Match that
|
|
65
|
+
// by treating empty strings as absent here too.
|
|
66
|
+
const cursor = e.CURSOR_PLUGIN_ROOT;
|
|
67
|
+
const claude = e.CLAUDE_PLUGIN_ROOT;
|
|
68
|
+
let root;
|
|
69
|
+
if (cursor && cursor.length > 0) {
|
|
70
|
+
root = cursor;
|
|
71
|
+
} else if (claude && claude.length > 0) {
|
|
72
|
+
root = claude;
|
|
73
|
+
} else {
|
|
74
|
+
root = path.join(sd, '..');
|
|
75
|
+
}
|
|
76
|
+
// Normalize backslashes -> forward slashes (Windows). Matches bash ${ROOT//\\//}.
|
|
77
|
+
return root.replace(/\\/g, '/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read the SKILL.md bootstrap contract. Defensive: never throw — return '' on any
|
|
82
|
+
* read failure so the emitted JSON envelope is still well-formed. Matches the
|
|
83
|
+
* bash `[[ -r "${SKILL}" ]]` guard (returns "" on missing / unreadable).
|
|
84
|
+
*
|
|
85
|
+
* Bash `CONTENT="$(cat "${SKILL}")"` strips ALL trailing newlines from the file —
|
|
86
|
+
* that's a fundamental POSIX command-substitution rule. To stay byte-identical
|
|
87
|
+
* with the original on every emitted JSON envelope, we replicate that here: trim
|
|
88
|
+
* the trailing run of LF/CRLF after reading. Interior newlines are untouched (so
|
|
89
|
+
* the multi-line round-trip the tests check still works).
|
|
90
|
+
*
|
|
91
|
+
* @param {string} root Plugin root (already normalized).
|
|
92
|
+
* @returns {string} File contents, or '' if missing/unreadable.
|
|
93
|
+
*/
|
|
94
|
+
function readSkill(root) {
|
|
95
|
+
const skillPath = `${root}/skills/using-gdd/SKILL.md`;
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = fs.readFileSync(skillPath, 'utf8');
|
|
99
|
+
} catch {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
// Match bash $() trailing-newline stripping. /(\r?\n)+$/ also handles CRLF
|
|
103
|
+
// line endings on Windows checkouts so a single CRLF tail strips to ''.
|
|
104
|
+
return raw.replace(/(?:\r?\n)+$/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* JSON-escape a string and wrap it in double-quotes — the bash original was
|
|
109
|
+
* hand-rolled in pure parameter substitution because the script must run with no
|
|
110
|
+
* jq/python dependency. In Node, JSON.stringify handles every code-point the bash
|
|
111
|
+
* version handled (backslash, double-quote, \t, \r, \n) AND the ones it didn't
|
|
112
|
+
* (other C0 controls, lone surrogates) more correctly. Output includes the
|
|
113
|
+
* surrounding quotes so callers can splice the value directly into a JSON object
|
|
114
|
+
* literal — matching the bash `printf '"%s"' "$s"` contract.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} s
|
|
117
|
+
* @returns {string} e.g. `"hello\nworld"`
|
|
118
|
+
*/
|
|
119
|
+
function escapeForJson(s) {
|
|
120
|
+
return JSON.stringify(String(s == null ? '' : s));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the harness-specific JSON envelope as the exact byte sequence the bash
|
|
125
|
+
* `printf` produces — single space after each colon, trailing `\n`. Branch order
|
|
126
|
+
* matches the bash: Cursor BEFORE Claude Code so a Cursor session that also
|
|
127
|
+
* exports CLAUDE_PLUGIN_ROOT still gets the Cursor shape.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} escapedJsonValue Already JSON-encoded string including quotes.
|
|
130
|
+
* @param {NodeJS.ProcessEnv} env Environment (defaults to process.env).
|
|
131
|
+
* @returns {string} The full stdout payload, terminated by '\n'.
|
|
132
|
+
*/
|
|
133
|
+
function buildEnvelope(escapedJsonValue, env) {
|
|
134
|
+
const e = env || process.env;
|
|
135
|
+
const cursor = e.CURSOR_PLUGIN_ROOT;
|
|
136
|
+
const claude = e.CLAUDE_PLUGIN_ROOT;
|
|
137
|
+
if (cursor && cursor.length > 0) {
|
|
138
|
+
// Cursor: top-level additional_context.
|
|
139
|
+
return `{"additional_context": ${escapedJsonValue}}\n`;
|
|
140
|
+
}
|
|
141
|
+
if (claude && claude.length > 0) {
|
|
142
|
+
// Claude Code: hookSpecificOutput envelope (mirrors gdd-decision-injector.js).
|
|
143
|
+
return `{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": ${escapedJsonValue}}}\n`;
|
|
144
|
+
}
|
|
145
|
+
// SDK-standard (COPILOT_CLI or none): top-level additionalContext.
|
|
146
|
+
return `{"additionalContext": ${escapedJsonValue}}\n`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* End-to-end main: resolve root, read skill, escape, emit, exit 0. All error
|
|
151
|
+
* paths swallow and still emit a well-formed envelope (silent-on-failure).
|
|
152
|
+
*
|
|
153
|
+
* @returns {number} Always 0.
|
|
154
|
+
*/
|
|
155
|
+
function main() {
|
|
156
|
+
let payload;
|
|
157
|
+
try {
|
|
158
|
+
const root = resolveRoot(process.env, __dirname);
|
|
159
|
+
const content = readSkill(root);
|
|
160
|
+
const escaped = escapeForJson(content);
|
|
161
|
+
payload = buildEnvelope(escaped, process.env);
|
|
162
|
+
} catch {
|
|
163
|
+
// Belt-and-braces: if anything above unexpectedly throws (e.g. JSON.stringify
|
|
164
|
+
// on a hostile input — shouldn't happen with a string), still emit a valid
|
|
165
|
+
// empty envelope so the SessionStart pipeline never breaks.
|
|
166
|
+
payload = buildEnvelope('""', process.env);
|
|
167
|
+
}
|
|
168
|
+
// Use process.stdout.write so we don't get a console.log-added newline on top
|
|
169
|
+
// of the one already in the payload. Matches bash printf '...\n' exactly.
|
|
170
|
+
process.stdout.write(payload);
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
resolveRoot,
|
|
176
|
+
readSkill,
|
|
177
|
+
escapeForJson,
|
|
178
|
+
buildEnvelope,
|
|
179
|
+
main,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Sourcing guard: only run main() when invoked as the entry point. Tests can
|
|
183
|
+
// `require('./inject-using-gdd.cjs')` to exercise helpers in isolation without
|
|
184
|
+
// firing the emit side-effect — mirrors the bash `[ "${BASH_SOURCE[0]}" = "$0" ]`
|
|
185
|
+
// pattern.
|
|
186
|
+
if (require.main === module) {
|
|
187
|
+
process.exit(main());
|
|
188
|
+
}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* get-design-done — update check (Phase 13.3) — Node port
|
|
4
|
+
*
|
|
5
|
+
* Original: hooks/update-check.sh
|
|
6
|
+
* SessionStart hook. Silent-on-failure by policy (D-04): exits 0 on every error path.
|
|
7
|
+
* 24h-cached unauthenticated GET of /releases/latest. Renders .design/update-available.md
|
|
8
|
+
* only when a newer version exists AND it is not dismissed AND stage-guard allows.
|
|
9
|
+
*
|
|
10
|
+
* Sourcing guard (Node equivalent): main() runs only when require.main === module.
|
|
11
|
+
* Helpers are exported for tests. This mirrors the bash `[ "${BASH_SOURCE[0]}" = "$0" ]`
|
|
12
|
+
* pattern — sourcing the .sh in tests loads functions without side effects; requiring
|
|
13
|
+
* this .cjs likewise loads exports without running main.
|
|
14
|
+
*
|
|
15
|
+
* Non-obvious behaviors preserved:
|
|
16
|
+
* - 4-segment semver tuple comparison (handles "v1.0.7.3" off-cadence builds).
|
|
17
|
+
* - LATEST_TAG safety regex /^v?\d+\.\d+(\.\d+)*$/ before trusting fetched data.
|
|
18
|
+
* - Body excerpt: stripped of control chars 0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F and
|
|
19
|
+
* double-quotes (prevents JSON read-back injection — body is display-only).
|
|
20
|
+
* - C_DELTA allowlist gate (major|minor|patch|off-cadence|none → else "unknown").
|
|
21
|
+
* - Atomic writes via .tmp.<pid> + rename for both cache and banner files.
|
|
22
|
+
* - State stage suppression: plan|design|verify silences the banner.
|
|
23
|
+
* - --refresh flag forces fresh fetch regardless of cache age.
|
|
24
|
+
* - GDD_UPDATE_DEBUG=1 enables '[gdd update-check]' prefixed stderr logging.
|
|
25
|
+
* - Cache freshness: mtime < 24h ago (86400s).
|
|
26
|
+
* - Plugin root: CLAUDE_PLUGIN_ROOT env override, else dirname(__dirname).
|
|
27
|
+
* - Windows backslashes in PLUGIN_ROOT normalized to forward slashes.
|
|
28
|
+
*/
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const https = require('node:https');
|
|
34
|
+
const process = require('node:process');
|
|
35
|
+
|
|
36
|
+
const CACHE_TTL_SECONDS = 86400; // 24h
|
|
37
|
+
|
|
38
|
+
// ---- Logger (silent unless GDD_UPDATE_DEBUG=1) ----
|
|
39
|
+
function log(...args) {
|
|
40
|
+
if (process.env.GDD_UPDATE_DEBUG === '1') {
|
|
41
|
+
process.stderr.write('[gdd update-check] ' + args.join(' ') + '\n');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- Path helpers — derive paths from cwd + plugin root ----
|
|
46
|
+
function getPluginRoot() {
|
|
47
|
+
let root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
48
|
+
if (!root || root.length === 0) {
|
|
49
|
+
// dirname of __dirname == project root (hooks/.. == plugin root)
|
|
50
|
+
root = path.resolve(__dirname, '..');
|
|
51
|
+
}
|
|
52
|
+
return root.replace(/\\/g, '/');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPaths(cwd) {
|
|
56
|
+
const designDir = path.join(cwd || process.cwd(), '.design');
|
|
57
|
+
return {
|
|
58
|
+
designDir,
|
|
59
|
+
cache: path.join(designDir, 'update-cache.json'),
|
|
60
|
+
banner: path.join(designDir, 'update-available.md'),
|
|
61
|
+
config: path.join(designDir, 'config.json'),
|
|
62
|
+
state: path.join(designDir, 'STATE.md'),
|
|
63
|
+
pluginJson: path.join(getPluginRoot(), '.claude-plugin', 'plugin.json'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- Read current plugin version from .claude-plugin/plugin.json ----
|
|
68
|
+
function readCurrentTag(pluginJsonPath) {
|
|
69
|
+
const p = pluginJsonPath || getPaths().pluginJson;
|
|
70
|
+
try {
|
|
71
|
+
if (!fs.existsSync(p)) return '';
|
|
72
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
73
|
+
const obj = JSON.parse(raw);
|
|
74
|
+
const v = obj && typeof obj.version === 'string' ? obj.version : '';
|
|
75
|
+
return v;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
log('readCurrentTag failed:', e && e.message);
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- Semver normalizer: "v1.0.7" → [1,0,7,0]; "v1.0.7.3" → [1,0,7,3] ----
|
|
83
|
+
// Returns 4-element array of non-negative integers. Sanitizes each segment to digits only.
|
|
84
|
+
function normalizeSemver(input) {
|
|
85
|
+
if (input == null) return [0, 0, 0, 0];
|
|
86
|
+
let t = String(input);
|
|
87
|
+
if (t.startsWith('v')) t = t.slice(1);
|
|
88
|
+
// strip any -pre/-beta suffix after first hyphen
|
|
89
|
+
const hyphenIdx = t.indexOf('-');
|
|
90
|
+
if (hyphenIdx >= 0) t = t.slice(0, hyphenIdx);
|
|
91
|
+
const parts = t.split('.');
|
|
92
|
+
const out = [0, 0, 0, 0];
|
|
93
|
+
for (let i = 0; i < 4; i++) {
|
|
94
|
+
const seg = parts[i] != null ? String(parts[i]).replace(/[^0-9]/g, '') : '';
|
|
95
|
+
out[i] = seg.length === 0 ? 0 : parseInt(seg, 10);
|
|
96
|
+
if (!Number.isFinite(out[i])) out[i] = 0;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- Classify delta: compare 4-segment tuples ----
|
|
102
|
+
// Returns { state: 'newer'|'older'|'same', kind: 'major'|'minor'|'patch'|'off-cadence'|'none' }
|
|
103
|
+
function classifyDelta(currentTag, latestTag) {
|
|
104
|
+
const cur = normalizeSemver(currentTag);
|
|
105
|
+
const lat = normalizeSemver(latestTag);
|
|
106
|
+
const kinds = ['major', 'minor', 'patch', 'off-cadence'];
|
|
107
|
+
for (let i = 0; i < 4; i++) {
|
|
108
|
+
if (lat[i] > cur[i]) return { state: 'newer', kind: kinds[i] };
|
|
109
|
+
if (lat[i] < cur[i]) return { state: 'older', kind: kinds[i] };
|
|
110
|
+
}
|
|
111
|
+
return { state: 'same', kind: 'none' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- Cache freshness: returns true if cache exists and mtime is < 24h ago ----
|
|
115
|
+
function isCacheFresh(cachePath) {
|
|
116
|
+
try {
|
|
117
|
+
const st = fs.statSync(cachePath);
|
|
118
|
+
if (!st || !st.isFile()) return false;
|
|
119
|
+
const now = Math.floor(Date.now() / 1000);
|
|
120
|
+
const mtime = Math.floor(st.mtimeMs / 1000);
|
|
121
|
+
const age = now - mtime;
|
|
122
|
+
return age < CACHE_TTL_SECONDS;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- Fetch latest release. Returns Promise<string> with raw body, or '' on failure. ----
|
|
129
|
+
function fetchLatest() {
|
|
130
|
+
const url = 'https://api.github.com/repos/hegemonart/get-design-done/releases/latest';
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
let done = false;
|
|
133
|
+
const finish = (val) => {
|
|
134
|
+
if (done) return;
|
|
135
|
+
done = true;
|
|
136
|
+
resolve(val);
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
const req = https.get(
|
|
140
|
+
url,
|
|
141
|
+
{
|
|
142
|
+
headers: {
|
|
143
|
+
Accept: 'application/vnd.github+json',
|
|
144
|
+
'User-Agent': 'gdd-update-check',
|
|
145
|
+
},
|
|
146
|
+
timeout: 3000,
|
|
147
|
+
},
|
|
148
|
+
(res) => {
|
|
149
|
+
// Follow one redirect (3xx). GitHub API rarely redirects but be defensive.
|
|
150
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
151
|
+
res.resume();
|
|
152
|
+
const redirected = https.get(
|
|
153
|
+
res.headers.location,
|
|
154
|
+
{
|
|
155
|
+
headers: {
|
|
156
|
+
Accept: 'application/vnd.github+json',
|
|
157
|
+
'User-Agent': 'gdd-update-check',
|
|
158
|
+
},
|
|
159
|
+
timeout: 3000,
|
|
160
|
+
},
|
|
161
|
+
(r2) => {
|
|
162
|
+
if (r2.statusCode < 200 || r2.statusCode >= 300) {
|
|
163
|
+
r2.resume();
|
|
164
|
+
log('fetch redirect status', r2.statusCode);
|
|
165
|
+
return finish('');
|
|
166
|
+
}
|
|
167
|
+
const chunks = [];
|
|
168
|
+
r2.on('data', (c) => chunks.push(c));
|
|
169
|
+
r2.on('end', () => finish(Buffer.concat(chunks).toString('utf8')));
|
|
170
|
+
r2.on('error', (e) => {
|
|
171
|
+
log('redirect read error', e && e.message);
|
|
172
|
+
finish('');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
redirected.on('error', (e) => {
|
|
177
|
+
log('redirect request error', e && e.message);
|
|
178
|
+
finish('');
|
|
179
|
+
});
|
|
180
|
+
redirected.on('timeout', () => {
|
|
181
|
+
try { redirected.destroy(); } catch (_) {}
|
|
182
|
+
finish('');
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
187
|
+
res.resume();
|
|
188
|
+
log('fetch status', res.statusCode);
|
|
189
|
+
return finish('');
|
|
190
|
+
}
|
|
191
|
+
const chunks = [];
|
|
192
|
+
res.on('data', (c) => chunks.push(c));
|
|
193
|
+
res.on('end', () => finish(Buffer.concat(chunks).toString('utf8')));
|
|
194
|
+
res.on('error', (e) => {
|
|
195
|
+
log('read error', e && e.message);
|
|
196
|
+
finish('');
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
req.on('timeout', () => {
|
|
201
|
+
try { req.destroy(); } catch (_) {}
|
|
202
|
+
log('timeout');
|
|
203
|
+
finish('');
|
|
204
|
+
});
|
|
205
|
+
req.on('error', (e) => {
|
|
206
|
+
log('request error', e && e.message);
|
|
207
|
+
finish('');
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
log('fetch threw', e && e.message);
|
|
211
|
+
finish('');
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- Extract tag_name from release JSON. Returns '' on failure. ----
|
|
217
|
+
function extractTag(raw) {
|
|
218
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
219
|
+
try {
|
|
220
|
+
const obj = JSON.parse(raw);
|
|
221
|
+
return obj && typeof obj.tag_name === 'string' ? obj.tag_name : '';
|
|
222
|
+
} catch (e) {
|
|
223
|
+
log('extractTag parse error:', e && e.message);
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- Extract body from release JSON. Slices to 500 chars, strips ctrl chars + double-quotes. ----
|
|
229
|
+
function extractBody(raw) {
|
|
230
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
231
|
+
try {
|
|
232
|
+
const obj = JSON.parse(raw);
|
|
233
|
+
let body = obj && typeof obj.body === 'string' ? obj.body : '';
|
|
234
|
+
body = body.slice(0, 500);
|
|
235
|
+
// Strip control chars: 0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F
|
|
236
|
+
// eslint-disable-next-line no-control-regex
|
|
237
|
+
body = body.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
238
|
+
// Strip double-quotes (display-only, prevents JSON read-back injection)
|
|
239
|
+
body = body.replace(/"/g, '');
|
|
240
|
+
return body;
|
|
241
|
+
} catch (e) {
|
|
242
|
+
log('extractBody parse error:', e && e.message);
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Read .design/STATE.md stage field. Returns "brief"|"explore"|"plan"|"design"|"verify"|"" ----
|
|
248
|
+
function readStateStage(statePath) {
|
|
249
|
+
const p = statePath || getPaths().state;
|
|
250
|
+
try {
|
|
251
|
+
if (!fs.existsSync(p)) return '';
|
|
252
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
253
|
+
const lines = raw.split(/\r?\n/);
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const m = line.match(/^stage:\s*"?([^"\s]+)"?/);
|
|
256
|
+
if (m) return m[1];
|
|
257
|
+
}
|
|
258
|
+
return '';
|
|
259
|
+
} catch (e) {
|
|
260
|
+
log('readStateStage failed:', e && e.message);
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- Read .design/config.json#update_dismissed. Returns tag string or ''. ----
|
|
266
|
+
function readDismissed(configPath) {
|
|
267
|
+
const p = configPath || getPaths().config;
|
|
268
|
+
try {
|
|
269
|
+
if (!fs.existsSync(p)) return '';
|
|
270
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
271
|
+
try {
|
|
272
|
+
const obj = JSON.parse(raw);
|
|
273
|
+
const v = obj && typeof obj.update_dismissed === 'string' ? obj.update_dismissed : '';
|
|
274
|
+
return v;
|
|
275
|
+
} catch (_) {
|
|
276
|
+
// Fall back to regex extraction if JSON is malformed — matches bash grep behavior.
|
|
277
|
+
const m = raw.match(/"update_dismissed"\s*:\s*"([^"]+)"/);
|
|
278
|
+
return m ? m[1] : '';
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
281
|
+
log('readDismissed failed:', e && e.message);
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---- Validate that a tag string is a safe semver before trusting it (CR-02). ----
|
|
287
|
+
function isSafeSemverTag(tag) {
|
|
288
|
+
if (!tag || typeof tag !== 'string') return false;
|
|
289
|
+
return /^v?\d+\.\d+(\.\d+)*$/.test(tag);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---- Atomic write: write to .tmp.<pid> then rename. On any error, attempt cleanup. ----
|
|
293
|
+
function atomicWrite(targetPath, contents) {
|
|
294
|
+
const tmp = `${targetPath}.tmp.${process.pid}`;
|
|
295
|
+
try {
|
|
296
|
+
fs.writeFileSync(tmp, contents);
|
|
297
|
+
fs.renameSync(tmp, targetPath);
|
|
298
|
+
return true;
|
|
299
|
+
} catch (e) {
|
|
300
|
+
log('atomicWrite failed:', e && e.message);
|
|
301
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---- Build the JSON cache contents. Body excerpt is JSON-string-escaped. ----
|
|
307
|
+
function buildCacheJson({ checkedAt, currentTag, latestTag, deltaKind, isNewer, bodyExcerpt }) {
|
|
308
|
+
// Match the bash output format closely: newlines and 2-space indent.
|
|
309
|
+
// JSON.stringify the body to handle escape sequences cleanly.
|
|
310
|
+
const escapedBody = JSON.stringify(bodyExcerpt || '').slice(1, -1); // strip outer quotes
|
|
311
|
+
const lines = [
|
|
312
|
+
'{',
|
|
313
|
+
` "checked_at": ${checkedAt},`,
|
|
314
|
+
` "current_tag": "${currentTag}",`,
|
|
315
|
+
` "latest_tag": "${latestTag}",`,
|
|
316
|
+
` "delta": "${deltaKind}",`,
|
|
317
|
+
` "is_newer": ${isNewer ? 'true' : 'false'},`,
|
|
318
|
+
` "changelog_excerpt": "${escapedBody}"`,
|
|
319
|
+
'}',
|
|
320
|
+
'',
|
|
321
|
+
];
|
|
322
|
+
return lines.join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---- Read cache and extract the four fields the main flow consumes. ----
|
|
326
|
+
function readCache(cachePath) {
|
|
327
|
+
try {
|
|
328
|
+
const raw = fs.readFileSync(cachePath, 'utf8');
|
|
329
|
+
try {
|
|
330
|
+
const obj = JSON.parse(raw);
|
|
331
|
+
return {
|
|
332
|
+
latest_tag: typeof obj.latest_tag === 'string' ? obj.latest_tag : '',
|
|
333
|
+
delta: typeof obj.delta === 'string' ? obj.delta : '',
|
|
334
|
+
is_newer: obj.is_newer === true,
|
|
335
|
+
changelog_excerpt:
|
|
336
|
+
typeof obj.changelog_excerpt === 'string'
|
|
337
|
+
? obj.changelog_excerpt.replace(/\\n/g, '\n')
|
|
338
|
+
: '',
|
|
339
|
+
};
|
|
340
|
+
} catch (_) {
|
|
341
|
+
// Regex fallback to mirror the bash grep+sed pipeline (handles slightly malformed caches).
|
|
342
|
+
const get = (key) => {
|
|
343
|
+
const m = raw.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*)"`));
|
|
344
|
+
return m ? m[1] : '';
|
|
345
|
+
};
|
|
346
|
+
const newerMatch = raw.match(/"is_newer"\s*:\s*(true|false)/);
|
|
347
|
+
return {
|
|
348
|
+
latest_tag: get('latest_tag'),
|
|
349
|
+
delta: get('delta'),
|
|
350
|
+
is_newer: newerMatch ? newerMatch[1] === 'true' : false,
|
|
351
|
+
changelog_excerpt: get('changelog_excerpt').replace(/\\n/g, '\n'),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
log('readCache failed:', e && e.message);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---- Banner renderer ----
|
|
361
|
+
function buildBanner({ displayCurrent, latestTag, deltaKind, body }) {
|
|
362
|
+
const bar = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
|
|
363
|
+
const lines = [];
|
|
364
|
+
lines.push(bar);
|
|
365
|
+
lines.push(` 📦 Plugin update: ${displayCurrent} → ${latestTag} (${deltaKind})`);
|
|
366
|
+
if (body && body.length > 0) {
|
|
367
|
+
lines.push(body);
|
|
368
|
+
}
|
|
369
|
+
lines.push(' Install: /gdd:update Dismiss: /gdd:check-update --dismiss');
|
|
370
|
+
lines.push(bar);
|
|
371
|
+
lines.push('');
|
|
372
|
+
return lines.join('\n');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---- Main control flow. argv is process.argv-style; defaults to process.argv. ----
|
|
376
|
+
async function main(argv) {
|
|
377
|
+
argv = argv || process.argv.slice(2);
|
|
378
|
+
const paths = getPaths();
|
|
379
|
+
|
|
380
|
+
// Ensure .design/ exists (belt+suspenders — bootstrap normally creates it).
|
|
381
|
+
try {
|
|
382
|
+
fs.mkdirSync(paths.designDir, { recursive: true });
|
|
383
|
+
} catch (_) {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const currentTag = readCurrentTag(paths.pluginJson);
|
|
388
|
+
if (!currentTag) {
|
|
389
|
+
log('no plugin.json or no current version parsed');
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
const displayCurrent = 'v' + currentTag.replace(/^v/, '');
|
|
393
|
+
|
|
394
|
+
let forceRefresh = false;
|
|
395
|
+
for (const arg of argv) {
|
|
396
|
+
if (arg === '--refresh') forceRefresh = true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 1. Populate cache if missing/stale or forced.
|
|
400
|
+
if (forceRefresh || !isCacheFresh(paths.cache)) {
|
|
401
|
+
let raw = '';
|
|
402
|
+
try {
|
|
403
|
+
raw = await fetchLatest();
|
|
404
|
+
} catch (_) {
|
|
405
|
+
raw = '';
|
|
406
|
+
}
|
|
407
|
+
if (raw && raw.length > 0) {
|
|
408
|
+
const latestTagRaw = extractTag(raw);
|
|
409
|
+
const bodyExcerpt = extractBody(raw);
|
|
410
|
+
let latestTag = latestTagRaw;
|
|
411
|
+
if (!isSafeSemverTag(latestTag)) {
|
|
412
|
+
log(`LATEST_TAG '${latestTag}' failed semver safety check — aborting cache write`);
|
|
413
|
+
latestTag = '';
|
|
414
|
+
}
|
|
415
|
+
if (latestTag) {
|
|
416
|
+
const delta = classifyDelta(displayCurrent, latestTag);
|
|
417
|
+
const isNewer = delta.state === 'newer';
|
|
418
|
+
const checkedAt = Math.floor(Date.now() / 1000);
|
|
419
|
+
const json = buildCacheJson({
|
|
420
|
+
checkedAt,
|
|
421
|
+
currentTag: displayCurrent,
|
|
422
|
+
latestTag,
|
|
423
|
+
deltaKind: delta.kind,
|
|
424
|
+
isNewer,
|
|
425
|
+
bodyExcerpt,
|
|
426
|
+
});
|
|
427
|
+
atomicWrite(paths.cache, json);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 2. Read cache (whether freshly written or still valid).
|
|
433
|
+
if (!fs.existsSync(paths.cache)) {
|
|
434
|
+
return 0; // no cache, nothing to do — silent exit
|
|
435
|
+
}
|
|
436
|
+
const cache = readCache(paths.cache);
|
|
437
|
+
if (!cache) return 0;
|
|
438
|
+
|
|
439
|
+
const cLatest = cache.latest_tag;
|
|
440
|
+
let cDelta = cache.delta;
|
|
441
|
+
// Allowlist-gate cDelta before it reaches any banner context (WR-04).
|
|
442
|
+
const allowedDeltas = new Set(['major', 'minor', 'patch', 'off-cadence', 'none']);
|
|
443
|
+
if (!allowedDeltas.has(cDelta)) cDelta = 'unknown';
|
|
444
|
+
const cNewer = cache.is_newer === true;
|
|
445
|
+
const cBody = cache.changelog_excerpt || '';
|
|
446
|
+
|
|
447
|
+
// 3. Gate: if cache says not newer, remove any stale banner and exit.
|
|
448
|
+
if (!cNewer) {
|
|
449
|
+
try { fs.unlinkSync(paths.banner); } catch (_) {}
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 4. Dismissal gate (D-13): if user already dismissed this exact tag, suppress.
|
|
454
|
+
const dismissed = readDismissed(paths.config);
|
|
455
|
+
if (dismissed && dismissed === cLatest) {
|
|
456
|
+
try { fs.unlinkSync(paths.banner); } catch (_) {}
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 5. State-machine guard (D-11/D-12): suppress during plan|design|verify.
|
|
461
|
+
const stage = readStateStage(paths.state);
|
|
462
|
+
if (stage === 'plan' || stage === 'design' || stage === 'verify') {
|
|
463
|
+
try { fs.unlinkSync(paths.banner); } catch (_) {}
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 6. All gates passed — render the banner atomically.
|
|
468
|
+
const banner = buildBanner({
|
|
469
|
+
displayCurrent,
|
|
470
|
+
latestTag: cLatest,
|
|
471
|
+
deltaKind: cDelta,
|
|
472
|
+
body: cBody,
|
|
473
|
+
});
|
|
474
|
+
atomicWrite(paths.banner, banner);
|
|
475
|
+
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ---- Exports for tests (mirrors bash sourcing pattern) ----
|
|
480
|
+
module.exports = {
|
|
481
|
+
// Public helpers (named to match the bash function names where reasonable).
|
|
482
|
+
normalizeSemver,
|
|
483
|
+
classifyDelta,
|
|
484
|
+
isCacheFresh,
|
|
485
|
+
readCurrentTag,
|
|
486
|
+
readStateStage,
|
|
487
|
+
readDismissed,
|
|
488
|
+
fetchLatest,
|
|
489
|
+
extractTag,
|
|
490
|
+
extractBody,
|
|
491
|
+
// Additional helpers useful for tests / orchestrator.
|
|
492
|
+
isSafeSemverTag,
|
|
493
|
+
atomicWrite,
|
|
494
|
+
buildCacheJson,
|
|
495
|
+
buildBanner,
|
|
496
|
+
readCache,
|
|
497
|
+
getPaths,
|
|
498
|
+
getPluginRoot,
|
|
499
|
+
main,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// ---- Entry-point guard: only run main when invoked directly (not when required). ----
|
|
503
|
+
if (require.main === module) {
|
|
504
|
+
// Always exit 0 (silent-on-failure). Promise rejections also exit 0.
|
|
505
|
+
main(process.argv.slice(2))
|
|
506
|
+
.then(() => process.exit(0))
|
|
507
|
+
.catch((e) => {
|
|
508
|
+
log('main threw:', e && e.message);
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
}
|