@hegemonart/get-design-done 1.45.0 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +97 -0
- package/README.md +4 -0
- package/SKILL.md +5 -1
- package/dist/claude-code/.claude/skills/figma-extract/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/graphify/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/list-pins/SKILL.md +27 -0
- package/dist/claude-code/.claude/skills/live/SKILL.md +98 -0
- package/dist/claude-code/.claude/skills/pin/SKILL.md +37 -0
- package/dist/claude-code/.claude/skills/unpin/SKILL.md +31 -0
- package/package.json +3 -1
- package/reference/live-mode-integration.md +80 -0
- package/reference/registry.json +14 -0
- package/reference/schemas/events.schema.json +1 -1
- package/reference/schemas/live-session.schema.json +64 -0
- package/reference/skill-metadata.md +117 -0
- package/scripts/lib/live/bandit-feed.cjs +64 -0
- package/scripts/lib/live/events.cjs +86 -0
- package/scripts/lib/live/harness-mode.cjs +93 -0
- package/scripts/lib/live/postcheck.cjs +158 -0
- package/scripts/lib/live/runtime.cjs +233 -0
- package/scripts/lib/live/scope-guard.cjs +145 -0
- package/scripts/lib/live/session-store.cjs +364 -0
- package/scripts/lib/manifest/schemas/skills.schema.json +42 -1
- package/scripts/lib/manifest/skills.json +415 -83
- package/scripts/lib/pin/cli.cjs +145 -0
- package/scripts/lib/pin/harness-detect.cjs +75 -0
- package/scripts/lib/pin/store.cjs +288 -0
- package/skills/figma-extract/SKILL.md +1 -1
- package/skills/graphify/SKILL.md +1 -1
- package/skills/list-pins/SKILL.md +27 -0
- package/skills/live/SKILL.md +98 -0
- package/skills/pin/SKILL.md +37 -0
- package/skills/unpin/SKILL.md +31 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/live/runtime.cjs — Phase 47 (Live Mode) browser-side runtime.
|
|
4
|
+
*
|
|
5
|
+
* The `/gdd:live` skill injects a small browser runtime into the running dev
|
|
6
|
+
* server via the Claude Preview MCP `preview_eval` tool. That runtime does two
|
|
7
|
+
* jobs:
|
|
8
|
+
*
|
|
9
|
+
* 1. PICK — install a one-shot click handler that captures the element the
|
|
10
|
+
* user clicks and reports `{selector, computedStyle subset, boundingRect,
|
|
11
|
+
* tagName, classList}` back to the agent (the live_pick payload).
|
|
12
|
+
* 2. SWAP — apply one of the N generated design variants in-place by setting a
|
|
13
|
+
* `data-gdd-variant="N"` attribute on the picked element and applying that
|
|
14
|
+
* variant's inline style / markup, and read back which variant is live.
|
|
15
|
+
*
|
|
16
|
+
* Why a string and not a real module: `preview_eval` ships a JS source string to
|
|
17
|
+
* the page and evaluates it there. There is no bundler in the loop, so the
|
|
18
|
+
* runtime is authored here as a plain template string (`RUNTIME_JS`) and injected
|
|
19
|
+
* verbatim. The IIFE is idempotent: re-injecting it rebinds the same singleton on
|
|
20
|
+
* `window.__gddLive` rather than stacking duplicate listeners.
|
|
21
|
+
*
|
|
22
|
+
* Design constraints (mirror the other scripts/lib/live/* modules):
|
|
23
|
+
* - Pure, dependency-free CommonJS. No `fs`, no network, no Date.now() at the
|
|
24
|
+
* module top level. `buildSelector` is a pure function of its input.
|
|
25
|
+
* - Cross-platform: this file is plain JS text; nothing here touches the OS.
|
|
26
|
+
* - Ships in the npm package (scripts/lib/ is in package.json `files`), so it
|
|
27
|
+
* stays runtime-safe (no dev-only requires).
|
|
28
|
+
*
|
|
29
|
+
* Exports:
|
|
30
|
+
* - RUNTIME_JS the browser IIFE source string injected via preview_eval.
|
|
31
|
+
* - pickReportShape a plain object documenting the live_pick payload fields.
|
|
32
|
+
* - buildSelector pure helper a test can assert against (id > data-attr >
|
|
33
|
+
* nth-of-type class path), shared with the in-page logic.
|
|
34
|
+
* - DATA_ATTR the variant marker attribute name ("data-gdd-variant").
|
|
35
|
+
* - GLOBAL_KEY the window singleton key ("__gddLive").
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** The attribute the runtime stamps on the picked element to mark the live variant. */
|
|
39
|
+
const DATA_ATTR = 'data-gdd-variant';
|
|
40
|
+
|
|
41
|
+
/** The window-singleton key the IIFE installs itself under (idempotent re-inject). */
|
|
42
|
+
const GLOBAL_KEY = '__gddLive';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Shape of the payload the runtime reports for a pick. Documented here so the
|
|
46
|
+
* skill and the tests agree on the contract without parsing RUNTIME_JS. Values
|
|
47
|
+
* are field descriptions, not live data.
|
|
48
|
+
*/
|
|
49
|
+
const pickReportShape = {
|
|
50
|
+
selector: 'string — a stable CSS selector for the picked element (see buildSelector)',
|
|
51
|
+
tagName: 'string — lowercased tag name, e.g. "button"',
|
|
52
|
+
classList: 'string[] — the element classList as an array',
|
|
53
|
+
boundingRect: 'object — {x, y, width, height, top, right, bottom, left} from getBoundingClientRect',
|
|
54
|
+
computedStyle:
|
|
55
|
+
'object — a curated subset of getComputedStyle: color, backgroundColor, fontSize, ' +
|
|
56
|
+
'fontWeight, fontFamily, lineHeight, padding, margin, borderRadius, borderColor, ' +
|
|
57
|
+
'borderWidth, display, boxShadow',
|
|
58
|
+
variant: 'number|null — the current data-gdd-variant value on the element, or null',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pure selector strategy, shared with the in-page runtime by being inlined into
|
|
63
|
+
* RUNTIME_JS below. Strategy, most-specific first:
|
|
64
|
+
* 1. `#id` when the element has an id.
|
|
65
|
+
* 2. `[data-testid="..."]` when a stable data-testid / data-test is present.
|
|
66
|
+
* 3. `tag.class1.class2:nth-of-type(k)` — tag + (up to 2) classes + structural
|
|
67
|
+
* nth-of-type index among same-tag siblings, for everything else.
|
|
68
|
+
*
|
|
69
|
+
* @param {{id?:string, dataTestId?:string, tagName?:string, classList?:string[], nthOfType?:number}} elInfo
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function buildSelector(elInfo) {
|
|
73
|
+
const info = elInfo || {};
|
|
74
|
+
if (info.id) return `#${info.id}`;
|
|
75
|
+
if (info.dataTestId) return `[data-testid="${info.dataTestId}"]`;
|
|
76
|
+
const tag = (info.tagName || 'div').toLowerCase();
|
|
77
|
+
const classes = Array.isArray(info.classList) ? info.classList.filter(Boolean).slice(0, 2) : [];
|
|
78
|
+
const classPart = classes.length ? '.' + classes.join('.') : '';
|
|
79
|
+
const nth = Number.isInteger(info.nthOfType) && info.nthOfType > 0 ? `:nth-of-type(${info.nthOfType})` : '';
|
|
80
|
+
return `${tag}${classPart}${nth}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The browser-side runtime, authored as a plain string. Injected verbatim by the
|
|
85
|
+
* skill via `preview_eval`. The in-page `buildSelector` mirrors the exported one
|
|
86
|
+
* above (kept in sync by the phase-47-runtime test asserting both prefer id over
|
|
87
|
+
* class). The IIFE is idempotent: it reuses window.__gddLive if already present.
|
|
88
|
+
*/
|
|
89
|
+
const RUNTIME_JS = `
|
|
90
|
+
(function () {
|
|
91
|
+
var GLOBAL_KEY = ${JSON.stringify(GLOBAL_KEY)};
|
|
92
|
+
var DATA_ATTR = ${JSON.stringify(DATA_ATTR)};
|
|
93
|
+
|
|
94
|
+
// Idempotent: reuse the singleton on re-inject so listeners never stack.
|
|
95
|
+
if (window[GLOBAL_KEY] && window[GLOBAL_KEY].__installed) {
|
|
96
|
+
return window[GLOBAL_KEY];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- pure selector strategy (mirror of runtime.cjs buildSelector) ---
|
|
100
|
+
function buildSelector(el) {
|
|
101
|
+
if (!el || el.nodeType !== 1) return '';
|
|
102
|
+
if (el.id) return '#' + el.id;
|
|
103
|
+
var testId = el.getAttribute && (el.getAttribute('data-testid') || el.getAttribute('data-test'));
|
|
104
|
+
if (testId) return '[data-testid="' + testId + '"]';
|
|
105
|
+
var tag = (el.tagName || 'DIV').toLowerCase();
|
|
106
|
+
var classes = [];
|
|
107
|
+
if (el.classList && el.classList.length) {
|
|
108
|
+
for (var i = 0; i < el.classList.length && classes.length < 2; i++) classes.push(el.classList[i]);
|
|
109
|
+
}
|
|
110
|
+
var classPart = classes.length ? '.' + classes.join('.') : '';
|
|
111
|
+
var nth = '';
|
|
112
|
+
if (el.parentNode) {
|
|
113
|
+
var sameTag = [];
|
|
114
|
+
var sibs = el.parentNode.children || [];
|
|
115
|
+
for (var j = 0; j < sibs.length; j++) if (sibs[j].tagName === el.tagName) sameTag.push(sibs[j]);
|
|
116
|
+
if (sameTag.length > 1) {
|
|
117
|
+
var idx = sameTag.indexOf(el) + 1;
|
|
118
|
+
if (idx > 0) nth = ':nth-of-type(' + idx + ')';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return tag + classPart + nth;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- curated computed-style subset for the pick report ---
|
|
125
|
+
var STYLE_KEYS = [
|
|
126
|
+
'color', 'backgroundColor', 'fontSize', 'fontWeight', 'fontFamily', 'lineHeight',
|
|
127
|
+
'padding', 'margin', 'borderRadius', 'borderColor', 'borderWidth', 'display', 'boxShadow'
|
|
128
|
+
];
|
|
129
|
+
function styleSubset(el) {
|
|
130
|
+
var out = {};
|
|
131
|
+
var cs = window.getComputedStyle ? window.getComputedStyle(el) : null;
|
|
132
|
+
if (!cs) return out;
|
|
133
|
+
for (var i = 0; i < STYLE_KEYS.length; i++) out[STYLE_KEYS[i]] = cs[STYLE_KEYS[i]];
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function report(el) {
|
|
138
|
+
var r = el.getBoundingClientRect ? el.getBoundingClientRect() : {};
|
|
139
|
+
var classList = [];
|
|
140
|
+
if (el.classList) for (var i = 0; i < el.classList.length; i++) classList.push(el.classList[i]);
|
|
141
|
+
var v = el.getAttribute(DATA_ATTR);
|
|
142
|
+
return {
|
|
143
|
+
selector: buildSelector(el),
|
|
144
|
+
tagName: (el.tagName || '').toLowerCase(),
|
|
145
|
+
classList: classList,
|
|
146
|
+
boundingRect: {
|
|
147
|
+
x: r.x, y: r.y, width: r.width, height: r.height,
|
|
148
|
+
top: r.top, right: r.right, bottom: r.bottom, left: r.left
|
|
149
|
+
},
|
|
150
|
+
computedStyle: styleSubset(el),
|
|
151
|
+
variant: v == null ? null : Number(v)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var state = {
|
|
156
|
+
__installed: true,
|
|
157
|
+
picked: null, // last picked element
|
|
158
|
+
lastReport: null, // last pick report (read by the agent after a click)
|
|
159
|
+
originals: new WeakMap() // element -> {style, html} captured before first swap
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// --- PICK: one-shot capture click handler ---
|
|
163
|
+
function onPick(ev) {
|
|
164
|
+
var el = ev.target;
|
|
165
|
+
if (!el || el.nodeType !== 1) return;
|
|
166
|
+
ev.preventDefault();
|
|
167
|
+
ev.stopPropagation();
|
|
168
|
+
state.picked = el;
|
|
169
|
+
state.lastReport = report(el);
|
|
170
|
+
document.removeEventListener('click', onPick, true);
|
|
171
|
+
state.picking = false;
|
|
172
|
+
return state.lastReport;
|
|
173
|
+
}
|
|
174
|
+
state.pick = function () {
|
|
175
|
+
state.picking = true;
|
|
176
|
+
document.addEventListener('click', onPick, true);
|
|
177
|
+
return true;
|
|
178
|
+
};
|
|
179
|
+
state.getLastPick = function () { return state.lastReport; };
|
|
180
|
+
|
|
181
|
+
// --- SWAP: apply / read a variant on the picked (or given) element ---
|
|
182
|
+
function captureOriginal(el) {
|
|
183
|
+
if (!state.originals.has(el)) {
|
|
184
|
+
state.originals.set(el, { style: el.getAttribute('style'), html: el.innerHTML });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// variant = {n:Number, style?:Object, html?:String}
|
|
188
|
+
state.swapVariant = function (variant, el) {
|
|
189
|
+
var target = el || state.picked;
|
|
190
|
+
if (!target || !variant) return null;
|
|
191
|
+
captureOriginal(target);
|
|
192
|
+
target.setAttribute(DATA_ATTR, String(variant.n));
|
|
193
|
+
if (variant.style && typeof variant.style === 'object') {
|
|
194
|
+
for (var k in variant.style) if (Object.prototype.hasOwnProperty.call(variant.style, k)) {
|
|
195
|
+
target.style[k] = variant.style[k];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (typeof variant.html === 'string') target.innerHTML = variant.html;
|
|
199
|
+
return Number(target.getAttribute(DATA_ATTR));
|
|
200
|
+
};
|
|
201
|
+
state.currentVariant = function (el) {
|
|
202
|
+
var target = el || state.picked;
|
|
203
|
+
if (!target) return null;
|
|
204
|
+
var v = target.getAttribute(DATA_ATTR);
|
|
205
|
+
return v == null ? null : Number(v);
|
|
206
|
+
};
|
|
207
|
+
state.revert = function (el) {
|
|
208
|
+
var target = el || state.picked;
|
|
209
|
+
if (!target) return false;
|
|
210
|
+
var orig = state.originals.get(target);
|
|
211
|
+
if (orig) {
|
|
212
|
+
if (orig.style == null) target.removeAttribute('style'); else target.setAttribute('style', orig.style);
|
|
213
|
+
target.innerHTML = orig.html;
|
|
214
|
+
}
|
|
215
|
+
target.removeAttribute(DATA_ATTR);
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// expose the pure helper for assertions / debugging
|
|
220
|
+
state.buildSelector = buildSelector;
|
|
221
|
+
|
|
222
|
+
window[GLOBAL_KEY] = state;
|
|
223
|
+
return state;
|
|
224
|
+
})();
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
RUNTIME_JS,
|
|
229
|
+
pickReportShape,
|
|
230
|
+
buildSelector,
|
|
231
|
+
DATA_ATTR,
|
|
232
|
+
GLOBAL_KEY,
|
|
233
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/live/scope-guard.cjs — Phase 47 (Live Mode) write-scope guard.
|
|
4
|
+
*
|
|
5
|
+
* `/gdd:live` lets the agent rewrite the source files behind a picked DOM
|
|
6
|
+
* element. To keep that powerful loop safe, EVERY write the live session makes
|
|
7
|
+
* must be inside an explicitly enumerated allow-set:
|
|
8
|
+
*
|
|
9
|
+
* (a) the session's own bookkeeping: anything under `.design/live-sessions/`
|
|
10
|
+
* and `.design/telemetry/` (relative to projectRoot), and
|
|
11
|
+
* (b) the `implicated` source files — the concrete files the picked element
|
|
12
|
+
* maps to (passed in by the caller from the element→source mapping).
|
|
13
|
+
*
|
|
14
|
+
* Anything else — a random repo file, a `package.json`, a `..` escape out of the
|
|
15
|
+
* project — is rejected. This is the runtime backstop for the same intent the
|
|
16
|
+
* Editor/Write protected-paths list expresses at author time.
|
|
17
|
+
*
|
|
18
|
+
* Resolution rules:
|
|
19
|
+
* - All paths are resolved to absolute with path.resolve so `..` segments are
|
|
20
|
+
* collapsed BEFORE comparison (a `../../etc/passwd` can never sneak past a
|
|
21
|
+
* string-prefix check).
|
|
22
|
+
* - Containment uses a normalized, separator-terminated prefix so that
|
|
23
|
+
* `.design/live-sessions-evil/x` does NOT count as inside
|
|
24
|
+
* `.design/live-sessions/` (sibling-directory false positive).
|
|
25
|
+
* - On case-insensitive / Windows filesystems we still compare case-sensitive
|
|
26
|
+
* prefixes after resolve; the implicated set is matched by exact resolved
|
|
27
|
+
* path, which is the conservative choice (a guard that is too strict fails
|
|
28
|
+
* closed, never open).
|
|
29
|
+
*
|
|
30
|
+
* Pure, dependency-free CommonJS (`path` only — no `fs`, no network, no clock).
|
|
31
|
+
* Cross-platform via path.resolve / path.join.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const path = require('path');
|
|
35
|
+
|
|
36
|
+
/** The two project-relative directory roots always writable by a live session. */
|
|
37
|
+
const ALWAYS_ALLOWED_SUBDIRS = Object.freeze([
|
|
38
|
+
path.join('.design', 'live-sessions'),
|
|
39
|
+
path.join('.design', 'telemetry'),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* True when `child` is the same path as, or nested inside, `parentDir`.
|
|
44
|
+
* Both inputs must already be absolute + resolved. Uses a separator-terminated
|
|
45
|
+
* prefix so sibling dirs that share a name prefix are NOT treated as inside.
|
|
46
|
+
*/
|
|
47
|
+
function isWithin(parentDir, child) {
|
|
48
|
+
if (child === parentDir) return true;
|
|
49
|
+
const withSep = parentDir.endsWith(path.sep) ? parentDir : parentDir + path.sep;
|
|
50
|
+
return child.startsWith(withSep);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalize the allowed write-set for a live session into a flat, resolved
|
|
55
|
+
* structure the predicates can test against.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} args
|
|
58
|
+
* @param {string} args.projectRoot project root (required to anchor the always-allowed dirs)
|
|
59
|
+
* @param {Array<string>} [args.implicated] source files the picked element maps to
|
|
60
|
+
* @returns {{ dirs: string[], files: Set<string> }}
|
|
61
|
+
* `dirs` — resolved directory prefixes any descendant of which is allowed.
|
|
62
|
+
* `files` — resolved exact file paths that are allowed.
|
|
63
|
+
*/
|
|
64
|
+
function enumerateScope(args = {}) {
|
|
65
|
+
const { projectRoot } = args;
|
|
66
|
+
if (!projectRoot) throw new TypeError('enumerateScope: projectRoot is required');
|
|
67
|
+
const root = path.resolve(projectRoot);
|
|
68
|
+
|
|
69
|
+
const dirs = ALWAYS_ALLOWED_SUBDIRS.map((rel) => path.resolve(root, rel));
|
|
70
|
+
|
|
71
|
+
const files = new Set();
|
|
72
|
+
const implicated = Array.isArray(args.implicated) ? args.implicated : [];
|
|
73
|
+
for (const f of implicated) {
|
|
74
|
+
if (f == null || !String(f).length) continue;
|
|
75
|
+
// Implicated paths may be absolute or relative-to-projectRoot; resolve both
|
|
76
|
+
// against the root so callers can pass either form.
|
|
77
|
+
files.add(path.resolve(root, String(f)));
|
|
78
|
+
}
|
|
79
|
+
return { dirs, files };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether `targetPath` is inside the enumerated scope.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} args
|
|
86
|
+
* @param {string} args.projectRoot
|
|
87
|
+
* @param {string} args.targetPath the path about to be written
|
|
88
|
+
* @param {Array<string>} [args.implicated] element→source files
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function isInScope(args = {}) {
|
|
92
|
+
const { projectRoot, targetPath } = args;
|
|
93
|
+
if (!projectRoot) throw new TypeError('isInScope: projectRoot is required');
|
|
94
|
+
if (targetPath == null || !String(targetPath).length) {
|
|
95
|
+
throw new TypeError('isInScope: targetPath is required');
|
|
96
|
+
}
|
|
97
|
+
const root = path.resolve(projectRoot);
|
|
98
|
+
const target = path.resolve(root, String(targetPath));
|
|
99
|
+
const { dirs, files } = enumerateScope({ projectRoot, implicated: args.implicated });
|
|
100
|
+
|
|
101
|
+
if (files.has(target)) return true;
|
|
102
|
+
for (const d of dirs) {
|
|
103
|
+
if (isWithin(d, target)) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Throw unless `targetPath` is inside the enumerated scope. The error message
|
|
110
|
+
* enumerates the allowed set so a violation is diagnosable.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} args same shape as isInScope
|
|
113
|
+
* @returns {{ targetPath: string, resolved: string }} on success
|
|
114
|
+
*/
|
|
115
|
+
function assertInScope(args = {}) {
|
|
116
|
+
const { projectRoot, targetPath } = args;
|
|
117
|
+
if (!projectRoot) throw new TypeError('assertInScope: projectRoot is required');
|
|
118
|
+
if (targetPath == null || !String(targetPath).length) {
|
|
119
|
+
throw new TypeError('assertInScope: targetPath is required');
|
|
120
|
+
}
|
|
121
|
+
if (isInScope(args)) {
|
|
122
|
+
const root = path.resolve(projectRoot);
|
|
123
|
+
return { targetPath: String(targetPath), resolved: path.resolve(root, String(targetPath)) };
|
|
124
|
+
}
|
|
125
|
+
const root = path.resolve(projectRoot);
|
|
126
|
+
const resolved = path.resolve(root, String(targetPath));
|
|
127
|
+
const { dirs, files } = enumerateScope({ projectRoot, implicated: args.implicated });
|
|
128
|
+
const allowed = [
|
|
129
|
+
...dirs.map((d) => `${d}${path.sep}* (always-allowed)`),
|
|
130
|
+
...[...files].map((f) => `${f} (implicated)`),
|
|
131
|
+
];
|
|
132
|
+
throw new Error(
|
|
133
|
+
`scope-guard: refusing to write "${resolved}" — outside the live-session write scope.\n` +
|
|
134
|
+
`Allowed:\n ${allowed.length ? allowed.join('\n ') : '(none)'}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
assertInScope,
|
|
140
|
+
isInScope,
|
|
141
|
+
enumerateScope,
|
|
142
|
+
// exported for callers + tests
|
|
143
|
+
isWithin,
|
|
144
|
+
ALWAYS_ALLOWED_SUBDIRS,
|
|
145
|
+
};
|