@code-yeongyu/senpi 2026.9.29-5 → 2026.9.30
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 +32 -0
- package/dist/bundle/chunks/{anthropic-messages-HEVKB3PL.js → anthropic-messages-ADSIWAJ5.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-32AFWZEH.js → app-server-command-B3ZYM2WN.js} +1 -1
- package/dist/bundle/chunks/{chunk-2WAKOPNA.js → chunk-4P4DUW66.js} +1 -1
- package/dist/bundle/chunks/{chunk-7RVWDN5Z.js → chunk-6GV3L3GZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-OUG6SDT7.js → chunk-6JZQLUAJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-MJQWYN2W.js → chunk-7MKMDWHC.js} +2 -2
- package/dist/bundle/chunks/{chunk-XBGO2JZD.js → chunk-A7AQSA6R.js} +1 -1
- package/dist/bundle/chunks/{chunk-BJICJY47.js → chunk-DVVROXLZ.js} +1 -1
- package/dist/bundle/chunks/chunk-EIG6K5U3.js +6 -0
- package/dist/bundle/chunks/chunk-FAWC2EIR.js +2 -0
- package/dist/bundle/chunks/{chunk-WCAKUCN3.js → chunk-FNCILR4E.js} +1 -1
- package/dist/bundle/chunks/{chunk-XHEMQA2T.js → chunk-HPESFBMB.js} +2 -2
- package/dist/bundle/chunks/{chunk-BBNWT72C.js → chunk-HVBFDRP4.js} +1 -1
- package/dist/bundle/chunks/{chunk-63QHOG5A.js → chunk-I77ENHCQ.js} +81 -74
- package/dist/bundle/chunks/{chunk-UMLU3I2G.js → chunk-ITXF6FAM.js} +1 -1
- package/dist/bundle/chunks/{chunk-77AUUH3Z.js → chunk-IXONH7ML.js} +2 -2
- package/dist/bundle/chunks/{chunk-K2SHQNAA.js → chunk-JX2CSKLG.js} +1 -1
- package/dist/bundle/chunks/{chunk-4VWWEMCJ.js → chunk-JXZ5LDJP.js} +1 -1
- package/dist/bundle/chunks/{chunk-VLTMFAWQ.js → chunk-KWI3KUF2.js} +1 -1
- package/dist/bundle/chunks/{chunk-7CQX3QFV.js → chunk-L4QGZJKQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-AGVPQLIU.js → chunk-MDXUQDGF.js} +1 -1
- package/dist/bundle/chunks/{chunk-2N3BZ5DV.js → chunk-OOA6OL42.js} +1 -1
- package/dist/bundle/chunks/{chunk-2POUJCWM.js → chunk-OVGH7VP3.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZDNR3YVJ.js → chunk-RMVQ5YRX.js} +1 -1
- package/dist/bundle/chunks/{chunk-SH6VUPE4.js → chunk-T5MOQDGL.js} +1 -1
- package/dist/bundle/chunks/{chunk-CNYWF72S.js → chunk-VKECIIYV.js} +1 -1
- package/dist/bundle/chunks/{chunk-U7BNOE23.js → chunk-VNYUGCC7.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZPMJIPQC.js → chunk-YSNNL5HU.js} +1 -1
- package/dist/bundle/chunks/cli-main-F6WNYPUT.js +3 -0
- package/dist/bundle/chunks/{help-fast-path-ZUNFOE53.js → help-fast-path-34Z6I2PH.js} +1 -1
- package/dist/bundle/chunks/{host-command-V2LDNNXB.js → host-command-PUOIPEBI.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-PFLJRXEN.js → host-lifecycle-4VJUHV5U.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{interactive-mode-J6KGDRR7.js → interactive-mode-KM454JPC.js} +1 -1
- package/dist/bundle/chunks/{list-tips-ASETMEFS.js → list-tips-GWRRD5UC.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-IBVJ3WN7.js → multi-session-host-ZFT6RM2S.js} +2 -2
- package/dist/bundle/chunks/{native-loader-WM7NDPEO.js → native-loader-HW7GQYPY.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-SA2UJW7Q.js → openai-completions-32RTG3Q4.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-JJ5R7UWV.js → package-manager-cli-LNQOWI3A.js} +1 -1
- package/dist/bundle/chunks/{rg-engine-G2YTYL2T.js → rg-engine-LWZA7TRH.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-EX7PWQ4S.js → rotation-stream-DIKK7U57.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-ITND2BVC.js → rpc-mode-H7QGQI4P.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-D6JNRP3D.js → schedule-command-BUFJOM5L.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-QIIA6MFR.js +3 -0
- package/dist/bundle/chunks/{session-picker-K5FIKV6C.js → session-picker-P53MTWTS.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +110 -103
- package/dist/bundle/chunks/{state-store-IIQL55GC.js → state-store-QGLITAI4.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli.js +1 -1
- package/dist/core/dynamic-prompt/build.d.ts +4 -3
- package/dist/core/dynamic-prompt/build.js +4 -2
- package/dist/core/dynamic-prompt/handoff.d.ts +9 -1
- package/dist/core/dynamic-prompt/handoff.js +11 -1
- package/dist/core/dynamic-prompt/intent-gate.js +1 -1
- package/dist/core/dynamic-prompt/style.js +4 -2
- package/dist/core/dynamic-prompt/types.d.ts +7 -1
- package/dist/core/dynamic-prompt/types.js +3 -1
- package/dist/core/dynamic-prompt/verification.d.ts +1 -1
- package/dist/core/dynamic-prompt/verification.js +2 -1
- package/dist/core/extensions/builtin/anthropic-subscription/stream-protocol.d.ts +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/tools.d.ts +9 -2
- package/dist/core/extensions/builtin/anthropic-subscription/tools.js +9 -4
- package/dist/core/extensions/builtin/ask-user/resume.js +20 -9
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +9 -0
- package/dist/core/extensions/builtin/ask-user/tool.js +1 -1
- package/dist/core/extensions/builtin/cache-keepalive/index.js +17 -4
- package/dist/core/extensions/builtin/chat-reply-scrub/index.d.ts +16 -0
- package/dist/core/extensions/builtin/chat-reply-scrub/index.js +39 -0
- package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.d.ts +7 -0
- package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.js +125 -0
- package/dist/core/extensions/builtin/index.js +3 -0
- package/dist/core/extensions/builtin/permission-system/cli.js +1 -0
- package/dist/core/extensions/builtin/permission-system/config.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/config.js +10 -0
- package/dist/core/extensions/builtin/permission-system/index.js +2 -2
- package/dist/core/extensions/builtin/permission-system/settings.js +1 -1
- package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +5 -5
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +6 -6
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +9 -6
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +10 -7
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +15 -6
- package/dist/core/extensions/builtin/prompt-preset/gpt-surface.d.ts +2 -2
- package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +3 -3
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +6 -6
- package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.js +1 -1
- package/dist/core/extensions/builtin/todotools/first-turn.d.ts +6 -4
- package/dist/core/extensions/builtin/todotools/first-turn.js +17 -5
- package/dist/core/extensions/builtin/todotools/todo-ask.d.ts +1 -0
- package/dist/core/extensions/builtin/todotools/todo-ask.js +1 -1
- package/dist/core/extensions/builtin/todotools/tools/todo.js +4 -1
- package/dist/core/resource-loader.js +7 -3
- package/dist/modes/interactive/components/ask-user-async-widget.d.ts +2 -0
- package/dist/modes/interactive/components/ask-user-async-widget.js +10 -0
- package/dist/modes/interactive/components/ask-user-question-state.js +3 -1
- package/dist/modes/interactive/interactive-mode.js +7 -1
- package/dist/modes/interactive/session-control-commands.d.ts +2 -1
- package/dist/modes/interactive/session-control-commands.js +15 -6
- package/dist/modes/rpc/connection-question-bridge.js +5 -5
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/extension-ui-response.d.ts +11 -0
- package/dist/modes/rpc/extension-ui-response.js +18 -0
- package/dist/modes/rpc/host-lifecycle.d.ts +5 -2
- package/dist/modes/rpc/host-lifecycle.js +14 -8
- package/dist/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/modes/rpc/rpc-input-validation.d.ts +1 -1
- package/dist/modes/rpc/rpc-input-validation.js +3 -3
- package/dist/modes/rpc/rpc-mode.d.ts +3 -2
- package/dist/modes/rpc/rpc-mode.js +3 -2
- package/dist/modes/rpc/rpc-types.d.ts +4 -3
- package/dist/modes/rpc/session-command-router.js +4 -1
- package/dist/runtime-snapshot/enter.d.ts +1 -1
- package/dist/runtime-snapshot/enter.js +17 -7
- package/dist/runtime-snapshot/file-copier.d.ts +24 -0
- package/dist/runtime-snapshot/file-copier.js +76 -0
- package/dist/runtime-snapshot/layout.d.ts +8 -4
- package/dist/runtime-snapshot/layout.js +137 -70
- package/dist/runtime-snapshot/registry.d.ts +7 -0
- package/dist/runtime-snapshot/registry.js +63 -1
- package/dist/senpi +1 -1
- package/docs/providers.md +1 -1
- package/docs/rpc.md +34 -11
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-AWEAVBCR.js +0 -2
- package/dist/bundle/chunks/chunk-YLQTKWEC.js +0 -6
- package/dist/bundle/chunks/cli-main-ZPMYM3A3.js +0 -3
- package/dist/bundle/chunks/session-control-endpoint-UJJGG4WI.js +0 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
|
|
1
2
|
import type { RpcExtensionUIResponse } from "./rpc-types.ts";
|
|
2
3
|
/**
|
|
3
4
|
* An `extension_ui_response` carries two ids: `id`, the frame's own correlation id that its reply
|
|
@@ -8,6 +9,16 @@ export declare function answeredUiRequestId(frame: {
|
|
|
8
9
|
readonly id?: unknown;
|
|
9
10
|
readonly uiRequestId?: unknown;
|
|
10
11
|
}): string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* How a `question` answer settles, identically on a host and a terminal control endpoint: it reads
|
|
14
|
+
* `answers` and `comment` (a `value` or `confirmed` beside them belongs to the other dialogs and is
|
|
15
|
+
* ignored). A non-blank comment settles it `comment-submitted`, the status that carries the comment
|
|
16
|
+
* to the model; otherwise any answer settles it `answered`; neither leaves it unsettled
|
|
17
|
+
* (`question_incomplete`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function settledQuestionStatus(answers: QuestionResponse["answers"], comment: string | undefined): "answered" | "comment-submitted" | undefined;
|
|
20
|
+
/** The questions an answer leaves without a selection or text, reported back to the model. */
|
|
21
|
+
export declare function unansweredQuestionIds(questions: QuestionRequest["questions"], answers: QuestionResponse["answers"]): string[];
|
|
11
22
|
export type ExtensionUiResponseReply = {
|
|
12
23
|
readonly id: string;
|
|
13
24
|
readonly type: "response";
|
|
@@ -8,6 +8,24 @@ export function answeredUiRequestId(frame) {
|
|
|
8
8
|
return frame.uiRequestId;
|
|
9
9
|
return typeof frame.id === "string" ? frame.id : undefined;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* How a `question` answer settles, identically on a host and a terminal control endpoint: it reads
|
|
13
|
+
* `answers` and `comment` (a `value` or `confirmed` beside them belongs to the other dialogs and is
|
|
14
|
+
* ignored). A non-blank comment settles it `comment-submitted`, the status that carries the comment
|
|
15
|
+
* to the model; otherwise any answer settles it `answered`; neither leaves it unsettled
|
|
16
|
+
* (`question_incomplete`).
|
|
17
|
+
*/
|
|
18
|
+
export function settledQuestionStatus(answers, comment) {
|
|
19
|
+
if (comment?.trim())
|
|
20
|
+
return "comment-submitted";
|
|
21
|
+
return Object.keys(answers).length > 0 ? "answered" : undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The questions an answer leaves without a selection or text, reported back to the model. */
|
|
24
|
+
export function unansweredQuestionIds(questions, answers) {
|
|
25
|
+
return questions
|
|
26
|
+
.filter((question) => !answers[question.id]?.selected.length && !answers[question.id]?.text?.trim())
|
|
27
|
+
.map((question) => question.id);
|
|
28
|
+
}
|
|
11
29
|
/**
|
|
12
30
|
* Settles one response on a host connection and returns its reply, keyed by the frame `id`.
|
|
13
31
|
* `undefined` only on an unrouted (single-session stdio) connection for a response no request here
|
|
@@ -56,8 +56,11 @@ export declare const INTERNAL_SUPERVISOR_FLAG = "--internal-rpc-host-supervisor"
|
|
|
56
56
|
export declare function findInternalSupervisorArgs(argv: readonly string[]): readonly string[] | undefined;
|
|
57
57
|
/** `--socket <path>` selects the public socket; every other argument is forwarded to the host CLI. */
|
|
58
58
|
export declare function parseSupervisorArgs(argv: readonly string[]): SupervisorLaunch | undefined;
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Resolves the committed CLI entry this supervisor wraps (source tree or built dist).
|
|
61
|
+
* Exported for tests, which pass the module path and layout of a bundled install.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveCliMainPath(modulePath?: string, bundled?: boolean): string;
|
|
61
64
|
/**
|
|
62
65
|
* Resolves the host child spawn. Explicit child commands (desktop launchers)
|
|
63
66
|
* are forwarded untouched. The default re-enters the committed CLI entry
|
|
@@ -193,18 +193,24 @@ function parseSocketIdentity(value) {
|
|
|
193
193
|
const match = /^(\d+):(\d+)$/.exec(value);
|
|
194
194
|
return match ? { dev: Number(match[1]), ino: Number(match[2]) } : undefined;
|
|
195
195
|
}
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Resolves the committed CLI entry this supervisor wraps (source tree or built dist).
|
|
198
|
+
* Exported for tests, which pass the module path and layout of a bundled install.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveCliMainPath(modulePath = fileURLToPath(import.meta.url), bundled = isBundledNode) {
|
|
201
|
+
// Bundled, take the entry from the package's own declared bin: the bundle's cli.js beside
|
|
202
|
+
// this chunk, so a host started from a runtime snapshot runs the snapshot's copy and claims
|
|
203
|
+
// it the way a session does (#2409). Counting ".." instead lands on dist/cli-main.js, the
|
|
204
|
+
// unbundled tree the package also ships, which a snapshot links back to the install that an
|
|
205
|
+
// upgrade replaces, or on the package root, where no cli-main was ever emitted.
|
|
206
|
+
const declared = bundled ? resolveDeclaredCliEntry(modulePath) : undefined;
|
|
207
|
+
if (declared !== undefined)
|
|
208
|
+
return declared;
|
|
199
209
|
const extension = modulePath.endsWith(".ts") ? ".ts" : ".js";
|
|
200
210
|
const unbundled = resolve(dirname(modulePath), "..", "..", `cli-main${extension}`);
|
|
201
211
|
if (existsSync(unbundled))
|
|
202
212
|
return unbundled;
|
|
203
|
-
//
|
|
204
|
-
// was never emitted. Take the entry from the package's own declared bin instead of
|
|
205
|
-
// counting directories: it is the one statement of where the CLI lives that holds in
|
|
206
|
-
// every layout. Falls back to the old path when nothing is declared, so a caller that
|
|
207
|
-
// was working keeps working.
|
|
213
|
+
// Falls back to the old path when nothing is declared, so a caller that was working keeps working.
|
|
208
214
|
return resolveDeclaredCliEntry(modulePath) ?? unbundled;
|
|
209
215
|
}
|
|
210
216
|
/** The CLI entry declared by the nearest enclosing package.json, when it exists on disk. */
|
|
@@ -116,7 +116,7 @@ export declare class RpcClient {
|
|
|
116
116
|
retain_on_disconnect?: boolean;
|
|
117
117
|
/** Per-session auto-titling; needs the host's `auto_title_per_session`. */
|
|
118
118
|
auto_title?: boolean;
|
|
119
|
-
/** Where this session's replies render; needs the host's `prompt_surface
|
|
119
|
+
/** Where this session's replies render; needs the host's `prompt_surface` (`prompt_surface_chat` for `chat`). */
|
|
120
120
|
promptSurface?: PromptSurface;
|
|
121
121
|
}): Promise<{
|
|
122
122
|
sessionId: string;
|
|
@@ -31,7 +31,7 @@ export declare function sessionAutoTitleError(value: unknown): string | undefine
|
|
|
31
31
|
/**
|
|
32
32
|
* Detail for an `open_session.promptSurface` the host refuses, or undefined when the value is
|
|
33
33
|
* absent or a known surface. An unknown value is never read as `terminal`: a client that asked
|
|
34
|
-
* for the app prompt must not silently get the routing line.
|
|
34
|
+
* for the app or chat prompt must not silently get the routing line.
|
|
35
35
|
*/
|
|
36
36
|
export declare function sessionPromptSurfaceError(value: unknown): string | undefined;
|
|
37
37
|
export declare function rpcCommandShapeError(command: unknown): string | undefined;
|
|
@@ -133,12 +133,12 @@ export function sessionAutoTitleError(value) {
|
|
|
133
133
|
/**
|
|
134
134
|
* Detail for an `open_session.promptSurface` the host refuses, or undefined when the value is
|
|
135
135
|
* absent or a known surface. An unknown value is never read as `terminal`: a client that asked
|
|
136
|
-
* for the app prompt must not silently get the routing line.
|
|
136
|
+
* for the app or chat prompt must not silently get the routing line.
|
|
137
137
|
*/
|
|
138
138
|
export function sessionPromptSurfaceError(value) {
|
|
139
|
-
if (value === undefined || value === "terminal" || value === "app")
|
|
139
|
+
if (value === undefined || value === "terminal" || value === "app" || value === "chat")
|
|
140
140
|
return undefined;
|
|
141
|
-
return `promptSurface must be "terminal" or "
|
|
141
|
+
return `promptSurface must be "terminal", "app" or "chat".`;
|
|
142
142
|
}
|
|
143
143
|
export function rpcCommandShapeError(command) {
|
|
144
144
|
if (typeof command !== "object" || command === null || Array.isArray(command)) {
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
* applies to CREATE only - re-opening an existing `sessionPath` keeps that file's header
|
|
50
50
|
* id, so identity is never rewritten by a resume.
|
|
51
51
|
*
|
|
52
|
-
* `open_session.promptSurface` (`terminal` | `app`, host
|
|
52
|
+
* `open_session.promptSurface` (`terminal` | `app` | `chat`, host capabilities `prompt_surface` and
|
|
53
|
+
* `prompt_surface_chat`) builds that
|
|
53
54
|
* session's system prompt for where its replies render; omitted, the host's `SENPI_PROMPT_SURFACE`
|
|
54
55
|
* decides. An attach naming another surface rebuilds the live session's prompt.
|
|
55
56
|
*
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
* `open_failed: <detail>`, `invalid_session_context: <detail>` (a `context` past a
|
|
61
62
|
* documented cap), `invalid_session_kind: <detail>` (a `kind` that is neither
|
|
62
63
|
* `interactive` nor `worker`), `invalid_launch_profile: <detail>` (a non-boolean
|
|
63
|
-
* `auto_title`, or a `promptSurface` other than `terminal`/`app`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
|
|
64
|
+
* `auto_title`, or a `promptSurface` other than `terminal`/`app`/`chat`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
|
|
64
65
|
* legal session id), `session_id_in_use` (a `durableSessionId` a LIVE session already
|
|
65
66
|
* holds).
|
|
66
67
|
*
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
* applies to CREATE only - re-opening an existing `sessionPath` keeps that file's header
|
|
50
50
|
* id, so identity is never rewritten by a resume.
|
|
51
51
|
*
|
|
52
|
-
* `open_session.promptSurface` (`terminal` | `app`, host
|
|
52
|
+
* `open_session.promptSurface` (`terminal` | `app` | `chat`, host capabilities `prompt_surface` and
|
|
53
|
+
* `prompt_surface_chat`) builds that
|
|
53
54
|
* session's system prompt for where its replies render; omitted, the host's `SENPI_PROMPT_SURFACE`
|
|
54
55
|
* decides. An attach naming another surface rebuilds the live session's prompt.
|
|
55
56
|
*
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
* `open_failed: <detail>`, `invalid_session_context: <detail>` (a `context` past a
|
|
61
62
|
* documented cap), `invalid_session_kind: <detail>` (a `kind` that is neither
|
|
62
63
|
* `interactive` nor `worker`), `invalid_launch_profile: <detail>` (a non-boolean
|
|
63
|
-
* `auto_title`, or a `promptSurface` other than `terminal`/`app`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
|
|
64
|
+
* `auto_title`, or a `promptSurface` other than `terminal`/`app`/`chat`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
|
|
64
65
|
* legal session id), `session_id_in_use` (a `durableSessionId` a LIVE session already
|
|
65
66
|
* holds).
|
|
66
67
|
*
|
|
@@ -478,9 +478,10 @@ export type RpcCommand = (RpcSessionCommand & {
|
|
|
478
478
|
/**
|
|
479
479
|
* Where THIS session's replies render (default: the host's `SENPI_PROMPT_SURFACE`). `app` builds
|
|
480
480
|
* a prompt with no visible routing line that keeps tool and hook feedback with the agent;
|
|
481
|
-
* `terminal` is the classic prompt
|
|
482
|
-
*
|
|
483
|
-
*
|
|
481
|
+
* `terminal` is the classic prompt; `chat` (a chat bridge) is `app` with no handoff block, ledger
|
|
482
|
+
* lines, or todo handoff cues, and needs `prompt_surface_chat`. A later open that attaches with
|
|
483
|
+
* another value rebuilds the session's prompt; an attach without it keeps the current surface.
|
|
484
|
+
* Requires the host capability `prompt_surface`. Any other value is refused with `invalid_launch_profile`.
|
|
484
485
|
*/
|
|
485
486
|
promptSurface?: PromptSurface;
|
|
486
487
|
} | {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { VERSION } from "../../config.js";
|
|
2
|
+
import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY } from "../../core/extensions/builtin/permission-system/config.js";
|
|
2
3
|
import { buildRpcSessionState } from "./connection-handler.js";
|
|
3
|
-
import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
4
|
+
import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
4
5
|
import { answerWarm } from "./host-warm.js";
|
|
5
6
|
import { protocolIdentity } from "./protocol-identity.js";
|
|
6
7
|
import { sessionAutoTitleError, sessionContextError, sessionKindError, sessionPromptSurfaceError, } from "./rpc-input-validation.js";
|
|
@@ -182,6 +183,8 @@ export class SessionCommandRouter {
|
|
|
182
183
|
DURABLE_SESSION_ID_CAPABILITY,
|
|
183
184
|
// Every session's prompt is built from its own launch profile, so one host serves both surfaces.
|
|
184
185
|
PROMPT_SURFACE_CAPABILITY,
|
|
186
|
+
PROMPT_SURFACE_CHAT_CAPABILITY,
|
|
187
|
+
ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY,
|
|
185
188
|
// Only an in-process runtime shares the loop a warm loads into (senpi#2314).
|
|
186
189
|
...(this.registry.warm ? [WARM_CAPABILITY] : []),
|
|
187
190
|
...(this.connectionOptions?.capabilities ?? []),
|
|
@@ -13,5 +13,5 @@ export type RuntimeSnapshotDecision = {
|
|
|
13
13
|
* snapshot taken once per build and install, and claims it so it outlives later upgrades.
|
|
14
14
|
* A launch already running from a snapshot only claims it. Any failure keeps the old behavior.
|
|
15
15
|
*/
|
|
16
|
-
export declare function prepareRuntimeSnapshot(entryPath: string, packageDir: string, agentDir: string): RuntimeSnapshotDecision
|
|
16
|
+
export declare function prepareRuntimeSnapshot(entryPath: string, packageDir: string, agentDir: string): Promise<RuntimeSnapshotDecision>;
|
|
17
17
|
//# sourceMappingURL=enter.d.ts.map
|
|
@@ -4,7 +4,7 @@ import { basename, dirname, join, relative } from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { materializeRuntimeSnapshot } from "./layout.js";
|
|
6
6
|
import { readRuntimeSnapshotMarker } from "./marker.js";
|
|
7
|
-
import { claimRuntimeSnapshot, pruneRuntimeSnapshots, withRuntimeLock } from "./registry.js";
|
|
7
|
+
import { claimRuntimeSnapshot, pruneRuntimeSnapshots, withBuildLock, withRuntimeLock } from "./registry.js";
|
|
8
8
|
export const RUNTIME_MANIFEST = "runtime-manifest.json";
|
|
9
9
|
function readRuntimeManifest(bundleDir) {
|
|
10
10
|
let parsed;
|
|
@@ -35,7 +35,7 @@ function readRuntimeManifest(bundleDir) {
|
|
|
35
35
|
* snapshot taken once per build and install, and claims it so it outlives later upgrades.
|
|
36
36
|
* A launch already running from a snapshot only claims it. Any failure keeps the old behavior.
|
|
37
37
|
*/
|
|
38
|
-
export function prepareRuntimeSnapshot(entryPath, packageDir, agentDir) {
|
|
38
|
+
export async function prepareRuntimeSnapshot(entryPath, packageDir, agentDir) {
|
|
39
39
|
const bundleDir = dirname(entryPath);
|
|
40
40
|
const runtimeRoot = join(agentDir, "runtime");
|
|
41
41
|
try {
|
|
@@ -49,15 +49,25 @@ export function prepareRuntimeSnapshot(entryPath, packageDir, agentDir) {
|
|
|
49
49
|
const installId = createHash("sha256").update(realpathSync(packageDir)).digest("hex").slice(0, 12);
|
|
50
50
|
const snapshotId = `${manifest.buildId}-${installId}`;
|
|
51
51
|
const snapshotDir = join(runtimeRoot, snapshotId);
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const isBuilt = () => readRuntimeSnapshotMarker(snapshotDir)?.buildId === manifest.buildId;
|
|
53
|
+
const claim = () => withRuntimeLock(runtimeRoot, () => {
|
|
54
|
+
if (!isBuilt())
|
|
55
|
+
return false;
|
|
56
56
|
claimRuntimeSnapshot(snapshotDir, process.pid);
|
|
57
57
|
pruneRuntimeSnapshots(runtimeRoot, snapshotId, Date.now());
|
|
58
58
|
return true;
|
|
59
59
|
});
|
|
60
|
-
|
|
60
|
+
let claimed = claim();
|
|
61
|
+
if (claimed === false) {
|
|
62
|
+
const built = await withBuildLock(runtimeRoot, snapshotId, async () => {
|
|
63
|
+
if (!isBuilt())
|
|
64
|
+
await materializeRuntimeSnapshot(packageDir, snapshotDir, manifest);
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
if (built)
|
|
68
|
+
claimed = claim();
|
|
69
|
+
}
|
|
70
|
+
if (claimed !== true)
|
|
61
71
|
return { kind: "run-here" };
|
|
62
72
|
const entryUrl = pathToFileURL(join(snapshotDir, "dist", "bundle", basename(entryPath))).href;
|
|
63
73
|
return { kind: "hand-off", entryUrl, snapshotDir };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a file gets into a snapshot. A clone (APFS, btrfs, XFS, ReFS) is copy-on-write: free, and
|
|
3
|
+
* independent of the install even if something rewrites the install's file in place. A hardlink
|
|
4
|
+
* is just as free but shares the file itself, so it is only taken where the filesystem cannot
|
|
5
|
+
* clone (ext4, NTFS); package managers replace files rather than rewriting them, which keeps a
|
|
6
|
+
* linked snapshot on the build it was taken from. A plain copy is the last resort (another
|
|
7
|
+
* volume, a filesystem that refuses links).
|
|
8
|
+
*/
|
|
9
|
+
export type SnapshotCopyMode = "clone" | "clone-or-copy" | "link" | "copy";
|
|
10
|
+
export interface SnapshotFileOperations {
|
|
11
|
+
copyFile(source: string, target: string, mode: number): Promise<void>;
|
|
12
|
+
link(source: string, target: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface SnapshotFileCopier {
|
|
15
|
+
copy(source: string, target: string): Promise<void>;
|
|
16
|
+
readonly mode: SnapshotCopyMode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A file copier that starts with the cheapest independent copy and settles on the first mode the
|
|
20
|
+
* filesystem accepts, so a snapshot of thousands of files pays for a refused mode once.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createSnapshotFileCopier(platform?: NodeJS.Platform, operations?: SnapshotFileOperations): SnapshotFileCopier;
|
|
23
|
+
export declare function copyFiles(pairs: readonly (readonly [string, string])[], copier: SnapshotFileCopier): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=file-copier.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { copyFile, link } from "node:fs/promises";
|
|
3
|
+
function errorCode(error) {
|
|
4
|
+
return error instanceof Error && "code" in error ? error.code : undefined;
|
|
5
|
+
}
|
|
6
|
+
const FILE_OPERATIONS = { copyFile, link };
|
|
7
|
+
/** Clones go through the filesystem's metadata path, which stops scaling past a few in flight. */
|
|
8
|
+
const COPY_CONCURRENCY = 8;
|
|
9
|
+
function nextCopyMode(mode, error, platform) {
|
|
10
|
+
const code = errorCode(error);
|
|
11
|
+
// A missing source or an occupied target is about this file, not about what the filesystem can do.
|
|
12
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EEXIST")
|
|
13
|
+
return undefined;
|
|
14
|
+
switch (mode) {
|
|
15
|
+
case "clone":
|
|
16
|
+
// Node on macOS cannot force a clone (ENOSYS), but its plain clone request clones on APFS
|
|
17
|
+
// and copies elsewhere, which is exactly the order wanted without ever sharing an inode.
|
|
18
|
+
return platform === "darwin" && code === "ENOSYS" ? "clone-or-copy" : "link";
|
|
19
|
+
case "link":
|
|
20
|
+
return "copy";
|
|
21
|
+
case "clone-or-copy":
|
|
22
|
+
case "copy":
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A file copier that starts with the cheapest independent copy and settles on the first mode the
|
|
28
|
+
* filesystem accepts, so a snapshot of thousands of files pays for a refused mode once.
|
|
29
|
+
*/
|
|
30
|
+
export function createSnapshotFileCopier(platform = process.platform, operations = FILE_OPERATIONS) {
|
|
31
|
+
let mode = "clone";
|
|
32
|
+
const attempt = (current, source, target) => {
|
|
33
|
+
switch (current) {
|
|
34
|
+
case "clone":
|
|
35
|
+
return operations.copyFile(source, target, constants.COPYFILE_FICLONE_FORCE);
|
|
36
|
+
case "clone-or-copy":
|
|
37
|
+
return operations.copyFile(source, target, constants.COPYFILE_FICLONE);
|
|
38
|
+
case "link":
|
|
39
|
+
return operations.link(source, target);
|
|
40
|
+
case "copy":
|
|
41
|
+
return operations.copyFile(source, target, 0);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
get mode() {
|
|
46
|
+
return mode;
|
|
47
|
+
},
|
|
48
|
+
async copy(source, target) {
|
|
49
|
+
for (let current = mode;;) {
|
|
50
|
+
try {
|
|
51
|
+
await attempt(current, source, target);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const next = nextCopyMode(current, error, platform);
|
|
56
|
+
if (next === undefined)
|
|
57
|
+
throw error;
|
|
58
|
+
current = next;
|
|
59
|
+
mode = next;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function copyFiles(pairs, copier) {
|
|
66
|
+
let next = 0;
|
|
67
|
+
const worker = async () => {
|
|
68
|
+
for (let index = next++; index < pairs.length; index = next++) {
|
|
69
|
+
const [source, target] = pairs[index] ?? [];
|
|
70
|
+
if (source !== undefined && target !== undefined)
|
|
71
|
+
await copier.copy(source, target);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
await Promise.all(Array.from({ length: Math.min(COPY_CONCURRENCY, pairs.length) }, worker));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=file-copier.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SnapshotFileCopier } from "./file-copier.ts";
|
|
1
2
|
export interface RuntimeManifest {
|
|
2
3
|
readonly buildId: string;
|
|
3
4
|
readonly externals: readonly string[];
|
|
@@ -6,10 +7,13 @@ export declare class RuntimeSnapshotLayoutError extends Error {
|
|
|
6
7
|
readonly packageName: string;
|
|
7
8
|
constructor(packageName: string);
|
|
8
9
|
}
|
|
10
|
+
export declare const STAGING_PREFIX = ".tmp-";
|
|
9
11
|
/**
|
|
10
|
-
* Builds `target` as a copy of the package that no reinstall can touch:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Builds `target` as a copy of the package that no reinstall can touch: the package itself and
|
|
13
|
+
* every package its dependency graph reaches, with nothing linked back to the install, so an
|
|
14
|
+
* upgrade that deletes, rewrites or re-lays-out the install (bundledDependencies on or off) never
|
|
15
|
+
* changes what a running session loads (#2408). Built beside the target and renamed into place,
|
|
16
|
+
* so a crash never leaves a half-built snapshot under its name.
|
|
13
17
|
*/
|
|
14
|
-
export declare function materializeRuntimeSnapshot(packageDir: string, target: string, manifest: RuntimeManifest): void
|
|
18
|
+
export declare function materializeRuntimeSnapshot(packageDir: string, target: string, manifest: RuntimeManifest, copier?: SnapshotFileCopier): Promise<void>;
|
|
15
19
|
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { copyFiles, createSnapshotFileCopier } from "./file-copier.js";
|
|
3
4
|
import { RUNTIME_SNAPSHOT_MARKER } from "./marker.js";
|
|
4
5
|
export class RuntimeSnapshotLayoutError extends Error {
|
|
5
6
|
constructor(packageName) {
|
|
@@ -7,44 +8,53 @@ export class RuntimeSnapshotLayoutError extends Error {
|
|
|
7
8
|
this.packageName = packageName;
|
|
8
9
|
}
|
|
9
10
|
}
|
|
11
|
+
export const STAGING_PREFIX = ".tmp-";
|
|
12
|
+
/** Type declarations and source maps: no runtime ever loads them, and they are half the files. */
|
|
13
|
+
const UNLOADED_FILE = /\.(?:d\.[cm]?ts|map)$/;
|
|
10
14
|
function isMissing(error) {
|
|
11
15
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function linkEntry(source, target) {
|
|
25
|
-
let isDirectory;
|
|
26
|
-
try {
|
|
27
|
-
isDirectory = statSync(source).isDirectory();
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
// A dangling link in the install stays unresolvable from the snapshot too.
|
|
31
|
-
if (isMissing(error))
|
|
32
|
-
return;
|
|
33
|
-
throw error;
|
|
34
|
-
}
|
|
35
|
-
// Windows file symlinks need a privilege; the files involved are small, so copy them.
|
|
36
|
-
if (process.platform === "win32" && !isDirectory) {
|
|
37
|
-
copyFileSync(source, target);
|
|
17
|
+
/**
|
|
18
|
+
* Creates the directories of a tree copy and lists the files to copy into them, following
|
|
19
|
+
* symlinks so nothing in the result points back at the source. `skipModules` leaves nested
|
|
20
|
+
* `node_modules` out: dependencies are placed on their own.
|
|
21
|
+
*/
|
|
22
|
+
function planTree(source, target, files, skipModules, seen = new Set()) {
|
|
23
|
+
const real = realpathSync(source);
|
|
24
|
+
if (seen.has(real))
|
|
38
25
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
for (const name of readdirSync(sourceDir)) {
|
|
44
|
-
if (skip.has(name) || occupied(join(targetDir, name)))
|
|
26
|
+
seen.add(real);
|
|
27
|
+
mkdirSync(target, { recursive: true });
|
|
28
|
+
for (const entry of readdirSync(real, { withFileTypes: true })) {
|
|
29
|
+
if (skipModules && entry.name === "node_modules")
|
|
45
30
|
continue;
|
|
46
|
-
|
|
31
|
+
const from = join(real, entry.name);
|
|
32
|
+
const to = join(target, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
planTree(from, to, files, skipModules, seen);
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile()) {
|
|
37
|
+
if (!UNLOADED_FILE.test(entry.name))
|
|
38
|
+
files.push([from, to]);
|
|
39
|
+
}
|
|
40
|
+
else if (entry.isSymbolicLink()) {
|
|
41
|
+
let isDirectory;
|
|
42
|
+
try {
|
|
43
|
+
isDirectory = statSync(from).isDirectory();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// A dangling link in the install stays unresolvable in the snapshot too.
|
|
47
|
+
if (isMissing(error))
|
|
48
|
+
continue;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
if (isDirectory)
|
|
52
|
+
planTree(from, to, files, skipModules, seen);
|
|
53
|
+
else if (!UNLOADED_FILE.test(entry.name))
|
|
54
|
+
files.push([realpathSync(from), to]);
|
|
55
|
+
}
|
|
47
56
|
}
|
|
57
|
+
seen.delete(real);
|
|
48
58
|
}
|
|
49
59
|
/** The node_modules directories Node's resolver walks from `dir`, nearest first. */
|
|
50
60
|
function moduleDirectoriesFrom(dir) {
|
|
@@ -59,27 +69,6 @@ function moduleDirectoriesFrom(dir) {
|
|
|
59
69
|
return directories;
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
|
-
/**
|
|
63
|
-
* One node_modules that resolves every package name the way the install does: the union of the
|
|
64
|
-
* install's resolution path, nearest directory first, so hoisted and nested layouts both hold.
|
|
65
|
-
*/
|
|
66
|
-
function linkModules(packageDir, target) {
|
|
67
|
-
mkdirSync(target, { recursive: true });
|
|
68
|
-
for (const source of moduleDirectoriesFrom(packageDir)) {
|
|
69
|
-
for (const name of readdirSync(source)) {
|
|
70
|
-
if (name === ".bin")
|
|
71
|
-
continue;
|
|
72
|
-
const scoped = name.startsWith("@") && statSync(join(source, name)).isDirectory();
|
|
73
|
-
if (!scoped) {
|
|
74
|
-
if (!occupied(join(target, name)))
|
|
75
|
-
linkEntry(join(source, name), join(target, name));
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
mkdirSync(join(target, name), { recursive: true });
|
|
79
|
-
linkChildren(join(source, name), join(target, name), new Set());
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
72
|
function packageRootFrom(dir, packageName) {
|
|
84
73
|
for (const modules of moduleDirectoriesFrom(dir)) {
|
|
85
74
|
const root = join(modules, packageName);
|
|
@@ -88,31 +77,109 @@ function packageRootFrom(dir, packageName) {
|
|
|
88
77
|
}
|
|
89
78
|
return undefined;
|
|
90
79
|
}
|
|
91
|
-
function
|
|
80
|
+
function dependencyNames(packageDir) {
|
|
81
|
+
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
82
|
+
if (typeof manifest !== "object" || manifest === null)
|
|
83
|
+
return [];
|
|
84
|
+
const names = new Set();
|
|
85
|
+
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
|
86
|
+
const block = manifest[field];
|
|
87
|
+
if (typeof block === "object" && block !== null)
|
|
88
|
+
for (const name of Object.keys(block))
|
|
89
|
+
names.add(name);
|
|
90
|
+
}
|
|
91
|
+
return [...names];
|
|
92
|
+
}
|
|
93
|
+
function closureNames(packageDir, extra) {
|
|
94
|
+
const names = new Set(extra);
|
|
95
|
+
const visited = new Set();
|
|
96
|
+
const pending = [realpathSync(packageDir)];
|
|
97
|
+
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
98
|
+
if (visited.has(dir))
|
|
99
|
+
continue;
|
|
100
|
+
visited.add(dir);
|
|
101
|
+
for (const name of dependencyNames(dir)) {
|
|
102
|
+
names.add(name);
|
|
103
|
+
const root = packageRootFrom(dir, name);
|
|
104
|
+
if (root !== undefined && !visited.has(root))
|
|
105
|
+
pending.push(root);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return names;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The snapshot's own `node_modules`: a copy of every package the install's dependency graph
|
|
112
|
+
* reaches, placed so each one resolves its dependencies to copies of exactly the packages the
|
|
113
|
+
* install resolves them to. Each name first goes where the package itself would find it (the
|
|
114
|
+
* nearest copy, nested or hoisted); a package that needs another copy of a name gets it nested
|
|
115
|
+
* under itself. Returns the snapshot path of each placed package and the install copy it holds.
|
|
116
|
+
*/
|
|
117
|
+
function planModules(packageDir, snapshotRoot, externals, files) {
|
|
118
|
+
const self = realpathSync(packageDir);
|
|
119
|
+
const placed = new Map();
|
|
120
|
+
const pending = [];
|
|
121
|
+
const place = (installDir, snapshotDir) => {
|
|
122
|
+
planTree(installDir, snapshotDir, files, true);
|
|
123
|
+
placed.set(snapshotDir, installDir);
|
|
124
|
+
pending.push({ installDir, snapshotDir });
|
|
125
|
+
};
|
|
126
|
+
const resolvePlaced = (from, name) => {
|
|
127
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
128
|
+
if (basename(dir) !== "node_modules") {
|
|
129
|
+
const found = placed.get(join(dir, "node_modules", name));
|
|
130
|
+
if (found !== undefined)
|
|
131
|
+
return found;
|
|
132
|
+
}
|
|
133
|
+
if (dir === snapshotRoot || dirname(dir) === dir)
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
for (const name of closureNames(packageDir, externals)) {
|
|
138
|
+
const source = packageRootFrom(packageDir, name);
|
|
139
|
+
if (source !== undefined && source !== self)
|
|
140
|
+
place(source, join(snapshotRoot, "node_modules", name));
|
|
141
|
+
}
|
|
142
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
143
|
+
for (const name of dependencyNames(next.installDir)) {
|
|
144
|
+
const expected = packageRootFrom(next.installDir, name);
|
|
145
|
+
if (expected === undefined || expected === self)
|
|
146
|
+
continue;
|
|
147
|
+
if (resolvePlaced(next.snapshotDir, name) !== expected) {
|
|
148
|
+
place(expected, join(next.snapshotDir, "node_modules", name));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return placed;
|
|
153
|
+
}
|
|
154
|
+
/** Each external the bundle imports must resolve, from the snapshot's bundle, to the snapshot's copy of the install's package. */
|
|
155
|
+
function verifyExternals(packageDir, snapshotBundleDir, externals, placed) {
|
|
92
156
|
for (const name of externals) {
|
|
93
157
|
const expected = packageRootFrom(packageDir, name);
|
|
94
|
-
if (expected
|
|
158
|
+
if (expected === undefined)
|
|
159
|
+
continue;
|
|
160
|
+
const found = moduleDirectoriesFrom(snapshotBundleDir)
|
|
161
|
+
.map((modules) => join(modules, name))
|
|
162
|
+
.find((root) => existsSync(join(root, "package.json")));
|
|
163
|
+
if (found === undefined || placed.get(found) !== expected)
|
|
95
164
|
throw new RuntimeSnapshotLayoutError(name);
|
|
96
|
-
}
|
|
97
165
|
}
|
|
98
166
|
}
|
|
99
167
|
/**
|
|
100
|
-
* Builds `target` as a copy of the package that no reinstall can touch:
|
|
101
|
-
*
|
|
102
|
-
*
|
|
168
|
+
* Builds `target` as a copy of the package that no reinstall can touch: the package itself and
|
|
169
|
+
* every package its dependency graph reaches, with nothing linked back to the install, so an
|
|
170
|
+
* upgrade that deletes, rewrites or re-lays-out the install (bundledDependencies on or off) never
|
|
171
|
+
* changes what a running session loads (#2408). Built beside the target and renamed into place,
|
|
172
|
+
* so a crash never leaves a half-built snapshot under its name.
|
|
103
173
|
*/
|
|
104
|
-
export function materializeRuntimeSnapshot(packageDir, target, manifest) {
|
|
105
|
-
const staging = join(dirname(target),
|
|
174
|
+
export async function materializeRuntimeSnapshot(packageDir, target, manifest, copier = createSnapshotFileCopier()) {
|
|
175
|
+
const staging = join(dirname(target), `${STAGING_PREFIX}${basename(target)}-${process.pid}`);
|
|
106
176
|
rmSync(staging, { recursive: true, force: true });
|
|
107
177
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
linkChildren(join(packageDir, "dist"), join(staging, "dist"), new Set(["bundle"]));
|
|
114
|
-
linkModules(packageDir, join(staging, "node_modules"));
|
|
115
|
-
verifyExternals(packageDir, join(snapshotBundleDir, "chunks"), manifest.externals);
|
|
178
|
+
const files = [];
|
|
179
|
+
planTree(packageDir, staging, files, true);
|
|
180
|
+
const placed = planModules(packageDir, staging, manifest.externals, files);
|
|
181
|
+
await copyFiles(files, copier);
|
|
182
|
+
verifyExternals(packageDir, join(staging, "dist", "bundle", "chunks"), manifest.externals, placed);
|
|
116
183
|
const marker = { buildId: manifest.buildId, installPackageDir: realpathSync(packageDir) };
|
|
117
184
|
writeFileSync(join(staging, RUNTIME_SNAPSHOT_MARKER), `${JSON.stringify(marker)}\n`);
|
|
118
185
|
rmSync(target, { recursive: true, force: true });
|