@selvakumaresra/specship 0.3.0 → 0.5.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.
Files changed (97) hide show
  1. package/README.md +11 -1
  2. package/commands/ss-brainstorm.md +68 -0
  3. package/commands/ss-design-implement.md +5 -0
  4. package/commands/ss-design-loop.md +125 -0
  5. package/dist/analytics/specship-impact.d.ts +72 -0
  6. package/dist/analytics/specship-impact.d.ts.map +1 -0
  7. package/dist/analytics/specship-impact.js +216 -0
  8. package/dist/analytics/specship-impact.js.map +1 -0
  9. package/dist/bin/specship.js +70 -4
  10. package/dist/bin/specship.js.map +1 -1
  11. package/dist/db/migrations.d.ts +1 -1
  12. package/dist/db/migrations.d.ts.map +1 -1
  13. package/dist/db/migrations.js +15 -1
  14. package/dist/db/migrations.js.map +1 -1
  15. package/dist/db/schema.sql +8 -0
  16. package/dist/designer/artifact-store.js +54 -0
  17. package/dist/designer/browser.js +141 -0
  18. package/dist/designer/cdp-ensure.js +60 -0
  19. package/dist/designer/cdp-env.js +18 -0
  20. package/dist/designer/cdp-trace.js +599 -0
  21. package/dist/designer/cross-platform.js +74 -0
  22. package/dist/designer/designer-controller.js +1413 -0
  23. package/dist/designer/file-panel.js +39 -0
  24. package/dist/designer/interstitials.js +97 -0
  25. package/dist/designer/oopif-reader.js +176 -0
  26. package/dist/designer/package-meta.js +18 -0
  27. package/dist/designer/preview-host.js +50 -0
  28. package/dist/designer/repo-root.js +31 -0
  29. package/dist/designer/run-state.js +353 -0
  30. package/dist/designer/session-store.js +59 -0
  31. package/dist/designer/ui-anchors.js +651 -0
  32. package/dist/index.d.ts +27 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +48 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/installer/index.d.ts +7 -2
  37. package/dist/installer/index.d.ts.map +1 -1
  38. package/dist/installer/index.js +3 -2
  39. package/dist/installer/index.js.map +1 -1
  40. package/dist/installer/instructions-template.d.ts +17 -0
  41. package/dist/installer/instructions-template.d.ts.map +1 -1
  42. package/dist/installer/instructions-template.js +31 -1
  43. package/dist/installer/instructions-template.js.map +1 -1
  44. package/dist/installer/targets/claude.d.ts +19 -0
  45. package/dist/installer/targets/claude.d.ts.map +1 -1
  46. package/dist/installer/targets/claude.js +100 -1
  47. package/dist/installer/targets/claude.js.map +1 -1
  48. package/dist/installer/targets/shared.d.ts +14 -0
  49. package/dist/installer/targets/shared.d.ts.map +1 -1
  50. package/dist/installer/targets/shared.js +49 -0
  51. package/dist/installer/targets/shared.js.map +1 -1
  52. package/dist/installer/targets/types.d.ts +8 -0
  53. package/dist/installer/targets/types.d.ts.map +1 -1
  54. package/dist/mcp/designer-tools.d.ts +33 -0
  55. package/dist/mcp/designer-tools.d.ts.map +1 -0
  56. package/dist/mcp/designer-tools.js +313 -0
  57. package/dist/mcp/designer-tools.js.map +1 -0
  58. package/dist/mcp/tools.d.ts.map +1 -1
  59. package/dist/mcp/tools.js +22 -1
  60. package/dist/mcp/tools.js.map +1 -1
  61. package/dist/server/ingest/impact-backfill.js +69 -0
  62. package/dist/server/ingest/impact-query.js +343 -0
  63. package/dist/server/ingest/index.js +2 -1
  64. package/dist/server/ingest/ingestor.js +41 -6
  65. package/dist/server/ingest/specship-classify.js +153 -0
  66. package/dist/server/routes/claude.js +32 -0
  67. package/dist/server/routes/spec.js +94 -0
  68. package/dist/server/server.js +26 -2
  69. package/dist/web/{chunk-JN6W7HCN.js → chunk-45QHGCB4.js} +1 -1
  70. package/dist/web/{chunk-RAAMPHPJ.js → chunk-A5R3MJMO.js} +1 -1
  71. package/dist/web/{chunk-2DHIGIOI.js → chunk-ASZ77FMZ.js} +1 -1
  72. package/dist/web/chunk-D5OCNEJA.js +2 -0
  73. package/dist/web/{chunk-3SEJX2BK.js → chunk-FHZHD2ZG.js} +1 -1
  74. package/dist/web/chunk-GR72OOCN.js +1 -0
  75. package/dist/web/{chunk-YAWCRPHV.js → chunk-NZEZCT65.js} +1 -1
  76. package/dist/web/chunk-O7434ZMN.js +1 -0
  77. package/dist/web/chunk-ODX6CT3I.js +6 -0
  78. package/dist/web/chunk-RASJHUXS.js +1 -0
  79. package/dist/web/chunk-TQ3P2QZO.js +1 -0
  80. package/dist/web/{chunk-BCZM5AXU.js → chunk-UBOZGQNK.js} +1 -1
  81. package/dist/web/chunk-WCHGDXWC.js +1 -0
  82. package/dist/web/{chunk-BPECIDVO.js → chunk-WCKHQIYN.js} +1 -1
  83. package/dist/web/{chunk-JFYVCXK3.js → chunk-WLIMNDS3.js} +1 -1
  84. package/dist/web/{chunk-LV4G6QFG.js → chunk-YAMRN47K.js} +1 -1
  85. package/dist/web/index.html +1 -1
  86. package/dist/web/main-X2KCYXZ4.js +1 -0
  87. package/dist/web/sw.js +69 -0
  88. package/dist/workflows/defaults/claude-design-implement.yaml +138 -49
  89. package/hooks/hooks.json +11 -0
  90. package/package.json +7 -3
  91. package/selectors.json +41 -0
  92. package/dist/web/chunk-2OKMB4KX.js +0 -2
  93. package/dist/web/chunk-4N5DWG46.js +0 -1
  94. package/dist/web/chunk-DA6SNNAF.js +0 -1
  95. package/dist/web/chunk-JT7P3DEK.js +0 -6
  96. package/dist/web/chunk-TWXZK6XM.js +0 -1
  97. package/dist/web/main-WVI3YTDU.js +0 -1
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OPEN_FILES_PANEL_EXPR = void 0;
4
+ // Opener for claude.ai/design's "Design Files" panel, shared by BOTH the live
5
+ // listFilesDetailed scrape (designer-controller) and the session.fileListScrape
6
+ // health anchor (ui-anchors) — one source so the probe exercises the exact path
7
+ // production runs. They diverged once and the probe went green while production
8
+ // silently no-op'd (PR #77 Codex P2); a shared constant prevents a repeat.
9
+ //
10
+ // Open-state is decided ONLY from the trigger's aria-expanded — the one reliable
11
+ // signal. Two tempting alternatives are dead ends Codex walked us through (PR #77):
12
+ // - a blind label.click() TOGGLES an already-open panel shut, and iterate()
13
+ // calls listFiles() before+after a generation, so the second scrape reads the
14
+ // bare page and corrupts newFiles/removedFiles;
15
+ // - counting filename-like body text to infer "already open" can't tell a real
16
+ // panel row from a filename mentioned in a chat turn (index.html), so it
17
+ // skips opening and scrapes incidental text.
18
+ // So: if the trigger exposes aria-expanded, obey it (open iff 'false'). If it
19
+ // does NOT, leave the panel as-is — never a blind toggle, never a body-text
20
+ // guess; the scrape then reads whatever is already visible (the long-standing
21
+ // behavior). React attaches handlers via root delegation, so element.onclick is
22
+ // null on the trigger — we click the label (the event bubbles to the delegated
23
+ // handler) rather than walking up for a non-null .onclick (which never fires).
24
+ //
25
+ // Returns true when the "Design Files" label is present (opened or already-open
26
+ // or left-as-is), false when it isn't found.
27
+ exports.OPEN_FILES_PANEL_EXPR = `(() => {
28
+ const spans = Array.from(document.querySelectorAll('span'));
29
+ const label = spans.find((s) => s.children.length === 0 && (s.textContent || '').trim() === 'Design Files');
30
+ if (!label) return false;
31
+ let t = label;
32
+ for (let i = 0; i < 4 && t; i++) {
33
+ const ex = t.getAttribute && t.getAttribute('aria-expanded');
34
+ if (ex === 'true') return true;
35
+ if (ex === 'false') { label.click(); return true; }
36
+ t = t.parentElement;
37
+ }
38
+ return true;
39
+ })()`;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ // Interstitial detection for claude.ai/design.
3
+ //
4
+ // The 2026-06 design UI interrupts the automated flow with content-only overlays
5
+ // that carry no stable data-testid or ARIA role — they can only be matched by
6
+ // their visible content. Each verb runs a pre-flight (DesignerController.
7
+ // clearInterstitials, wired through ensureReady) so automation doesn't silently
8
+ // stall on a banner, misread a frozen view as "done", or call a transient error
9
+ // a context ceiling. Captured live 2026-06-19 against Chrome 149.
10
+ //
11
+ // Detection lives here — pure and unit-tested — so the copy heuristics have one
12
+ // source of truth; the DOM/CDP glue (probe + click/reload/wait) stays in the
13
+ // controller. This mirrors preview-host.ts / run-state.ts: classify in a tested
14
+ // module, act in the controller.
15
+ //
16
+ // STRUCTURAL GUARD (review #1): the two TAKEOVER kinds (cloudflare,
17
+ // transient-error) replace the whole app shell, so they're classified ONLY when
18
+ // the shell is gone — `appShellPresent === false`. Without this, a design session
19
+ // whose chat transcript mentions "verify you are human" (an auth/CAPTCHA mockup)
20
+ // or "something went wrong … try again" (a Claude apology) would be misread as a
21
+ // real overlay and hard-block every verb. The token banner is benign (it sits
22
+ // over a live shell) and is gated on its own action button instead.
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.CONTINUE_HERE_TEXT = exports.TOKEN_BANNER_RE = exports.TRANSIENT_ERROR_BUTTON_RE = exports.TRANSIENT_ERROR_RE = exports.CLOUDFLARE_RE = exports.INTERSTITIAL_PROBE_EXPR = void 0;
25
+ exports.classifyInterstitial = classifyInterstitial;
26
+ exports.plannedAction = plannedAction;
27
+ exports.isBlockingInterstitial = isBlockingInterstitial;
28
+ // A single DOM read producing the InterstitialProbe shape. Exported as the ONE
29
+ // source so the live pre-flight (designer-controller) and the CI diagnostic
30
+ // (ci-health) probe identically — including the appShellPresent guard (review
31
+ // #5 / below-gate dedup). Evaluated via browser.evalValue.
32
+ exports.INTERSTITIAL_PROBE_EXPR = `(() => ({
33
+ bodyText: ((document.body && document.body.innerText) || '').slice(0, 20000),
34
+ buttonTexts: Array.from(document.querySelectorAll('button'))
35
+ .map((b) => (b.textContent || '').trim())
36
+ .filter(Boolean)
37
+ .slice(0, 300),
38
+ appShellPresent: !!document.querySelector('[data-testid="chat-composer-input"], [data-testid="chat-messages"]')
39
+ }))()`;
40
+ // Cloudflare bot-check / "verify you are human" takeover. The most blocking of
41
+ // the three — it replaces the app shell and can't be auto-solved, only waited
42
+ // out (it often self-clears) or handed to a human.
43
+ exports.CLOUDFLARE_RE = /verify you are human|performing security verification|review the security of your connection|checking your browser before|needs to review the security/i;
44
+ // Transient "Something went wrong" error page. Gated on app-shell-absence AND a
45
+ // real action button (review #1) so an inline "something went wrong" string in
46
+ // the transcript can't trip a reload storm.
47
+ exports.TRANSIENT_ERROR_RE = /something went wrong/i;
48
+ exports.TRANSIENT_ERROR_BUTTON_RE = /^(try again|back to projects)$/i;
49
+ // Context-save nudge: "Start a new chat to save 483k tokens of context" with
50
+ // New chat / Continue here. We click "Continue here" to keep the session's
51
+ // context — "New chat" would discard it.
52
+ exports.TOKEN_BANNER_RE = /start a new chat to save\b|save \d+k tokens of context/i;
53
+ exports.CONTINUE_HERE_TEXT = 'Continue here';
54
+ function hasButtonText(buttonTexts, text) {
55
+ const want = text.trim().toLowerCase();
56
+ return buttonTexts.some((t) => (t || '').trim().toLowerCase() === want);
57
+ }
58
+ function hasButtonMatching(buttonTexts, re) {
59
+ return buttonTexts.some((t) => re.test((t || '').trim()));
60
+ }
61
+ /**
62
+ * Classify the most pressing interstitial present, or null when the page is
63
+ * clear. Order is by severity: a Cloudflare takeover hides everything beneath
64
+ * it, so it's checked first; the token banner is the most benign (the composer
65
+ * stays usable beneath it) so it's checked last. Takeover kinds require the app
66
+ * shell to be ABSENT (see module header); the token banner requires its action
67
+ * button to be present (the phrase alone — e.g. echoed in chat — is not enough).
68
+ */
69
+ function classifyInterstitial(probe, opts = {}) {
70
+ const body = probe.bodyText || '';
71
+ const buttons = probe.buttonTexts || [];
72
+ if (!probe.appShellPresent) {
73
+ if (exports.CLOUDFLARE_RE.test(body))
74
+ return 'cloudflare';
75
+ if (exports.TRANSIENT_ERROR_RE.test(body) && hasButtonMatching(buttons, exports.TRANSIENT_ERROR_BUTTON_RE))
76
+ return 'transient-error';
77
+ }
78
+ if (exports.TOKEN_BANNER_RE.test(body) && hasButtonText(buttons, opts.continueHere || exports.CONTINUE_HERE_TEXT))
79
+ return 'token-banner';
80
+ return null;
81
+ }
82
+ /** The handling strategy for each interstitial kind. */
83
+ function plannedAction(kind) {
84
+ switch (kind) {
85
+ case 'token-banner':
86
+ return 'click-continue';
87
+ case 'transient-error':
88
+ return 'reload';
89
+ case 'cloudflare':
90
+ return 'await-human';
91
+ }
92
+ }
93
+ /** Blocking interstitials make a verb impossible; the token banner does not
94
+ * (the shell stays usable beneath it), so a residual one is never fatal. */
95
+ function isBlockingInterstitial(kind) {
96
+ return kind !== 'token-banner';
97
+ }
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OopifHtmlReader = void 0;
4
+ exports.captureOopifHtml = captureOopifHtml;
5
+ const cdp_trace_1 = require("./cdp-trace");
6
+ const preview_host_1 = require("./preview-host");
7
+ function evaluatedString(result) {
8
+ const rec = (0, cdp_trace_1.asRec)(result);
9
+ // An exceptionDetails means the evaluate threw — treat as no value.
10
+ if (rec.exceptionDetails)
11
+ return null;
12
+ const inner = (0, cdp_trace_1.asRec)(rec.result);
13
+ return typeof inner.value === 'string' && inner.value.length > 0 ? inner.value : null;
14
+ }
15
+ // Pick the preview child. The project renders its preview in a SINGLE OOPIF, so a
16
+ // unique preview-host child is unambiguously THE preview. Zero, or many (e.g. old
17
+ // and new preview coexisting during a file switch), -> null, so we never serve an
18
+ // arbitrary or stale frame as the requested file (PR #67 review).
19
+ //
20
+ // NOTE (live-verified): the OOPIF *document* URL is per-file
21
+ // (`…claudeusercontent.com/.../serve/<filename>?…`) — the `_bootstrap` is only the
22
+ // iframe element's src; the loader navigates the frame to the real serve URL. So
23
+ // matching the child against getIframeSrc() (`_bootstrap`) is wrong; host+uniqueness
24
+ // is the correct signal. A future multi-preview disambiguation can match the active
25
+ // file via that `/serve/<filename>` path, but single-preview is the steady state.
26
+ function pickPreviewChild(children, isPreviewUrl) {
27
+ // type === 'iframe' guards against a same-origin worker/service-worker on
28
+ // claudeusercontent.com counting as a second "preview" and flooring the read to
29
+ // null (#67 review) — the preview is an iframe document, not a worker target.
30
+ const previews = children.filter((c) => typeof c.url === 'string' && c.type === 'iframe' && isPreviewUrl(c.url));
31
+ const only = previews[0];
32
+ return previews.length === 1 && only ? only : null;
33
+ }
34
+ /**
35
+ * PURE orchestrator for reading a cross-origin preview OOPIF's rendered HTML.
36
+ * Owns the CDP command sequence but not the socket; every call is timeout-bounded
37
+ * and bounded polling uses opts.now. Returns the serialized outerHTML string, or
38
+ * null on no-match / no-value / timeout / any throw — never throws, never hangs.
39
+ */
40
+ async function captureOopifHtml(send, opts) {
41
+ const now = opts.now ?? (() => Date.now());
42
+ const waitForAttachMs = opts.waitForAttachMs ?? 1500;
43
+ const pollMs = opts.pollMs ?? 25;
44
+ const sendTimeoutMs = opts.sendTimeoutMs ?? 4000;
45
+ // Bound every CDP call: a live-but-silent socket must degrade to null, not hang
46
+ // forever (PR #67 review). A synchronous throw from `send` becomes a rejection.
47
+ const sendBounded = (method, params, sessionId) => {
48
+ let timer;
49
+ const timeout = new Promise((_resolve, reject) => {
50
+ timer = setTimeout(() => reject(new Error('cdp-send-timeout')), sendTimeoutMs);
51
+ });
52
+ const call = (async () => send(method, params, sessionId))();
53
+ return Promise.race([call, timeout]).finally(() => clearTimeout(timer));
54
+ };
55
+ let armed = false;
56
+ try {
57
+ // (a) arm auto-attach. flatten:true is LOAD-BEARING — without it Chrome routes
58
+ // child traffic via the deprecated nested sendMessageToTarget the base send()
59
+ // does not speak. waitForDebuggerOnStart:false so OOPIFs aren't paused.
60
+ await sendBounded('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
61
+ armed = true;
62
+ // (b) bounded-poll the injected snapshot for the unique preview child.
63
+ const deadline = now() + waitForAttachMs;
64
+ let child = pickPreviewChild(opts.attachedTargets(), opts.isPreviewUrl);
65
+ while (!child && now() < deadline) {
66
+ await new Promise((r) => setTimeout(r, pollMs));
67
+ child = pickPreviewChild(opts.attachedTargets(), opts.isPreviewUrl);
68
+ }
69
+ if (!child)
70
+ return null;
71
+ // (c) PRIMARY: DOM serialization on the CHILD session. No page-world execution
72
+ // context to depend on or be spoofed by; DOM.getDocument implicitly enables the
73
+ // DOM domain, and depth:0 fetches just the root node before getOuterHTML
74
+ // serializes the tree.
75
+ try {
76
+ const doc = (0, cdp_trace_1.asRec)(await sendBounded('DOM.getDocument', { depth: 0, pierce: false }, child.sessionId));
77
+ const root = (0, cdp_trace_1.asRec)(doc.root);
78
+ const nodeId = typeof root.nodeId === 'number' ? root.nodeId : null;
79
+ if (nodeId !== null) {
80
+ const outer = (0, cdp_trace_1.asRec)(await sendBounded('DOM.getOuterHTML', { nodeId }, child.sessionId));
81
+ if (typeof outer.outerHTML === 'string' && outer.outerHTML.length > 0)
82
+ return outer.outerHTML;
83
+ }
84
+ }
85
+ catch {
86
+ // fall through to the Runtime fallback
87
+ }
88
+ // (d) FALLBACK: page-world Runtime.evaluate on the CHILD sessionId (without it
89
+ // the eval runs in the parent page and returns the shell). For builds/states
90
+ // where the DOM route returns nothing.
91
+ const evalRes = await sendBounded('Runtime.evaluate', { expression: 'document.documentElement.outerHTML', returnByValue: true, awaitPromise: false }, child.sessionId);
92
+ return evaluatedString(evalRes);
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ finally {
98
+ // (e) bounded, self-contained teardown — never let cleanup throw or hang.
99
+ if (armed) {
100
+ try {
101
+ await sendBounded('Target.setAutoAttach', { autoAttach: false, waitForDebuggerOnStart: false, flatten: true });
102
+ }
103
+ catch {
104
+ /* best-effort */
105
+ }
106
+ }
107
+ }
108
+ }
109
+ class OopifHtmlReader extends cdp_trace_1.CdpSession {
110
+ // Keyed by sessionId; each entry also carries targetId for targetInfoChanged.
111
+ childTargets = new Map();
112
+ constructor(ws, target, opts = {}) {
113
+ // One-shot reader: PIN reconnect:false (a mid-capture gap degrades to null ->
114
+ // node-fetch fallback; there is no one-shot terminal to recover). Pinned last
115
+ // so a caller can't accidentally route the base reconnect path into the no-op
116
+ // enableDomains() (#67 review, below-gate).
117
+ super(ws, target, { ...opts, reconnect: false });
118
+ }
119
+ static async attach(opts = {}) {
120
+ if (typeof WebSocket === 'undefined')
121
+ return null;
122
+ try {
123
+ const { ws, target } = await OopifHtmlReader.connectTarget(opts);
124
+ return new OopifHtmlReader(ws, target, opts);
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ // No Network.enable for a one-shot OOPIF read; captureOopifHtml issues the
131
+ // Target.setAutoAttach itself so the pure unit owns the full sequence.
132
+ async enableDomains() { }
133
+ onEvent(method, params, _sessionId) {
134
+ const p = (0, cdp_trace_1.asRec)(params);
135
+ if (method === 'Target.attachedToTarget') {
136
+ const sessionId = typeof p.sessionId === 'string' ? p.sessionId : '';
137
+ const info = (0, cdp_trace_1.asRec)(p.targetInfo);
138
+ const url = typeof info.url === 'string' ? info.url : '';
139
+ const type = typeof info.type === 'string' ? info.type : '';
140
+ const targetId = typeof info.targetId === 'string' ? info.targetId : undefined;
141
+ if (sessionId)
142
+ this.childTargets.set(sessionId, { sessionId, url, type, targetId });
143
+ return;
144
+ }
145
+ if (method === 'Target.targetInfoChanged') {
146
+ // A child can attach as about:blank then navigate to _bootstrap (would be a
147
+ // false null), or attach as _bootstrap then navigate away (would be stale
148
+ // wrong-content). Keep the stored URL current, correlated by targetId (#67).
149
+ const info = (0, cdp_trace_1.asRec)(p.targetInfo);
150
+ const targetId = typeof info.targetId === 'string' ? info.targetId : '';
151
+ if (!targetId)
152
+ return;
153
+ for (const child of this.childTargets.values()) {
154
+ if (child.targetId === targetId) {
155
+ if (typeof info.url === 'string')
156
+ child.url = info.url;
157
+ if (typeof info.type === 'string')
158
+ child.type = info.type;
159
+ }
160
+ }
161
+ return;
162
+ }
163
+ if (method === 'Target.detachedFromTarget') {
164
+ const sessionId = typeof p.sessionId === 'string' ? p.sessionId : '';
165
+ if (sessionId)
166
+ this.childTargets.delete(sessionId);
167
+ }
168
+ }
169
+ async readPreviewHtml() {
170
+ return captureOopifHtml(this.send.bind(this), {
171
+ attachedTargets: () => [...this.childTargets.values()],
172
+ isPreviewUrl: preview_host_1.isPreviewIframeSrc
173
+ });
174
+ }
175
+ }
176
+ exports.OopifHtmlReader = OopifHtmlReader;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PACKAGE_VERSION = exports.PACKAGE_METADATA = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const repo_root_1 = require("./repo-root");
10
+ function readPackageMetadata() {
11
+ const raw = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(repo_root_1.REPO_ROOT, 'package.json'), 'utf8'));
12
+ return {
13
+ name: typeof raw.name === 'string' ? raw.name : 'designer',
14
+ version: typeof raw.version === 'string' ? raw.version : '0.0.0'
15
+ };
16
+ }
17
+ exports.PACKAGE_METADATA = readPackageMetadata();
18
+ exports.PACKAGE_VERSION = exports.PACKAGE_METADATA.version;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPreviewIframeSrc = isPreviewIframeSrc;
4
+ exports.previewIframeVariant = previewIframeVariant;
5
+ exports.isBootstrapShellHtml = isBootstrapShellHtml;
6
+ // The design preview iframe is a load-bearing invariant: it must serve from
7
+ // claudeusercontent.com. How it's addressed has drifted — the legacy form was
8
+ // `claudeusercontent.com/...?t=<signed-token>`; the 2026-06 redesign (issue #61)
9
+ // moved it to a per-project `<uuid>.claudeusercontent.com/_bootstrap...` subdomain
10
+ // with no token. Assert the HOST (real drift = the preview leaving that domain),
11
+ // not a substring — a substring match would wave through a suffix-attached host
12
+ // (`claudeusercontent.com.evil.test`) or a query/path mention
13
+ // (`evil.test/?u=claudeusercontent.com`), defeating the drift anchor this backs.
14
+ function isPreviewIframeSrc(src) {
15
+ let host;
16
+ try {
17
+ host = new URL(src).hostname;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ return host === 'claudeusercontent.com' || host.endsWith('.claudeusercontent.com');
23
+ }
24
+ // Which addressing scheme the (already host-validated) preview src uses. Reported
25
+ // in the health anchor's detail so drift between the legacy signed-token form and
26
+ // the current per-project bootstrap subdomain stays legible.
27
+ function previewIframeVariant(src) {
28
+ if (/[?&]t=/.test(src))
29
+ return 'signed-token';
30
+ if (/\/_bootstrap/.test(src))
31
+ return 'bootstrap-subdomain';
32
+ return 'other';
33
+ }
34
+ // True when `html` is the unauthenticated ~1.1KB loader shell the bootstrap
35
+ // iframe serves to the parent origin (not the rendered design). A node fetch of a
36
+ // bootstrap-subdomain src always returns this shell; the real DOM only exists in
37
+ // the cross-origin OOPIF (see oopif-reader.ts). The shell's stable signature is
38
+ // its postMessage init handshake ('omelette-preview-init') — class/markup hashes
39
+ // drift, that string does not. Used to assert the OOPIF read returned rendered
40
+ // content, not the loader, and to defend callers against saving a shell as the
41
+ // captured artifact. Empty / non-shell HTML → false (don't misread "no sample").
42
+ //
43
+ // Size-bounded (review below-gate): a rendered design that legitimately
44
+ // DOCUMENTS the preview protocol could contain the marker string, so require the
45
+ // document also be loader-sized. The real shell is ~1.1KB; any rendered design is
46
+ // far larger, so the marker + a sub-4KB body is an unambiguous shell signal.
47
+ const BOOTSTRAP_SHELL_MAX_BYTES = 4000;
48
+ function isBootstrapShellHtml(html) {
49
+ return typeof html === 'string' && html.length < BOOTSTRAP_SHELL_MAX_BYTES && html.includes('omelette-preview-init');
50
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.REPO_ROOT = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ // Walk up from this file's location until we find package.json. Lets the
10
+ // vendored designer subtree locate its shipped resource (selectors.json) at
11
+ // the SpecShip install root regardless of layout:
12
+ // - source mode: repo-root.ts at src/designer/ → walks up to the repo root.
13
+ // - compiled mode (tsc → dist/designer/): repo-root.js → walks up past
14
+ // dist/ to the dir holding SpecShip's package.json (the install root).
15
+ //
16
+ // Uses CommonJS `__dirname` — this subtree compiles to CJS under
17
+ // src/designer/tsconfig.json, so there is no `import.meta` to reconcile with
18
+ // SpecShip's `module: commonjs` toolchain.
19
+ function findRepoRoot() {
20
+ let dir = __dirname;
21
+ for (let i = 0; i < 8; i++) {
22
+ if (node_fs_1.default.existsSync(node_path_1.default.join(dir, 'package.json')))
23
+ return dir;
24
+ const parent = node_path_1.default.dirname(dir);
25
+ if (parent === dir)
26
+ break;
27
+ dir = parent;
28
+ }
29
+ throw new Error('repo-root: could not find package.json walking up from ' + __dirname);
30
+ }
31
+ exports.REPO_ROOT = findRepoRoot();