cli-jaw 2.2.11 → 2.2.13
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/bin/commands/doctor.js +20 -0
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/init.js +7 -0
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/bin/commands/slack.js +47 -5
- package/dist/bin/commands/slack.js.map +1 -1
- package/dist/bin/commands/tui/slash-command-runner.js +11 -2
- package/dist/bin/commands/tui/slash-command-runner.js.map +1 -1
- package/dist/server.js +18 -2
- package/dist/server.js.map +1 -1
- package/dist/src/agent/error-classifier.js +11 -2
- package/dist/src/agent/error-classifier.js.map +1 -1
- package/dist/src/agent/events/claude.js +7 -1
- package/dist/src/agent/events/claude.js.map +1 -1
- package/dist/src/agent/events/fulltext-bound.js +32 -0
- package/dist/src/agent/events/fulltext-bound.js.map +1 -0
- package/dist/src/agent/events/grok.js +7 -1
- package/dist/src/agent/events/grok.js.map +1 -1
- package/dist/src/agent/events/helpers.js +17 -4
- package/dist/src/agent/events/helpers.js.map +1 -1
- package/dist/src/agent/lifecycle-handler.js +56 -5
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/pi-runtime.js +30 -1
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/spawn/exit-drain.js +96 -0
- package/dist/src/agent/spawn/exit-drain.js.map +1 -0
- package/dist/src/agent/spawn/line-buffer.js +26 -0
- package/dist/src/agent/spawn/line-buffer.js.map +1 -0
- package/dist/src/agent/spawn/process-kill.js +31 -0
- package/dist/src/agent/spawn/process-kill.js.map +1 -1
- package/dist/src/agent/spawn.js +80 -23
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/agent/watchdog.js +21 -5
- package/dist/src/agent/watchdog.js.map +1 -1
- package/dist/src/cli/commands.js +68 -15
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/opencodex-models.js +67 -14
- package/dist/src/cli/opencodex-models.js.map +1 -1
- package/dist/src/cli/registry-live.js +55 -2
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/cli/registry.js +8 -2
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/core/config.js +26 -1
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/messaging/channel-health.js +6 -0
- package/dist/src/messaging/channel-health.js.map +1 -1
- package/dist/src/messaging/channel-validate.js +85 -0
- package/dist/src/messaging/channel-validate.js.map +1 -0
- package/dist/src/routes/link-preview.js +14 -0
- package/dist/src/routes/link-preview.js.map +1 -1
- package/dist/src/routes/messaging.js +9 -0
- package/dist/src/routes/messaging.js.map +1 -1
- package/dist/src/routes/system.js +5 -0
- package/dist/src/routes/system.js.map +1 -1
- package/dist/src/slack/api.js +32 -3
- package/dist/src/slack/api.js.map +1 -1
- package/dist/src/slack/bot.js +37 -1
- package/dist/src/slack/bot.js.map +1 -1
- package/dist/src/slack/events.js +23 -0
- package/dist/src/slack/events.js.map +1 -1
- package/dist/src/slack/hot-notify.js +33 -0
- package/dist/src/slack/hot-notify.js.map +1 -0
- package/dist/src/slack/manifest.js +2 -1
- package/dist/src/slack/manifest.js.map +1 -1
- package/dist/src/slack/progress.js +98 -0
- package/dist/src/slack/progress.js.map +1 -0
- package/dist/src/slack/send-only-client.js +3 -2
- package/dist/src/slack/send-only-client.js.map +1 -1
- package/dist/src/slack/slack-file.js +4 -2
- package/dist/src/slack/slack-file.js.map +1 -1
- package/package.json +3 -1
- package/public/css/sidebar.css +200 -0
- package/public/dist/assets/Agent-BjbZVZ9p.js +1 -0
- package/public/dist/assets/CodeCanvas-CbfPP7Ad.js +3 -0
- package/public/dist/assets/{DocPanel-DtYwv3n3.js → DocPanel-Dz-J_l5M.js} +1 -1
- package/public/dist/assets/{Heartbeat-vDGhPfct.js → Heartbeat-DDmZARWw.js} +1 -1
- package/public/dist/assets/MarkdownRenderer-CXrilOsG.js +15 -0
- package/public/dist/assets/MarkdownRenderer-DZVh_xX2.js +1 -0
- package/public/dist/assets/{Memory-B_UZ-iCA.js → Memory-CUbviW7N.js} +1 -1
- package/public/dist/assets/{MilkdownWysiwygEditor-BNoLGbFC.js → MilkdownWysiwygEditor-Em7iC6y9.js} +8 -8
- package/public/dist/assets/ModelProvider-ngg8xTuB.js +1 -0
- package/public/dist/assets/{agent-meta-C8DqYXM6.js → agent-meta-Y3DE6sxh.js} +1 -1
- package/public/dist/assets/app-BzD_ZLvt.js +55 -0
- package/public/dist/assets/{app-Nu2VPaO5.css → app-DX4e6isX.css} +1 -1
- package/public/dist/assets/bgtask-badge-Dqbp-Y4e.js +1 -0
- package/public/dist/assets/{bgtask-badge-DXca8E7c.js → bgtask-badge-qMkKOZx0.js} +1 -1
- package/public/dist/assets/{react-dom-AWKMTIe7.js → bounded-set-CFlZiEF4.js} +1 -1
- package/public/dist/assets/{constants-CSZ011-j.js → constants-Df7-7Rky.js} +1 -1
- package/public/dist/assets/{employees-BL4qMxJj.js → employees-BnobWM_2.js} +1 -1
- package/public/dist/assets/{iframe-renderer-DUI0LYBy.js → iframe-renderer-C5vRbbl9.js} +2 -2
- package/public/dist/assets/iframe-renderer-Df4KIVyx.js +1 -0
- package/public/dist/assets/manager-DaN201mN.js +13 -0
- package/public/dist/assets/memory-D-bULTHl.js +1 -0
- package/public/dist/assets/{memory-B_Y0L0KD.js → memory-DF4kPwwC.js} +1 -1
- package/public/dist/assets/{message-history-Dl8dJzez.js → message-history-C4WOisGx.js} +1 -1
- package/public/dist/assets/message-history-ZBoza6ew.js +1 -0
- package/public/dist/assets/{render-BI1aqKoc.js → render-DYX6GRQ0.js} +1 -1
- package/public/dist/assets/{settings-B0Sj4sUH.js → settings-DbJpGoaX.js} +1 -1
- package/public/dist/assets/settings-SreihUje.js +1 -0
- package/public/dist/assets/settings-core-B1Y5Nb4Z.js +1 -0
- package/public/dist/assets/settings-core-DobZWIZ7.js +140 -0
- package/public/dist/assets/{sidebar-DOR9fTgN.js → sidebar-Cp22g0Gv.js} +3 -3
- package/public/dist/assets/skills-DwPIP08e.js +1 -0
- package/public/dist/assets/{skills-wt0fkV9Y.js → skills-IGquen_j.js} +1 -1
- package/public/dist/assets/slash-commands-BBVfwcnW.js +1 -0
- package/public/dist/assets/{slash-commands-BZbqTg3v.js → slash-commands-ZRDOL99E.js} +1 -1
- package/public/dist/assets/{trace-drawer-kMYzaVFW.js → trace-drawer-D8To8fYt.js} +1 -1
- package/public/dist/assets/ui-CAicpUZc.js +1 -0
- package/public/dist/assets/{ui-D4D5HUfo.js → ui-Hx_mNoDf.js} +1 -1
- package/public/dist/assets/{wiki-link-resolver-BfNXN9px.js → wiki-link-resolver-DyFzBTIQ.js} +1 -1
- package/public/dist/index.html +90 -47
- package/public/dist/manager/index.html +1 -1
- package/public/index.html +88 -45
- package/public/js/constants.ts +50 -0
- package/public/js/features/channel-onboarding-flow.ts +197 -0
- package/public/js/features/channel-onboarding.ts +322 -0
- package/public/js/features/channel-setup-guide.ts +26 -0
- package/public/js/features/channel-setup-rules.ts +32 -0
- package/public/js/features/notifications.ts +38 -0
- package/public/js/features/settings-channel.ts +4 -0
- package/public/js/features/settings-core.ts +73 -9
- package/public/js/features/settings-discord.ts +2 -0
- package/public/js/features/settings-slack.ts +60 -2
- package/public/js/features/settings-telegram.ts +2 -0
- package/public/js/features/settings-types.ts +1 -1
- package/public/js/features/settings.ts +1 -1
- package/public/js/features/transport-status-row.ts +7 -3
- package/public/js/main.ts +5 -1
- package/public/locales/en.json +65 -3
- package/public/locales/ja.json +76 -3
- package/public/locales/ko.json +65 -3
- package/public/locales/zh.json +76 -3
- package/public/manager/src/SidebarRailRouter.tsx +9 -3
- package/public/manager/src/browser-panel/use-embedded-target-sync.ts +19 -1
- package/public/manager/src/code/CodeCanvas.tsx +14 -2
- package/public/manager/src/code/CodeTranscript.tsx +33 -9
- package/public/manager/src/code/CodeWorkbench.tsx +1 -1
- package/public/manager/src/code/code-event-dedupe.ts +27 -3
- package/public/manager/src/code/use-code-transcript-scroll.ts +28 -3
- package/public/manager/src/code/use-throttled-markdown.ts +75 -0
- package/public/manager/src/code/useCodeTranscriptVirtualRows.ts +19 -0
- package/public/manager/src/electron-metrics.tsx +7 -0
- package/public/manager/src/jaw-ceo/useJawCeo.ts +6 -1
- package/public/manager/src/jaw-ceo/useJawCeoVirtualTimeline.ts +5 -0
- package/public/manager/src/lib/bounded-set.ts +15 -0
- package/public/manager/src/lib/fnv1a.ts +14 -0
- package/public/manager/src/notes/rendering/CodeBlock.tsx +13 -4
- package/public/manager/src/notes/rendering/MarkdownRenderer.tsx +14 -2
- package/public/manager/src/notes/rendering/highlight-cache.ts +111 -0
- package/public/manager/src/notes/useNotesExternalSync.ts +6 -1
- package/public/manager/src/settings/pages/Agent.tsx +25 -4
- package/public/manager/src/settings/pages/components/PerCliRow.tsx +13 -5
- package/public/manager/src/settings/pages/components/agent/agent-meta.ts +98 -0
- package/scripts/bundle-sidecar.sh +8 -0
- package/scripts/check-electron-no-native.cjs +90 -4
- package/scripts/check-native-load.cjs +157 -0
- package/scripts/check-sidecar-smoke.mjs +151 -0
- package/scripts/ensure-native-modules.cjs +222 -18
- package/scripts/release-gates.mjs +29 -0
- package/public/dist/assets/Agent-BgkP4EnS.js +0 -1
- package/public/dist/assets/CodeCanvas-BZo3dohZ.js +0 -3
- package/public/dist/assets/MarkdownRenderer-CHacL2M_.js +0 -15
- package/public/dist/assets/MarkdownRenderer-DyrB9FbZ.js +0 -1
- package/public/dist/assets/ModelProvider-CgUvs6pE.js +0 -1
- package/public/dist/assets/app-BGIAy4he.js +0 -55
- package/public/dist/assets/bgtask-badge-GRSiNILQ.js +0 -1
- package/public/dist/assets/iframe-renderer-BUSk0Rw9.js +0 -1
- package/public/dist/assets/manager-B-gpf-3j.js +0 -13
- package/public/dist/assets/memory-CX6YahTc.js +0 -1
- package/public/dist/assets/message-history-Det7u76S.js +0 -1
- package/public/dist/assets/settings-C7NutP6X.js +0 -1
- package/public/dist/assets/settings-core-CyX-px6_.js +0 -100
- package/public/dist/assets/settings-core-DVpFmrhY.js +0 -1
- package/public/dist/assets/skills-BWhKreQ2.js +0 -1
- package/public/dist/assets/slash-commands-Di4NDA4w.js +0 -1
- package/public/dist/assets/ui-BghexWrB.js +0 -1
|
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
|
|
4
4
|
import { TextField, SelectField, ToggleField } from '../../fields';
|
|
5
5
|
import type { SettingsClient } from '../../types';
|
|
6
6
|
import type { DirtyEntry } from '../../types';
|
|
7
|
+
import { coerceEffortForModel, effortChoicesForModel } from './agent/agent-meta';
|
|
7
8
|
import type { CliMeta, PerCliEntry } from './agent/agent-meta';
|
|
8
9
|
import { PiProfileDialog } from './PiProfileDialog';
|
|
9
10
|
import { piModelOptions, piProfileOptions, type PiSettingsView } from './pi-profile';
|
|
@@ -36,9 +37,10 @@ export function PerCliRow({ cli, meta, original, value, setValue, setEntry, clie
|
|
|
36
37
|
: hasProviders
|
|
37
38
|
? (meta.modelsByProvider?.[provider] ?? meta.models)
|
|
38
39
|
: meta.models;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const providerEfforts = hasProviders ? (meta.effortsByProvider?.[provider] ?? meta.efforts) : undefined;
|
|
41
|
+
// Per-model effort sets from a live opencodex catalog win over the
|
|
42
|
+
// provider/registry lists; the chosen value is forwarded to the wire.
|
|
43
|
+
const effortOptions = effortChoicesForModel(meta, value.model || '', providerEfforts, hasProviders ? provider : undefined);
|
|
42
44
|
|
|
43
45
|
return (
|
|
44
46
|
<div className="settings-percli-row" data-cli={cli}>
|
|
@@ -68,7 +70,9 @@ export function PerCliRow({ cli, meta, original, value, setValue, setEntry, clie
|
|
|
68
70
|
const nextModels = meta.modelsByProvider?.[next] ?? [];
|
|
69
71
|
const nextEfforts = meta.effortsByProvider?.[next] ?? [];
|
|
70
72
|
const nextModel = nextModels.includes(value.model || '') ? (value.model || '') : (nextModels[0] || '');
|
|
71
|
-
|
|
73
|
+
// Resolve against the NEW provider+model pair: the same
|
|
74
|
+
// model id can allow different efforts per provider.
|
|
75
|
+
const nextEffort = coerceEffortForModel(meta, nextModel, value.effort ?? '', nextEfforts, next);
|
|
72
76
|
setValue({ ...value, provider: next, model: nextModel, effort: nextEffort });
|
|
73
77
|
setEntry(`perCli.${cli}.provider`, entryFor(next, original.provider ?? meta.defaultProvider ?? 'claude'));
|
|
74
78
|
setEntry(`perCli.${cli}.model`, entryFor(nextModel, original.model ?? ''));
|
|
@@ -84,8 +88,12 @@ export function PerCliRow({ cli, meta, original, value, setValue, setEntry, clie
|
|
|
84
88
|
value={value.model ?? modelOptions[0] ?? ''}
|
|
85
89
|
options={modelOptions.map((m) => ({ value: m, label: m }))}
|
|
86
90
|
onChange={(next) => {
|
|
87
|
-
|
|
91
|
+
const nextEffort = coerceEffortForModel(meta, next, value.effort ?? '', providerEfforts, hasProviders ? provider : undefined);
|
|
92
|
+
setValue({ ...value, model: next, effort: nextEffort });
|
|
88
93
|
setEntry(`perCli.${cli}.model`, entryFor(next, original.model ?? ''));
|
|
94
|
+
if (nextEffort !== (value.effort ?? '')) {
|
|
95
|
+
setEntry(`perCli.${cli}.effort`, entryFor(nextEffort, original.effort ?? ''));
|
|
96
|
+
}
|
|
89
97
|
}}
|
|
90
98
|
/>
|
|
91
99
|
) : (
|
|
@@ -6,6 +6,22 @@ export type CliMeta = {
|
|
|
6
6
|
providers?: ReadonlyArray<string>;
|
|
7
7
|
modelsByProvider?: Record<string, ReadonlyArray<string>>;
|
|
8
8
|
effortsByProvider?: Record<string, ReadonlyArray<string>>;
|
|
9
|
+
/**
|
|
10
|
+
* Per-model reasoning-effort sets from a live opencodex catalog. An entry
|
|
11
|
+
* that exists but is EMPTY means the model takes no effort at all, so it
|
|
12
|
+
* must not fall back to `efforts`.
|
|
13
|
+
*/
|
|
14
|
+
effortsByModel?: Record<string, ReadonlyArray<string>>;
|
|
15
|
+
defaultEffortByModel?: Record<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* Provider-scoped per-model efforts, used by runtimes whose model list is
|
|
18
|
+
* split by provider (`ai-e`). A flat map would collide on ids shared across
|
|
19
|
+
* providers — `gpt-5.6-sol` exists under both codex and kiro with different
|
|
20
|
+
* allowed efforts.
|
|
21
|
+
*/
|
|
22
|
+
effortsByModelByProvider?: Record<string, Record<string, ReadonlyArray<string>>>;
|
|
23
|
+
defaultEffortByModelByProvider?: Record<string, Record<string, string>>;
|
|
24
|
+
modelSource?: string;
|
|
9
25
|
effortNote?: string;
|
|
10
26
|
modelNote?: string;
|
|
11
27
|
};
|
|
@@ -230,6 +246,35 @@ function stringArrayRecord(value: unknown): Record<string, string[]> | undefined
|
|
|
230
246
|
return out;
|
|
231
247
|
}
|
|
232
248
|
|
|
249
|
+
function stringRecord(value: unknown): Record<string, string> | undefined {
|
|
250
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
251
|
+
const out: Record<string, string> = {};
|
|
252
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
253
|
+
if (typeof raw === 'string') out[key] = raw;
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function nestedStringArrayRecord(value: unknown): Record<string, Record<string, string[]>> | undefined {
|
|
259
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
260
|
+
const out: Record<string, Record<string, string[]>> = {};
|
|
261
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
262
|
+
const inner = stringArrayRecord(raw);
|
|
263
|
+
if (inner) out[key] = inner;
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function nestedStringRecord(value: unknown): Record<string, Record<string, string>> | undefined {
|
|
269
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
270
|
+
const out: Record<string, Record<string, string>> = {};
|
|
271
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
272
|
+
const inner = stringRecord(raw);
|
|
273
|
+
if (inner) out[key] = inner;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
233
278
|
export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta> {
|
|
234
279
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
235
280
|
const out: Record<string, CliMeta> = {};
|
|
@@ -238,6 +283,10 @@ export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta>
|
|
|
238
283
|
const record = value as Record<string, unknown>;
|
|
239
284
|
const modelsByProvider = stringArrayRecord(record['modelsByProvider']);
|
|
240
285
|
const effortsByProvider = stringArrayRecord(record['effortsByProvider']);
|
|
286
|
+
const effortsByModel = stringArrayRecord(record['effortsByModel']);
|
|
287
|
+
const defaultEffortByModel = stringRecord(record['defaultEffortByModel']);
|
|
288
|
+
const effortsByModelByProvider = nestedStringArrayRecord(record['effortsByModelByProvider']);
|
|
289
|
+
const defaultEffortByModelByProvider = nestedStringRecord(record['defaultEffortByModelByProvider']);
|
|
241
290
|
out[key] = {
|
|
242
291
|
label: typeof record['label'] === 'string' ? record['label'] : key,
|
|
243
292
|
models: stringArray(record['models']),
|
|
@@ -246,6 +295,11 @@ export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta>
|
|
|
246
295
|
...(record['providers'] ? { providers: stringArray(record['providers']) } : {}),
|
|
247
296
|
...(modelsByProvider ? { modelsByProvider } : {}),
|
|
248
297
|
...(effortsByProvider ? { effortsByProvider } : {}),
|
|
298
|
+
...(effortsByModel ? { effortsByModel } : {}),
|
|
299
|
+
...(defaultEffortByModel ? { defaultEffortByModel } : {}),
|
|
300
|
+
...(effortsByModelByProvider ? { effortsByModelByProvider } : {}),
|
|
301
|
+
...(defaultEffortByModelByProvider ? { defaultEffortByModelByProvider } : {}),
|
|
302
|
+
...(typeof record['modelSource'] === 'string' ? { modelSource: record['modelSource'] } : {}),
|
|
249
303
|
...(typeof record['effortNote'] === 'string' ? { effortNote: record['effortNote'] } : {}),
|
|
250
304
|
...(typeof record['modelNote'] === 'string' ? { modelNote: record['modelNote'] } : {}),
|
|
251
305
|
};
|
|
@@ -253,6 +307,50 @@ export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta>
|
|
|
253
307
|
return out;
|
|
254
308
|
}
|
|
255
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Effort choices for one model.
|
|
312
|
+
*
|
|
313
|
+
* A live opencodex catalog advertises a different effort set per model, and the
|
|
314
|
+
* chosen value reaches the wire as `-c model_reasoning_effort=`, so the
|
|
315
|
+
* per-model set wins. An entry that exists but is EMPTY means the model takes
|
|
316
|
+
* no effort and must not fall back to the provider/registry lists.
|
|
317
|
+
*/
|
|
318
|
+
export function effortChoicesForModel(
|
|
319
|
+
meta: CliMeta,
|
|
320
|
+
model: string,
|
|
321
|
+
providerEfforts?: ReadonlyArray<string>,
|
|
322
|
+
provider?: string,
|
|
323
|
+
): ReadonlyArray<string> {
|
|
324
|
+
const key = (model || '').trim();
|
|
325
|
+
// Provider-scoped map wins for provider-split runtimes. When the map exists
|
|
326
|
+
// but the model is absent (e.g. a Claude model under ai-e/claude), fall back
|
|
327
|
+
// to the provider list rather than inventing an empty "no effort" set.
|
|
328
|
+
if (provider) {
|
|
329
|
+
const scoped = meta.effortsByModelByProvider?.[provider]?.[key];
|
|
330
|
+
if (scoped) return scoped;
|
|
331
|
+
}
|
|
332
|
+
const byModel = meta.effortsByModel?.[key];
|
|
333
|
+
if (byModel) return byModel;
|
|
334
|
+
return providerEfforts ?? meta.efforts;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Keep a saved effort only when the model still advertises it. */
|
|
338
|
+
export function coerceEffortForModel(
|
|
339
|
+
meta: CliMeta,
|
|
340
|
+
model: string,
|
|
341
|
+
effort: string,
|
|
342
|
+
providerEfforts?: ReadonlyArray<string>,
|
|
343
|
+
provider?: string,
|
|
344
|
+
): string {
|
|
345
|
+
const choices = effortChoicesForModel(meta, model, providerEfforts, provider);
|
|
346
|
+
if (effort && choices.includes(effort)) return effort;
|
|
347
|
+
const key = (model || '').trim();
|
|
348
|
+
const fallback = (provider ? meta.defaultEffortByModelByProvider?.[provider]?.[key] : undefined)
|
|
349
|
+
?? meta.defaultEffortByModel?.[key]
|
|
350
|
+
?? '';
|
|
351
|
+
return choices.includes(fallback) ? fallback : '';
|
|
352
|
+
}
|
|
353
|
+
|
|
256
354
|
export function metaFor(cli: string, registry?: Record<string, CliMeta> | null): CliMeta {
|
|
257
355
|
return registry?.[cli] || CLI_META[cli] || { label: cli, models: [], efforts: [] };
|
|
258
356
|
}
|
|
@@ -165,5 +165,13 @@ fi
|
|
|
165
165
|
|
|
166
166
|
node "$PROJECT_ROOT/scripts/check-electron-sidecar-no-jwc.cjs" --server-root "$SIDECAR_DIR"
|
|
167
167
|
|
|
168
|
+
# Static prune analysis runs before the build; this runs after, on the artifact
|
|
169
|
+
# that will actually ship. The prune guard reasons about bare specifiers and
|
|
170
|
+
# cannot see a computed `import(spec)`, so it can only ever be as complete as
|
|
171
|
+
# its manual RUNTIME_LOADED list. Importing the critical modules for real
|
|
172
|
+
# closes that gap by construction — a dashboard returning 200 never proved the
|
|
173
|
+
# Telegram bot could load.
|
|
174
|
+
node "$PROJECT_ROOT/scripts/check-sidecar-smoke.mjs" --server-root "$SIDECAR_DIR"
|
|
175
|
+
|
|
168
176
|
echo "=== Sidecar ready ==="
|
|
169
177
|
du -sh "$SIDECAR_DIR"
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const { execSync } = require('node:child_process');
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
// Split, because a single list encoded a false invariant. node-pty IS a native
|
|
5
|
+
// addon, IS an electron dependency, and IS imported by the terminal module —
|
|
6
|
+
// so "electron source has no native deps" was simply untrue, and the one addon
|
|
7
|
+
// that actually ships was the one nothing checked.
|
|
8
|
+
//
|
|
9
|
+
// FORBIDDEN: must never appear in electron/src. Any of these would drag a
|
|
10
|
+
// second native build into the Electron ABI.
|
|
11
|
+
const FORBIDDEN = ['better-sqlite3', 'playwright-core', 'sharp', 'canvas'];
|
|
12
|
+
// EXPECTED_NATIVE: legitimately shipped. Presence here is not a failure, but it
|
|
13
|
+
// IS a promise that a runtime load probe covers it — see check-native-load.cjs.
|
|
14
|
+
const EXPECTED_NATIVE = ['node-pty'];
|
|
15
|
+
const pat = FORBIDDEN.join('|');
|
|
5
16
|
// Match any of:
|
|
6
17
|
// from "pkg" / from 'pkg'
|
|
7
18
|
// require("pkg") / require('pkg')
|
|
@@ -13,7 +24,82 @@ try {
|
|
|
13
24
|
} catch (e) { out = ''; }
|
|
14
25
|
const matches = out.trim();
|
|
15
26
|
if (matches) {
|
|
16
|
-
console.error('❌
|
|
27
|
+
console.error('❌ Forbidden native dep imports detected in electron/src:\n' + matches);
|
|
17
28
|
process.exit(1);
|
|
18
29
|
}
|
|
19
|
-
|
|
30
|
+
|
|
31
|
+
// A bare-specifier grep only sees the first hop. electron/src already reaches
|
|
32
|
+
// across the tree boundary with relative paths (electron/src/main/lib/folder/ipc.ts
|
|
33
|
+
// imports into src/manager/git/*), and src/manager/reminders/store.ts imports
|
|
34
|
+
// better-sqlite3 — so the guard is one careless import away from being wrong
|
|
35
|
+
// while still printing a pass. Follow the relative graph transitively.
|
|
36
|
+
const { existsSync, readFileSync } = require('node:fs');
|
|
37
|
+
const { dirname, resolve, join } = require('node:path');
|
|
38
|
+
|
|
39
|
+
function resolveModule(spec, fromFile) {
|
|
40
|
+
const base = resolve(dirname(fromFile), spec.replace(/\.js$/, ''));
|
|
41
|
+
for (const candidate of [
|
|
42
|
+
`${base}.ts`, `${base}.tsx`, `${base}.mts`, `${base}.cts`,
|
|
43
|
+
join(base, 'index.ts'), join(base, 'index.tsx'),
|
|
44
|
+
`${base}.js`,
|
|
45
|
+
]) {
|
|
46
|
+
if (existsSync(candidate)) return candidate;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `sharp` and `sharp/lib/x.js` are the same dependency. */
|
|
52
|
+
function isForbidden(spec) {
|
|
53
|
+
return FORBIDDEN.some(pkg => spec === pkg || spec.startsWith(`${pkg}/`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function crawl(entryFiles) {
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const queue = [...entryFiles];
|
|
59
|
+
const violations = [];
|
|
60
|
+
let unresolved = 0;
|
|
61
|
+
while (queue.length > 0) {
|
|
62
|
+
const file = queue.pop();
|
|
63
|
+
if (!file || seen.has(file)) continue;
|
|
64
|
+
seen.add(file);
|
|
65
|
+
let source = '';
|
|
66
|
+
try { source = readFileSync(file, 'utf8'); } catch { continue; }
|
|
67
|
+
// Covers `from 'x'`, `require('x')`, `import('x')`, and the bare
|
|
68
|
+
// side-effect form `import 'x';` — the last one is easy to miss and is
|
|
69
|
+
// exactly how an unnoticed dependency creeps in.
|
|
70
|
+
const specRe = /(?:from|require\(|import\(|^\s*import)\s*['"]([^'"]+)['"]/gm;
|
|
71
|
+
let match;
|
|
72
|
+
while ((match = specRe.exec(source)) !== null) {
|
|
73
|
+
const spec = match[1];
|
|
74
|
+
if (isForbidden(spec)) {
|
|
75
|
+
violations.push(`${file}: imports ${spec}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!spec.startsWith('.')) continue;
|
|
79
|
+
const resolved = resolveModule(spec, file);
|
|
80
|
+
if (resolved) queue.push(resolved);
|
|
81
|
+
// A silently dropped specifier is a hole in the crawl that looks
|
|
82
|
+
// identical to a clean result, so count and report them.
|
|
83
|
+
else unresolved += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { violations, scanned: seen.size, unresolved };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let entries = [];
|
|
90
|
+
try {
|
|
91
|
+
entries = execSync('find electron/src -name "*.ts" -o -name "*.tsx"', { encoding: 'utf8' })
|
|
92
|
+
.split('\n').map(s => s.trim()).filter(Boolean).map(f => resolve(f));
|
|
93
|
+
} catch { entries = []; }
|
|
94
|
+
|
|
95
|
+
const { violations, scanned, unresolved } = crawl(entries);
|
|
96
|
+
if (violations.length > 0) {
|
|
97
|
+
console.error('❌ Forbidden native dep reachable from electron/src via relative imports:');
|
|
98
|
+
for (const line of violations) console.error(` - ${line}`);
|
|
99
|
+
console.error('\nThese would need an Electron-ABI rebuild; the sidecar builds for Node.');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
// Known limit, stated rather than implied: computed specifiers
|
|
103
|
+
// (`import(`../${name}.js`)`) and `createRequire(...)('pkg')` are invisible to
|
|
104
|
+
// a text scan — the same blind spot the sidecar prune guard documents.
|
|
105
|
+
console.log(`✅ Electron source has no forbidden native deps (scanned ${scanned} files transitively${unresolved ? `, ${unresolved} relative specifiers unresolved` : ''}; expected native: ${EXPECTED_NATIVE.join(', ')})`);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runtime load probe for shipped native addons (260803 unit, 040 phase D1).
|
|
4
|
+
*
|
|
5
|
+
* Static grep proves a module is imported. It does not prove the binary loads.
|
|
6
|
+
* The v2.2.10 incident made that concrete: the dashboard returned HTTP 200
|
|
7
|
+
* while the Telegram bot could not resolve a pruned dependency. Presence is
|
|
8
|
+
* not liveness.
|
|
9
|
+
*
|
|
10
|
+
* node-pty@1.1.0 is N-API based (node-addon-api, 38 napi_ symbols, zero v8/Nan),
|
|
11
|
+
* so a NODE_MODULE_VERSION mismatch is NOT the risk here. What can still break
|
|
12
|
+
* is architecture, asarUnpack placement, and the executable bit on
|
|
13
|
+
* `spawn-helper` — none of which a grep can see. So we dlopen the binary and,
|
|
14
|
+
* where possible, actually run a pty.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node scripts/check-native-load.cjs # check the repo tree
|
|
18
|
+
* node scripts/check-native-load.cjs --app <path/to.app> # check a packaged app
|
|
19
|
+
*/
|
|
20
|
+
const { existsSync, statSync, readdirSync, constants, accessSync } = require('node:fs');
|
|
21
|
+
const { join } = require('node:path');
|
|
22
|
+
const { execFileSync } = require('node:child_process');
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const appIndex = args.indexOf('--app');
|
|
26
|
+
const appPath = appIndex >= 0 ? args[appIndex + 1] : null;
|
|
27
|
+
|
|
28
|
+
const failures = [];
|
|
29
|
+
const notes = [];
|
|
30
|
+
|
|
31
|
+
function fail(message) { failures.push(message); }
|
|
32
|
+
function note(message) { notes.push(message); }
|
|
33
|
+
|
|
34
|
+
function findPtyRoot() {
|
|
35
|
+
if (appPath) {
|
|
36
|
+
// electron-builder unpacks asarUnpack entries next to the asar.
|
|
37
|
+
const unpacked = join(appPath, 'Contents', 'Resources', 'app.asar.unpacked', 'node_modules', 'node-pty');
|
|
38
|
+
if (existsSync(unpacked)) return unpacked;
|
|
39
|
+
const plain = join(appPath, 'Contents', 'Resources', 'app', 'node_modules', 'node-pty');
|
|
40
|
+
if (existsSync(plain)) return plain;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const local = join(process.cwd(), 'electron', 'node_modules', 'node-pty');
|
|
44
|
+
return existsSync(local) ? local : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ptyRoot = findPtyRoot();
|
|
48
|
+
// Exit 3 = nothing was probed. The caller must not present this as a pass;
|
|
49
|
+
// a green "loads fine" when nothing loaded is the exact dishonesty this
|
|
50
|
+
// script exists to remove.
|
|
51
|
+
const EXIT_SKIPPED = 3;
|
|
52
|
+
if (!ptyRoot) {
|
|
53
|
+
if (appPath) {
|
|
54
|
+
fail(`node-pty not found in ${appPath}. asarUnpack must keep it outside the asar (electron-builder.yml asarUnpack: node_modules/node-pty/**/*); a packed addon cannot be dlopen'd.`);
|
|
55
|
+
} else if (process.env.JAW_GATE_REQUIRE_NATIVE === '1') {
|
|
56
|
+
// Opt-in requirement rather than a blanket CI check. The node-tests
|
|
57
|
+
// workflow runs `npm ci --ignore-scripts` at the root only, so it never
|
|
58
|
+
// has electron/node_modules at all: keying on CI made this gate demand an
|
|
59
|
+
// artifact that context cannot produce, and every PR went red for it.
|
|
60
|
+
// The flag is set where the artifact really exists (desktop-release, right
|
|
61
|
+
// after `npm ci --prefix electron`), so a miss there is a genuine failure.
|
|
62
|
+
fail('electron/node_modules/node-pty is absent but JAW_GATE_REQUIRE_NATIVE=1 demanded a real probe (run npm i in electron/)');
|
|
63
|
+
} else {
|
|
64
|
+
console.log('ℹ node-pty not installed in electron/node_modules — nothing probed (run npm i in electron/)');
|
|
65
|
+
process.exit(EXIT_SKIPPED);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const binary = join(ptyRoot, 'build', 'Release', 'pty.node');
|
|
69
|
+
if (!existsSync(binary)) {
|
|
70
|
+
fail(`missing native binary: ${binary}`);
|
|
71
|
+
} else {
|
|
72
|
+
// 1. The binary must actually dlopen under this runtime.
|
|
73
|
+
try {
|
|
74
|
+
const handle = { exports: {} };
|
|
75
|
+
process.dlopen(handle, binary, constants.dlopen?.RTLD_NOW ?? undefined);
|
|
76
|
+
note(`dlopen ok: ${binary}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
fail(`dlopen failed for ${binary}: ${(error && error.message) || error}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. spawn-helper must be present AND executable, or every pty dies at
|
|
82
|
+
// runtime with a permission error that no import check would catch.
|
|
83
|
+
const helper = join(ptyRoot, 'build', 'Release', 'spawn-helper');
|
|
84
|
+
if (process.platform !== 'win32') {
|
|
85
|
+
if (!existsSync(helper)) {
|
|
86
|
+
fail(`missing spawn-helper: ${helper}`);
|
|
87
|
+
} else {
|
|
88
|
+
try {
|
|
89
|
+
accessSync(helper, constants.X_OK);
|
|
90
|
+
note(`spawn-helper executable: mode ${(statSync(helper).mode & 0o777).toString(8)}`);
|
|
91
|
+
} catch {
|
|
92
|
+
fail(`spawn-helper is not executable: ${helper} (mode ${(statSync(helper).mode & 0o777).toString(8)})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Architecture must match the host, or dlopen succeeds nowhere useful.
|
|
98
|
+
try {
|
|
99
|
+
const prebuilds = join(ptyRoot, 'prebuilds');
|
|
100
|
+
if (existsSync(prebuilds)) {
|
|
101
|
+
note(`prebuilds present: ${readdirSync(prebuilds).join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
} catch { /* informational only */ }
|
|
104
|
+
|
|
105
|
+
// 4. Actually spawn a pty and read from it. access(X_OK) says the bit is
|
|
106
|
+
// set; it does not say codesign, quarantine, or arch let the helper run.
|
|
107
|
+
// Only a real spawn covers that, and it is the failure a user would hit
|
|
108
|
+
// on their first terminal session in the shipped app.
|
|
109
|
+
if (!appPath) {
|
|
110
|
+
// Run the round-trip in a child so we can wait on it without blocking
|
|
111
|
+
// this process's event loop — pty data arrives via callbacks, so a
|
|
112
|
+
// synchronous wait here would guarantee a false negative.
|
|
113
|
+
const probe = `
|
|
114
|
+
const pty = require(${JSON.stringify(ptyRoot)});
|
|
115
|
+
// cmd.exe does not understand POSIX -c; it needs /c (with /d /s to skip
|
|
116
|
+
// AutoRun and keep quote handling predictable). Passing -c made the
|
|
117
|
+
// shell exit non-zero and print usage, so the probe would have reported
|
|
118
|
+
// a broken spawn-helper on Windows even when the addon was healthy.
|
|
119
|
+
const isWin = process.platform === 'win32';
|
|
120
|
+
const shell = isWin ? 'cmd.exe' : '/bin/sh';
|
|
121
|
+
const shellArgs = isWin ? ['/d', '/s', '/c', 'echo jaw-pty-ok'] : ['-c', 'echo jaw-pty-ok'];
|
|
122
|
+
const term = pty.spawn(shell, shellArgs, {
|
|
123
|
+
name: 'xterm-color', cols: 80, rows: 24, cwd: process.cwd(), env: process.env,
|
|
124
|
+
});
|
|
125
|
+
let seen = '';
|
|
126
|
+
const done = (code) => { try { term.kill(); } catch {} process.exit(code); };
|
|
127
|
+
term.onData((chunk) => { seen += chunk; if (seen.includes('jaw-pty-ok')) done(0); });
|
|
128
|
+
setTimeout(() => done(seen.includes('jaw-pty-ok') ? 0 : 7), 5000);
|
|
129
|
+
`;
|
|
130
|
+
try {
|
|
131
|
+
execFileSync(process.execPath, ['-e', probe], { timeout: 15_000, stdio: 'ignore' });
|
|
132
|
+
note('pty spawn round-trip ok (spawn-helper genuinely executes)');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const status = error && error.status;
|
|
135
|
+
if (status === 7) {
|
|
136
|
+
fail('pty spawned but produced no output within 5s — spawn-helper may be blocked by codesign/quarantine');
|
|
137
|
+
} else {
|
|
138
|
+
// The child's message embeds the whole probe source; keep only the
|
|
139
|
+
// first meaningful line so the build log stays readable.
|
|
140
|
+
const raw = String((error && error.message) || error);
|
|
141
|
+
const firstLine = raw.split('\n').find(l => /Error|EACCES|EPERM|ENOENT/.test(l)) || raw.split('\n')[0];
|
|
142
|
+
fail(`pty spawn failed: ${firstLine.trim()}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
note('packaged app: spawn round-trip skipped (needs the Electron runtime); dlopen + permissions checked');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const line of notes) console.log(` ${line}`);
|
|
152
|
+
if (failures.length > 0) {
|
|
153
|
+
console.error('❌ native load probe failed:');
|
|
154
|
+
for (const line of failures) console.error(` - ${line}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
console.log(`✅ native addons load${appPath ? ` in ${appPath}` : ''}`);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Post-bundle import smoke for the packaged sidecar (260803 unit, 040 phase D3).
|
|
4
|
+
*
|
|
5
|
+
* `check-sidecar-prune-safety.mjs` reasons about the prune list statically: it
|
|
6
|
+
* parses PRUNE_PKGS out of the shell script, scans source for bare specifiers,
|
|
7
|
+
* and walks the transitive closure through dependencies/optionalDependencies/
|
|
8
|
+
* peerDependencies. That closure is what catches the second half of the v2.2.10
|
|
9
|
+
* incident (`node-fetch → fetch-blob → web-streams-polyfill`).
|
|
10
|
+
*
|
|
11
|
+
* What it cannot see is a computed specifier — `import(someVariable)`. The
|
|
12
|
+
* authors knew, which is why RUNTIME_LOADED exists as a manual escape hatch
|
|
13
|
+
* with exactly one entry. Any future dynamic import that nobody remembers to
|
|
14
|
+
* register reproduces the incident in a new shape.
|
|
15
|
+
*
|
|
16
|
+
* This closes that by construction: after the bundle exists, actually import
|
|
17
|
+
* the modules whose failure was invisible last time. A dashboard returning 200
|
|
18
|
+
* never proved the Telegram bot could load; importing it does.
|
|
19
|
+
*
|
|
20
|
+
* Usage: node scripts/check-sidecar-smoke.mjs [--server-root <dir>]
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
23
|
+
import { join, resolve } from 'node:path';
|
|
24
|
+
import { pathToFileURL } from 'node:url';
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
import { tmpdir } from 'node:os';
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const rootIndex = args.indexOf('--server-root');
|
|
30
|
+
// An explicit --server-root means the caller built the bundle and knows where
|
|
31
|
+
// it should be (bundle-sidecar.sh passes it immediately after bundling), so a
|
|
32
|
+
// missing tree there is a real failure rather than "nothing to check".
|
|
33
|
+
const explicitServerRoot = rootIndex >= 0;
|
|
34
|
+
if (explicitServerRoot && !args[rootIndex + 1]) {
|
|
35
|
+
console.error('❌ --server-root requires a directory argument');
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
const serverRoot = resolve(explicitServerRoot ? args[rootIndex + 1] : 'electron/sidecar/server');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Entry surfaces whose load failure would be silent in a running app: each is
|
|
42
|
+
* reached only on a specific user action, so a healthy dashboard says nothing
|
|
43
|
+
* about them. `telegram/bot.js` is the exact module the v2.2.10 prune broke.
|
|
44
|
+
*/
|
|
45
|
+
const CRITICAL_MODULES = [
|
|
46
|
+
'dist/src/telegram/bot.js',
|
|
47
|
+
'dist/server.js',
|
|
48
|
+
'dist/src/manager/server.js',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Exit code 3 = "nothing to check". The caller must NOT report this as a pass:
|
|
53
|
+
* a gate that says "imports verified" when it imported nothing is the same
|
|
54
|
+
* dishonest-green this script exists to eliminate.
|
|
55
|
+
*/
|
|
56
|
+
const EXIT_SKIPPED = 3;
|
|
57
|
+
|
|
58
|
+
if (!existsSync(serverRoot)) {
|
|
59
|
+
// Keying this on CI made the gate demand a bundle that the node-tests
|
|
60
|
+
// workflow never builds, so every PR failed here. Require a real check
|
|
61
|
+
// only where the caller actually produced (or explicitly named) the tree.
|
|
62
|
+
if (explicitServerRoot || process.env['JAW_GATE_REQUIRE_SIDECAR'] === '1') {
|
|
63
|
+
console.error(`❌ sidecar not bundled at ${serverRoot} but the caller required a real smoke test`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
console.log(`ℹ sidecar not bundled at ${serverRoot} — skipping smoke (run scripts/bundle-sidecar.sh first)`);
|
|
67
|
+
process.exit(EXIT_SKIPPED);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const failures = [];
|
|
71
|
+
let checked = 0;
|
|
72
|
+
const seenCauses = new Set();
|
|
73
|
+
|
|
74
|
+
// Redirect the probe's home to a throwaway directory. Importing these modules
|
|
75
|
+
// regenerates AGENTS.md and touches the DB; a gate must not mutate the
|
|
76
|
+
// developer's real ~/.cli-jaw just by checking that the bundle loads.
|
|
77
|
+
const probeHome = mkdtempSync(join(tmpdir(), 'jaw-smoke-home-'));
|
|
78
|
+
|
|
79
|
+
for (const relative of CRITICAL_MODULES) {
|
|
80
|
+
const target = join(serverRoot, relative);
|
|
81
|
+
if (!existsSync(target)) {
|
|
82
|
+
failures.push(`${relative}: not present in the bundle`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
checked += 1;
|
|
86
|
+
// Import in a CHILD process, never in-process. These modules are not inert:
|
|
87
|
+
// dist/server.js binds ports, opens the DB, starts timers and writes user
|
|
88
|
+
// state. Importing them here would hang the build forever and mutate the
|
|
89
|
+
// developer's ~/.cli-jaw. We only need to know that resolution + top-level
|
|
90
|
+
// evaluation get far enough to not throw, so a bounded child that we kill
|
|
91
|
+
// is both sufficient and safe.
|
|
92
|
+
const child = spawnSync(
|
|
93
|
+
process.execPath,
|
|
94
|
+
['--input-type=module', '-e', `await import(${JSON.stringify(pathToFileURL(target).href)}); process.exit(0);`],
|
|
95
|
+
{
|
|
96
|
+
encoding: 'utf8',
|
|
97
|
+
timeout: 30_000,
|
|
98
|
+
killSignal: 'SIGKILL',
|
|
99
|
+
env: {
|
|
100
|
+
...process.env,
|
|
101
|
+
JAW_SMOKE_PROBE: '1',
|
|
102
|
+
CLI_JAW_HOME: probeHome,
|
|
103
|
+
// Keep the probe off any port the developer or CI is using.
|
|
104
|
+
PORT: '0',
|
|
105
|
+
DASHBOARD_PORT: '0',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// A module that boots a server never exits on its own; the timeout kill is
|
|
111
|
+
// the SUCCESS signal there, because it means the import itself resolved.
|
|
112
|
+
const timedOut = child.error && child.error.code === 'ETIMEDOUT';
|
|
113
|
+
const importFailed = !timedOut
|
|
114
|
+
&& child.status !== 0
|
|
115
|
+
&& /ERR_MODULE_NOT_FOUND|ERR_REQUIRE_ESM|Cannot find package|Cannot find module/.test(child.stderr || '');
|
|
116
|
+
|
|
117
|
+
if (importFailed) {
|
|
118
|
+
// Prefer the human-readable "Cannot find package 'x'" line over the
|
|
119
|
+
// internal `throw new ERR_MODULE_NOT_FOUND(...)` frame, which names no
|
|
120
|
+
// package and is useless in a build log.
|
|
121
|
+
const stderrLines = (child.stderr || '').split('\n');
|
|
122
|
+
const line = stderrLines.find(l => /Cannot find (package|module)/.test(l))
|
|
123
|
+
|| stderrLines.find(l => /ERR_[A-Z_]+/.test(l))
|
|
124
|
+
|| `exit ${child.status}`;
|
|
125
|
+
const cause = line.trim();
|
|
126
|
+
if (!seenCauses.has(cause)) {
|
|
127
|
+
seenCauses.add(cause);
|
|
128
|
+
failures.push(`${relative}: ${cause}`);
|
|
129
|
+
} else {
|
|
130
|
+
failures.push(`${relative}: (same cause as above)`);
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
console.log(` loaded: ${relative}${timedOut ? ' (kept running — import resolved)' : ''}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
rmSync(probeHome, { recursive: true, force: true });
|
|
139
|
+
} catch {
|
|
140
|
+
// best effort
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (failures.length > 0) {
|
|
144
|
+
console.error('❌ sidecar smoke failed — the bundle is missing something it needs at runtime:');
|
|
145
|
+
for (const line of failures) console.error(` - ${line}`);
|
|
146
|
+
console.error('\nThis is the v2.2.10 class: the app would start and look healthy, then fail');
|
|
147
|
+
console.error('on first use of the affected surface. Check the prune list in scripts/bundle-sidecar.sh.');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`✅ sidecar smoke ok (${checked} critical modules imported from ${serverRoot})`);
|