@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.
- package/CHANGELOG.md +14 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
- package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
- package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +22 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +9 -3
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/docs/tui.md +7 -2
- package/docs/workflows.md +1 -1
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
- 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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 =
|
|
99
|
-
svelteComponentManifest,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|