@synergenius/flow-weaver 0.24.3 → 0.25.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/dist/agent/cli-session.d.ts +18 -1
- package/dist/agent/cli-session.js +146 -78
- package/dist/agent/types.d.ts +15 -0
- package/dist/cli/flow-weaver.mjs +18 -5
- package/dist/generated-version.d.ts +1 -1
- package/dist/generated-version.js +1 -1
- package/dist/parser.js +19 -5
- package/package.json +1 -1
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
import { type ChildProcess } from 'node:child_process';
|
|
23
23
|
import type { StreamEvent, CliSessionOptions } from './types.js';
|
|
24
24
|
export declare class CliSession {
|
|
25
|
-
private readonly options;
|
|
26
25
|
readonly sessionId: `${string}-${string}-${string}-${string}-${string}`;
|
|
27
26
|
private child;
|
|
28
27
|
private alive;
|
|
@@ -35,8 +34,14 @@ export declare class CliSession {
|
|
|
35
34
|
private readonly log;
|
|
36
35
|
private readonly spawnFn;
|
|
37
36
|
private readonly idleTimeout;
|
|
37
|
+
private readonly opts;
|
|
38
38
|
constructor(options: CliSessionOptions);
|
|
39
39
|
get ready(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Check if this session was created with equivalent CLI-relevant options.
|
|
42
|
+
* Used by the session cache to detect option drift.
|
|
43
|
+
*/
|
|
44
|
+
matchesOptions(other: CliSessionOptions): boolean;
|
|
40
45
|
/**
|
|
41
46
|
* Inject a mock child process for testing.
|
|
42
47
|
* @internal — test only
|
|
@@ -49,12 +54,22 @@ export declare class CliSession {
|
|
|
49
54
|
/**
|
|
50
55
|
* Send a user message and stream back events.
|
|
51
56
|
* Auto-respawns if the process has died.
|
|
57
|
+
*
|
|
58
|
+
* Phase 1.3: Concurrent send() guard — throws if a previous turn is still active.
|
|
59
|
+
* The activeTurn lock is claimed synchronously BEFORE any async work (spawn)
|
|
60
|
+
* to prevent TOCTOU races.
|
|
52
61
|
*/
|
|
53
62
|
send(userMessage: string, systemPromptPrefix?: string): AsyncGenerator<StreamEvent>;
|
|
54
63
|
/**
|
|
55
64
|
* Kill the CLI process.
|
|
56
65
|
*/
|
|
57
66
|
kill(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Compute a fingerprint of CLI-relevant options for cache comparison.
|
|
69
|
+
* Field order is significant for JSON.stringify comparison.
|
|
70
|
+
* Add new CLI-relevant fields here when they're added to CliSessionOptions.
|
|
71
|
+
*/
|
|
72
|
+
private static fingerprint;
|
|
58
73
|
private pushEvent;
|
|
59
74
|
private completeTurn;
|
|
60
75
|
private markDead;
|
|
@@ -64,6 +79,8 @@ export declare class CliSession {
|
|
|
64
79
|
}
|
|
65
80
|
/**
|
|
66
81
|
* Get an existing session or create a new one.
|
|
82
|
+
* Phase 1.4: Validates that cached sessions have matching CLI-relevant options.
|
|
83
|
+
* If options changed on the same key, the old session is killed and recreated.
|
|
67
84
|
*/
|
|
68
85
|
export declare function getOrCreateCliSession(key: string, options: CliSessionOptions): CliSession;
|
|
69
86
|
/**
|
|
@@ -24,7 +24,6 @@ import { spawn as nodeSpawn } from 'node:child_process';
|
|
|
24
24
|
import { StreamJsonParser } from './streaming.js';
|
|
25
25
|
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
26
26
|
export class CliSession {
|
|
27
|
-
options;
|
|
28
27
|
sessionId = randomUUID();
|
|
29
28
|
child = null;
|
|
30
29
|
alive = false;
|
|
@@ -37,8 +36,9 @@ export class CliSession {
|
|
|
37
36
|
log;
|
|
38
37
|
spawnFn;
|
|
39
38
|
idleTimeout;
|
|
39
|
+
opts;
|
|
40
40
|
constructor(options) {
|
|
41
|
-
this.
|
|
41
|
+
this.opts = options;
|
|
42
42
|
this.log = options.logger;
|
|
43
43
|
this.spawnFn = options.spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, { ...opts, stdio: opts.stdio }));
|
|
44
44
|
this.idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
@@ -48,6 +48,13 @@ export class CliSession {
|
|
|
48
48
|
get ready() {
|
|
49
49
|
return this.alive;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if this session was created with equivalent CLI-relevant options.
|
|
53
|
+
* Used by the session cache to detect option drift.
|
|
54
|
+
*/
|
|
55
|
+
matchesOptions(other) {
|
|
56
|
+
return CliSession.fingerprint(this.opts) === CliSession.fingerprint(other);
|
|
57
|
+
}
|
|
51
58
|
/**
|
|
52
59
|
* Inject a mock child process for testing.
|
|
53
60
|
* @internal — test only
|
|
@@ -67,7 +74,11 @@ export class CliSession {
|
|
|
67
74
|
* Spawn the CLI process. Must be called before send().
|
|
68
75
|
*/
|
|
69
76
|
async spawn() {
|
|
70
|
-
|
|
77
|
+
// Kill existing child to prevent orphaned processes from concurrent spawn
|
|
78
|
+
if (this.child && this.alive) {
|
|
79
|
+
this.child.kill('SIGTERM');
|
|
80
|
+
}
|
|
81
|
+
const { binPath, cwd, env, model, mcpConfigPath } = this.opts;
|
|
71
82
|
const args = [
|
|
72
83
|
'-p',
|
|
73
84
|
'--input-format',
|
|
@@ -87,6 +98,21 @@ export class CliSession {
|
|
|
87
98
|
if (mcpConfigPath) {
|
|
88
99
|
args.push('--mcp-config', mcpConfigPath, '--strict-mcp-config');
|
|
89
100
|
}
|
|
101
|
+
const { disallowedTools, tools, systemPrompt, appendSystemPrompt } = this.opts;
|
|
102
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
103
|
+
args.push('--disallowed-tools', disallowedTools.join(','));
|
|
104
|
+
}
|
|
105
|
+
// Phase 1.1: --tools flag (whitelist / disable all)
|
|
106
|
+
if (tools !== undefined) {
|
|
107
|
+
args.push('--tools', tools);
|
|
108
|
+
}
|
|
109
|
+
if (systemPrompt) {
|
|
110
|
+
args.push('--system-prompt', systemPrompt);
|
|
111
|
+
}
|
|
112
|
+
// Phase 1.2: --append-system-prompt flag
|
|
113
|
+
if (appendSystemPrompt) {
|
|
114
|
+
args.push('--append-system-prompt', appendSystemPrompt);
|
|
115
|
+
}
|
|
90
116
|
const spawnResult = this.spawnFn(binPath, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: env ?? process.env });
|
|
91
117
|
const child = 'child' in spawnResult ? spawnResult.child : spawnResult;
|
|
92
118
|
const cleanup = 'cleanup' in spawnResult ? spawnResult.cleanup : undefined;
|
|
@@ -115,91 +141,104 @@ export class CliSession {
|
|
|
115
141
|
/**
|
|
116
142
|
* Send a user message and stream back events.
|
|
117
143
|
* Auto-respawns if the process has died.
|
|
144
|
+
*
|
|
145
|
+
* Phase 1.3: Concurrent send() guard — throws if a previous turn is still active.
|
|
146
|
+
* The activeTurn lock is claimed synchronously BEFORE any async work (spawn)
|
|
147
|
+
* to prevent TOCTOU races.
|
|
118
148
|
*/
|
|
119
149
|
async *send(userMessage, systemPromptPrefix) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
150
|
+
// Phase 1.3: Guard against concurrent sends
|
|
151
|
+
if (this.activeTurn) {
|
|
152
|
+
throw new Error('CliSession: concurrent send() calls are not supported. Previous turn still active.');
|
|
123
153
|
}
|
|
124
|
-
|
|
125
|
-
this.parser.reset();
|
|
126
|
-
const content = systemPromptPrefix ? `${systemPromptPrefix}\n\n${userMessage}` : userMessage;
|
|
127
|
-
// Write NDJSON message to stdin
|
|
128
|
-
const ndjsonMessage = JSON.stringify({
|
|
129
|
-
type: 'user',
|
|
130
|
-
message: { role: 'user', content },
|
|
131
|
-
parent_tool_use_id: null,
|
|
132
|
-
}) + '\n';
|
|
154
|
+
// Claim the turn synchronously BEFORE any await to prevent TOCTOU race
|
|
133
155
|
const turn = { resolve: () => { }, events: [], done: false };
|
|
134
156
|
this.activeTurn = turn;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.parser = new StreamJsonParser((event) => {
|
|
140
|
-
// The result event emits message_stop — but in session mode,
|
|
141
|
-
// we need to detect it as the turn boundary
|
|
142
|
-
if (event.type === 'message_stop' && !sawResult) {
|
|
143
|
-
// This is a stream_event message_stop (API turn), not CLI turn end.
|
|
144
|
-
// Push it but don't complete the turn.
|
|
145
|
-
originalPush(event);
|
|
146
|
-
return;
|
|
157
|
+
try {
|
|
158
|
+
if (!this.alive) {
|
|
159
|
+
this.log?.info('CLI session dead, respawning', { sessionId: this.sessionId });
|
|
160
|
+
await this.spawn();
|
|
147
161
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
this.resetIdleTimer();
|
|
163
|
+
this.parser.reset();
|
|
164
|
+
const content = systemPromptPrefix ? `${systemPromptPrefix}\n\n${userMessage}` : userMessage;
|
|
165
|
+
// Write NDJSON message to stdin
|
|
166
|
+
const ndjsonMessage = JSON.stringify({
|
|
167
|
+
type: 'user',
|
|
168
|
+
message: { role: 'user', content },
|
|
169
|
+
parent_tool_use_id: null,
|
|
170
|
+
}) + '\n';
|
|
171
|
+
// Track whether this turn saw a 'result' event (definitive turn end)
|
|
172
|
+
let sawResult = false;
|
|
173
|
+
// Wrap pushEvent to detect result-driven message_stop as turn end
|
|
174
|
+
const originalPush = this.pushEvent.bind(this);
|
|
175
|
+
this.parser = new StreamJsonParser((event) => {
|
|
176
|
+
// The result event emits message_stop — but in session mode,
|
|
177
|
+
// we need to detect it as the turn boundary
|
|
178
|
+
if (event.type === 'message_stop' && !sawResult) {
|
|
179
|
+
// This is a stream_event message_stop (API turn), not CLI turn end.
|
|
180
|
+
// Push it but don't complete the turn.
|
|
181
|
+
originalPush(event);
|
|
182
|
+
return;
|
|
160
183
|
}
|
|
184
|
+
originalPush(event);
|
|
185
|
+
});
|
|
186
|
+
// Override parser feed to detect result events for turn completion
|
|
187
|
+
const baseFeed = this.parser.feed.bind(this.parser);
|
|
188
|
+
this.parser.feed = (line) => {
|
|
189
|
+
// Check if this line is a result event before parsing
|
|
190
|
+
try {
|
|
191
|
+
let parsed = JSON.parse(line);
|
|
192
|
+
if (parsed.type === 'stream_event' && parsed.event)
|
|
193
|
+
parsed = parsed.event;
|
|
194
|
+
if (parsed.type === 'result') {
|
|
195
|
+
sawResult = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Not JSON, let parser handle it
|
|
200
|
+
}
|
|
201
|
+
baseFeed(line);
|
|
202
|
+
if (sawResult) {
|
|
203
|
+
this.completeTurn();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
this.child.stdin.write(ndjsonMessage, (err) => {
|
|
208
|
+
if (err) {
|
|
209
|
+
this.log?.error('stdin write error', { sessionId: this.sessionId, err });
|
|
210
|
+
this.markDead();
|
|
211
|
+
turn.done = true;
|
|
212
|
+
turn.resolve();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
161
215
|
}
|
|
162
|
-
catch {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (sawResult) {
|
|
167
|
-
this.completeTurn();
|
|
216
|
+
catch (err) {
|
|
217
|
+
this.log?.error('stdin write exception', { sessionId: this.sessionId, err });
|
|
218
|
+
this.markDead();
|
|
219
|
+
throw new Error('CLI session stdin write failed');
|
|
168
220
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
221
|
+
// Yield events as they arrive
|
|
222
|
+
while (!turn.done || turn.events.length > 0) {
|
|
223
|
+
if (turn.events.length > 0) {
|
|
224
|
+
yield turn.events.shift();
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
await new Promise((r) => {
|
|
228
|
+
turn.resolve = r;
|
|
229
|
+
setTimeout(r, 50);
|
|
230
|
+
});
|
|
177
231
|
}
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
this.log?.error('stdin write exception', { sessionId: this.sessionId, err });
|
|
182
|
-
this.markDead();
|
|
183
|
-
throw new Error('CLI session stdin write failed');
|
|
184
|
-
}
|
|
185
|
-
// Yield events as they arrive
|
|
186
|
-
while (!turn.done || turn.events.length > 0) {
|
|
187
|
-
if (turn.events.length > 0) {
|
|
188
|
-
yield turn.events.shift();
|
|
189
232
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
setTimeout(r, 50);
|
|
194
|
-
});
|
|
233
|
+
// Yield any remaining events
|
|
234
|
+
while (turn.events.length > 0) {
|
|
235
|
+
yield turn.events.shift();
|
|
195
236
|
}
|
|
196
237
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
238
|
+
finally {
|
|
239
|
+
this.activeTurn = null;
|
|
240
|
+
this.resetIdleTimer();
|
|
200
241
|
}
|
|
201
|
-
this.activeTurn = null;
|
|
202
|
-
this.resetIdleTimer();
|
|
203
242
|
}
|
|
204
243
|
/**
|
|
205
244
|
* Kill the CLI process.
|
|
@@ -209,17 +248,34 @@ export class CliSession {
|
|
|
209
248
|
if (this.child && this.alive) {
|
|
210
249
|
this.log?.info('Killing CLI session', { sessionId: this.sessionId });
|
|
211
250
|
this.child.kill('SIGTERM');
|
|
212
|
-
|
|
251
|
+
// Phase 1.5: unref the SIGKILL fallback timer so it doesn't keep Node alive
|
|
252
|
+
const sigkillTimer = setTimeout(() => {
|
|
213
253
|
if (this.child && !this.child.killed) {
|
|
214
254
|
this.child.kill('SIGKILL');
|
|
215
255
|
}
|
|
216
256
|
}, 2000);
|
|
257
|
+
sigkillTimer.unref();
|
|
217
258
|
}
|
|
218
259
|
this.markDead();
|
|
219
260
|
}
|
|
220
261
|
// ---------------------------------------------------------------------------
|
|
221
262
|
// Private
|
|
222
263
|
// ---------------------------------------------------------------------------
|
|
264
|
+
/**
|
|
265
|
+
* Compute a fingerprint of CLI-relevant options for cache comparison.
|
|
266
|
+
* Field order is significant for JSON.stringify comparison.
|
|
267
|
+
* Add new CLI-relevant fields here when they're added to CliSessionOptions.
|
|
268
|
+
*/
|
|
269
|
+
static fingerprint(options) {
|
|
270
|
+
return JSON.stringify({
|
|
271
|
+
model: options.model,
|
|
272
|
+
mcpConfigPath: options.mcpConfigPath,
|
|
273
|
+
disallowedTools: options.disallowedTools,
|
|
274
|
+
tools: options.tools,
|
|
275
|
+
systemPrompt: options.systemPrompt,
|
|
276
|
+
appendSystemPrompt: options.appendSystemPrompt,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
223
279
|
pushEvent(event) {
|
|
224
280
|
if (!this.activeTurn)
|
|
225
281
|
return;
|
|
@@ -233,6 +289,8 @@ export class CliSession {
|
|
|
233
289
|
this.activeTurn.resolve();
|
|
234
290
|
}
|
|
235
291
|
markDead() {
|
|
292
|
+
// Phase 1.5: Clear idle timer to prevent dangling timer on process crash
|
|
293
|
+
this.clearIdleTimer();
|
|
236
294
|
this.alive = false;
|
|
237
295
|
this.cleanupFn?.();
|
|
238
296
|
this.cleanupFn = null;
|
|
@@ -258,6 +316,8 @@ export class CliSession {
|
|
|
258
316
|
this.log?.info('CLI session idle timeout, killing', { sessionId: this.sessionId });
|
|
259
317
|
this.kill();
|
|
260
318
|
}, this.idleTimeout);
|
|
319
|
+
// Phase 1.5: unref so idle timer doesn't keep Node alive on shutdown
|
|
320
|
+
this.idleTimer.unref();
|
|
261
321
|
}
|
|
262
322
|
clearIdleTimer() {
|
|
263
323
|
if (this.idleTimer) {
|
|
@@ -272,14 +332,22 @@ export class CliSession {
|
|
|
272
332
|
const sessions = new Map();
|
|
273
333
|
/**
|
|
274
334
|
* Get an existing session or create a new one.
|
|
335
|
+
* Phase 1.4: Validates that cached sessions have matching CLI-relevant options.
|
|
336
|
+
* If options changed on the same key, the old session is killed and recreated.
|
|
275
337
|
*/
|
|
276
338
|
export function getOrCreateCliSession(key, options) {
|
|
277
339
|
const existing = sessions.get(key);
|
|
278
340
|
if (existing && existing.ready) {
|
|
279
|
-
|
|
341
|
+
// Phase 1.4: Check if CLI-relevant options match
|
|
342
|
+
if (existing.matchesOptions(options)) {
|
|
343
|
+
return existing;
|
|
344
|
+
}
|
|
345
|
+
// Options changed — kill old session and recreate
|
|
346
|
+
existing.kill();
|
|
347
|
+
sessions.delete(key);
|
|
280
348
|
}
|
|
281
|
-
|
|
282
|
-
|
|
349
|
+
else if (existing) {
|
|
350
|
+
// Kill stale (dead) session
|
|
283
351
|
existing.kill();
|
|
284
352
|
sessions.delete(key);
|
|
285
353
|
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -191,6 +191,21 @@ export interface CliSessionOptions {
|
|
|
191
191
|
mcpConfigPath?: string;
|
|
192
192
|
/** Disable specific built-in tools (e.g. ['Read', 'Edit', 'Write', 'Bash'] to force MCP tools). */
|
|
193
193
|
disallowedTools?: string[];
|
|
194
|
+
/**
|
|
195
|
+
* Restrict built-in tools via --tools. Empty string "" disables all. Comma-separated PascalCase names.
|
|
196
|
+
* Case-sensitive — use exact names: "Read", "Bash", "Edit" (not "read", "bash").
|
|
197
|
+
* MCP tools are unaffected by this flag.
|
|
198
|
+
*/
|
|
199
|
+
tools?: string;
|
|
200
|
+
/** System prompt passed to the CLI via --system-prompt. Overrides the default Claude Code prompt. */
|
|
201
|
+
systemPrompt?: string;
|
|
202
|
+
/**
|
|
203
|
+
* Appended to the active system prompt via --append-system-prompt.
|
|
204
|
+
* Keeps Claude Code's built-in guidance and adds custom instructions.
|
|
205
|
+
* WARNING: If used with systemPrompt, appends to the custom prompt (not the default).
|
|
206
|
+
* Built-in Claude Code guidance is lost when systemPrompt is set.
|
|
207
|
+
*/
|
|
208
|
+
appendSystemPrompt?: string;
|
|
194
209
|
/** Custom spawn function. Defaults to child_process.spawn. */
|
|
195
210
|
spawnFn?: SpawnFn;
|
|
196
211
|
/** Idle timeout in milliseconds. Defaults to 600000 (10 minutes). */
|
package/dist/cli/flow-weaver.mjs
CHANGED
|
@@ -9886,7 +9886,7 @@ var VERSION;
|
|
|
9886
9886
|
var init_generated_version = __esm({
|
|
9887
9887
|
"src/generated-version.ts"() {
|
|
9888
9888
|
"use strict";
|
|
9889
|
-
VERSION = "0.
|
|
9889
|
+
VERSION = "0.25.0";
|
|
9890
9890
|
}
|
|
9891
9891
|
});
|
|
9892
9892
|
|
|
@@ -36231,9 +36231,22 @@ var init_parser2 = __esm({
|
|
|
36231
36231
|
const cacheKey = `npm:${imp.importSource}`;
|
|
36232
36232
|
if (this.importCache.has(cacheKey)) {
|
|
36233
36233
|
const cached2 = this.importCache.get(cacheKey);
|
|
36234
|
-
const
|
|
36235
|
-
|
|
36236
|
-
|
|
36234
|
+
const resolvedDts = resolvePackageTypesPath(imp.importSource, currentDir);
|
|
36235
|
+
let cacheValid = false;
|
|
36236
|
+
if (resolvedDts) {
|
|
36237
|
+
try {
|
|
36238
|
+
const dtsStats = fs5.statSync(resolvedDts);
|
|
36239
|
+
cacheValid = cached2.mtime === dtsStats.mtimeMs;
|
|
36240
|
+
} catch {
|
|
36241
|
+
}
|
|
36242
|
+
} else {
|
|
36243
|
+
cacheValid = true;
|
|
36244
|
+
}
|
|
36245
|
+
if (cacheValid) {
|
|
36246
|
+
const found = cached2.nodeTypes.find((nt) => nt.functionName === imp.functionName);
|
|
36247
|
+
if (found) {
|
|
36248
|
+
return { ...found, name: imp.name, importSource: imp.importSource };
|
|
36249
|
+
}
|
|
36237
36250
|
}
|
|
36238
36251
|
}
|
|
36239
36252
|
const dtsPath = resolvePackageTypesPath(imp.importSource, currentDir);
|
|
@@ -95960,7 +95973,7 @@ function parseIntStrict(value) {
|
|
|
95960
95973
|
// src/cli/index.ts
|
|
95961
95974
|
init_logger();
|
|
95962
95975
|
init_error_utils();
|
|
95963
|
-
var version2 = true ? "0.
|
|
95976
|
+
var version2 = true ? "0.25.0" : "0.0.0-dev";
|
|
95964
95977
|
var program2 = new Command();
|
|
95965
95978
|
program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
|
|
95966
95979
|
logger.banner(version2);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.25.0";
|
|
2
2
|
//# sourceMappingURL=generated-version.d.ts.map
|
package/dist/parser.js
CHANGED
|
@@ -610,14 +610,28 @@ export class AnnotationParser {
|
|
|
610
610
|
* Resolve an npm package @fwImport to a node type by reading .d.ts declarations.
|
|
611
611
|
*/
|
|
612
612
|
resolveNpmImportAnnotation(imp, currentDir) {
|
|
613
|
-
// Check cache
|
|
613
|
+
// Check cache (with mtime validation — same pattern as resolveNpmImports)
|
|
614
614
|
const cacheKey = `npm:${imp.importSource}`;
|
|
615
615
|
if (this.importCache.has(cacheKey)) {
|
|
616
616
|
const cached = this.importCache.get(cacheKey);
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
617
|
+
const resolvedDts = resolvePackageTypesPath(imp.importSource, currentDir);
|
|
618
|
+
let cacheValid = false;
|
|
619
|
+
if (resolvedDts) {
|
|
620
|
+
try {
|
|
621
|
+
const dtsStats = fs.statSync(resolvedDts);
|
|
622
|
+
cacheValid = cached.mtime === dtsStats.mtimeMs;
|
|
623
|
+
}
|
|
624
|
+
catch { /* file gone — re-parse */ }
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
// No .d.ts found — trust cache (package may have been removed)
|
|
628
|
+
cacheValid = true;
|
|
629
|
+
}
|
|
630
|
+
if (cacheValid) {
|
|
631
|
+
const found = cached.nodeTypes.find((nt) => nt.functionName === imp.functionName);
|
|
632
|
+
if (found) {
|
|
633
|
+
return { ...found, name: imp.name, importSource: imp.importSource };
|
|
634
|
+
}
|
|
621
635
|
}
|
|
622
636
|
}
|
|
623
637
|
// Resolve .d.ts path
|
package/package.json
CHANGED