@bastani/atomic 0.9.11-alpha.6 → 0.9.11-alpha.7

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/package.json +1 -1
  7. package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
  8. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
  9. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
  10. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
  11. package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
  12. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
  13. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
  14. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  15. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
  16. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
  17. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
  18. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
  19. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
  20. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
  21. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
  22. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
  23. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
  24. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  25. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
  26. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  27. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
  28. package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
  29. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
  30. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
  31. package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
  32. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
  33. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
  34. package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
  35. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
  36. package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
  37. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
  38. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
  39. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
  40. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  41. package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
  42. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
  43. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
  44. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
  46. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
  48. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
  49. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
  50. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
  65. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
  66. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
  67. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
  68. package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  69. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
  70. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
  71. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
  72. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
  73. package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
  75. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  80. package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
  81. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
  82. package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
  88. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  89. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
  90. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
  91. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  92. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
  93. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
  94. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  95. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
  96. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
  97. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
  98. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
  99. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
  100. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
  101. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
  102. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
  103. package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
  104. package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
  105. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
  106. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  107. package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
  108. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
  109. package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
  110. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
  111. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
  112. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
  113. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
  115. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
  116. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
  117. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
  118. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
  120. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  121. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
  123. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  124. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
  125. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
  127. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  128. package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
  129. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/chat-session-host.js +22 -1
  131. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  132. package/dist/modes/interactive/interactive-agent-events.js +9 -3
  133. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  134. package/docs/tui.md +7 -2
  135. package/docs/workflows.md +1 -1
  136. package/npm-shrinkwrap.json +23 -23
  137. package/package.json +2 -2
  138. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
  139. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
  140. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
