@hegemonart/get-design-done 1.22.0 → 1.23.5

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.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * design-solidify.mjs — solidify-with-rollback gate (Plan 23-02).
3
+ *
4
+ * Code-level gate that runs the validation triplet for a task and, on
5
+ * any failure, rolls the working tree back via git stash (configurable)
6
+ * and appends a `solidify.rollback` event onto the Phase 22 causal chain.
7
+ *
8
+ * Replaces the prompt-encoded "stash if it broke" instruction in today's
9
+ * solidify agents with a typed, testable function.
10
+ *
11
+ * Why .mjs, not .ts:
12
+ * * Node 24 + Windows + .mjs dynamic-importing .ts triggers
13
+ * STATUS_STACK_BUFFER_OVERRUN (Phase 22 lesson). Keep this file as
14
+ * plain .mjs with a CJS test wrapper.
15
+ *
16
+ * Usage:
17
+ * import { solidify } from './design-solidify.mjs';
18
+ * const result = await solidify({ taskId: '23-02' });
19
+ * // result.outcome === 'pass' | 'fail'
20
+ */
21
+
22
+ import { spawnSync } from 'node:child_process';
23
+ import { existsSync } from 'node:fs';
24
+ import { dirname, resolve, join } from 'node:path';
25
+ import { createRequire } from 'node:module';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ // Anchor on the .mjs file's own directory — NOT on the caller's cwd —
29
+ // because callers pass in arbitrary `cwd` values (test scaffolds,
30
+ // sub-repos) where event-chain.cjs is unreachable. Walk up from
31
+ // scripts/lib/ to repo root.
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ function _findRepoRoot(start) {
34
+ let dir = start;
35
+ for (let i = 0; i < 12; i++) {
36
+ if (existsSync(join(dir, 'package.json'))) return dir;
37
+ const parent = dirname(dir);
38
+ if (parent === dir) break;
39
+ dir = parent;
40
+ }
41
+ return start;
42
+ }
43
+ const PLUGIN_ROOT = _findRepoRoot(dirname(__filename));
44
+ const _nodeRequire = createRequire(join(PLUGIN_ROOT, 'package.json'));
45
+ const { appendChainEvent } = _nodeRequire(
46
+ resolve(PLUGIN_ROOT, 'scripts/lib/event-chain.cjs'),
47
+ );
48
+
49
+ /**
50
+ * @typedef {Object} SolidifyValidation
51
+ * @property {string} name
52
+ * @property {string} cmd
53
+ * @property {string[]} args
54
+ * @property {number} [timeout_ms]
55
+ */
56
+
57
+ /**
58
+ * @typedef {Object} SolidifyOptions
59
+ * @property {string} taskId
60
+ * @property {SolidifyValidation[]} [validations]
61
+ * @property {'stash'|'hard'|'none'} [rollback]
62
+ * @property {string} [cwd]
63
+ * @property {string[]} [decisionRefs]
64
+ * @property {string} [parentEventId]
65
+ * @property {(ev: object) => void} [emit]
66
+ * @property {string} [chainPath]
67
+ */
68
+
69
+ /**
70
+ * @typedef {Object} SolidifyStep
71
+ * @property {string} name
72
+ * @property {'pass'|'fail'} status
73
+ * @property {string} [stdout]
74
+ * @property {string} [stderr]
75
+ * @property {number|null} [code]
76
+ * @property {string|null} [signal]
77
+ */
78
+
79
+ /**
80
+ * @typedef {Object} SolidifyResult
81
+ * @property {'pass'|'fail'} outcome
82
+ * @property {SolidifyStep[]} steps
83
+ * @property {'stash'|'hard'|'none'|'skipped'} [rolledBackVia]
84
+ * @property {string} eventId
85
+ * @property {string} [stashRef]
86
+ */
87
+
88
+ /**
89
+ * Build the default validation triplet for a task. Caller may override
90
+ * by supplying `opts.validations` directly.
91
+ *
92
+ * @param {string} taskId
93
+ * @returns {SolidifyValidation[]}
94
+ */
95
+ function defaultValidations(taskId) {
96
+ return [
97
+ { name: 'typecheck', cmd: 'npm', args: ['run', 'typecheck'], timeout_ms: 120_000 },
98
+ { name: 'build', cmd: 'npm', args: ['run', 'build'], timeout_ms: 300_000 },
99
+ {
100
+ name: 'targeted-test',
101
+ cmd: 'npm',
102
+ args: ['test', '--', '--testPathPattern', String(taskId)],
103
+ timeout_ms: 120_000,
104
+ },
105
+ ];
106
+ }
107
+
108
+ /**
109
+ * Run one validation step via spawnSync. Always returns a step record;
110
+ * never throws.
111
+ *
112
+ * @param {SolidifyValidation} v
113
+ * @param {string} cwd
114
+ * @returns {SolidifyStep}
115
+ */
116
+ function runStep(v, cwd) {
117
+ const r = spawnSync(v.cmd, v.args, {
118
+ cwd,
119
+ encoding: 'utf8',
120
+ shell: false,
121
+ timeout: v.timeout_ms ?? 120_000,
122
+ });
123
+ const status = r.status === 0 && !r.error ? 'pass' : 'fail';
124
+ return {
125
+ name: v.name,
126
+ status,
127
+ stdout: r.stdout,
128
+ stderr: r.stderr || (r.error ? String(r.error.message || r.error) : undefined),
129
+ code: r.status,
130
+ signal: r.signal,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Roll the tree back per the configured strategy. Never throws —
136
+ * returns the resolved verb (or 'skipped' if the strategy could not be
137
+ * applied) and an optional stash ref.
138
+ *
139
+ * @param {'stash'|'hard'|'none'} mode
140
+ * @param {string} cwd
141
+ * @param {string} taskId
142
+ * @returns {{via: 'stash'|'hard'|'none'|'skipped', stashRef?: string}}
143
+ */
144
+ function rollback(mode, cwd, taskId) {
145
+ if (mode === 'none') return { via: 'none' };
146
+ if (!existsSync(join(cwd, '.git'))) return { via: 'skipped' };
147
+ const ts = new Date().toISOString();
148
+
149
+ if (mode === 'stash') {
150
+ const r = spawnSync(
151
+ 'git',
152
+ ['stash', 'push', '-u', '-m', `solidify-rollback:${taskId}:${ts}`],
153
+ { cwd, encoding: 'utf8', shell: false, timeout: 30_000 },
154
+ );
155
+ if (r.status !== 0) return { via: 'skipped' };
156
+ const refRes = spawnSync('git', ['stash', 'list', '-1', '--format=%gd'], {
157
+ cwd,
158
+ encoding: 'utf8',
159
+ shell: false,
160
+ timeout: 10_000,
161
+ });
162
+ const stashRef = refRes.status === 0 ? refRes.stdout.trim() : undefined;
163
+ return { via: 'stash', stashRef };
164
+ }
165
+
166
+ if (mode === 'hard') {
167
+ const r = spawnSync('git', ['reset', '--hard', 'HEAD'], {
168
+ cwd,
169
+ encoding: 'utf8',
170
+ shell: false,
171
+ timeout: 30_000,
172
+ });
173
+ if (r.status !== 0) return { via: 'skipped' };
174
+ return { via: 'hard' };
175
+ }
176
+
177
+ return { via: 'skipped' };
178
+ }
179
+
180
+ /**
181
+ * Run the solidify gate.
182
+ *
183
+ * @param {SolidifyOptions} opts
184
+ * @returns {Promise<SolidifyResult>}
185
+ */
186
+ export async function solidify(opts) {
187
+ if (!opts || typeof opts.taskId !== 'string' || opts.taskId.length === 0) {
188
+ throw new TypeError('solidify: opts.taskId is required (non-empty string)');
189
+ }
190
+ const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
191
+ const validations =
192
+ Array.isArray(opts.validations) && opts.validations.length > 0
193
+ ? opts.validations
194
+ : defaultValidations(opts.taskId);
195
+ const mode = opts.rollback ?? 'stash';
196
+
197
+ /** @type {SolidifyStep[]} */
198
+ const steps = [];
199
+ /** @type {SolidifyStep|null} */
200
+ let failingStep = null;
201
+ for (const v of validations) {
202
+ const step = runStep(v, cwd);
203
+ steps.push(step);
204
+ if (step.status === 'fail') {
205
+ failingStep = step;
206
+ break;
207
+ }
208
+ }
209
+
210
+ let rolledBackVia = 'none';
211
+ let stashRef;
212
+ if (failingStep && mode !== 'none') {
213
+ const r = rollback(mode, cwd, opts.taskId);
214
+ rolledBackVia = r.via;
215
+ stashRef = r.stashRef;
216
+ } else if (failingStep && mode === 'none') {
217
+ rolledBackVia = 'none';
218
+ }
219
+
220
+ const chainEvent = {
221
+ parent_event_id: opts.parentEventId ?? null,
222
+ agent: 'design-solidify',
223
+ decision_refs: opts.decisionRefs ?? [],
224
+ outcome: failingStep ? 'rolled-back' : 'pass',
225
+ task_id: opts.taskId,
226
+ rolled_back_via: rolledBackVia,
227
+ steps: steps.map((s) => ({ name: s.name, status: s.status, code: s.code })),
228
+ };
229
+ if (failingStep) {
230
+ chainEvent.rollback_reason = `${failingStep.name} failed (code=${failingStep.code})`;
231
+ }
232
+ if (stashRef) chainEvent.stash_ref = stashRef;
233
+ if (opts.chainPath) chainEvent.path = opts.chainPath;
234
+ if (opts.cwd) chainEvent.baseDir = opts.cwd;
235
+
236
+ const eventId = appendChainEvent(chainEvent);
237
+
238
+ if (typeof opts.emit === 'function') {
239
+ try {
240
+ opts.emit({
241
+ type: 'solidify.rollback',
242
+ timestamp: new Date().toISOString(),
243
+ sessionId: process.env.GDD_SESSION_ID || 'unknown',
244
+ payload: {
245
+ task_id: opts.taskId,
246
+ outcome: chainEvent.outcome,
247
+ rolled_back_via: rolledBackVia,
248
+ failing_step: failingStep ? failingStep.name : null,
249
+ },
250
+ });
251
+ } catch {
252
+ /* swallow — emission must never bubble */
253
+ }
254
+ }
255
+
256
+ /** @type {SolidifyResult} */
257
+ const result = {
258
+ outcome: failingStep ? 'fail' : 'pass',
259
+ steps,
260
+ rolledBackVia,
261
+ eventId,
262
+ };
263
+ if (stashRef) result.stashRef = stashRef;
264
+ return result;
265
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * _js-harness.cjs — child process worker for js-const.cjs.
4
+ *
5
+ * Reads argv[2] as a file path, attempts to require() it (CJS first,
6
+ * then dynamic import for ESM), extracts tokens per recognised shapes,
7
+ * prints `{tokens: ..., error?: string}` JSON on stdout, exits 0.
8
+ *
9
+ * Never throws — errors are returned as JSON so the parent can render
10
+ * them as warnings.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const target = process.argv[2];
16
+
17
+ function main() {
18
+ if (!target) {
19
+ return { tokens: {}, error: 'no-target' };
20
+ }
21
+ // Try CJS require first.
22
+ try {
23
+ // eslint-disable-next-line node/no-missing-require, global-require
24
+ const mod = require(target);
25
+ return { tokens: extract(mod) };
26
+ } catch (cjsErr) {
27
+ // Fall through to dynamic import.
28
+ }
29
+ // ESM dynamic import — async; wrap in IIFE.
30
+ return new Promise((resolve) => {
31
+ const url = require('node:url').pathToFileURL(target).href;
32
+ import(url).then(
33
+ (mod) => {
34
+ const candidate = mod.tokens ?? mod.default?.tokens ?? mod.default ?? mod;
35
+ resolve({ tokens: extract(candidate) });
36
+ },
37
+ (err) => resolve({ tokens: {}, error: `import-failed: ${err.message || String(err)}` }),
38
+ );
39
+ });
40
+ }
41
+
42
+ function extract(mod) {
43
+ if (mod && typeof mod === 'object') {
44
+ if (mod.tokens && typeof mod.tokens === 'object') return mod.tokens;
45
+ if (mod.default && typeof mod.default === 'object' && mod.default.tokens) {
46
+ return mod.default.tokens;
47
+ }
48
+ return mod;
49
+ }
50
+ return {};
51
+ }
52
+
53
+ Promise.resolve(main()).then(
54
+ (result) => {
55
+ try {
56
+ process.stdout.write(JSON.stringify(result));
57
+ } catch {
58
+ process.stdout.write(JSON.stringify({ tokens: {}, error: 'stringify-failed' }));
59
+ }
60
+ process.exit(0);
61
+ },
62
+ (err) => {
63
+ process.stdout.write(JSON.stringify({ tokens: {}, error: String(err) }));
64
+ process.exit(0);
65
+ },
66
+ );
@@ -0,0 +1,55 @@
1
+ /**
2
+ * design-tokens/css-vars.cjs — extract custom-property declarations
3
+ * from CSS / SCSS source (Plan 23-08).
4
+ *
5
+ * Parses any `--name: value;` declarations regardless of selector
6
+ * context. Last-write-wins on duplicate names. Block comments stripped.
7
+ * Strips the `--` prefix in the returned token map.
8
+ *
9
+ * Not supported (caller-warned via warnings):
10
+ * * SCSS `$var: value;` syntax — use the JS reader for SCSS exports
11
+ * * calc() / var() reference resolution — values returned verbatim
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const CUSTOM_PROP_RE = /--([A-Za-z0-9][A-Za-z0-9_-]*)\s*:\s*([^;]+?)\s*(?=[;}])/g;
20
+ const BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
21
+ const SCSS_VAR_RE = /^\s*\$[A-Za-z][A-Za-z0-9_-]*\s*:/m;
22
+
23
+ /**
24
+ * Read a CSS/SCSS file and extract `--token: value;` declarations.
25
+ *
26
+ * @param {string} filePath
27
+ * @returns {{tokens: Record<string, string>, source: string, format: 'css-vars', warnings: string[]}}
28
+ */
29
+ function readCssVars(filePath) {
30
+ const abs = path.resolve(filePath);
31
+ const raw = fs.readFileSync(abs, 'utf8');
32
+ const stripped = raw.replace(BLOCK_COMMENT_RE, '');
33
+ /** @type {string[]} */
34
+ const warnings = [];
35
+ if (SCSS_VAR_RE.test(stripped)) {
36
+ warnings.push('scss-vars-detected: $var: ... declarations are not parsed by css-vars.cjs');
37
+ }
38
+ /** @type {Record<string, string>} */
39
+ const tokens = {};
40
+ CUSTOM_PROP_RE.lastIndex = 0;
41
+ let m;
42
+ while ((m = CUSTOM_PROP_RE.exec(stripped)) !== null) {
43
+ const name = m[1];
44
+ const value = m[2].trim();
45
+ tokens[name] = value;
46
+ }
47
+ return {
48
+ tokens,
49
+ source: abs,
50
+ format: 'css-vars',
51
+ warnings,
52
+ };
53
+ }
54
+
55
+ module.exports = { readCssVars };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * design-tokens/figma.cjs — parse a Figma variable export JSON into
3
+ * a flat token map (Plan 23-08).
4
+ *
5
+ * Consumes the shape returned by mcp__figma__get_variable_defs (and the
6
+ * official Figma Variables Export JSON). Handles either:
7
+ * * `{ variableCollections: { <id>: { name, modes, variables: { <id>: {name, valuesByMode} } } } }`
8
+ * * Already-flattened `{ name: value }` map (passes through with format='figma')
9
+ *
10
+ * Mode handling: when a variable has multiple modes, we emit one token
11
+ * per mode using `<collection>.<varName>.<modeName>`. Single-mode
12
+ * variables emit the bare path.
13
+ *
14
+ * Color values are emitted as `rgba(R, G, B, A)` strings; numeric +
15
+ * string values pass through verbatim.
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+
23
+ function rgbaFor(c) {
24
+ if (typeof c !== 'object' || c === null) return String(c);
25
+ const r = Math.round((Number(c.r) || 0) * 255);
26
+ const g = Math.round((Number(c.g) || 0) * 255);
27
+ const b = Math.round((Number(c.b) || 0) * 255);
28
+ const a = c.a === undefined ? 1 : Number(c.a);
29
+ return a === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
30
+ }
31
+
32
+ function valueToString(v) {
33
+ if (v === null || v === undefined) return '';
34
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
35
+ return String(v);
36
+ }
37
+ if (typeof v === 'object') {
38
+ // Color shape: {r, g, b, a?}.
39
+ if (Object.prototype.hasOwnProperty.call(v, 'r') &&
40
+ Object.prototype.hasOwnProperty.call(v, 'g') &&
41
+ Object.prototype.hasOwnProperty.call(v, 'b')) {
42
+ return rgbaFor(v);
43
+ }
44
+ // Alias / reference — emit the full reference id for the consumer to resolve.
45
+ if (v.type === 'VARIABLE_ALIAS' && v.id) return `var(--${v.id})`;
46
+ return JSON.stringify(v);
47
+ }
48
+ return String(v);
49
+ }
50
+
51
+ /**
52
+ * @param {string} filePath
53
+ * @returns {{tokens: Record<string, string>, source: string, format: 'figma', warnings: string[]}}
54
+ */
55
+ function readFigma(filePath) {
56
+ const abs = path.resolve(filePath);
57
+ const raw = fs.readFileSync(abs, 'utf8');
58
+ /** @type {string[]} */
59
+ const warnings = [];
60
+ /** @type {unknown} */
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ } catch (err) {
65
+ return {
66
+ tokens: {},
67
+ source: abs,
68
+ format: 'figma',
69
+ warnings: [`json-parse-failed: ${err.message}`],
70
+ };
71
+ }
72
+
73
+ /** @type {Record<string, string>} */
74
+ const out = {};
75
+ // Branch 1: variableCollections shape.
76
+ if (parsed && typeof parsed === 'object' && parsed.variableCollections) {
77
+ for (const collId of Object.keys(parsed.variableCollections)) {
78
+ const coll = parsed.variableCollections[collId];
79
+ const collName = coll.name || collId;
80
+ const modes = coll.modes || {};
81
+ // modes might be an array of {modeId, name} or a map.
82
+ const modeNames = Array.isArray(modes)
83
+ ? new Map(modes.map((m) => [m.modeId, m.name]))
84
+ : new Map(Object.entries(modes).map(([id, m]) => [id, (m && m.name) || id]));
85
+ const vars = coll.variables || {};
86
+ for (const varId of Object.keys(vars)) {
87
+ const v = vars[varId];
88
+ const varName = v.name || varId;
89
+ const valuesByMode = v.valuesByMode || {};
90
+ const modeIds = Object.keys(valuesByMode);
91
+ if (modeIds.length === 0) {
92
+ warnings.push(`no-modes: ${collName}.${varName}`);
93
+ continue;
94
+ }
95
+ if (modeIds.length === 1) {
96
+ const key = `${collName}.${varName}`;
97
+ out[key] = valueToString(valuesByMode[modeIds[0]]);
98
+ } else {
99
+ for (const mid of modeIds) {
100
+ const modeName = modeNames.get(mid) || mid;
101
+ out[`${collName}.${varName}.${modeName}`] = valueToString(valuesByMode[mid]);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return { tokens: out, source: abs, format: 'figma', warnings };
107
+ }
108
+
109
+ // Branch 2: already-flattened bag.
110
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
111
+ for (const k of Object.keys(parsed)) {
112
+ out[k] = valueToString(parsed[k]);
113
+ }
114
+ return { tokens: out, source: abs, format: 'figma', warnings };
115
+ }
116
+
117
+ warnings.push('unrecognised-figma-shape');
118
+ return { tokens: out, source: abs, format: 'figma', warnings };
119
+ }
120
+
121
+ module.exports = { readFigma };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * design-tokens/index.cjs — facade over the four token readers
3
+ * (Plan 23-08).
4
+ *
5
+ * Auto-detects format from extension + content sniff, dispatches to
6
+ * css-vars / js-const / tailwind / figma. Returns the uniform
7
+ * `{tokens, source, format, warnings}` shape from each reader.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const { readCssVars } = require('./css-vars.cjs');
16
+ const { readJsConst } = require('./js-const.cjs');
17
+ const { readTailwind } = require('./tailwind.cjs');
18
+ const { readFigma } = require('./figma.cjs');
19
+
20
+ /**
21
+ * @typedef {Object} TokenSet
22
+ * @property {Object<string, string>} tokens
23
+ * @property {string} source
24
+ * @property {'css-vars'|'js-const'|'tailwind'|'figma'} format
25
+ * @property {string[]} [warnings]
26
+ */
27
+
28
+ /**
29
+ * Sniff format from a file's extension + a head-snippet of its content.
30
+ *
31
+ * @param {string} filePath
32
+ * @returns {'css-vars'|'js-const'|'tailwind'|'figma'}
33
+ */
34
+ function detectFormat(filePath) {
35
+ const lower = filePath.toLowerCase();
36
+ if (lower.endsWith('.css') || lower.endsWith('.scss')) return 'css-vars';
37
+ if (
38
+ /tailwind\.config\.(js|cjs|mjs|ts)$/.test(lower) ||
39
+ lower.endsWith('tailwind.config.js')
40
+ ) {
41
+ return 'tailwind';
42
+ }
43
+ if (lower.endsWith('.json')) {
44
+ // Sniff: variableCollections → figma, else js-const path with the JSON.
45
+ try {
46
+ const head = fs.readFileSync(filePath, 'utf8').slice(0, 4096);
47
+ if (head.includes('"variableCollections"')) return 'figma';
48
+ } catch {
49
+ /* fall through */
50
+ }
51
+ return 'figma';
52
+ }
53
+ // Default: any other JS/TS file is js-const.
54
+ return 'js-const';
55
+ }
56
+
57
+ /**
58
+ * Read tokens from a file with auto-detected format.
59
+ *
60
+ * @param {string} filePath
61
+ * @returns {TokenSet}
62
+ */
63
+ function read(filePath) {
64
+ const format = detectFormat(filePath);
65
+ switch (format) {
66
+ case 'css-vars':
67
+ return readCssVars(filePath);
68
+ case 'tailwind':
69
+ return readTailwind(filePath);
70
+ case 'figma':
71
+ return readFigma(filePath);
72
+ case 'js-const':
73
+ default:
74
+ return readJsConst(filePath);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Read multiple files into separate TokenSets.
80
+ *
81
+ * @param {string[]} filePaths
82
+ * @returns {TokenSet[]}
83
+ */
84
+ function readAll(filePaths) {
85
+ if (!Array.isArray(filePaths)) {
86
+ throw new TypeError('design-tokens: filePaths must be an array');
87
+ }
88
+ return filePaths.map((p) => read(p));
89
+ }
90
+
91
+ module.exports = {
92
+ read,
93
+ readAll,
94
+ detectFormat,
95
+ // re-export for callers that already know the format
96
+ readCssVars,
97
+ readJsConst,
98
+ readTailwind,
99
+ readFigma,
100
+ };