@selvakumaresra/specship 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/ss-design-implement.md +5 -0
- package/commands/ss-design-loop.md +125 -0
- package/dist/bin/specship.js +66 -0
- package/dist/bin/specship.js.map +1 -1
- package/dist/designer/artifact-store.js +54 -0
- package/dist/designer/browser.js +141 -0
- package/dist/designer/cdp-ensure.js +60 -0
- package/dist/designer/cdp-env.js +18 -0
- package/dist/designer/cdp-trace.js +599 -0
- package/dist/designer/cross-platform.js +74 -0
- package/dist/designer/designer-controller.js +1413 -0
- package/dist/designer/file-panel.js +39 -0
- package/dist/designer/interstitials.js +97 -0
- package/dist/designer/oopif-reader.js +176 -0
- package/dist/designer/package-meta.js +18 -0
- package/dist/designer/preview-host.js +50 -0
- package/dist/designer/repo-root.js +31 -0
- package/dist/designer/run-state.js +353 -0
- package/dist/designer/session-store.js +59 -0
- package/dist/designer/ui-anchors.js +651 -0
- package/dist/installer/index.d.ts +5 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +3 -2
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/instructions-template.d.ts +17 -0
- package/dist/installer/instructions-template.d.ts.map +1 -1
- package/dist/installer/instructions-template.js +31 -1
- package/dist/installer/instructions-template.js.map +1 -1
- package/dist/installer/targets/claude.d.ts +19 -0
- package/dist/installer/targets/claude.d.ts.map +1 -1
- package/dist/installer/targets/claude.js +98 -1
- package/dist/installer/targets/claude.js.map +1 -1
- package/dist/installer/targets/shared.d.ts +14 -0
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +49 -0
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/installer/targets/types.d.ts +8 -0
- package/dist/installer/targets/types.d.ts.map +1 -1
- package/dist/mcp/designer-tools.d.ts +33 -0
- package/dist/mcp/designer-tools.d.ts.map +1 -0
- package/dist/mcp/designer-tools.js +313 -0
- package/dist/mcp/designer-tools.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +22 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/web/{chunk-JT7P3DEK.js → chunk-2YUJNZ2Y.js} +3 -3
- package/dist/web/{chunk-JN6W7HCN.js → chunk-45QHGCB4.js} +1 -1
- package/dist/web/{chunk-RAAMPHPJ.js → chunk-A5R3MJMO.js} +1 -1
- package/dist/web/{chunk-2DHIGIOI.js → chunk-ASZ77FMZ.js} +1 -1
- package/dist/web/{chunk-TWXZK6XM.js → chunk-B3YPFY6A.js} +1 -1
- package/dist/web/chunk-D5OCNEJA.js +2 -0
- package/dist/web/{chunk-3SEJX2BK.js → chunk-FHZHD2ZG.js} +1 -1
- package/dist/web/chunk-GR72OOCN.js +1 -0
- package/dist/web/{chunk-DA6SNNAF.js → chunk-GWPVKJIY.js} +1 -1
- package/dist/web/{chunk-YAWCRPHV.js → chunk-NZEZCT65.js} +1 -1
- package/dist/web/{chunk-BCZM5AXU.js → chunk-UBOZGQNK.js} +1 -1
- package/dist/web/{chunk-BPECIDVO.js → chunk-WCKHQIYN.js} +1 -1
- package/dist/web/{chunk-JFYVCXK3.js → chunk-WLIMNDS3.js} +1 -1
- package/dist/web/{chunk-LV4G6QFG.js → chunk-YAMRN47K.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/main-R53HA54V.js +1 -0
- package/dist/web/sw.js +69 -0
- package/dist/workflows/defaults/claude-design-implement.yaml +138 -49
- package/hooks/hooks.json +11 -0
- package/package.json +7 -3
- package/selectors.json +41 -0
- package/dist/web/chunk-2OKMB4KX.js +0 -2
- package/dist/web/chunk-4N5DWG46.js +0 -1
- package/dist/web/main-WVI3YTDU.js +0 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RunStateObserver = exports.TURN_RPCS = exports.OMELETTE_TURN_SERVICE = void 0;
|
|
4
|
+
exports.turnRpcFromUrl = turnRpcFromUrl;
|
|
5
|
+
exports.observedRpcPathFromUrl = observedRpcPathFromUrl;
|
|
6
|
+
exports.isTurnRpcUrl = isTurnRpcUrl;
|
|
7
|
+
exports.classifyEvent = classifyEvent;
|
|
8
|
+
const cdp_trace_1 = require("./cdp-trace");
|
|
9
|
+
exports.OMELETTE_TURN_SERVICE = 'anthropic.omelette.api.v1alpha.OmeletteService';
|
|
10
|
+
exports.TURN_RPCS = ['Chat', 'RenewTurn', 'ReleaseTurn'];
|
|
11
|
+
function num(v) {
|
|
12
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
13
|
+
}
|
|
14
|
+
function eventWallMs(params) {
|
|
15
|
+
const syntheticTraceTs = num(params.ts);
|
|
16
|
+
if (syntheticTraceTs !== null)
|
|
17
|
+
return syntheticTraceTs;
|
|
18
|
+
const wallTime = num(params.wallTime);
|
|
19
|
+
return wallTime !== null ? wallTime * 1000 : null;
|
|
20
|
+
}
|
|
21
|
+
function requestId(params) {
|
|
22
|
+
return typeof params.requestId === 'string' ? params.requestId : undefined;
|
|
23
|
+
}
|
|
24
|
+
function turnRpcFromUrl(url) {
|
|
25
|
+
if (!url)
|
|
26
|
+
return null;
|
|
27
|
+
let path = url;
|
|
28
|
+
try {
|
|
29
|
+
path = new URL(url).pathname;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Some tests pass path fragments directly.
|
|
33
|
+
}
|
|
34
|
+
const escapedService = exports.OMELETTE_TURN_SERVICE.replace(/\./g, '\\.');
|
|
35
|
+
const match = path.match(new RegExp(`(?:^|/)${escapedService}/(Chat|RenewTurn|ReleaseTurn)(?:$|[/?#])`));
|
|
36
|
+
if (!match?.[1])
|
|
37
|
+
return null;
|
|
38
|
+
return exports.TURN_RPCS.includes(match[1]) ? match[1] : null;
|
|
39
|
+
}
|
|
40
|
+
function observedRpcPathFromUrl(url) {
|
|
41
|
+
if (!url)
|
|
42
|
+
return null;
|
|
43
|
+
let path = url;
|
|
44
|
+
try {
|
|
45
|
+
path = new URL(url).pathname;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Some tests pass path fragments directly.
|
|
49
|
+
}
|
|
50
|
+
const serviceIdx = path.indexOf(`${exports.OMELETTE_TURN_SERVICE}/`);
|
|
51
|
+
if (serviceIdx >= 0)
|
|
52
|
+
return path.slice(serviceIdx);
|
|
53
|
+
const idx = path.indexOf('OmeletteService/');
|
|
54
|
+
return idx >= 0 ? path.slice(idx) : null;
|
|
55
|
+
}
|
|
56
|
+
function isTurnRpcUrl(url) {
|
|
57
|
+
return turnRpcFromUrl(url) !== null;
|
|
58
|
+
}
|
|
59
|
+
function urlFromParams(method, params) {
|
|
60
|
+
if (typeof params.requestUrl === 'string')
|
|
61
|
+
return params.requestUrl;
|
|
62
|
+
if (typeof params.url === 'string')
|
|
63
|
+
return params.url;
|
|
64
|
+
if (method === 'Network.requestWillBeSent') {
|
|
65
|
+
const req = (0, cdp_trace_1.asRec)(params.request);
|
|
66
|
+
return typeof req.url === 'string' ? req.url : '';
|
|
67
|
+
}
|
|
68
|
+
if (method === 'Network.responseReceived') {
|
|
69
|
+
const resp = (0, cdp_trace_1.asRec)(params.response);
|
|
70
|
+
return typeof resp.url === 'string' ? resp.url : '';
|
|
71
|
+
}
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
function isCriticalRpc(rpc) {
|
|
75
|
+
return rpc === 'Chat' || rpc === 'RenewTurn';
|
|
76
|
+
}
|
|
77
|
+
function classifyEvent(method, rawParams, runStartTs) {
|
|
78
|
+
const params = (0, cdp_trace_1.asRec)(rawParams);
|
|
79
|
+
const wallMs = eventWallMs(params);
|
|
80
|
+
if (wallMs !== null && wallMs < runStartTs)
|
|
81
|
+
return null;
|
|
82
|
+
const url = urlFromParams(method, params);
|
|
83
|
+
const rpc = turnRpcFromUrl(url);
|
|
84
|
+
if (method === 'Network.requestWillBeSent') {
|
|
85
|
+
if (rpc === 'Chat')
|
|
86
|
+
return { kind: 'chat-open', requestId: requestId(params) };
|
|
87
|
+
if (rpc === 'RenewTurn')
|
|
88
|
+
return { kind: 'heartbeat', requestId: requestId(params) };
|
|
89
|
+
if (rpc === 'ReleaseTurn')
|
|
90
|
+
return { kind: 'release', requestId: requestId(params) };
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (method === 'Network.dataReceived' && rpc === 'Chat') {
|
|
94
|
+
return { kind: 'chat-chunk', requestId: requestId(params) };
|
|
95
|
+
}
|
|
96
|
+
if (method === 'Network.responseReceived') {
|
|
97
|
+
const response = (0, cdp_trace_1.asRec)(params.response);
|
|
98
|
+
const status = num(response.status);
|
|
99
|
+
if (status !== null && status >= 400 && isCriticalRpc(rpc)) {
|
|
100
|
+
return { kind: 'critical-error', rpc, status };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (method === 'Network.loadingFailed' && isCriticalRpc(rpc)) {
|
|
105
|
+
return { kind: 'critical-error', rpc, status: 'failed' };
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
class RunStateObserver extends cdp_trace_1.CdpSession {
|
|
110
|
+
now;
|
|
111
|
+
currentState = 'idle';
|
|
112
|
+
runStartTs = 0;
|
|
113
|
+
lastActivity = 0;
|
|
114
|
+
priorRunSignals = 0;
|
|
115
|
+
terminalResult = null;
|
|
116
|
+
watchdog = null;
|
|
117
|
+
waiters = [];
|
|
118
|
+
requestUrls = new Map();
|
|
119
|
+
observedRpcPaths = new Set();
|
|
120
|
+
signalCounts = {
|
|
121
|
+
chatOpen: 0,
|
|
122
|
+
chatChunk: 0,
|
|
123
|
+
heartbeat: 0,
|
|
124
|
+
release: 0,
|
|
125
|
+
criticalError: 0,
|
|
126
|
+
observerLost: 0
|
|
127
|
+
};
|
|
128
|
+
closeCount = 0;
|
|
129
|
+
constructor(ws, target, opts = {}) {
|
|
130
|
+
super(ws, target, opts);
|
|
131
|
+
this.now = opts.now ?? (() => Date.now());
|
|
132
|
+
}
|
|
133
|
+
static async attach(opts = {}) {
|
|
134
|
+
if (typeof WebSocket === 'undefined')
|
|
135
|
+
return null;
|
|
136
|
+
try {
|
|
137
|
+
const { ws, target } = await RunStateObserver.connectTarget(opts);
|
|
138
|
+
const observer = new RunStateObserver(ws, target, opts);
|
|
139
|
+
await observer.start();
|
|
140
|
+
return observer;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async start() {
|
|
147
|
+
await this.enableDomains();
|
|
148
|
+
}
|
|
149
|
+
beginRun() {
|
|
150
|
+
if (this.terminalResult)
|
|
151
|
+
return;
|
|
152
|
+
this.runStartTs = this.now();
|
|
153
|
+
this.lastActivity = this.runStartTs;
|
|
154
|
+
this.priorRunSignals = 0;
|
|
155
|
+
this.requestUrls.clear();
|
|
156
|
+
this.observedRpcPaths.clear();
|
|
157
|
+
this.resetSignalCounts();
|
|
158
|
+
this.currentState = 'running';
|
|
159
|
+
}
|
|
160
|
+
get state() {
|
|
161
|
+
if (this.terminalResult)
|
|
162
|
+
return this.terminalResult.terminal;
|
|
163
|
+
return this.currentState;
|
|
164
|
+
}
|
|
165
|
+
awaitTerminal({ stallMs = 25_000, hardTimeoutMs = 20 * 60_000 } = {}) {
|
|
166
|
+
if (this.terminalResult)
|
|
167
|
+
return Promise.resolve(this.terminalResult);
|
|
168
|
+
this.armWatchdog(stallMs, hardTimeoutMs);
|
|
169
|
+
this.checkSilence(stallMs, hardTimeoutMs);
|
|
170
|
+
if (this.terminalResult)
|
|
171
|
+
return Promise.resolve(this.terminalResult);
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
this.waiters.push(resolve);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
close() {
|
|
177
|
+
this.clearWatchdog();
|
|
178
|
+
if (this.closeSocket())
|
|
179
|
+
this.closeCount++;
|
|
180
|
+
}
|
|
181
|
+
signalSummary() {
|
|
182
|
+
return {
|
|
183
|
+
...this.signalCounts,
|
|
184
|
+
observedRpcPaths: [...this.observedRpcPaths].sort()
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
closeCountForTest() {
|
|
188
|
+
return this.closeCount;
|
|
189
|
+
}
|
|
190
|
+
consumeSignalForTest(signal) {
|
|
191
|
+
this.consumeSignal(signal);
|
|
192
|
+
}
|
|
193
|
+
socketGapForTest(detail = { reason: 'test' }) {
|
|
194
|
+
this.onSocketGap(detail);
|
|
195
|
+
}
|
|
196
|
+
tickForTest({ stallMs = 25_000, hardTimeoutMs = 20 * 60_000 } = {}) {
|
|
197
|
+
this.checkSilence(stallMs, hardTimeoutMs);
|
|
198
|
+
}
|
|
199
|
+
onEvent(method, rawParams, _sessionId) {
|
|
200
|
+
if (this.currentState === 'idle' && !this.terminalResult)
|
|
201
|
+
return;
|
|
202
|
+
const params = this.enrichParams(method, rawParams);
|
|
203
|
+
// Terminal events (responseReceived / loadingFailed) carry no `wallTime`, so
|
|
204
|
+
// classifyEvent's stale-timestamp guard cannot filter a prior turn's late
|
|
205
|
+
// failure. Only honor them when the request was first seen *this run* — its
|
|
206
|
+
// id is in `requestUrls`, which `enrichParams` populates at requestWillBeSent
|
|
207
|
+
// for events at/after `runStartTs`. Otherwise a stale 4xx for a pre-run RPC
|
|
208
|
+
// would read its own `response.url` and falsely latch BLOCKED.
|
|
209
|
+
if (method === 'Network.responseReceived' || method === 'Network.loadingFailed') {
|
|
210
|
+
const id = requestId(params);
|
|
211
|
+
if (!id || !this.requestUrls.has(id))
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const signal = classifyEvent(method, params, this.runStartTs);
|
|
215
|
+
if (signal)
|
|
216
|
+
this.consumeSignal(signal);
|
|
217
|
+
}
|
|
218
|
+
onSocketGap(_detail) {
|
|
219
|
+
// A one-shot ReleaseTurn fired during a CDP reconnect gap can't be recovered
|
|
220
|
+
// (CDP won't re-deliver events from the gap), so even a *successful* reconnect
|
|
221
|
+
// would leave an active run waiting out the hard timeout. Degrade to
|
|
222
|
+
// observer-lost immediately for an active run so the controller hands off to
|
|
223
|
+
// the HTML waiter. The observer-lost latch also stops the base reconnect loop
|
|
224
|
+
// (it sets `stopped`), which is what we want once we've handed off.
|
|
225
|
+
if (this.currentState === 'running' || this.currentState === 'stalled') {
|
|
226
|
+
this.consumeSignal({ kind: 'observer-lost' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
onSocketReconnectFailed() {
|
|
230
|
+
this.consumeSignal({ kind: 'observer-lost' });
|
|
231
|
+
}
|
|
232
|
+
enrichParams(method, rawParams) {
|
|
233
|
+
const params = { ...(0, cdp_trace_1.asRec)(rawParams) };
|
|
234
|
+
const id = requestId(params);
|
|
235
|
+
if (method === 'Network.requestWillBeSent' && id) {
|
|
236
|
+
const wallMs = eventWallMs(params);
|
|
237
|
+
if (wallMs !== null && wallMs < this.runStartTs)
|
|
238
|
+
return params;
|
|
239
|
+
const req = (0, cdp_trace_1.asRec)(params.request);
|
|
240
|
+
const url = typeof req.url === 'string' ? req.url : '';
|
|
241
|
+
if (url) {
|
|
242
|
+
this.requestUrls.set(id, url);
|
|
243
|
+
const rpcPath = observedRpcPathFromUrl(url);
|
|
244
|
+
if (rpcPath)
|
|
245
|
+
this.observedRpcPaths.add(rpcPath);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (id && !params.requestUrl) {
|
|
249
|
+
const url = this.requestUrls.get(id);
|
|
250
|
+
if (url)
|
|
251
|
+
params.requestUrl = url;
|
|
252
|
+
}
|
|
253
|
+
return params;
|
|
254
|
+
}
|
|
255
|
+
consumeSignal(signal) {
|
|
256
|
+
if (this.terminalResult)
|
|
257
|
+
return;
|
|
258
|
+
this.countSignal(signal);
|
|
259
|
+
if (signal.kind === 'observer-lost') {
|
|
260
|
+
this.latch('observer-lost');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (this.currentState === 'idle')
|
|
264
|
+
return;
|
|
265
|
+
if (signal.kind === 'chat-open' || signal.kind === 'chat-chunk' || signal.kind === 'heartbeat') {
|
|
266
|
+
this.lastActivity = this.now();
|
|
267
|
+
this.priorRunSignals++;
|
|
268
|
+
this.currentState = 'running';
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (signal.kind === 'release') {
|
|
272
|
+
if (this.priorRunSignals === 0)
|
|
273
|
+
return;
|
|
274
|
+
this.latch('finished');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (signal.kind === 'critical-error') {
|
|
278
|
+
this.latch('blocked', `${signal.rpc} ${signal.status === 'failed' ? 'failed' : `HTTP ${signal.status}`}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
countSignal(signal) {
|
|
282
|
+
if (signal.kind === 'chat-open')
|
|
283
|
+
this.signalCounts.chatOpen++;
|
|
284
|
+
else if (signal.kind === 'chat-chunk')
|
|
285
|
+
this.signalCounts.chatChunk++;
|
|
286
|
+
else if (signal.kind === 'heartbeat')
|
|
287
|
+
this.signalCounts.heartbeat++;
|
|
288
|
+
else if (signal.kind === 'release')
|
|
289
|
+
this.signalCounts.release++;
|
|
290
|
+
else if (signal.kind === 'critical-error')
|
|
291
|
+
this.signalCounts.criticalError++;
|
|
292
|
+
else if (signal.kind === 'observer-lost')
|
|
293
|
+
this.signalCounts.observerLost++;
|
|
294
|
+
}
|
|
295
|
+
resetSignalCounts() {
|
|
296
|
+
this.signalCounts.chatOpen = 0;
|
|
297
|
+
this.signalCounts.chatChunk = 0;
|
|
298
|
+
this.signalCounts.heartbeat = 0;
|
|
299
|
+
this.signalCounts.release = 0;
|
|
300
|
+
this.signalCounts.criticalError = 0;
|
|
301
|
+
this.signalCounts.observerLost = 0;
|
|
302
|
+
}
|
|
303
|
+
checkSilence(stallMs, hardTimeoutMs) {
|
|
304
|
+
if (this.terminalResult || this.currentState === 'idle')
|
|
305
|
+
return;
|
|
306
|
+
const now = this.now();
|
|
307
|
+
const silence = now - this.lastActivity;
|
|
308
|
+
const elapsed = now - this.runStartTs;
|
|
309
|
+
if (silence > hardTimeoutMs) {
|
|
310
|
+
this.latch('timeout', `silent for ${silence}ms`);
|
|
311
|
+
}
|
|
312
|
+
else if (elapsed > hardTimeoutMs) {
|
|
313
|
+
// Absolute wall-clock cap. A run that keeps heartbeating (RenewTurn ~10s)
|
|
314
|
+
// but never releases never accumulates enough *silence* to time out — so
|
|
315
|
+
// without this it would hang indefinitely (worse than the old HTML waiter,
|
|
316
|
+
// which always had a true Date.now()-based budget). Bound total duration.
|
|
317
|
+
this.latch('timeout', `exceeded ${hardTimeoutMs}ms wall-clock budget (elapsed ${elapsed}ms)`);
|
|
318
|
+
}
|
|
319
|
+
else if (silence > stallMs) {
|
|
320
|
+
this.currentState = 'stalled';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
armWatchdog(stallMs, hardTimeoutMs) {
|
|
324
|
+
if (this.watchdog)
|
|
325
|
+
return;
|
|
326
|
+
const intervalMs = Math.max(250, Math.min(1000, stallMs, hardTimeoutMs));
|
|
327
|
+
this.watchdog = setInterval(() => this.checkSilence(stallMs, hardTimeoutMs), intervalMs);
|
|
328
|
+
}
|
|
329
|
+
clearWatchdog() {
|
|
330
|
+
if (!this.watchdog)
|
|
331
|
+
return;
|
|
332
|
+
clearInterval(this.watchdog);
|
|
333
|
+
this.watchdog = null;
|
|
334
|
+
}
|
|
335
|
+
latch(terminal, reason) {
|
|
336
|
+
if (this.terminalResult)
|
|
337
|
+
return;
|
|
338
|
+
this.terminalResult = {
|
|
339
|
+
terminal,
|
|
340
|
+
// runStartTs stays 0 if the socket died before beginRun() ran; report 0
|
|
341
|
+
// elapsed rather than now()-0 (epoch millis), which would otherwise
|
|
342
|
+
// collapse the controller's HTML-fallback budget to ~1ms.
|
|
343
|
+
elapsedMs: this.runStartTs === 0 ? 0 : Math.max(0, this.now() - this.runStartTs),
|
|
344
|
+
...(reason ? { reason } : {})
|
|
345
|
+
};
|
|
346
|
+
this.clearWatchdog();
|
|
347
|
+
this.close();
|
|
348
|
+
const waiters = this.waiters.splice(0);
|
|
349
|
+
for (const resolve of waiters)
|
|
350
|
+
resolve(this.terminalResult);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
exports.RunStateObserver = RunStateObserver;
|
|
@@ -0,0 +1,59 @@
|
|
|
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.getSession = getSession;
|
|
7
|
+
exports.upsertSession = upsertSession;
|
|
8
|
+
exports.listSessions = listSessions;
|
|
9
|
+
exports.appendHistory = appendHistory;
|
|
10
|
+
exports.stateDir = stateDir;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
|
+
const ROOT = process.env.DESIGNER_STATE_DIR || node_path_1.default.join(node_os_1.default.homedir(), '.designer');
|
|
15
|
+
const SESSIONS_FILE = node_path_1.default.join(ROOT, 'sessions.json');
|
|
16
|
+
function ensureRoot() {
|
|
17
|
+
node_fs_1.default.mkdirSync(ROOT, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
function readAll() {
|
|
20
|
+
ensureRoot();
|
|
21
|
+
if (!node_fs_1.default.existsSync(SESSIONS_FILE))
|
|
22
|
+
return {};
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(node_fs_1.default.readFileSync(SESSIONS_FILE, 'utf8')) || {};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function writeAll(data) {
|
|
31
|
+
ensureRoot();
|
|
32
|
+
node_fs_1.default.writeFileSync(SESSIONS_FILE, JSON.stringify(data, null, 2));
|
|
33
|
+
}
|
|
34
|
+
function getSession(key) {
|
|
35
|
+
return readAll()[key] || null;
|
|
36
|
+
}
|
|
37
|
+
function upsertSession(key, patch) {
|
|
38
|
+
const all = readAll();
|
|
39
|
+
const prev = all[key] || { key, createdAt: new Date().toISOString(), history: [] };
|
|
40
|
+
const next = { ...prev, ...patch, updatedAt: new Date().toISOString() };
|
|
41
|
+
all[key] = next;
|
|
42
|
+
writeAll(all);
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
function listSessions() {
|
|
46
|
+
return Object.values(readAll());
|
|
47
|
+
}
|
|
48
|
+
function appendHistory(key, entry) {
|
|
49
|
+
const all = readAll();
|
|
50
|
+
const prev = all[key] || { key, createdAt: new Date().toISOString(), history: [] };
|
|
51
|
+
prev.history = [...(prev.history || []), { ...entry, at: new Date().toISOString() }];
|
|
52
|
+
prev.updatedAt = new Date().toISOString();
|
|
53
|
+
all[key] = prev;
|
|
54
|
+
writeAll(all);
|
|
55
|
+
return prev;
|
|
56
|
+
}
|
|
57
|
+
function stateDir() {
|
|
58
|
+
return ROOT;
|
|
59
|
+
}
|