@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.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +16 -17
  2. package/.claude-plugin/plugin.json +7 -1
  3. package/README.md +44 -15
  4. package/README.zh.md +37 -12
  5. package/agents/ds-flash.md +16 -18
  6. package/agents/ds-pro.md +16 -24
  7. package/agents/ds-reviewer.md +15 -15
  8. package/agents/ds-worker.md +16 -16
  9. package/bin/dsh-crew.mjs +16 -12
  10. package/codex/AGENTS.md +37 -85
  11. package/codex/agents/ds-flash.toml +18 -14
  12. package/codex/agents/ds-pro.toml +18 -15
  13. package/codex/agents/ds-reviewer.toml +17 -13
  14. package/codex/agents/ds-worker.toml +17 -13
  15. package/codex/prompts/dsh-config.md +17 -1
  16. package/codex/prompts/dsh-status.md +5 -1
  17. package/commands/config.md +16 -4
  18. package/commands/status.md +6 -2
  19. package/docs/installation.md +54 -9
  20. package/docs/readiness-matrix.md +14 -6
  21. package/docs/ui-surfaces.md +32 -17
  22. package/lib/client.js +5434 -4844
  23. package/official-web-bridge/lib/client.js +567 -479
  24. package/official-web-bridge/overlay-entry.mjs +59 -0
  25. package/package.json +6 -3
  26. package/scripts/build-client.mjs +2 -0
  27. package/scripts/setup.mjs +8 -7
  28. package/scripts/verify-crew-ui-polish.mjs +145 -0
  29. package/scripts/verify-history-ui.mjs +97 -0
  30. package/scripts/verify-npm-install.mjs +1 -1
  31. package/src/client/history-panel.tsx +108 -0
  32. package/src/client/host-readiness.mjs +15 -6
  33. package/src/client/index.tsx +86 -64
  34. package/src/client/model-callability-view.mjs +17 -0
  35. package/src/client/panel-chrome.tsx +40 -0
  36. package/src/client/quick-panel.tsx +88 -73
  37. package/src/client/readiness-envelope.mjs +34 -0
  38. package/src/config-readiness.mjs +181 -115
  39. package/src/extension-contract.mjs +44 -26
  40. package/src/history/admission-gate.mjs +51 -0
  41. package/src/history/archive-store.mjs +261 -0
  42. package/src/history/cleanup-plan.mjs +64 -0
  43. package/src/history/http.mjs +32 -0
  44. package/src/history/operation.mjs +86 -0
  45. package/src/history/runner.mjs +51 -0
  46. package/src/history/runtime.mjs +28 -0
  47. package/src/history/service.mjs +137 -0
  48. package/src/history/state.mjs +27 -0
  49. package/src/hub/index.mjs +141 -24
  50. package/src/hub-client.mjs +17 -15
  51. package/src/image-output.mjs +71 -0
  52. package/src/install/install-legacy.mjs +126 -23
  53. package/src/install/npx-lifecycle.mjs +373 -64
  54. package/src/install/official-frontend-assets.mjs +78 -0
  55. package/src/install/payload-content.mjs +88 -0
  56. package/src/install/windows-startup.mjs +107 -19
  57. package/src/install/windows-supervisor-adapter.mjs +443 -0
  58. package/src/install/windows-supervisor-lifecycle.mjs +782 -0
  59. package/src/install/zcode.mjs +53 -14
  60. package/src/job-contracts.mjs +37 -31
  61. package/src/jobs.mjs +2 -2
  62. package/src/mcp-runtime.mjs +74 -9
  63. package/src/model-callability-contract.mjs +79 -0
  64. package/src/multimodal.mjs +0 -0
  65. package/src/official-web-bridge.mjs +0 -2
  66. package/src/provider-health.mjs +130 -124
  67. package/src/provider-profile-store.mjs +12 -0
  68. package/src/provider-settings-store.mjs +347 -5
  69. package/src/readiness-matrix.mjs +20 -15
  70. package/src/runtime-identity-contract.mjs +34 -0
  71. package/src/runtime-identity.mjs +14 -4
  72. package/src/runtime-readiness-snapshot.mjs +285 -87
  73. package/src/server.mjs +86 -76
  74. package/src/standalone-sdk.mjs +23 -0
  75. package/src/supervisor/restart-request.mjs +64 -10
  76. package/src/workflow-runtime.mjs +28 -13
  77. package/windows/start-dsh-crew.cmd +57 -57
  78. package/windows/start-dsh-crew.ps1 +327 -55
  79. package/windows/supervisor-control.ps1 +467 -0
  80. package/zcode/AGENTS.md +4 -1
  81. package/zcode/agents/ds-reviewer.md +31 -36
  82. package/zcode/agents/ds-worker.md +31 -39
  83. package/zcode/commands/dsh-config.md +17 -6
  84. package/zcode/commands/dsh-status.md +5 -4
  85. 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 { dirname, join } from 'node:path';
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
- let ready = existsSync(resolved.startupFile)
76
- && existsSync(resolved.launcherFile)
77
- && existsSync(resolved.helperFile);
78
- if (ready) {
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
- const launcher = readFileSync(resolved.launcherFile, 'utf8');
82
- const helper = readFileSync(resolved.helperFile, 'utf8');
83
- ready = startup.includes(resolved.launcherFile)
84
- && startup.includes('--watch')
85
- && launcher.includes(WINDOWS_HELPER_FILENAME)
86
- && helper.includes('DSH Crew managed Windows launcher')
87
- && helper.includes('DSHCrewServiceSupervisor');
88
- } catch { ready = false; }
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);