@ran-sh/dsh-crew 1.1.0 → 1.2.0-rc.4
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/.claude-plugin/marketplace.json +16 -17
- package/.claude-plugin/plugin.json +7 -1
- package/README.md +44 -15
- package/README.zh.md +37 -12
- package/agents/ds-flash.md +16 -18
- package/agents/ds-pro.md +16 -24
- package/agents/ds-reviewer.md +15 -15
- package/agents/ds-worker.md +16 -16
- package/bin/dsh-crew.mjs +16 -12
- package/codex/AGENTS.md +37 -85
- package/codex/agents/ds-flash.toml +18 -14
- package/codex/agents/ds-pro.toml +18 -15
- package/codex/agents/ds-reviewer.toml +17 -13
- package/codex/agents/ds-worker.toml +17 -13
- package/codex/prompts/dsh-config.md +17 -1
- package/codex/prompts/dsh-status.md +5 -1
- package/commands/config.md +16 -4
- package/commands/status.md +6 -2
- package/docs/installation.md +54 -9
- package/docs/readiness-matrix.md +14 -6
- package/docs/ui-surfaces.md +32 -17
- package/lib/client.js +5434 -4844
- package/official-web-bridge/lib/client.js +567 -479
- package/official-web-bridge/overlay-entry.mjs +59 -0
- package/package.json +6 -3
- package/scripts/build-client.mjs +2 -0
- package/scripts/setup.mjs +8 -7
- package/scripts/verify-crew-ui-polish.mjs +145 -0
- package/scripts/verify-history-ui.mjs +97 -0
- package/scripts/verify-npm-install.mjs +1 -1
- package/src/client/history-panel.tsx +108 -0
- package/src/client/host-readiness.mjs +15 -6
- package/src/client/index.tsx +86 -64
- package/src/client/model-callability-view.mjs +17 -0
- package/src/client/panel-chrome.tsx +40 -0
- package/src/client/quick-panel.tsx +88 -73
- package/src/client/readiness-envelope.mjs +34 -0
- package/src/config-readiness.mjs +181 -115
- package/src/extension-contract.mjs +44 -26
- package/src/history/admission-gate.mjs +51 -0
- package/src/history/archive-store.mjs +261 -0
- package/src/history/cleanup-plan.mjs +64 -0
- package/src/history/http.mjs +32 -0
- package/src/history/operation.mjs +86 -0
- package/src/history/runner.mjs +51 -0
- package/src/history/runtime.mjs +28 -0
- package/src/history/service.mjs +137 -0
- package/src/history/state.mjs +27 -0
- package/src/hub/index.mjs +141 -24
- package/src/hub-client.mjs +17 -15
- package/src/image-output.mjs +71 -0
- package/src/install/install-legacy.mjs +126 -23
- package/src/install/npx-lifecycle.mjs +373 -64
- package/src/install/official-frontend-assets.mjs +78 -0
- package/src/install/payload-content.mjs +88 -0
- package/src/install/windows-startup.mjs +107 -19
- package/src/install/windows-supervisor-adapter.mjs +443 -0
- package/src/install/windows-supervisor-lifecycle.mjs +782 -0
- package/src/install/zcode.mjs +53 -14
- package/src/job-contracts.mjs +37 -31
- package/src/jobs.mjs +2 -2
- package/src/mcp-runtime.mjs +74 -9
- package/src/model-callability-contract.mjs +79 -0
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +0 -2
- package/src/provider-health.mjs +130 -124
- package/src/provider-profile-store.mjs +12 -0
- package/src/provider-settings-store.mjs +347 -5
- package/src/readiness-matrix.mjs +20 -15
- package/src/runtime-identity-contract.mjs +34 -0
- package/src/runtime-identity.mjs +14 -4
- package/src/runtime-readiness-snapshot.mjs +285 -87
- package/src/server.mjs +86 -76
- package/src/standalone-sdk.mjs +23 -0
- package/src/supervisor/restart-request.mjs +64 -10
- package/src/workflow-runtime.mjs +28 -13
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +327 -55
- package/windows/supervisor-control.ps1 +467 -0
- package/zcode/AGENTS.md +4 -1
- package/zcode/agents/ds-reviewer.md +31 -36
- package/zcode/agents/ds-worker.md +31 -39
- package/zcode/commands/dsh-config.md +17 -6
- package/zcode/commands/dsh-status.md +5 -4
- package/.mcp.json +0 -8
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
function plan(home, root) {
|
|
8
|
+
const bridge = join(root, 'official-web-bridge');
|
|
9
|
+
const metadata = JSON.parse(readFileSync(join(bridge, 'package.json'), 'utf8'));
|
|
10
|
+
if (metadata.name !== '@ran-sh/dsh-crew-web-bridge') throw new Error('invalid frontend package');
|
|
11
|
+
const files = new Map([
|
|
12
|
+
['official-web-bridge/entry.mjs', readFileSync(join(bridge, 'overlay-entry.mjs'))],
|
|
13
|
+
['official-web-bridge/lib/client.js', readFileSync(join(bridge, 'lib', 'client.js'))],
|
|
14
|
+
['src/local-request-guard.mjs', readFileSync(join(root, 'src', 'local-request-guard.mjs'))],
|
|
15
|
+
]);
|
|
16
|
+
const hash = createHash('sha256').update(JSON.stringify(metadata));
|
|
17
|
+
for (const [name, bytes] of files) hash.update(name).update(bytes);
|
|
18
|
+
const revision = hash.digest('hex');
|
|
19
|
+
files.set('official-web-bridge/package.json', Buffer.from(JSON.stringify({ ...metadata,
|
|
20
|
+
dshCrewManagedFrontend: true, dshCrewFrontendRevision: revision,
|
|
21
|
+
dsh: { ...metadata.dsh, bundle: undefined },
|
|
22
|
+
}, null, 2) + '\n'));
|
|
23
|
+
const frontendRoot = join(home, '.config', 'dsh-crew', 'frontend');
|
|
24
|
+
const snapshotRoot = join(frontendRoot, 'revisions', revision);
|
|
25
|
+
const overlayFile = join(frontendRoot, 'official-web.patch.json');
|
|
26
|
+
const overlay = JSON.stringify([{ insert: [{ id: 'dsh-crew-official-web-bridge',
|
|
27
|
+
name: pathToFileURL(join(snapshotRoot, 'official-web-bridge', 'entry.mjs')).href,
|
|
28
|
+
}] }], null, 2) + '\n';
|
|
29
|
+
return { frontendRoot, snapshotRoot, revision, overlayFile, overlay, files };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matches(p) {
|
|
33
|
+
try { return [...p.files].every(([name, bytes]) => {
|
|
34
|
+
const file = join(p.snapshotRoot, name);
|
|
35
|
+
return lstatSync(file).isFile() && !lstatSync(file).isSymbolicLink() && readFileSync(file).equals(bytes);
|
|
36
|
+
}); } catch { return false; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function officialFrontendAssetsReady({ home = homedir(), root } = {}) {
|
|
40
|
+
try {
|
|
41
|
+
const p = plan(home, root);
|
|
42
|
+
return matches(p) && readFileSync(p.overlayFile, 'utf8') === p.overlay;
|
|
43
|
+
} catch { return false; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function installOfficialFrontendAssets({ home = homedir(), root } = {}) {
|
|
47
|
+
let stage;
|
|
48
|
+
let temp;
|
|
49
|
+
try {
|
|
50
|
+
const p = plan(home, root);
|
|
51
|
+
for (const path of [p.frontendRoot, join(p.frontendRoot, 'revisions'), p.snapshotRoot, p.overlayFile]) {
|
|
52
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink()) throw new Error('linked frontend destination');
|
|
53
|
+
}
|
|
54
|
+
const already = officialFrontendAssetsReady({ home, root });
|
|
55
|
+
if (already) return { ok: true, changed: false, revision: p.revision, snapshotRoot: p.snapshotRoot, overlayFile: p.overlayFile };
|
|
56
|
+
mkdirSync(join(p.frontendRoot, 'revisions'), { recursive: true });
|
|
57
|
+
if (!existsSync(p.snapshotRoot)) {
|
|
58
|
+
stage = mkdtempSync(join(p.frontendRoot, '.stage-'));
|
|
59
|
+
for (const [name, bytes] of p.files) {
|
|
60
|
+
const file = join(stage, name);
|
|
61
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
62
|
+
writeFileSync(file, bytes);
|
|
63
|
+
}
|
|
64
|
+
try { renameSync(stage, p.snapshotRoot); stage = null; }
|
|
65
|
+
catch (error) { if (!matches(p)) throw error; }
|
|
66
|
+
}
|
|
67
|
+
if (!matches(p)) throw new Error('frontend snapshot conflicts with its revision');
|
|
68
|
+
temp = `${p.overlayFile}.${randomUUID()}.tmp`;
|
|
69
|
+
writeFileSync(temp, p.overlay, { flag: 'wx', mode: 0o600 });
|
|
70
|
+
renameSync(temp, p.overlayFile); temp = null;
|
|
71
|
+
return { ok: true, changed: true, revision: p.revision, snapshotRoot: p.snapshotRoot, overlayFile: p.overlayFile };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { ok: false, code: 'OFFICIAL_FRONTEND_ASSETS_FAILED', error: error.message };
|
|
74
|
+
} finally {
|
|
75
|
+
if (stage) rmSync(stage, { recursive: true, force: true });
|
|
76
|
+
if (temp) rmSync(temp, { force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, posix } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function canonical(value) {
|
|
6
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
7
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function runtimeManifest(manifest) {
|
|
12
|
+
const normalized = { ...manifest, dependencies: { ...manifest.peerDependencies, ...manifest.dependencies } };
|
|
13
|
+
delete normalized.devDependencies;
|
|
14
|
+
delete normalized.peerDependencies;
|
|
15
|
+
delete normalized.peerDependenciesMeta;
|
|
16
|
+
return canonical(normalized);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Capture bounded first-party package bytes once for both validation and copying. */
|
|
20
|
+
export function capturePayloadContent(root, suppliedManifest) {
|
|
21
|
+
try {
|
|
22
|
+
const manifest = suppliedManifest ?? JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
23
|
+
if (!Array.isArray(manifest.files)) return null;
|
|
24
|
+
const files = new Map();
|
|
25
|
+
const modes = new Map();
|
|
26
|
+
const directories = new Set();
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
const visit = relative => {
|
|
29
|
+
let parent = root;
|
|
30
|
+
for (const segment of posix.dirname(relative).split('/').filter(part => part && part !== '.')) {
|
|
31
|
+
parent = join(parent, segment);
|
|
32
|
+
const info = lstatSync(parent);
|
|
33
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('unsupported payload parent');
|
|
34
|
+
}
|
|
35
|
+
const file = join(root, relative);
|
|
36
|
+
const stat = lstatSync(file);
|
|
37
|
+
if (stat.isSymbolicLink()) throw new Error('unsupported payload symlink');
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
if (directories.size >= 4096) throw new Error('payload directory limit exceeded');
|
|
40
|
+
directories.add(relative);
|
|
41
|
+
for (const name of readdirSync(file)) visit(posix.join(relative, name));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!stat.isFile() || files.size >= 4096) throw new Error('unsupported payload entry');
|
|
45
|
+
if (files.has(relative)) return;
|
|
46
|
+
bytes += stat.size;
|
|
47
|
+
if (bytes > 64 * 1024 * 1024) throw new Error('payload content limit exceeded');
|
|
48
|
+
const content = relative === 'package.json'
|
|
49
|
+
? Buffer.from(JSON.stringify(runtimeManifest(manifest))) : readFileSync(file);
|
|
50
|
+
if (content.length > stat.size && relative !== 'package.json') throw new Error('payload changed while being read');
|
|
51
|
+
files.set(relative, content);
|
|
52
|
+
modes.set(relative, stat.mode & 0o777);
|
|
53
|
+
};
|
|
54
|
+
visit('package.json');
|
|
55
|
+
for (const entry of manifest.files ?? []) {
|
|
56
|
+
if (typeof entry !== 'string') throw new Error('invalid payload pattern');
|
|
57
|
+
const pattern = entry.replaceAll('\\', '/');
|
|
58
|
+
const segments = pattern.split('/');
|
|
59
|
+
if (!pattern.trim() || entry.includes('\\') || posix.normalize(pattern) === '.'
|
|
60
|
+
|| isAbsolute(pattern) || pattern.includes(':')
|
|
61
|
+
|| segments.some(segment => ['..', '.git', 'node_modules'].includes(segment.toLowerCase()))) throw new Error('invalid payload path');
|
|
62
|
+
if (!pattern.includes('*')) {
|
|
63
|
+
if (existsSync(join(root, pattern))) visit(pattern);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const base = posix.dirname(pattern);
|
|
67
|
+
if (base.includes('*')) throw new Error('unsupported payload wildcard');
|
|
68
|
+
const expression = new RegExp('^' + posix.basename(pattern).split('*').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*') + '$');
|
|
69
|
+
if (existsSync(join(root, base))) {
|
|
70
|
+
for (const name of readdirSync(join(root, base))) if (expression.test(name)) visit(posix.join(base, name));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { files, directories, modes };
|
|
74
|
+
} catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** This digest covers first-party shipped files, not dependency tamper attestation. */
|
|
78
|
+
export function payloadContentDigest(root) {
|
|
79
|
+
const snapshot = capturePayloadContent(root);
|
|
80
|
+
if (!snapshot) return null;
|
|
81
|
+
const hashes = [...snapshot.files].map(([name, bytes]) => [name, createHash('sha256').update(bytes).digest('hex')]);
|
|
82
|
+
return createHash('sha256').update(JSON.stringify(hashes.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))).digest('hex');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function samePayloadContent(sourceRoot, installedRoot) {
|
|
86
|
+
const source = payloadContentDigest(sourceRoot);
|
|
87
|
+
return source !== null && source === payloadContentDigest(installedRoot);
|
|
88
|
+
}
|
|
@@ -3,15 +3,20 @@ import {
|
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readFileSync,
|
|
6
|
+
renameSync,
|
|
6
7
|
rmSync,
|
|
7
8
|
writeFileSync,
|
|
8
9
|
} from 'node:fs';
|
|
9
|
-
import {
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
12
|
import { homedir } from 'node:os';
|
|
13
|
+
import { installOfficialFrontendAssets, officialFrontendAssetsReady } from './official-frontend-assets.mjs';
|
|
11
14
|
|
|
12
15
|
export const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
|
|
13
16
|
export const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
|
|
14
17
|
export const WINDOWS_HELPER_FILENAME = 'start-dsh-crew.ps1';
|
|
18
|
+
export const WINDOWS_CONTROL_FILENAME = 'supervisor-control.ps1';
|
|
19
|
+
export const WINDOWS_SUPERVISOR_ASSET_MANIFEST = 'supervisor-assets.json';
|
|
15
20
|
|
|
16
21
|
function defaultStartupDir({ home, env }) {
|
|
17
22
|
if (home === homedir() && env.APPDATA) {
|
|
@@ -23,10 +28,14 @@ function defaultStartupDir({ home, env }) {
|
|
|
23
28
|
function paths({ home, startupDir, env }) {
|
|
24
29
|
const launcherFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_LAUNCHER_FILENAME);
|
|
25
30
|
const helperFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_HELPER_FILENAME);
|
|
31
|
+
const controlFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_CONTROL_FILENAME);
|
|
32
|
+
const supervisorManifestFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_SUPERVISOR_ASSET_MANIFEST);
|
|
26
33
|
const resolvedStartupDir = startupDir ?? defaultStartupDir({ home, env });
|
|
27
34
|
return {
|
|
28
35
|
launcherFile,
|
|
29
36
|
helperFile,
|
|
37
|
+
controlFile,
|
|
38
|
+
supervisorManifestFile,
|
|
30
39
|
startupFile: join(resolvedStartupDir, WINDOWS_STARTUP_FILENAME),
|
|
31
40
|
};
|
|
32
41
|
}
|
|
@@ -50,6 +59,14 @@ function isManagedStartupFile(file, resolved) {
|
|
|
50
59
|
const text = readFileSync(file, 'utf8');
|
|
51
60
|
return /DSH Crew managed Windows launcher/i.test(text) || /DSHCrewServiceSupervisor/i.test(text);
|
|
52
61
|
}
|
|
62
|
+
if (file === resolved.controlFile) {
|
|
63
|
+
const text = readFileSync(file, 'utf8');
|
|
64
|
+
return /DSH Crew Windows supervisor process control/i.test(text) && /Stop-ExactManagedWatcher/i.test(text);
|
|
65
|
+
}
|
|
66
|
+
if (file === resolved.supervisorManifestFile) {
|
|
67
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
68
|
+
return parsed?.schema_version === 1 && parsed.managed_by === 'dsh-crew';
|
|
69
|
+
}
|
|
53
70
|
} catch {
|
|
54
71
|
return false;
|
|
55
72
|
}
|
|
@@ -57,12 +74,13 @@ function isManagedStartupFile(file, resolved) {
|
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
function startupTargetConflicts(resolved) {
|
|
60
|
-
return [resolved.startupFile, resolved.launcherFile, resolved.helperFile]
|
|
77
|
+
return [resolved.startupFile, resolved.launcherFile, resolved.helperFile, resolved.controlFile, resolved.supervisorManifestFile]
|
|
61
78
|
.filter((file) => existsSync(file) && !isManagedStartupFile(file, resolved));
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
export function windowsStartupStatus({
|
|
65
82
|
home = homedir(),
|
|
83
|
+
root,
|
|
66
84
|
startupDir,
|
|
67
85
|
platform = process.platform,
|
|
68
86
|
env = process.env,
|
|
@@ -71,23 +89,72 @@ export function windowsStartupStatus({
|
|
|
71
89
|
const resolved = paths({ home, startupDir, env });
|
|
72
90
|
const installed = existsSync(resolved.startupFile)
|
|
73
91
|
|| existsSync(resolved.launcherFile)
|
|
74
|
-
|| existsSync(resolved.helperFile)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
|| existsSync(resolved.helperFile)
|
|
93
|
+
|| existsSync(resolved.controlFile)
|
|
94
|
+
|| existsSync(resolved.supervisorManifestFile);
|
|
95
|
+
const components = {
|
|
96
|
+
startup_entry_content: false,
|
|
97
|
+
launcher_content: false,
|
|
98
|
+
helper_content: false,
|
|
99
|
+
control_content: false,
|
|
100
|
+
supervisor_manifest: false,
|
|
101
|
+
};
|
|
102
|
+
if (typeof root === 'string' && root.trim()) {
|
|
79
103
|
try {
|
|
104
|
+
const sourceLauncher = readFileSync(join(root, 'windows', WINDOWS_LAUNCHER_FILENAME), 'utf8');
|
|
105
|
+
const sourceHelper = readFileSync(join(root, 'windows', WINDOWS_HELPER_FILENAME), 'utf8');
|
|
106
|
+
if (sourceHelper.includes('Get-OfficialFrontendOverlay')) components.frontend_overlay = officialFrontendAssetsReady({ home, root });
|
|
107
|
+
const sourceControl = readFileSync(join(root, 'windows', WINDOWS_CONTROL_FILENAME), 'utf8');
|
|
108
|
+
const sourceVbs = readFileSync(join(root, 'windows', 'start-dsh-crew.vbs'), 'utf8');
|
|
80
109
|
const startup = readFileSync(resolved.startupFile, 'utf16le').replace(/^\uFEFF/, '');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
&&
|
|
88
|
-
|
|
110
|
+
components.startup_entry_content = startup === renderVbs(sourceVbs, resolved.launcherFile);
|
|
111
|
+
components.launcher_content = readFileSync(resolved.launcherFile, 'utf8') === sourceLauncher;
|
|
112
|
+
components.helper_content = readFileSync(resolved.helperFile, 'utf8') === sourceHelper;
|
|
113
|
+
components.control_content = readFileSync(resolved.controlFile, 'utf8') === sourceControl;
|
|
114
|
+
const assets = readWindowsSupervisorAssets({ home });
|
|
115
|
+
components.supervisor_manifest = assets.ok
|
|
116
|
+
&& assets.helper_hash === sha256File(join(root, 'windows', WINDOWS_HELPER_FILENAME))
|
|
117
|
+
&& assets.control_hash === sha256File(join(root, 'windows', WINDOWS_CONTROL_FILENAME));
|
|
118
|
+
} catch { /* missing or unreadable content remains false */ }
|
|
119
|
+
}
|
|
120
|
+
const missing = Object.entries(components).filter(([, ready]) => !ready).map(([key]) => key);
|
|
121
|
+
return { supported: true, installed, ready: missing.length === 0, components, missing, ...resolved };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sha256File(file) {
|
|
125
|
+
try { return createHash('sha256').update(readFileSync(file)).digest('hex'); } catch { return null; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function samePath(left, right) {
|
|
129
|
+
try { return resolve(left).toLowerCase() === resolve(right).toLowerCase(); } catch { return false; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeAtomic(file, content) {
|
|
133
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
134
|
+
writeFileSync(temp, content, { encoding: 'utf8', flag: 'wx' });
|
|
135
|
+
try { renameSync(temp, file); } finally { try { rmSync(temp, { force: true }); } catch {} }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readWindowsSupervisorAssets({ home = homedir() } = {}) {
|
|
139
|
+
const resolved = paths({ home, env: process.env });
|
|
140
|
+
try {
|
|
141
|
+
const manifest = JSON.parse(readFileSync(resolved.supervisorManifestFile, 'utf8'));
|
|
142
|
+
const helperHash = sha256File(resolved.helperFile);
|
|
143
|
+
const controlHash = sha256File(resolved.controlFile);
|
|
144
|
+
const valid = manifest?.schema_version === 1
|
|
145
|
+
&& manifest.managed_by === 'dsh-crew'
|
|
146
|
+
&& samePath(manifest.helper_path, resolved.helperFile)
|
|
147
|
+
&& samePath(manifest.control_path, resolved.controlFile)
|
|
148
|
+
&& /^[a-f0-9]{64}$/u.test(manifest.helper_hash ?? '')
|
|
149
|
+
&& /^[a-f0-9]{64}$/u.test(manifest.control_hash ?? '')
|
|
150
|
+
&& manifest.helper_hash === helperHash
|
|
151
|
+
&& manifest.control_hash === controlHash;
|
|
152
|
+
return valid
|
|
153
|
+
? { ok: true, helper_path: resolved.helperFile, helper_hash: helperHash, control_path: resolved.controlFile, control_hash: controlHash }
|
|
154
|
+
: { ok: false, code: 'SUPERVISOR_ASSET_MANIFEST_MISMATCH' };
|
|
155
|
+
} catch {
|
|
156
|
+
return { ok: false, code: 'SUPERVISOR_ASSET_MANIFEST_UNAVAILABLE' };
|
|
89
157
|
}
|
|
90
|
-
return { supported: true, installed, ready, ...resolved };
|
|
91
158
|
}
|
|
92
159
|
|
|
93
160
|
export function installWindowsStartup({
|
|
@@ -101,8 +168,9 @@ export function installWindowsStartup({
|
|
|
101
168
|
if (!root) return { ok: false, supported: true, code: 'STARTUP_SOURCE_REQUIRED' };
|
|
102
169
|
const sourceLauncher = join(root, 'windows', WINDOWS_LAUNCHER_FILENAME);
|
|
103
170
|
const sourceHelper = join(root, 'windows', WINDOWS_HELPER_FILENAME);
|
|
171
|
+
const sourceControl = join(root, 'windows', WINDOWS_CONTROL_FILENAME);
|
|
104
172
|
const sourceVbs = join(root, 'windows', 'start-dsh-crew.vbs');
|
|
105
|
-
if (!existsSync(sourceLauncher) || !existsSync(sourceHelper) || !existsSync(sourceVbs)) {
|
|
173
|
+
if (!existsSync(sourceLauncher) || !existsSync(sourceHelper) || !existsSync(sourceControl) || !existsSync(sourceVbs)) {
|
|
106
174
|
return { ok: false, supported: true, code: 'STARTUP_ASSET_MISSING' };
|
|
107
175
|
}
|
|
108
176
|
const resolved = paths({ home, startupDir, env });
|
|
@@ -110,17 +178,37 @@ export function installWindowsStartup({
|
|
|
110
178
|
if (conflicts.length > 0) {
|
|
111
179
|
return { ok: false, supported: true, code: 'STARTUP_TARGET_COLLISION', conflicts, ...resolved };
|
|
112
180
|
}
|
|
181
|
+
const needsFrontend = readFileSync(sourceHelper, 'utf8').includes('Get-OfficialFrontendOverlay');
|
|
182
|
+
const frontend = needsFrontend ? installOfficialFrontendAssets({ home, root }) : null;
|
|
183
|
+
if (frontend && !frontend.ok) return { ...frontend, supported: true };
|
|
113
184
|
mkdirSync(dirname(resolved.launcherFile), { recursive: true });
|
|
114
185
|
mkdirSync(dirname(resolved.startupFile), { recursive: true });
|
|
115
186
|
const beforeLauncher = existsSync(resolved.launcherFile) ? readFileSync(resolved.launcherFile) : null;
|
|
116
187
|
const beforeHelper = existsSync(resolved.helperFile) ? readFileSync(resolved.helperFile) : null;
|
|
188
|
+
const beforeControl = existsSync(resolved.controlFile) ? readFileSync(resolved.controlFile) : null;
|
|
189
|
+
const beforeManifest = existsSync(resolved.supervisorManifestFile) ? readFileSync(resolved.supervisorManifestFile) : null;
|
|
117
190
|
const beforeStartup = existsSync(resolved.startupFile) ? readFileSync(resolved.startupFile) : null;
|
|
118
191
|
copyFileSync(sourceLauncher, resolved.launcherFile);
|
|
119
192
|
copyFileSync(sourceHelper, resolved.helperFile);
|
|
193
|
+
copyFileSync(sourceControl, resolved.controlFile);
|
|
194
|
+
const supervisorManifest = {
|
|
195
|
+
schema_version: 1,
|
|
196
|
+
managed_by: 'dsh-crew',
|
|
197
|
+
helper_path: resolved.helperFile,
|
|
198
|
+
helper_hash: sha256File(resolved.helperFile),
|
|
199
|
+
control_path: resolved.controlFile,
|
|
200
|
+
control_hash: sha256File(resolved.controlFile),
|
|
201
|
+
};
|
|
202
|
+
writeAtomic(resolved.supervisorManifestFile, `${JSON.stringify(supervisorManifest, null, 2)}\n`);
|
|
203
|
+
if (!readWindowsSupervisorAssets({ home }).ok) {
|
|
204
|
+
return { ok: false, supported: true, code: 'SUPERVISOR_ASSET_MANIFEST_VERIFY_FAILED', ...resolved };
|
|
205
|
+
}
|
|
120
206
|
const rendered = renderVbs(readFileSync(sourceVbs, 'utf8'), resolved.launcherFile);
|
|
121
207
|
writeFileSync(resolved.startupFile, `\uFEFF${rendered}`, 'utf16le');
|
|
122
|
-
const changed = !beforeLauncher?.equals(readFileSync(resolved.launcherFile))
|
|
208
|
+
const changed = frontend?.changed === true || !beforeLauncher?.equals(readFileSync(resolved.launcherFile))
|
|
123
209
|
|| !beforeHelper?.equals(readFileSync(resolved.helperFile))
|
|
210
|
+
|| !beforeControl?.equals(readFileSync(resolved.controlFile))
|
|
211
|
+
|| !beforeManifest?.equals(readFileSync(resolved.supervisorManifestFile))
|
|
124
212
|
|| !beforeStartup?.equals(readFileSync(resolved.startupFile));
|
|
125
213
|
return { ok: true, supported: true, changed, ...resolved };
|
|
126
214
|
}
|
|
@@ -135,7 +223,7 @@ export function uninstallWindowsStartup({
|
|
|
135
223
|
const resolved = paths({ home, startupDir, env });
|
|
136
224
|
let removed = false;
|
|
137
225
|
const preserved = [];
|
|
138
|
-
for (const file of [resolved.startupFile, resolved.launcherFile, resolved.helperFile]) {
|
|
226
|
+
for (const file of [resolved.startupFile, resolved.launcherFile, resolved.helperFile, resolved.controlFile, resolved.supervisorManifestFile]) {
|
|
139
227
|
if (!existsSync(file)) continue;
|
|
140
228
|
if (!isManagedStartupFile(file, resolved)) {
|
|
141
229
|
preserved.push(file);
|