@khorsheed/dsh-ankh-guard 0.1.0 → 0.2.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/CHANGELOG.md +28 -0
- package/README.en.md +75 -29
- package/README.i18n.yaml +2 -2
- package/README.md +74 -29
- package/lib/cli.js +2383 -209
- package/lib/client.js +257 -0
- package/lib/exit-agent.js +5 -2
- package/lib/index.js +722 -38
- package/lib/invariant.js +1 -1
- package/lib/preflight-runner.js +125 -47
- package/lib/processes-BjZgJjQr.js +344 -0
- package/lib/restart-context-D6nISh28.js +1245 -0
- package/lib/restart-context-DUyExi9O.js +1245 -0
- package/lib/{state-Dhx9VG44.js → state-4f7yny39.js} +60 -13
- package/lib/state-CZMypGkB.js +323 -0
- package/lib/test-seam-DnvLWTeO.js +119 -0
- package/lib/test-seam-cli.js +24 -0
- package/lib/test-seam-dwvaKjRp.js +459 -0
- package/lib/test-seam.js +2 -0
- package/lib/types/browser-handoff.d.ts +55 -0
- package/lib/types/browser-handoff.js +489 -0
- package/lib/types/cli.d.ts +34 -4
- package/lib/types/cli.js +1487 -225
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +264 -0
- package/lib/types/deployment-proof.d.ts +24 -0
- package/lib/types/deployment-proof.js +314 -0
- package/lib/types/exit-agent.js +2 -0
- package/lib/types/git.d.ts +12 -3
- package/lib/types/git.js +69 -7
- package/lib/types/index.d.ts +66 -3
- package/lib/types/index.js +157 -39
- package/lib/types/launch-spec.d.ts +263 -0
- package/lib/types/launch-spec.js +823 -0
- package/lib/types/preflight-runner.d.ts +23 -12
- package/lib/types/preflight-runner.js +152 -57
- package/lib/types/processes.d.ts +38 -6
- package/lib/types/processes.js +236 -10
- package/lib/types/restart-context.d.ts +50 -0
- package/lib/types/restart-context.js +106 -0
- package/lib/types/restart-request.d.ts +32 -0
- package/lib/types/restart-request.js +128 -0
- package/lib/types/state-files.d.ts +30 -0
- package/lib/types/state-files.js +55 -0
- package/lib/types/state.d.ts +29 -2
- package/lib/types/state.js +52 -7
- package/lib/types/temp-artifact.d.ts +15 -0
- package/lib/types/temp-artifact.js +17 -0
- package/lib/types/test-seam-cli.d.ts +3 -0
- package/lib/types/test-seam-cli.js +27 -0
- package/lib/types/test-seam.d.ts +55 -0
- package/lib/types/test-seam.js +112 -0
- package/lib/types/transition.d.ts +118 -0
- package/lib/types/transition.js +717 -0
- package/package.json +29 -9
- package/scripts/dsh-watchdog.sh +1388 -80
- package/scripts/install-launchd.sh +43 -5
- package/scripts/install-systemd.sh +43 -5
- package/scripts/on-install.js +1 -1
- package/skills/dsh-self-restart-guard/SKILL.md +38 -12
- package/lib/processes-hCAmwma-.js +0 -127
- package/lib/restart-context-DmnQXNf-.js +0 -421
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* deploy line) was wiped by the upstream reset and cannot be re-applied
|
|
6
6
|
* without re-forking apps/cli on every release — which we cannot upstream
|
|
7
7
|
* (no PR access). This runner replaces it WITHOUT touching the harness: it
|
|
8
|
-
* resolves the official
|
|
9
|
-
* `dsh-home-paths`, `dsh-launch-environment`, `dsh-cmdline`) from
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
8
|
+
* resolves the official packages (`@deepseek-ai/dsh-app-boot`,
|
|
9
|
+
* `dsh-home-paths`, `dsh-launch-environment`, `dsh-cmdline`) from an explicit
|
|
10
|
+
* execution binding: source files in the host checkout for a source launch,
|
|
11
|
+
* or the npm toolchain selected by a built launch's dsh package.json. It never
|
|
12
|
+
* infers that choice from Node's execArgv.
|
|
12
13
|
*
|
|
13
14
|
* What it verifies (same contract as the old patch):
|
|
14
15
|
* - the profile's full patch stack composes (bundle layers from
|
|
@@ -27,11 +28,17 @@
|
|
|
27
28
|
* error, env load failure): NOT a verdict on the composition.
|
|
28
29
|
*
|
|
29
30
|
* Run directly (any profile) or via the guard CLI:
|
|
30
|
-
* node
|
|
31
|
-
*
|
|
31
|
+
* node packages/ankh-guard/lib/preflight-runner.js --profile web \
|
|
32
|
+
* --host-surface built --install-anchor <toolchain>/node_modules/@deepseek-ai/dsh/package.json
|
|
32
33
|
*
|
|
33
34
|
* @module @khorsheed/dsh-ankh-guard/preflight-runner
|
|
34
35
|
*/
|
|
36
|
+
export type PreflightHostSurface = 'source' | 'built';
|
|
37
|
+
export interface PreflightHostBinding {
|
|
38
|
+
surface: PreflightHostSurface;
|
|
39
|
+
/** Real dsh package.json used by the successor's module graph. */
|
|
40
|
+
installAnchor: string;
|
|
41
|
+
}
|
|
35
42
|
/** The guarded (or tracking) harness checkout the live instance boots from. */
|
|
36
43
|
export declare function resolveHarnessRoot(env?: Record<string, string | undefined>): string;
|
|
37
44
|
/** The composed composition of one profile for a preflight (or a drift check). */
|
|
@@ -49,9 +56,12 @@ export interface PreflightComposition {
|
|
|
49
56
|
/**
|
|
50
57
|
* Compose one profile's full patch stack through the launcher's layering —
|
|
51
58
|
* bundle layers in `dsh.profile.bundles` order, the profile user layer, the
|
|
52
|
-
* home-level user layer, `--patch` overlays, the agent-presets roots overlay
|
|
53
|
-
*
|
|
54
|
-
*
|
|
59
|
+
* home-level user layer, `--patch` overlays, the agent-presets roots overlay
|
|
60
|
+
* (rc host line only; the 0.1.2 line's preset package self-ships its root),
|
|
61
|
+
* then the telemetry switch. Two host API generations are mirrored and
|
|
62
|
+
* feature-detected per run — see the `hostLine` branch below. Exported so
|
|
63
|
+
* the drift tripwire can compare this assembly against the launcher's own
|
|
64
|
+
* dump without booting anything.
|
|
55
65
|
* @param profile - the profile name (same resolution as `--profile`).
|
|
56
66
|
* @param patchFiles - `--patch` overlay paths, in argv order.
|
|
57
67
|
* @param root - harness checkout root.
|
|
@@ -60,7 +70,7 @@ export interface PreflightComposition {
|
|
|
60
70
|
* specific deployment must not silently compose a different home's tree.
|
|
61
71
|
* @returns the patch stack and composed rows.
|
|
62
72
|
*/
|
|
63
|
-
export declare function composePreflightPatches(profile: string, patchFiles: readonly string[], root: string, home?: string): Promise<PreflightComposition>;
|
|
73
|
+
export declare function composePreflightPatches(profile: string, patchFiles: readonly string[], root: string, home?: string, binding?: PreflightHostBinding): Promise<PreflightComposition>;
|
|
64
74
|
/**
|
|
65
75
|
* Boot the profile's full tree once, tear it down, and report the verdict on
|
|
66
76
|
* the process streams. No HMR, no user-patch watchers, no signal wiring —
|
|
@@ -70,11 +80,12 @@ export declare function composePreflightPatches(profile: string, patchFiles: rea
|
|
|
70
80
|
* @param root - harness checkout root (default: DSH_HARNESS or ~/code/deepseek-harness).
|
|
71
81
|
* @returns the process exit code (see the module contract).
|
|
72
82
|
*/
|
|
73
|
-
export declare function runPreflight(profile: string, patchFiles?: readonly string[], root?: string): Promise<number>;
|
|
74
|
-
/** Minimal argv parse
|
|
83
|
+
export declare function runPreflight(profile: string, patchFiles?: readonly string[], root?: string, binding?: PreflightHostBinding): Promise<number>;
|
|
84
|
+
/** Minimal argv parse; the host execution surface is mandatory and explicit. */
|
|
75
85
|
export declare function parsePreflightArgs(argv: readonly string[]): {
|
|
76
86
|
profile: string;
|
|
77
87
|
patchFiles: string[];
|
|
88
|
+
binding?: PreflightHostBinding;
|
|
78
89
|
error?: string;
|
|
79
90
|
};
|
|
80
91
|
//# sourceMappingURL=preflight-runner.d.ts.map
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* deploy line) was wiped by the upstream reset and cannot be re-applied
|
|
6
6
|
* without re-forking apps/cli on every release — which we cannot upstream
|
|
7
7
|
* (no PR access). This runner replaces it WITHOUT touching the harness: it
|
|
8
|
-
* resolves the official
|
|
9
|
-
* `dsh-home-paths`, `dsh-launch-environment`, `dsh-cmdline`) from
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
8
|
+
* resolves the official packages (`@deepseek-ai/dsh-app-boot`,
|
|
9
|
+
* `dsh-home-paths`, `dsh-launch-environment`, `dsh-cmdline`) from an explicit
|
|
10
|
+
* execution binding: source files in the host checkout for a source launch,
|
|
11
|
+
* or the npm toolchain selected by a built launch's dsh package.json. It never
|
|
12
|
+
* infers that choice from Node's execArgv.
|
|
12
13
|
*
|
|
13
14
|
* What it verifies (same contract as the old patch):
|
|
14
15
|
* - the profile's full patch stack composes (bundle layers from
|
|
@@ -27,8 +28,8 @@
|
|
|
27
28
|
* error, env load failure): NOT a verdict on the composition.
|
|
28
29
|
*
|
|
29
30
|
* Run directly (any profile) or via the guard CLI:
|
|
30
|
-
* node
|
|
31
|
-
*
|
|
31
|
+
* node packages/ankh-guard/lib/preflight-runner.js --profile web \
|
|
32
|
+
* --host-surface built --install-anchor <toolchain>/node_modules/@deepseek-ai/dsh/package.json
|
|
32
33
|
*
|
|
33
34
|
* @module @khorsheed/dsh-ankh-guard/preflight-runner
|
|
34
35
|
*/
|
|
@@ -40,9 +41,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
40
41
|
}
|
|
41
42
|
return path;
|
|
42
43
|
};
|
|
43
|
-
import { existsSync, mkdtempSync,
|
|
44
|
+
import { existsSync, mkdtempSync, statSync, writeFileSync } from 'node:fs';
|
|
44
45
|
import { homedir, tmpdir } from 'node:os';
|
|
45
|
-
import {
|
|
46
|
+
import { createRequire } from 'node:module';
|
|
47
|
+
import { dirname, join, resolve } from 'node:path';
|
|
46
48
|
import { pathToFileURL } from 'node:url';
|
|
47
49
|
import { isDirectInvocation } from "./defaults.js";
|
|
48
50
|
const NAME = 'dsh';
|
|
@@ -50,6 +52,17 @@ const PROFILE_ROOT_FILENAME = 'cordis.yml';
|
|
|
50
52
|
const HOME_PATCH_FILENAME = 'cordis.patch.yml';
|
|
51
53
|
const TELEMETRY_ROW_ID = 'session-telemetry-otel';
|
|
52
54
|
const DSH_HARNESS_ENV = 'DSH_HARNESS';
|
|
55
|
+
/**
|
|
56
|
+
* The empty root entry list every profile tree patches over — the exact
|
|
57
|
+
* bytes the launcher's prepareProfile rewrites on every boot (the vendored
|
|
58
|
+
* Loader's tree write-back can bake composed rows into this file, so both
|
|
59
|
+
* the launcher and this dry-run always start from the empty root).
|
|
60
|
+
*/
|
|
61
|
+
const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches:
|
|
62
|
+
# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any
|
|
63
|
+
# --patch overlays. Edit cordis.patch.yml, not this file.
|
|
64
|
+
[]
|
|
65
|
+
`;
|
|
53
66
|
/** The guarded (or tracking) harness checkout the live instance boots from. */
|
|
54
67
|
export function resolveHarnessRoot(env = process.env) {
|
|
55
68
|
const fromEnv = env[DSH_HARNESS_ENV];
|
|
@@ -67,18 +80,9 @@ const HARNESS_PACKAGE_DIRS = {
|
|
|
67
80
|
'@deepseek-ai/dsh-cmdline': 'packages/boot/cmdline',
|
|
68
81
|
};
|
|
69
82
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
function canImportTypeScript() {
|
|
74
|
-
return process.execArgv.some(arg => arg.includes('tsx'));
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Dynamically import a harness package from the live checkout, SOURCE FIRST:
|
|
78
|
-
* a source-launched prod instance (tsx … apps/cli/src/bin.ts) boots the
|
|
79
|
-
* source, so the preflight engine must be the source too — a stale `lib/`
|
|
80
|
-
* must never make the preflight greener than the real boot. The built entry
|
|
81
|
-
* is the fallback for harnesses that ship only artifacts.
|
|
83
|
+
* Dynamically import one host package from the explicitly selected surface.
|
|
84
|
+
* A source launch uses checkout source and fails rather than falling back to
|
|
85
|
+
* stale lib; a built launch resolves only through its npm install anchor.
|
|
82
86
|
*
|
|
83
87
|
* The composition layers below (bundle layers, user layers, overlays, the
|
|
84
88
|
* agent-presets roots, the telemetry switch) mirror the launcher's private
|
|
@@ -90,27 +94,34 @@ function canImportTypeScript() {
|
|
|
90
94
|
* @param name - package name (a key of {@link HARNESS_PACKAGE_DIRS}).
|
|
91
95
|
* @returns the imported module.
|
|
92
96
|
*/
|
|
93
|
-
async function loadHarnessPackage(root, name) {
|
|
97
|
+
async function loadHarnessPackage(root, name, binding) {
|
|
98
|
+
if (binding.surface === 'built') {
|
|
99
|
+
let entry;
|
|
100
|
+
try {
|
|
101
|
+
entry = createRequire(binding.installAnchor).resolve(name);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw new Error(`built host package ${name} is not resolvable from ${binding.installAnchor}: ${String(error)}`, { cause: error });
|
|
105
|
+
}
|
|
106
|
+
return await import(__rewriteRelativeImportExtension(pathToFileURL(entry).href));
|
|
107
|
+
}
|
|
94
108
|
const relative = HARNESS_PACKAGE_DIRS[name];
|
|
95
109
|
if (relative === undefined)
|
|
96
110
|
throw new Error(`no known harness layout entry for ${name}`);
|
|
97
111
|
const base = join(root, relative);
|
|
98
112
|
const source = join(base, 'src', 'index.ts');
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
if (!existsSync(source)) {
|
|
114
|
+
throw new Error(`harness source entry does not exist: ${source}`);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
return await import(__rewriteRelativeImportExtension(pathToFileURL(source).href));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// A broken source import (syntax error, unresolved workspace dep) is
|
|
121
|
+
// EXACTLY what the next source boot would hit — surface it as a
|
|
122
|
+
// composition verdict, never silently fall back to a stale build.
|
|
123
|
+
throw new Error(`harness source ${source} failed to import (this is what a source boot would hit): ${String(error)}`, { cause: error });
|
|
109
124
|
}
|
|
110
|
-
const manifest = JSON.parse(readFileSync(join(base, 'package.json'), 'utf8'));
|
|
111
|
-
const entry = manifest.main ?? 'lib/index.js';
|
|
112
|
-
const module = await import(__rewriteRelativeImportExtension(pathToFileURL(join(base, entry)).href));
|
|
113
|
-
return module;
|
|
114
125
|
}
|
|
115
126
|
/** Thrown for harness-side load failures — preflight infrastructure, never a composition verdict. */
|
|
116
127
|
class PreflightInfraError extends Error {
|
|
@@ -118,9 +129,12 @@ class PreflightInfraError extends Error {
|
|
|
118
129
|
/**
|
|
119
130
|
* Compose one profile's full patch stack through the launcher's layering —
|
|
120
131
|
* bundle layers in `dsh.profile.bundles` order, the profile user layer, the
|
|
121
|
-
* home-level user layer, `--patch` overlays, the agent-presets roots overlay
|
|
122
|
-
*
|
|
123
|
-
*
|
|
132
|
+
* home-level user layer, `--patch` overlays, the agent-presets roots overlay
|
|
133
|
+
* (rc host line only; the 0.1.2 line's preset package self-ships its root),
|
|
134
|
+
* then the telemetry switch. Two host API generations are mirrored and
|
|
135
|
+
* feature-detected per run — see the `hostLine` branch below. Exported so
|
|
136
|
+
* the drift tripwire can compare this assembly against the launcher's own
|
|
137
|
+
* dump without booting anything.
|
|
124
138
|
* @param profile - the profile name (same resolution as `--profile`).
|
|
125
139
|
* @param patchFiles - `--patch` overlay paths, in argv order.
|
|
126
140
|
* @param root - harness checkout root.
|
|
@@ -129,12 +143,15 @@ class PreflightInfraError extends Error {
|
|
|
129
143
|
* specific deployment must not silently compose a different home's tree.
|
|
130
144
|
* @returns the patch stack and composed rows.
|
|
131
145
|
*/
|
|
132
|
-
export async function composePreflightPatches(profile, patchFiles, root, home
|
|
146
|
+
export async function composePreflightPatches(profile, patchFiles, root, home, binding = {
|
|
147
|
+
surface: 'source',
|
|
148
|
+
installAnchor: join(root, 'apps', 'cli', 'package.json'),
|
|
149
|
+
}) {
|
|
133
150
|
let appBoot;
|
|
134
151
|
let homePaths;
|
|
135
152
|
try {
|
|
136
|
-
appBoot = await loadHarnessPackage(root, '@deepseek-ai/dsh-app-boot');
|
|
137
|
-
homePaths = await loadHarnessPackage(root, '@deepseek-ai/dsh-home-paths');
|
|
153
|
+
appBoot = await loadHarnessPackage(root, '@deepseek-ai/dsh-app-boot', binding);
|
|
154
|
+
homePaths = await loadHarnessPackage(root, '@deepseek-ai/dsh-home-paths', binding);
|
|
138
155
|
}
|
|
139
156
|
catch (error) {
|
|
140
157
|
throw new PreflightInfraError(`harness packages unavailable under ${root}: ${String(error)}`, { cause: error });
|
|
@@ -151,10 +168,30 @@ export async function composePreflightPatches(profile, patchFiles, root, home) {
|
|
|
151
168
|
// healing then re-points fallback links into self-referential loops
|
|
152
169
|
// (observed on a second preflight from the installed CLI: ~20 links looped,
|
|
153
170
|
// the next real boot would have failed).
|
|
154
|
-
const anchor =
|
|
171
|
+
const anchor = binding.installAnchor;
|
|
155
172
|
const resolvedHome = resolveDshHome(home);
|
|
156
|
-
|
|
173
|
+
// Which app-boot API generation this host speaks. The 0.1.2 line re-layered
|
|
174
|
+
// the profile composition: the heal moved behind the async options API and
|
|
175
|
+
// below the profile load (see HealProfilesModuleFallback), and the launcher
|
|
176
|
+
// dropped its agent-presets shipped-root overlay because the preset package
|
|
177
|
+
// now self-ships its root. DEFAULT_PROFILE_PATCH_RELOAD is a value export
|
|
178
|
+
// only the new line carries, so it is the feature marker; a version parse
|
|
179
|
+
// would break on exactly the unreleased builds this runner must dry-run.
|
|
180
|
+
// Both lines stay supported: prod hosts run the rc line until 0.1.2 lands
|
|
181
|
+
// on npm.
|
|
182
|
+
const hostLine = 'DEFAULT_PROFILE_PATCH_RELOAD' in appBoot ? '0.1.2' : 'rc';
|
|
183
|
+
if (hostLine === 'rc')
|
|
184
|
+
healProfilesModuleFallback(anchor, resolvedHome);
|
|
157
185
|
const composed = loadProfile(NAME, profile, anchor, resolvedHome, { userLayer: true });
|
|
186
|
+
// Mirror prepareProfile: rewrite the empty root config the tree patches
|
|
187
|
+
// over. The Loader needs the real file to anchor the include, and on a
|
|
188
|
+
// fresh home (the launcher never booted it) the file does not exist yet —
|
|
189
|
+
// the dry-run would otherwise fail on exactly the tree a first boot
|
|
190
|
+
// composes fine.
|
|
191
|
+
writeFileSync(join(composed.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG);
|
|
192
|
+
if (hostLine === '0.1.2') {
|
|
193
|
+
await healProfilesModuleFallback({ installAnchor: anchor, profile: composed, home: resolvedHome });
|
|
194
|
+
}
|
|
158
195
|
const homePatches = loadOptionalPatches(NAME, join(resolvedHome, HOME_PATCH_FILENAME)) ?? [];
|
|
159
196
|
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)));
|
|
160
197
|
const bundlePatches = composed.layers.flatMap(layer => layer.patches);
|
|
@@ -165,12 +202,19 @@ export async function composePreflightPatches(profile, patchFiles, root, home) {
|
|
|
165
202
|
rows.set(row.id, row);
|
|
166
203
|
}
|
|
167
204
|
const composedOverlays = [...overlays];
|
|
168
|
-
|
|
205
|
+
// The launcher's shipped preset root exists only on the rc line — the
|
|
206
|
+
// 0.1.2 line removed apps/cli/config/agent-presets and lets the preset
|
|
207
|
+
// package self-ship its root, so the overlay follows the directory, not
|
|
208
|
+
// the host line.
|
|
209
|
+
const shippedPresetRoot = binding.surface === 'built'
|
|
210
|
+
? join(dirname(binding.installAnchor), 'config', 'agent-presets')
|
|
211
|
+
: join(root, 'apps', 'cli', 'config', 'agent-presets');
|
|
212
|
+
if (rows.has('agent-presets') && existsSync(shippedPresetRoot)) {
|
|
169
213
|
composedOverlays.push({
|
|
170
214
|
id: 'agent-presets',
|
|
171
215
|
config: {
|
|
172
216
|
...(rows.get('agent-presets')?.config ?? {}),
|
|
173
|
-
roots: [{ path:
|
|
217
|
+
roots: [{ path: shippedPresetRoot, trust: 'system' }],
|
|
174
218
|
},
|
|
175
219
|
});
|
|
176
220
|
}
|
|
@@ -210,6 +254,36 @@ export async function composePreflightPatches(profile, patchFiles, root, home) {
|
|
|
210
254
|
patches.push(...composedOverlays);
|
|
211
255
|
return { patches, rows, profileDir: composed.dir };
|
|
212
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* The launcher's readiness signal, mirrored: 0.1.2's runProfile provides an
|
|
259
|
+
* `appReady` service through provideCmdline and commits it once boot and host
|
|
260
|
+
* setup settle. The rc line's provideCmdline ignores the field, so one
|
|
261
|
+
* implementation serves both host lines.
|
|
262
|
+
*/
|
|
263
|
+
function createAppReadyStub() {
|
|
264
|
+
let committed = false;
|
|
265
|
+
const listeners = new Set();
|
|
266
|
+
return {
|
|
267
|
+
service: {
|
|
268
|
+
onReady(listener) {
|
|
269
|
+
if (committed) {
|
|
270
|
+
listener();
|
|
271
|
+
return () => { };
|
|
272
|
+
}
|
|
273
|
+
listeners.add(listener);
|
|
274
|
+
return () => { listeners.delete(listener); };
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
commit() {
|
|
278
|
+
if (committed)
|
|
279
|
+
return;
|
|
280
|
+
committed = true;
|
|
281
|
+
for (const listener of [...listeners])
|
|
282
|
+
listener();
|
|
283
|
+
listeners.clear();
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
213
287
|
/** Stat every registered client bundle; report one line per missing/unreadable artifact. */
|
|
214
288
|
function missingClientArtifacts(ctx) {
|
|
215
289
|
const registry = ctx.get?.('clientModules');
|
|
@@ -240,16 +314,19 @@ function missingClientArtifacts(ctx) {
|
|
|
240
314
|
* @param root - harness checkout root (default: DSH_HARNESS or ~/code/deepseek-harness).
|
|
241
315
|
* @returns the process exit code (see the module contract).
|
|
242
316
|
*/
|
|
243
|
-
export async function runPreflight(profile, patchFiles = [], root = resolveHarnessRoot()
|
|
317
|
+
export async function runPreflight(profile, patchFiles = [], root = resolveHarnessRoot(), binding = {
|
|
318
|
+
surface: 'source',
|
|
319
|
+
installAnchor: join(root, 'apps', 'cli', 'package.json'),
|
|
320
|
+
}) {
|
|
244
321
|
// Environment loading sits outside the composition pipeline; a failure here
|
|
245
322
|
// is preflight infrastructure, not a verdict on the tree.
|
|
246
323
|
let appBoot;
|
|
247
324
|
let launchEnvironment;
|
|
248
325
|
let cmdline;
|
|
249
326
|
try {
|
|
250
|
-
appBoot = await loadHarnessPackage(root, '@deepseek-ai/dsh-app-boot');
|
|
251
|
-
launchEnvironment = await loadHarnessPackage(root, '@deepseek-ai/dsh-launch-environment');
|
|
252
|
-
cmdline = await loadHarnessPackage(root, '@deepseek-ai/dsh-cmdline');
|
|
327
|
+
appBoot = await loadHarnessPackage(root, '@deepseek-ai/dsh-app-boot', binding);
|
|
328
|
+
launchEnvironment = await loadHarnessPackage(root, '@deepseek-ai/dsh-launch-environment', binding);
|
|
329
|
+
cmdline = await loadHarnessPackage(root, '@deepseek-ai/dsh-cmdline', binding);
|
|
253
330
|
}
|
|
254
331
|
catch (error) {
|
|
255
332
|
process.stderr.write(`preflight could not execute (harness packages unavailable under ${root}): ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -268,7 +345,7 @@ export async function runPreflight(profile, patchFiles = [], root = resolveHarne
|
|
|
268
345
|
return 3;
|
|
269
346
|
}
|
|
270
347
|
try {
|
|
271
|
-
const composed = await composePreflightPatches(profile, patchFiles, root);
|
|
348
|
+
const composed = await composePreflightPatches(profile, patchFiles, root, undefined, binding);
|
|
272
349
|
const patches = [...composed.patches];
|
|
273
350
|
const rows = composed.rows;
|
|
274
351
|
// Never collide with the live instance on its configured port: 0 asks the
|
|
@@ -282,12 +359,17 @@ export async function runPreflight(profile, patchFiles = [], root = resolveHarne
|
|
|
282
359
|
});
|
|
283
360
|
}
|
|
284
361
|
const rootConfig = join(composed.profileDir, PROFILE_ROOT_FILENAME);
|
|
362
|
+
const appReady = createAppReadyStub();
|
|
285
363
|
// Cloned for the same insert-aliasing reason the launcher documents: boot
|
|
286
364
|
// application mutates rows by reference.
|
|
287
365
|
const ctx = await boot(NAME, rootConfig, structuredClone(patches), (hostCtx) => {
|
|
288
366
|
hostCtx.provide?.(launchEnvironmentKey, environment);
|
|
289
|
-
provideCmdline(hostCtx, { args: [], exit: () => { } });
|
|
367
|
+
provideCmdline(hostCtx, { args: [], exit: () => { }, ready: appReady.service });
|
|
290
368
|
});
|
|
369
|
+
// The launcher commits readiness once boot and host setup settle; a
|
|
370
|
+
// dry-run's host setup is the no-op cmdline above, so boot settling is
|
|
371
|
+
// that point.
|
|
372
|
+
appReady.commit();
|
|
291
373
|
const missing = missingClientArtifacts(ctx);
|
|
292
374
|
// A repeated dispose returns the settled single-shot result when boot
|
|
293
375
|
// already tore the tree down, so this is safe on every path.
|
|
@@ -310,10 +392,12 @@ export async function runPreflight(profile, patchFiles = [], root = resolveHarne
|
|
|
310
392
|
return 1;
|
|
311
393
|
}
|
|
312
394
|
}
|
|
313
|
-
/** Minimal argv parse
|
|
395
|
+
/** Minimal argv parse; the host execution surface is mandatory and explicit. */
|
|
314
396
|
export function parsePreflightArgs(argv) {
|
|
315
397
|
const patchFiles = [];
|
|
316
398
|
let profile = '';
|
|
399
|
+
let surface = '';
|
|
400
|
+
let installAnchor = '';
|
|
317
401
|
for (let i = 0; i < argv.length; i++) {
|
|
318
402
|
const arg = argv[i];
|
|
319
403
|
if (arg === '--profile') {
|
|
@@ -324,20 +408,31 @@ export function parsePreflightArgs(argv) {
|
|
|
324
408
|
if (file !== undefined)
|
|
325
409
|
patchFiles.push(file);
|
|
326
410
|
}
|
|
411
|
+
else if (arg === '--host-surface') {
|
|
412
|
+
surface = argv[++i] ?? '';
|
|
413
|
+
}
|
|
414
|
+
else if (arg === '--install-anchor') {
|
|
415
|
+
installAnchor = argv[++i] ?? '';
|
|
416
|
+
}
|
|
327
417
|
else if (arg === '--help' || arg === '-h') {
|
|
328
|
-
return { profile: '', patchFiles: [], error: 'usage: preflight-runner --profile <name> [--patch FILE]...' };
|
|
418
|
+
return { profile: '', patchFiles: [], error: 'usage: preflight-runner --profile <name> --host-surface source|built --install-anchor FILE [--patch FILE]...' };
|
|
329
419
|
}
|
|
330
420
|
}
|
|
331
421
|
if (profile === '')
|
|
332
422
|
return { profile: '', patchFiles: [], error: 'preflight requires --profile <name>' };
|
|
333
|
-
|
|
423
|
+
if (surface !== 'source' && surface !== 'built') {
|
|
424
|
+
return { profile, patchFiles, error: 'preflight requires --host-surface source|built' };
|
|
425
|
+
}
|
|
426
|
+
if (installAnchor === '')
|
|
427
|
+
return { profile, patchFiles, error: 'preflight requires --install-anchor FILE' };
|
|
428
|
+
return { profile, patchFiles, binding: { surface, installAnchor } };
|
|
334
429
|
}
|
|
335
430
|
// Standalone entry: only when executed directly (not imported by the CLI).
|
|
336
431
|
if (isDirectInvocation(import.meta.url)) {
|
|
337
|
-
const { profile, patchFiles, error } = parsePreflightArgs(process.argv.slice(2));
|
|
338
|
-
if (error !== undefined) {
|
|
432
|
+
const { profile, patchFiles, binding, error } = parsePreflightArgs(process.argv.slice(2));
|
|
433
|
+
if (error !== undefined || binding === undefined) {
|
|
339
434
|
process.stderr.write(`${error}\n`);
|
|
340
435
|
process.exit(2);
|
|
341
436
|
}
|
|
342
|
-
process.exitCode = await runPreflight(profile, patchFiles);
|
|
437
|
+
process.exitCode = await runPreflight(profile, patchFiles, resolveHarnessRoot(), binding);
|
|
343
438
|
}
|
package/lib/types/processes.d.ts
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
|
+
/** A PID plus the kernel-visible process start instant used to reject PID reuse. */
|
|
2
|
+
export interface ProcessIdentity {
|
|
3
|
+
pid: number;
|
|
4
|
+
startToken: string;
|
|
5
|
+
}
|
|
6
|
+
/** The old supervisor's exact child root and the listener inside that tree. */
|
|
7
|
+
export interface OwnedListener {
|
|
8
|
+
child: ProcessIdentity;
|
|
9
|
+
listener: ProcessIdentity;
|
|
10
|
+
}
|
|
11
|
+
/** Every process listening on a TCP port. An empty list also covers unavailable lsof. */
|
|
12
|
+
export declare function findPidsOnPort(port: number): number[];
|
|
13
|
+
/** TCP listen ports owned directly by one PID, using the same absolute lsof resolution. */
|
|
14
|
+
export declare function listeningPortsForPid(pid: number): number[];
|
|
1
15
|
/** The first process listening on a TCP port, or null when none is (via lsof). */
|
|
2
16
|
export declare function findPidOnPort(port: number): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Current identity for a live process, or null when it cannot be proved.
|
|
19
|
+
* Linux exposes a boot-scoped kernel start tick. Other POSIX hosts do not,
|
|
20
|
+
* so bind the start instant to the boot, uid, session, and settled command.
|
|
21
|
+
* This materially strengthens macOS ps(1)'s second-granularity lstart; all
|
|
22
|
+
* guard captures occur after the watchdog/child has reached its steady argv.
|
|
23
|
+
*/
|
|
24
|
+
export declare function processIdentity(pid: number): ProcessIdentity | null;
|
|
25
|
+
/** Whether the same, non-recycled process is still alive. */
|
|
26
|
+
export declare function processIdentityMatches(identity: ProcessIdentity): boolean;
|
|
27
|
+
/** The live POSIX process-group id for one PID, or null when unavailable. */
|
|
28
|
+
export declare function processGroupId(pid: number): number | null;
|
|
29
|
+
/** Whether candidate is root itself or a live descendant of root. */
|
|
30
|
+
export declare function pidBelongsToTree(root: number, candidate: number): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Prove that exactly one port listener belongs to a supervisor and return the
|
|
33
|
+
* direct child root through which that supervisor owns it. This is captured
|
|
34
|
+
* before cutover; the successor must stop this identity, never an arbitrary
|
|
35
|
+
* process discovered later from the shared port.
|
|
36
|
+
*/
|
|
37
|
+
export declare function findOwnedListener(port: number, supervisorPid: number): OwnedListener | null;
|
|
3
38
|
/**
|
|
4
39
|
* Discover how the process on a port was launched — its exact argv from
|
|
5
40
|
* `ps -o command=`, its cwd from lsof, and its DSH_* environment from
|
|
@@ -12,12 +47,9 @@ export declare function findPidOnPort(port: number): string | null;
|
|
|
12
47
|
*/
|
|
13
48
|
export declare function discoverLaunchCommand(pid: string): string | null;
|
|
14
49
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* orphan them (the EADDRINUSE race the watchdog's EADDRINUSE branch exists
|
|
18
|
-
* for). The process-group model is NOT assumed — the instance is not
|
|
19
|
-
* setsid'd — so the sweep walks `pgrep -P` instead. `pgrep` missing or
|
|
20
|
-
* returning nothing is fine: the pid itself still gets the signal.
|
|
50
|
+
* Freeze, then revalidate, then signal an authorized process identity. A
|
|
51
|
+
* mismatch resumes the PID without delivering the requested signal.
|
|
21
52
|
*/
|
|
53
|
+
export declare function signalProcessIdentity(identity: ProcessIdentity, signal: NodeJS.Signals): 'signalled' | 'gone' | 'mismatch';
|
|
22
54
|
export declare function killPidTree(pid: number, signal: NodeJS.Signals): void;
|
|
23
55
|
//# sourceMappingURL=processes.d.ts.map
|