@@ -0,0 +1,280 @@
1
+ /**
2
+ * TanStack Start live-mode adapter.
3
+ *
4
+ * TanStack Start is SSR: there is no static index.html to patch. The document
5
+ * shell is a React component (`shellComponent`/`component`) defined in the root
6
+ * route file, `src/routes/__root.tsx`, which renders `<html>…<body>{children}
7
+ * <Scripts /></body></html>`.
8
+ *
9
+ * A raw `<script src>` placed in that JSX is server-rendered into the streamed
10
+ * HTML, but React's script handling and hydration make it an unreliable place
11
+ * to load a cross-origin dev bundle. So, like the Nuxt and SvelteKit adapters,
12
+ * this keeps the injected code in a dev-only managed component that appends the
13
+ * live script on mount (client-only, after hydration). The adapter mounts that
14
+ * component from the root document and removes it cleanly on stop.
15
+ *
16
+ * The managed component lives OUTSIDE `src/routes/` (in `src/impeccable/`) so
17
+ * the TanStack Router file-based route generator never treats it as a route.
18
+ */
19
+
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { buildLiveScriptSrc } from '../live-inject.mjs';
23
+
24
+ export const TANSTACK_MARKER_OPEN = '{/* impeccable-live-tanstack-start */}';
25
+ export const TANSTACK_MARKER_CLOSE = '{/* impeccable-live-tanstack-end */}';
26
+ export const TANSTACK_COMPONENT_DIR = 'src/impeccable';
27
+ export const TANSTACK_COMPONENT_BASENAME = 'ImpeccableLiveRoot';
28
+
29
+ const ROOT_ROUTE_CANDIDATES = [
30
+ 'src/routes/__root.tsx',
31
+ 'src/routes/__root.jsx',
32
+ 'src/routes/__root.ts',
33
+ 'src/routes/__root.js',
34
+ 'app/routes/__root.tsx',
35
+ 'app/routes/__root.jsx',
36
+ ];
37
+
38
+ const START_PACKAGES = [
39
+ '@tanstack/react-start',
40
+ '@tanstack/solid-start',
41
+ '@tanstack/start',
42
+ ];
43
+
44
+ export function detectTanStackStartProject(cwd = process.cwd()) {
45
+ if (!packageHasTanStackStart(cwd)) return null;
46
+ const rootRoute = findRootRouteFile(cwd);
47
+ if (!rootRoute) return null;
48
+
49
+ const ext = path.extname(rootRoute);
50
+ const componentExt = ext === '.jsx' || ext === '.js' ? '.jsx' : '.tsx';
51
+ const componentFile = `${TANSTACK_COMPONENT_DIR}/${TANSTACK_COMPONENT_BASENAME}${componentExt}`;
52
+ const componentImport = relativeImportSpecifier(rootRoute, componentFile);
53
+
54
+ return { rootRoute, componentFile, componentImport, ext };
55
+ }
56
+
57
+ export function applyTanStackLiveAdapter({ cwd = process.cwd(), port, token, project = detectTanStackStartProject(cwd) } = {}) {
58
+ if (!project) return { error: 'tanstack_not_detected' };
59
+ if (!Number.isFinite(Number(port))) {
60
+ throw new Error('TanStack Start live adapter requires a numeric port');
61
+ }
62
+
63
+ // Write the managed mount component.
64
+ const componentAbs = path.join(cwd, project.componentFile);
65
+ const componentBody = buildTanStackLiveRootComponent(Number(port), token);
66
+ const componentExisted = fs.existsSync(componentAbs);
67
+ if (componentExisted && !isManagedComponent(fs.readFileSync(componentAbs, 'utf-8'))) {
68
+ // A non-Impeccable file already sits at our managed path — refuse to clobber.
69
+ return {
70
+ file: project.componentFile,
71
+ error: 'tanstack_component_conflict',
72
+ hint: `${project.componentFile} already exists and is not managed by Impeccable Live`,
73
+ };
74
+ }
75
+ fs.mkdirSync(path.dirname(componentAbs), { recursive: true });
76
+ fs.writeFileSync(componentAbs, componentBody, 'utf-8');
77
+
78
+ // Patch the root document to import + render the mount component.
79
+ const rootAbs = path.join(cwd, project.rootRoute);
80
+ const before = fs.readFileSync(rootAbs, 'utf-8');
81
+ const after = patchTanStackRoot(before, project.componentImport);
82
+ const changed = after !== before;
83
+ if (changed) fs.writeFileSync(rootAbs, after, 'utf-8');
84
+
85
+ return {
86
+ file: project.rootRoute,
87
+ adapter: 'tanstack-start',
88
+ inserted: changed || !componentExisted,
89
+ componentFile: project.componentFile,
90
+ devOnly: true,
91
+ };
92
+ }
93
+
94
+ export function removeTanStackLiveAdapter({ cwd = process.cwd(), project = detectTanStackStartProject(cwd) } = {}) {
95
+ if (!project) return { error: 'tanstack_not_detected' };
96
+ let removed = false;
97
+
98
+ const rootAbs = path.join(cwd, project.rootRoute);
99
+ if (fs.existsSync(rootAbs)) {
100
+ const before = fs.readFileSync(rootAbs, 'utf-8');
101
+ const after = unpatchTanStackRoot(before);
102
+ if (after !== before) {
103
+ fs.writeFileSync(rootAbs, after, 'utf-8');
104
+ removed = true;
105
+ }
106
+ }
107
+
108
+ const componentAbs = path.join(cwd, project.componentFile);
109
+ if (fs.existsSync(componentAbs)) {
110
+ fs.rmSync(componentAbs, { force: true });
111
+ removed = true;
112
+ }
113
+ pruneEmptyDir(path.dirname(componentAbs), path.join(cwd, 'src'));
114
+
115
+ return {
116
+ file: project.rootRoute,
117
+ adapter: 'tanstack-start',
118
+ removed,
119
+ componentFile: project.componentFile,
120
+ };
121
+ }
122
+
123
+ export function patchTanStackRoot(content, componentImport) {
124
+ let out = String(content || '');
125
+ const importStatement = `import ImpeccableLiveRoot from '${componentImport}';`;
126
+
127
+ if (!out.includes(importStatement)) {
128
+ out = insertAfterLastImport(out, importStatement);
129
+ }
130
+
131
+ if (!out.includes(TANSTACK_MARKER_OPEN)) {
132
+ const block =
133
+ `${TANSTACK_MARKER_OPEN}\n`
134
+ + ` <ImpeccableLiveRoot />\n`
135
+ + ` ${TANSTACK_MARKER_CLOSE}\n `;
136
+ // Anchor before <Scripts …/> (the stable TanStack Start document marker);
137
+ // fall back to before </body>.
138
+ const scriptsMatch = out.match(/<Scripts\b/);
139
+ if (scriptsMatch) {
140
+ out = out.slice(0, scriptsMatch.index) + block + out.slice(scriptsMatch.index);
141
+ } else {
142
+ const bodyClose = out.lastIndexOf('</body>');
143
+ if (bodyClose !== -1) {
144
+ out = out.slice(0, bodyClose) + block + out.slice(bodyClose);
145
+ }
146
+ }
147
+ }
148
+
149
+ return out;
150
+ }
151
+
152
+ export function unpatchTanStackRoot(content) {
153
+ let out = String(content || '');
154
+ // Remove exactly the inserted block (open marker → component → close marker →
155
+ // trailing newline + the indent that leads back to the anchor). Leaving the
156
+ // leading indent before the open marker intact hands it back to the anchor
157
+ // (e.g. `<Scripts />`) so the file round-trips byte-for-byte.
158
+ const blockRe = new RegExp(
159
+ escapeRegExp(TANSTACK_MARKER_OPEN)
160
+ + '\\s*<ImpeccableLiveRoot\\s*/>\\s*'
161
+ + escapeRegExp(TANSTACK_MARKER_CLOSE)
162
+ + '\\r?\\n?[ \\t]*',
163
+ 'g',
164
+ );
165
+ out = out.replace(blockRe, '');
166
+ // Remove only the managed import line — not any following blank line.
167
+ out = out.replace(
168
+ new RegExp("^import ImpeccableLiveRoot from '[^']*';[ \\t]*\\r?\\n", 'gm'),
169
+ '',
170
+ );
171
+ return out;
172
+ }
173
+
174
+ export function buildTanStackLiveRootComponent(port, token) {
175
+ const liveSrc = buildLiveScriptSrc(Number(port), token);
176
+ return `/* impeccable-live-tanstack-start */
177
+ import { useEffect } from 'react';
178
+
179
+ const LIVE_SRC = '${liveSrc}';
180
+ const LIVE_SELECTOR = 'script[data-impeccable-live-tanstack]';
181
+
182
+ // Dev-only mount for Impeccable Live. TanStack Start server-renders the root
183
+ // document, so this appends the live-mode bundle from the client after
184
+ // hydration (mirrors the Nuxt/SvelteKit adapters). Renders nothing on the
185
+ // server, so there is no hydration mismatch.
186
+ export default function ImpeccableLiveRoot() {
187
+ useEffect(() => {
188
+ if (typeof document === 'undefined') return;
189
+ const expected = new URL(LIVE_SRC, window.location.href).href;
190
+ let script = document.querySelector(LIVE_SELECTOR);
191
+ if (script && script.src === expected) return;
192
+ if (script) script.remove();
193
+
194
+ script = document.createElement('script');
195
+ script.src = LIVE_SRC;
196
+ script.async = true;
197
+ script.setAttribute('data-impeccable-live-tanstack', '');
198
+ script.setAttribute('data-impeccable-live-script', 'true');
199
+ document.head.appendChild(script);
200
+
201
+ return () => {
202
+ if (script && script.isConnected) script.remove();
203
+ };
204
+ }, []);
205
+
206
+ return null;
207
+ }
208
+ `;
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // helpers
213
+ // ---------------------------------------------------------------------------
214
+
215
+ // The managed mount component carries the `impeccable-live-tanstack` marker in
216
+ // its leading comment and its script data-attribute; user files never do.
217
+ function isManagedComponent(content) {
218
+ return String(content || '').includes('impeccable-live-tanstack');
219
+ }
220
+
221
+ function findRootRouteFile(cwd) {
222
+ for (const rel of ROOT_ROUTE_CANDIDATES) {
223
+ if (fs.existsSync(path.join(cwd, rel))) return rel;
224
+ }
225
+ return null;
226
+ }
227
+
228
+ function packageHasTanStackStart(cwd) {
229
+ const file = path.join(cwd, 'package.json');
230
+ if (!fs.existsSync(file)) return false;
231
+ try {
232
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
233
+ const deps = {
234
+ ...(pkg.dependencies || {}),
235
+ ...(pkg.devDependencies || {}),
236
+ ...(pkg.peerDependencies || {}),
237
+ };
238
+ return START_PACKAGES.some((name) => Boolean(deps[name]));
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+
244
+ function relativeImportSpecifier(fromFile, toFile) {
245
+ const rel = path.posix.relative(
246
+ path.posix.dirname(fromFile.split(path.sep).join('/')),
247
+ toFile.split(path.sep).join('/'),
248
+ ).replace(/\.(tsx|ts|jsx|js)$/, '');
249
+ return rel.startsWith('.') ? rel : `./${rel}`;
250
+ }
251
+
252
+ function insertAfterLastImport(content, importStatement) {
253
+ const importRe = /^import\b[^\n]*\n/gm;
254
+ let lastEnd = -1;
255
+ let m;
256
+ while ((m = importRe.exec(content)) !== null) {
257
+ lastEnd = m.index + m[0].length;
258
+ }
259
+ if (lastEnd === -1) {
260
+ return `${importStatement}\n${content}`;
261
+ }
262
+ return content.slice(0, lastEnd) + importStatement + '\n' + content.slice(lastEnd);
263
+ }
264
+
265
+ function pruneEmptyDir(dir, stopDir) {
266
+ let current = dir;
267
+ while (current.startsWith(stopDir) && current !== stopDir) {
268
+ try {
269
+ if (fs.readdirSync(current).length > 0) return;
270
+ fs.rmdirSync(current);
271
+ current = path.dirname(current);
272
+ } catch {
273
+ return;
274
+ }
275
+ }
276
+ }
277
+
278
+ function escapeRegExp(value) {
279
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
280
+ }
@@ -16,7 +16,11 @@
16
16
  import fs from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import { isGeneratedFile } from './lib/is-generated.mjs';
19
+ import { getLiveDir, safeSessionId } from './lib/impeccable-paths.mjs';
20
+ import { resolveLiveTemplateExtensions } from './lib/template-extensions.mjs';
19
21
  import { readBuffer as readManualEditsBuffer, writeBuffer as writeManualEditsBuffer } from './live/manual-edits-buffer.mjs';
22
+ import { NEVER_SOURCE_DIRS, findSourceFile } from './live/source-search.mjs';
23
+ import { withSourceLockSync } from './live/source-lock.mjs';
20
24
  import {
21
25
  applyDeferredSvelteComponentAccepts,
22
26
  findSvelteComponentManifest,
@@ -24,7 +28,46 @@ import {
24
28
  removeSvelteComponentSession,
25
29
  } from './live/svelte-component.mjs';
26
30
 
27
- const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
31
+ const ACCEPT_LOCK_WAIT_MS = 1_000;
32
+ // Mirrors VARIANT_ID_PATTERN in live/event-validation.mjs, which gates the same
33
+ // value arriving over HTTP.
34
+ const VARIANT_NUM_PATTERN = /^[0-9]{1,3}$/;
35
+
36
+ /**
37
+ * A thrown accept/discard is a real failure, not a manual handoff.
38
+ *
39
+ * live/completion.mjs only classifies a result as `error` when it carries
40
+ * `mode: 'error'`; anything else unhandled falls through to `agent_done` with a
41
+ * successful ack, and reference/live.md then tells the agent to finish the edit
42
+ * by hand. That is right for the documented fallback paths and wrong here: a
43
+ * `source_locked` contention needs a retry (hand-editing races the publisher
44
+ * holding the lock), and a crash needs surfacing, not a hand-applied guess.
45
+ */
46
+ function operationFailure(err, extra = {}) {
47
+ return { handled: false, mode: 'error', error: err.message, ...extra };
48
+ }
49
+
50
+ /**
51
+ * Mark an unhandled preview-path result as a real failure.
52
+ *
53
+ * operationFailure only covers results built from a *thrown* error. The accept
54
+ * implementations also return `{handled: false, error}` for their own checks
55
+ * (variant missing, template empty, original text ambiguous), and those arrived
56
+ * without `mode`, so completion.mjs classified them as agent_done and
57
+ * reference/live.md routed the agent to "read file, find markers, edit".
58
+ *
59
+ * That handoff only makes sense for a plain wrapper session, which is the one
60
+ * shape with markers in the user's source to edit. Component and isolated
61
+ * artifact previews keep the source clean until Accept, so there is nothing to
62
+ * hand-edit and an unhandled result is always a failure. `previewMode` is
63
+ * exactly that discriminator: only the preview branches set it.
64
+ */
65
+ function markPreviewFailure(result) {
66
+ if (result?.handled === false && !result.mode && result.previewMode) {
67
+ return { ...result, mode: 'error' };
68
+ }
69
+ return result;
70
+ }
28
71
 
29
72
  // ---------------------------------------------------------------------------
30
73
  // CLI
@@ -63,7 +106,52 @@ Output (JSON):
63
106
  const isDiscard = args.includes('--discard');
64
107
 
65
108
  if (!id) { console.error('Missing --id'); process.exit(1); }
109
+ // `id` becomes a path segment (accept receipts, preview manifests, generated
110
+ // component dirs). Reject separators and traversal here so one check covers
111
+ // every downstream sink.
112
+ try { safeSessionId(id); } catch { console.error('Invalid --id'); process.exit(1); }
66
113
  if (!isDiscard && !variantNum) { console.error('Need --discard or --variant N'); process.exit(1); }
114
+ // `variantNum` is interpolated into a RegExp and into the markup written back
115
+ // to source. The browser and the /events schema both constrain it to digits;
116
+ // enforce the same here, or `--variant '.*'` matches the `original` block
117
+ // first and silently accepts the original while reporting success.
118
+ if (!isDiscard && !VARIANT_NUM_PATTERN.test(variantNum)) {
119
+ console.error('Invalid --variant');
120
+ process.exit(1);
121
+ }
122
+
123
+ const requestedOperation = isDiscard ? 'discard' : 'accept';
124
+ const priorReceipt = readAcceptReceipt(process.cwd(), id);
125
+ if (priorReceipt) {
126
+ const sameOperation = priorReceipt.operation === requestedOperation
127
+ && (isDiscard || String(priorReceipt.variantId) === String(variantNum));
128
+ console.log(JSON.stringify(sameOperation
129
+ ? { ...priorReceipt.result, handled: true, alreadyApplied: true }
130
+ : {
131
+ // mode: 'error' is what marks this a real failure rather than a manual
132
+ // handoff. Without it, live/completion.mjs classifies the reply as
133
+ // agent_done and reference/live.md tells the agent to "read file, find
134
+ // markers, edit" by hand — which would apply a second, conflicting
135
+ // accept on top of the one the receipt already recorded.
136
+ handled: false,
137
+ mode: 'error',
138
+ error: 'accept_receipt_conflict',
139
+ priorOperation: priorReceipt.operation,
140
+ priorVariantId: priorReceipt.variantId ?? null,
141
+ }));
142
+ return;
143
+ }
144
+ const emitResult = (rawResult) => {
145
+ const result = markPreviewFailure(rawResult);
146
+ if (result?.handled !== false) {
147
+ writeAcceptReceipt(process.cwd(), id, {
148
+ operation: requestedOperation,
149
+ variantId: isDiscard ? null : String(variantNum),
150
+ result,
151
+ });
152
+ }
153
+ console.log(JSON.stringify(result));
154
+ };
67
155
 
68
156
  let paramValues = null;
69
157
  if (paramValuesRaw) {
@@ -82,39 +170,55 @@ Output (JSON):
82
170
 
83
171
  if (svelteComponentManifest) {
84
172
  if (isDiscard) {
85
- removeSvelteComponentSession(id, process.cwd());
86
- console.log(JSON.stringify({
87
- handled: true,
173
+ let result;
174
+ try {
175
+ result = withSourceLockSync(
176
+ path.resolve(process.cwd(), svelteComponentManifest.sourceFile),
177
+ 'discard:' + id,
178
+ () => {
179
+ removeSvelteComponentSession(id, process.cwd());
180
+ return { handled: true };
181
+ },
182
+ { waitMs: ACCEPT_LOCK_WAIT_MS },
183
+ );
184
+ } catch (err) {
185
+ result = operationFailure(err);
186
+ }
187
+ emitResult({
188
+ ...result,
88
189
  file: svelteComponentManifest.sourceFile,
89
190
  carbonize: false,
90
191
  previewMode: 'svelte-component',
91
192
  componentDir: svelteComponentManifest.componentDir,
92
- }));
193
+ });
93
194
  return;
94
195
  }
95
196
 
96
197
  let result;
97
198
  try {
98
- result = inlineSvelteComponentAccept(
99
- svelteComponentManifest,
100
- variantNum,
101
- paramValues,
102
- process.cwd(),
199
+ result = withSourceLockSync(
200
+ path.resolve(process.cwd(), svelteComponentManifest.sourceFile),
201
+ 'accept:' + id,
202
+ () => inlineSvelteComponentAccept(
203
+ svelteComponentManifest,
204
+ variantNum,
205
+ paramValues,
206
+ process.cwd(),
207
+ ),
208
+ { waitMs: ACCEPT_LOCK_WAIT_MS },
103
209
  );
104
210
  } catch (err) {
105
- result = {
106
- handled: false,
107
- error: err.message,
211
+ result = operationFailure(err, {
108
212
  file: svelteComponentManifest.sourceFile,
109
213
  sourceFile: svelteComponentManifest.sourceFile,
110
214
  previewMode: 'svelte-component',
111
215
  componentDir: svelteComponentManifest.componentDir,
112
- };
216
+ });
113
217
  }
114
218
  if (result.carbonize) {
115
219
  result.todo = 'REQUIRED before next poll: carbonize cleanup in ' + result.file + '. See reference/live.md "Required after accept".';
116
220
  }
117
- console.log(JSON.stringify({ handled: result.handled !== false, ...result }));
221
+ emitResult({ handled: result.handled !== false, ...result });
118
222
  return;
119
223
  }
120
224
 
@@ -145,10 +249,25 @@ Output (JSON):
145
249
  }
146
250
 
147
251
  if (isDiscard) {
148
- const result = handleDiscard(id, lines, targetFile);
149
- console.log(JSON.stringify({ handled: true, file: relFile, carbonize: false, ...result }));
252
+ let result;
253
+ // handleDiscard takes the source lock, which throws SOURCE_LOCKED under
254
+ // contention. Without this catch the CLI exits non-zero with empty stdout
255
+ // and the agent gets no JSON to act on.
256
+ try {
257
+ result = handleDiscard(id, lines, targetFile);
258
+ } catch (err) {
259
+ emitResult(operationFailure(err, { file: relFile }));
260
+ return;
261
+ }
262
+ emitResult({ handled: true, file: relFile, carbonize: false, ...result });
150
263
  } else {
151
- const result = handleAccept(id, variantNum, lines, targetFile, paramValues);
264
+ let result;
265
+ try {
266
+ result = handleAccept(id, variantNum, lines, targetFile, paramValues);
267
+ } catch (err) {
268
+ emitResult(operationFailure(err, { file: relFile }));
269
+ return;
270
+ }
152
271
  const acceptedOriginalText = result.acceptedOriginalText || '';
153
272
  delete result.acceptedOriginalText;
154
273
  // Single-line attention-grabber when cleanup is required. The full
@@ -167,7 +286,7 @@ Output (JSON):
167
286
  // Non-fatal; the buffer stays as-is and the user can discard later.
168
287
  }
169
288
  }
170
- console.log(JSON.stringify({ handled: true, file: relFile, ...result }));
289
+ emitResult({ handled: true, file: relFile, ...result });
171
290
  }
172
291
  }
173
292
 
@@ -235,7 +354,14 @@ function scrubManualEditsAgainstFile(_targetFile, cwd = process.cwd(), originalB
235
354
  // Discard
236
355
  // ---------------------------------------------------------------------------
237
356
 
238
- function handleDiscard(id, lines, targetFile) {
357
+ function handleDiscard(id, _lines, targetFile) {
358
+ return withSourceLockSync(targetFile, 'discard:' + id, () => {
359
+ const lines = fs.readFileSync(targetFile, 'utf-8').split('\n');
360
+ return handleDiscardUnlocked(id, lines, targetFile);
361
+ }, { waitMs: ACCEPT_LOCK_WAIT_MS });
362
+ }
363
+
364
+ function handleDiscardUnlocked(id, lines, targetFile) {
239
365
  const block = findMarkerBlock(id, lines);
240
366
  if (!block) return { handled: false, error: 'Markers not found' };
241
367
 
@@ -330,7 +456,24 @@ function reindentContent(contentLines, fromIndent, toIndent) {
330
456
  });
331
457
  }
332
458
 
333
- function handleAccept(id, variantNum, lines, targetFile, paramValues) {
459
+ function handleAccept(id, variantNum, _lines, targetFile, paramValues) {
460
+ return withSourceLockSync(targetFile, 'accept:' + id, () => {
461
+ const lines = fs.readFileSync(targetFile, 'utf-8').split('\n');
462
+ return handleAcceptUnlocked(id, variantNum, lines, targetFile, paramValues);
463
+ }, { waitMs: ACCEPT_LOCK_WAIT_MS });
464
+ }
465
+
466
+ function handleAcceptUnlocked(id, variantNum, lines, targetFile, paramValues) {
467
+ const built = buildAcceptedWrappedSource(id, variantNum, lines, targetFile, paramValues);
468
+ if (built.handled === false) return built;
469
+ fs.writeFileSync(targetFile, built.content, 'utf-8');
470
+ return {
471
+ carbonize: built.carbonize,
472
+ acceptedOriginalText: built.acceptedOriginalText,
473
+ };
474
+ }
475
+
476
+ function buildAcceptedWrappedSource(id, variantNum, lines, targetFile, paramValues) {
334
477
  const block = findMarkerBlock(id, lines);
335
478
  if (!block) return { handled: false, error: 'Markers not found' };
336
479
 
@@ -375,11 +518,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
375
518
  ...replacement,
376
519
  ...lines.slice(replaceRange.end + 1),
377
520
  ];
378
- fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
379
-
380
- return { carbonize: needsCarbonize, acceptedOriginalText: originalContent.join('\n') };
521
+ return {
522
+ content: newLines.join('\n'),
523
+ carbonize: needsCarbonize,
524
+ acceptedOriginalText: originalContent.join('\n'),
525
+ };
381
526
  }
382
527
 
528
+
383
529
  function readSourceShadowPreviewMeta(content, id) {
384
530
  const escaped = escapeRegExp(id);
385
531
  const wrapperRe = new RegExp('<[^>]+data-impeccable-variants=(["\'])' + escaped + '\\1[^>]*>');
@@ -766,57 +912,51 @@ function detectCommentSyntax(filePath) {
766
912
  // File search (find the file containing session markers)
767
913
  // ---------------------------------------------------------------------------
768
914
 
915
+ /**
916
+ * Accept also skips `dist` / `build` outright, where wrap descends into them so
917
+ * its `includeGenerated` second pass can report a `generatedMatch`. Accept has
918
+ * no such pass: a marker found in build output is only ever a stale copy of the
919
+ * marker in source.
920
+ */
921
+ const SEARCH_SKIP_DIRS = [...NEVER_SOURCE_DIRS, 'dist', 'build'];
922
+
769
923
  function findSessionFile(id, cwd) {
770
- const marker = 'impeccable-variants-start ' + id;
771
- const searchDirs = ['src', 'app', 'pages', 'components', 'public', 'views', 'templates', '.'];
772
- const seen = new Set();
773
-
774
- for (const dir of searchDirs) {
775
- const absDir = path.join(cwd, dir);
776
- if (!fs.existsSync(absDir)) continue;
777
- const result = searchDir(absDir, marker, seen, 0);
778
- if (result) {
779
- const content = fs.readFileSync(result, 'utf-8');
780
- return { file: result, content, lines: content.split('\n') };
781
- }
782
- }
783
- return null;
924
+ const result = findSourceFile({
925
+ query: 'impeccable-variants-start ' + id,
926
+ cwd,
927
+ extensions: resolveLiveTemplateExtensions(cwd),
928
+ skipDirs: SEARCH_SKIP_DIRS,
929
+ });
930
+ if (!result) return null;
931
+ const content = fs.readFileSync(result, 'utf-8');
932
+ return { file: result, content, lines: content.split('\n') };
784
933
  }
785
934
 
786
- function searchDir(dir, query, seen, depth) {
787
- if (depth > 5) return null;
788
- let realDir;
789
- try { realDir = fs.realpathSync(dir); } catch { return null; }
790
- if (seen.has(realDir)) return null;
791
- seen.add(realDir);
792
-
793
- let entries;
794
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
795
- catch { return null; }
796
-
797
- for (const entry of entries) {
798
- if (!entry.isFile()) continue;
799
- if (!EXTENSIONS.includes(path.extname(entry.name).toLowerCase())) continue;
800
- const filePath = path.join(dir, entry.name);
801
- try {
802
- const content = fs.readFileSync(filePath, 'utf-8');
803
- if (content.includes(query)) return filePath;
804
- } catch { /* skip */ }
805
- }
935
+ // ---------------------------------------------------------------------------
936
+ // Utilities
937
+ // ---------------------------------------------------------------------------
806
938
 
807
- for (const entry of entries) {
808
- if (!entry.isDirectory()) continue;
809
- if (['node_modules', '.git', 'dist', 'build'].includes(entry.name)) continue;
810
- const result = searchDir(path.join(dir, entry.name), query, seen, depth + 1);
811
- if (result) return result;
812
- }
939
+ function acceptReceiptPath(cwd, id) {
940
+ return path.join(getLiveDir(cwd), 'accept-receipts', `${safeSessionId(id)}.json`);
941
+ }
813
942
 
814
- return null;
943
+ function readAcceptReceipt(cwd, id) {
944
+ try { return JSON.parse(fs.readFileSync(acceptReceiptPath(cwd, id), 'utf-8')); } catch { return null; }
815
945
  }
816
946
 
817
- // ---------------------------------------------------------------------------
818
- // Utilities
819
- // ---------------------------------------------------------------------------
947
+ function writeAcceptReceipt(cwd, id, receipt) {
948
+ const file = acceptReceiptPath(cwd, id);
949
+ fs.mkdirSync(path.dirname(file), { recursive: true });
950
+ const value = {
951
+ id,
952
+ ...receipt,
953
+ completedAt: new Date().toISOString(),
954
+ };
955
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
956
+ fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', 'utf-8');
957
+ fs.renameSync(temporary, file);
958
+ return value;
959
+ }
820
960
 
821
961
  function argVal(args, flag) {
822
962
  const idx = args.indexOf(flag);