external-subagent 0.5.2 → 0.5.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 (44) hide show
  1. package/README.md +8 -5
  2. package/bin/external-subagent-mcp.mjs +3 -2
  3. package/cli/commands/agents.mjs +4 -9
  4. package/cli/commands/config.mjs +1 -1
  5. package/cli/commands/daemon.mjs +8 -3
  6. package/cli/commands/maintenance.mjs +158 -1
  7. package/cli/commands/plugin.mjs +28 -3
  8. package/cli/commands/tasks.mjs +7 -1
  9. package/cli/commands/update.mjs +18 -7
  10. package/cli/config/write.mjs +195 -14
  11. package/cli/constants.mjs +2 -2
  12. package/cli/install/codex.mjs +1 -1
  13. package/cli/install/dsh.mjs +613 -0
  14. package/cli/install/init.mjs +23 -33
  15. package/cli/install/layout.mjs +10 -2
  16. package/cli/install/payload.mjs +59 -12
  17. package/cli/install/service-activation.mjs +10 -2
  18. package/cli/install/service-linux.mjs +1 -1
  19. package/cli/install/service-macos.mjs +40 -14
  20. package/cli/install/service-windows.mjs +463 -0
  21. package/cli/install/update.mjs +6 -5
  22. package/cli/main.mjs +10 -14
  23. package/cli/maintenance.mjs +7 -1
  24. package/cli/paths.mjs +152 -8
  25. package/npm/native/README.md +2 -2
  26. package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
  27. package/npm/native/darwin-arm64/external-subagentd +0 -0
  28. package/npm/native/darwin-arm64/payload.json +5 -5
  29. package/npm/native/linux-x64/external-subagent-mcp +0 -0
  30. package/npm/native/linux-x64/external-subagentd +0 -0
  31. package/npm/native/linux-x64/payload.json +5 -5
  32. package/package.json +3 -2
  33. package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +5 -3
  34. package/schema/external-subagent-public-api.json +3 -3
  35. package/scripts/release/build-native-payload.mjs +22 -5
  36. package/scripts/release/check-native-tarball.mjs +34 -14
  37. package/scripts/release/stage-debug-plugin.mjs +9 -3
  38. package/scripts/release/test-installed-tarball.mjs +1 -1
  39. package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +0 -55
  40. package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +0 -19
  41. package/plugins/codex/external-subagent/hooks/guard-bash.mjs +0 -15
  42. package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +0 -206
  43. package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +0 -149
  44. package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +0 -58
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { spawnSync } from 'node:child_process';
4
3
  import { LAUNCH_AGENT_LABEL, NATIVE_DIR_NAME } from '../constants.mjs';
5
4
  import { CliError } from '../errors.mjs';
6
5
  import { platform } from '../paths.mjs';
@@ -13,7 +12,7 @@ import { updateInstallation } from './update.mjs';
13
12
  import { loadInstallState, markInstallStep, removeCreatedDirectories, rollbackFiles, snapshotFile } from './recovery.mjs';
14
13
  import { SYSTEMD_UNIT_NAME, bootstrapService, bootoutService, installServiceDefinition, runtimeObservations } from './service-macos.mjs';
15
14
 
16
- // Standalone-install coordination (S05, AUD-005 decision D1). A plain npm
15
+ // Standalone-install coordination (AUD-005 decision D1). A plain npm
17
16
  // install only stages the package and payload; an explicit `init` installs
18
17
  // the STANDALONE SERVICE ONLY — every state-changing step below is
19
18
  // host-neutral:
@@ -47,12 +46,6 @@ import { SYSTEMD_UNIT_NAME, bootstrapService, bootoutService, installServiceDefi
47
46
  // lets `init --resume` continue after environmental failures. Journals from
48
47
  // pre-D1 inits may still list the retired codex steps; unknown completed ids
49
48
  // are simply never consulted again.
50
- const HOOK_INSTALLER = ['plugins', 'codex', 'external-subagent', 'scripts', 'install-agent-hooks.mjs'];
51
-
52
- function hookInstallerPath() {
53
- return path.join(packageRoot(), ...HOOK_INSTALLER);
54
- }
55
-
56
49
  export function installPlan(paths, options = {}) {
57
50
  // The plan reports the payload the verifier will read. When the running
58
51
  // tuple has a supported payload this is the resolved manifest path; on an
@@ -70,22 +63,9 @@ export function installPlan(paths, options = {}) {
70
63
  { id: 'start-service', action: darwin ? 'bootstrap the daemon service' : 'enable and start the daemon service', path: paths.launchAgent, ...(darwin ? { label: LAUNCH_AGENT_LABEL } : { unit: SYSTEMD_UNIT_NAME }) },
71
64
  { id: 'publish-active-payload', action: 'publish the verified active payload and retained-byte baseline', path: paths.state },
72
65
  ];
73
- if (options.installHooks) plan.splice(plan.findIndex((step) => step.id === 'publish-active-payload'), 0, { id: 'install-hooks', action: 'install ZCode policy hooks', path: paths.zcodeConfig, provenance: paths.hookProvenance });
74
66
  return plan;
75
67
  }
76
68
 
77
- export function installHooks(paths, options = {}) {
78
- if (options.dryRun) return { dry_run: true, plan: [{ id: 'install-hooks', action: 'install ZCode policy hooks', path: paths.zcodeConfig, provenance: paths.hookProvenance }] };
79
- const result = spawnSync(process.execPath, [hookInstallerPath(), '--config', paths.zcodeConfig, '--provenance', paths.hookProvenance], { encoding: 'utf8' });
80
- if (result.error) throw result.error;
81
- if (result.status !== 0) {
82
- let failure;
83
- try { failure = JSON.parse((result.stderr || '').trim()); } catch { /* plain-text failure */ }
84
- throw new CliError(failure?.code || 'HOOK_INSTALL_FAILED', failure?.error || (result.stderr || 'hook installation failed').trim());
85
- }
86
- try { return JSON.parse(result.stdout); } catch { throw new CliError('HOOK_INSTALL_FAILED', 'hook installer returned invalid JSON'); }
87
- }
88
-
89
69
  export function runInit(options = {}) {
90
70
  const paths = options.paths;
91
71
  if (!paths) throw new CliError('INTERNAL_ERROR', 'runInit requires product paths');
@@ -104,12 +84,13 @@ export function runInit(options = {}) {
104
84
  const payload = skipPayload ? { status: 'skipped', platform: null, version: null, files: [] } : verifyPayload();
105
85
  const pathFindings = pathReport();
106
86
 
87
+ // Windows has no file-based service definition (paths.launchAgent is null),
88
+ // so there is nothing to snapshot or roll back there; the SCM registration
89
+ // is created by the start-service step instead.
107
90
  const tracked = {
108
- zcodeConfig: { file: paths.zcodeConfig, snapshot: snapshotFile(paths.zcodeConfig) },
109
- hookProvenance: { file: paths.hookProvenance, snapshot: snapshotFile(paths.hookProvenance) },
110
91
  state: { file: paths.state, snapshot: snapshotFile(paths.state) },
111
92
  config: { file: paths.config, snapshot: snapshotFile(paths.config) },
112
- launchAgent: { file: paths.launchAgent, snapshot: snapshotFile(paths.launchAgent) },
93
+ ...(paths.launchAgent ? { launchAgent: { file: paths.launchAgent, snapshot: snapshotFile(paths.launchAgent) } } : {}),
113
94
  };
114
95
  const directories = {
115
96
  data: { path: paths.data, existed: fs.existsSync(paths.data) },
@@ -130,6 +111,19 @@ export function runInit(options = {}) {
130
111
  // A service that was already loaded before this init is never touched.
131
112
  let serviceLoadedByThisRun = false;
132
113
  let baseline = null;
114
+ // The service backends are selected by process platform. Windows has no
115
+ // process.getuid(), and every backend treats an undefined uid as "not
116
+ // applicable", so resolve it lazily instead of crashing before dispatch.
117
+ const currentUid = () => (typeof process.getuid === 'function' ? process.getuid() : undefined);
118
+ // Forward the injectable control seams (launchctl/systemctl/sc) and the
119
+ // explicit daemon artifact so a test or a controlled activation can own the
120
+ // service definition without touching the real service manager.
121
+ const serviceOptions = () => ({
122
+ launchctl: options.launchctl,
123
+ systemctl: options.systemctl,
124
+ sc: options.sc,
125
+ daemonPath: options.daemonPath,
126
+ });
133
127
  try {
134
128
  if (!completed.has('verify-payload')) mark('verify-payload');
135
129
  if (!completed.has('check-path')) mark('check-path');
@@ -142,8 +136,8 @@ export function runInit(options = {}) {
142
136
  // The daemon takes its database/socket/runtime from service arguments,
143
137
  // never from this file; drop the retired top-level path fields so an
144
138
  // install over an older local config republishes the current schema.
145
- // S05-F01: `subagents.zcode.runtime_path` was a first-class config key
146
- // before D1 rejected it, so a pre-S05 install can carry it in either
139
+ // The runtime_path remediation: `subagents.zcode.runtime_path` was a first-class config key
140
+ // before D1 rejected it, so an install predating that rejection can carry it in either
147
141
  // the schema-2 or the legacy schema-1 shape. Retire it here — before
148
142
  // parseConfig migrates/validates — so `npm new -> init` self-heals such
149
143
  // configs instead of failing closed at the read gate (which stays
@@ -158,19 +152,15 @@ export function runInit(options = {}) {
158
152
  mark('write-product-config');
159
153
  }
160
154
  if (!completed.has('install-launch-agent')) {
161
- installServiceDefinition(paths);
155
+ installServiceDefinition(paths, serviceOptions());
162
156
  failAt('install-launch-agent');
163
157
  mark('install-launch-agent');
164
158
  }
165
159
  if (!completed.has('start-service') && !options.skipServiceStart) {
166
- service = bootstrapService(paths, process.getuid(), { launchctl: options.launchctl, systemctl: options.systemctl });
160
+ service = bootstrapService(paths, currentUid(), serviceOptions());
167
161
  serviceLoadedByThisRun = !service.skipped && !service.already_loaded;
168
162
  mark('start-service');
169
163
  }
170
- if (options.installHooks && !completed.has('install-hooks')) {
171
- installHooks(paths);
172
- mark('install-hooks');
173
- }
174
164
  // The baseline publication runs LAST and only for an init that verified
175
165
  // the payload it installed (a skipped probe — test harnesses only —
176
166
  // leaves the resume journal as the sole state). It reuses the locked
@@ -202,7 +192,7 @@ export function runInit(options = {}) {
202
192
  ...removeCreatedDirectories(directories),
203
193
  ];
204
194
  if (serviceLoadedByThisRun) {
205
- try { bootoutService(paths, process.getuid(), { launchctl: options.launchctl, systemctl: options.systemctl }); } catch (bootoutError) {
195
+ try { bootoutService(paths, currentUid(), serviceOptions()); } catch (bootoutError) {
206
196
  rollbackErrors.push(`service rollback (bootout) failed: ${bootoutError.message}`);
207
197
  }
208
198
  }
@@ -16,10 +16,18 @@ export const PLUGIN_NAME = PRODUCT_NAME;
16
16
  // payload.mjs all key off the same value. A tuple outside this table has no
17
17
  // payload and is rejected by the installer, never silently downgraded.
18
18
  // NATIVE_PLATFORM stays the reference platform (the released macOS identity).
19
- export const NATIVE_PLATFORMS = Object.freeze(['darwin-arm64', 'linux-x64']);
19
+ export const NATIVE_PLATFORMS = Object.freeze(['darwin-arm64', 'linux-x64', 'win32-x64']);
20
20
  export const NATIVE_PLATFORM = NATIVE_PLATFORMS[0];
21
21
  export const NATIVE_BINARIES = Object.freeze([DAEMON_BIN_NAME, MCP_BIN_NAME]);
22
22
 
23
+ // Payload artifacts are named with their host executable suffix. The staged
24
+ // Windows payload is the same product binaries with `.exe`; every consumer
25
+ // (verification, retention, service definition, MCP facade) resolves names
26
+ // through this one helper so a tuple never mixes stem and extension.
27
+ export function nativeExecutableName(name, platform = nativePlatform()) {
28
+ return typeof platform === 'string' && platform.startsWith('win32-') ? `${name}.exe` : name;
29
+ }
30
+
23
31
  export function packageRoot() {
24
32
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
25
33
  }
@@ -37,7 +45,7 @@ export function nativePayloadDir(platform = nativePlatform()) {
37
45
  export function nativeBinary(name, platform = nativePlatform()) {
38
46
  const dir = nativePayloadDir(platform);
39
47
  if (!dir || !NATIVE_BINARIES.includes(name)) return null;
40
- return path.join(dir, name);
48
+ return path.join(dir, nativeExecutableName(name, platform));
41
49
  }
42
50
 
43
51
  export function payloadManifestPath(platform = nativePlatform()) {
@@ -4,7 +4,7 @@ import { CliError } from '../errors.mjs';
4
4
  import { sha256 } from '../fs-atomic.mjs';
5
5
  import { NATIVE_DIR_NAME, PRODUCT_NAME } from '../constants.mjs';
6
6
  import {
7
- NATIVE_PLATFORMS, cliVersion, nativePayloadDir, nativePlatform, packageVersion, payloadManifestPath,
7
+ NATIVE_PLATFORMS, cliVersion, nativePayloadDir, nativePlatform, packageRoot, packageVersion,
8
8
  } from './layout.mjs';
9
9
 
10
10
  const MH_MAGIC_64 = 0xfeedfacf;
@@ -24,12 +24,37 @@ const EM_X86_64 = 0x3e;
24
24
  const ELF_E_MACHINE_OFFSET = 18;
25
25
  const ELF_HEADER_MIN_BYTES = ELF_E_MACHINE_OFFSET + 2;
26
26
 
27
+ // PE/COFF identity for the Windows payload:
28
+ // DOS header "MZ"
29
+ // e_lfanew uint32 LE at offset 0x3c (offset of the PE signature)
30
+ // PE signature "PE\0\0"
31
+ // Machine uint16 LE at peOffset + 4 (0x8664 = IMAGE_FILE_MACHINE_AMD64)
32
+ // Optional magic uint16 LE at peOffset + 24 (0x20b = PE32+)
33
+ // The image is a supported win32-x64 payload only when all four agree; any
34
+ // truncated header or other machine/format returns null.
35
+ const DOS_MAGIC = 0x5a4d; // "MZ"
36
+ const PE_SIGNATURE = 0x00004550; // "PE\0\0" as a LE uint32
37
+ const IMAGE_FILE_MACHINE_AMD64 = 0x8664;
38
+ const PE32_PLUS_MAGIC = 0x20b;
39
+ const PE_SIGNATURE_OFFSET = 0x3c;
40
+ const PE_HEADER_MIN_BYTES = PE_SIGNATURE_OFFSET + 4 + 20 + 2;
41
+
27
42
  export function machoArch(bytes) {
28
43
  if (bytes.length < 8) return null;
29
44
  if (bytes.readUInt32LE(0) !== MH_MAGIC_64) return null;
30
45
  return bytes.readUInt32LE(4) === CPU_TYPE_ARM64 ? 'arm64' : null;
31
46
  }
32
47
 
48
+ export function peArch(bytes) {
49
+ if (bytes.length < PE_HEADER_MIN_BYTES) return null;
50
+ if (bytes.readUInt16LE(0) !== DOS_MAGIC) return null;
51
+ const peOffset = bytes.readUInt32LE(PE_SIGNATURE_OFFSET);
52
+ if (peOffset + 4 + 20 + 2 > bytes.length) return null;
53
+ if (bytes.readUInt32LE(peOffset) !== PE_SIGNATURE) return null;
54
+ if (bytes.readUInt16LE(peOffset + 4) !== IMAGE_FILE_MACHINE_AMD64) return null;
55
+ return bytes.readUInt16LE(peOffset + 24) === PE32_PLUS_MAGIC ? 'x64' : null;
56
+ }
57
+
33
58
  // The Linux image check mirrors machoArch: only a 64-bit little-endian x86-64
34
59
  // ELF is a supported payload. A truncated header (shorter than e_machine), a
35
60
  // 32-bit or big-endian image, or any other machine returns null and is
@@ -47,12 +72,24 @@ export function elfArch(bytes) {
47
72
  const IMAGE_CHECKS = Object.freeze({
48
73
  'darwin-arm64': { arch: 'arm64', image: 'Mach-O arm64 image', probe: machoArch },
49
74
  'linux-x64': { arch: 'x64', image: 'ELF x86-64 image', probe: elfArch },
75
+ 'win32-x64': { arch: 'x64', image: 'PE32+ x86-64 image', probe: peArch },
50
76
  });
51
77
 
78
+ // Every released tuple carries a prebuilt payload, including win32-x64; the
79
+ // table is the verification identity set, not a distribution gate.
80
+ const PAYLOAD_PLATFORMS = Object.freeze([...NATIVE_PLATFORMS]);
81
+
82
+ function payloadDirectory(platform, root) {
83
+ if (root) return path.join(root, 'npm', NATIVE_DIR_NAME, platform);
84
+ return nativePayloadDir(platform)
85
+ ?? (PAYLOAD_PLATFORMS.includes(platform) ? path.join(packageRoot(), 'npm', NATIVE_DIR_NAME, platform) : null);
86
+ }
87
+
52
88
  export function readPayloadManifest(options = {}) {
53
89
  const platform = options.platform || nativePlatform();
54
90
  const root = options.root || undefined;
55
- const file = root ? path.join(root, 'npm', NATIVE_DIR_NAME, platform || '', 'payload.json') : payloadManifestPath(platform);
91
+ const dir = payloadDirectory(platform, root);
92
+ const file = dir === null ? null : path.join(dir, 'payload.json');
56
93
  if (file === null || !fs.existsSync(file)) {
57
94
  throw new CliError('PAYLOAD_MANIFEST_MISSING', `the ${platform || 'current'} platform payload manifest is missing from this package`);
58
95
  }
@@ -73,9 +110,15 @@ export function readPayloadManifest(options = {}) {
73
110
  // little-endian Mach-O arm64 image. A failed verification is an installation
74
111
  // defect, never a silent downgrade path.
75
112
  export function verifyPayload(options = {}) {
76
- const platform = options.platform || nativePlatform();
77
- if (!NATIVE_PLATFORMS.includes(platform)) {
78
- throw new CliError('UNSUPPORTED_PAYLOAD_PLATFORM', `no native payload exists for ${platform ?? `${process.platform}-${process.arch}`}; supported platforms: ${NATIVE_PLATFORMS.join(', ')}`);
113
+ const explicitPlatform = options.platform !== undefined;
114
+ const platform = explicitPlatform ? options.platform : nativePlatform();
115
+ if (!PAYLOAD_PLATFORMS.includes(platform)) {
116
+ // The default resolver (nativePlatform) only produces the release tuples;
117
+ // win32-x64 is a controlled artifact reachable through an explicit option
118
+ // until the NATIVE_PLATFORMS gate opens. The message must not claim the
119
+ // rejected host tuple is available by default.
120
+ const supported = explicitPlatform ? PAYLOAD_PLATFORMS : NATIVE_PLATFORMS;
121
+ throw new CliError('UNSUPPORTED_PAYLOAD_PLATFORM', `no native payload exists for ${platform ?? `${process.platform}-${process.arch}`}; supported platforms: ${supported.join(', ')}`);
79
122
  }
80
123
  const image = IMAGE_CHECKS[platform];
81
124
  const manifest = readPayloadManifest({ platform, root: options.root });
@@ -91,7 +134,8 @@ export function verifyPayload(options = {}) {
91
134
  if (versions.package !== versions.cli || versions.payload !== versions.cli) {
92
135
  throw new CliError('PAYLOAD_VERSION_MISMATCH', `payload ${versions.payload}, package ${versions.package}, and CLI ${versions.cli} versions disagree`);
93
136
  }
94
- const dir = options.root ? path.join(options.root, 'npm', NATIVE_DIR_NAME, platform) : nativePayloadDir(platform);
137
+ const dir = payloadDirectory(platform, options.root);
138
+ const posixMode = platform !== 'win32-x64' && process.platform !== 'win32';
95
139
  const files = manifest.files.map((record) => {
96
140
  if (!record || typeof record.name !== 'string' || path.basename(record.name) !== record.name || record.name.includes('..')) {
97
141
  throw new CliError('PAYLOAD_MANIFEST_INVALID', 'payload file name must be a direct child');
@@ -101,7 +145,10 @@ export function verifyPayload(options = {}) {
101
145
  try { stat = fs.statSync(target); } catch (error) {
102
146
  throw new CliError('PAYLOAD_FILE_MISSING', `native payload file is missing: ${record.name} (${error.code})`);
103
147
  }
104
- if (!stat.isFile() || fs.lstatSync(target).isSymbolicLink() || (stat.mode & 0o777) !== 0o755) {
148
+ // Windows has no POSIX execute bit: npm extraction cannot produce mode 755
149
+ // and the PE loader does not consult one, so the mode gate applies only to
150
+ // the POSIX payload identities (and only on a POSIX host).
151
+ if (!stat.isFile() || fs.lstatSync(target).isSymbolicLink() || (posixMode && (stat.mode & 0o777) !== 0o755)) {
105
152
  throw new CliError('PAYLOAD_PERMISSIONS_INVALID', `native payload file ${record.name} must be a regular file with mode 755`);
106
153
  }
107
154
  const bytes = fs.readFileSync(target);
@@ -112,7 +159,7 @@ export function verifyPayload(options = {}) {
112
159
  if (arch !== image.arch) {
113
160
  throw new CliError('PAYLOAD_ARCH_UNSUPPORTED', `native payload file ${record.name} is not a ${image.image}`);
114
161
  }
115
- return { name: record.name, bytes: bytes.length, mode: '755', sha256: record.sha256, arch };
162
+ return { name: record.name, bytes: bytes.length, mode: posixMode ? '755' : null, sha256: record.sha256, arch };
116
163
  });
117
164
  return { status: 'verified', platform, version: manifest.version, files };
118
165
  }
@@ -144,12 +191,12 @@ export function normalizePayloadMode(target) {
144
191
  // mode so the install step can report what it changed.
145
192
  export function normalizePayloadFiles(options = {}) {
146
193
  const platform = options.platform || nativePlatform();
147
- if (!NATIVE_PLATFORMS.includes(platform)) return [];
194
+ // Windows has no POSIX mode to repair; the extraction/permission repair is a
195
+ // POSIX-payload concern only.
196
+ if (!PAYLOAD_PLATFORMS.includes(platform) || platform === 'win32-x64') return [];
148
197
  let manifest;
149
198
  try { manifest = readPayloadManifest({ platform, root: options.root }); } catch { return []; }
150
- const dir = options.root
151
- ? path.join(options.root, 'npm', NATIVE_DIR_NAME, platform)
152
- : nativePayloadDir(platform);
199
+ const dir = payloadDirectory(platform, options.root);
153
200
  const normalized = [];
154
201
  for (const record of manifest.files) {
155
202
  if (!record || typeof record.name !== 'string' || path.basename(record.name) !== record.name || record.name.includes('..')) continue;
@@ -7,6 +7,7 @@ import { LAUNCH_AGENT_LABEL } from '../constants.mjs';
7
7
  import { platform } from '../paths.mjs';
8
8
  import { launchAgentPlist, launchctl } from './service-macos.mjs';
9
9
  import { SYSTEMD_UNIT_NAME, execStartProgram, replaceExecStartProgram, systemctl, systemdUnit, unitServiceStatus } from './service-linux.mjs';
10
+ import { activateServiceWindows } from './service-windows.mjs';
10
11
 
11
12
  const xml = (s) => s.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
12
13
  const unxml = (s) => s.replaceAll('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&amp;', '&');
@@ -17,10 +18,17 @@ export function hasInstalledService(paths) {
17
18
  return Boolean(paths.launchAgent && fs.existsSync(paths.launchAgent));
18
19
  }
19
20
 
20
- // Platform dispatch (S03): the same activation health/rollback contract runs
21
+ // Platform dispatch: the same activation health/rollback contract runs
21
22
  // over launchd on macOS (verbatim) and the systemd user manager on Linux.
22
23
  export async function activateService(paths, candidate, options = {}) {
23
- return platform() === 'darwin' ? activateLaunchdService(paths, candidate, options) : activateSystemdService(paths, candidate, options);
24
+ const currentPlatform = platform();
25
+ if (currentPlatform === 'darwin') {
26
+ return activateLaunchdService(paths, candidate, options);
27
+ }
28
+ if (currentPlatform === 'win32') {
29
+ return activateServiceWindows(paths, candidate, options);
30
+ }
31
+ return activateSystemdService(paths, candidate, options);
24
32
  }
25
33
 
26
34
  // The service definition is derived from the CURRENT config: activation
@@ -23,7 +23,7 @@ import { readConfig } from '../config/read.mjs';
23
23
  export const SYSTEMD_UNIT_NAME = `${PRODUCT_NAME}.service`;
24
24
  export const SYSTEMCTL_PATH = '/usr/bin/systemctl';
25
25
 
26
- // D-S04: a host without a reachable user manager gets an actionable
26
+ // A host without a reachable user manager gets an actionable
27
27
  // explanation, never an automatic `loginctl enable-linger` and never a detour
28
28
  // through the system manager.
29
29
  const NO_SESSION_HINT = 'log in through a real session (which provides $XDG_RUNTIME_DIR), or run \'loginctl enable-linger <user>\' so the user manager stays available without one; this product never enables linger, never modifies unrelated units, and never contacts the system (PID 1) manager';
@@ -12,6 +12,10 @@ import {
12
12
  SYSTEMD_UNIT_NAME, bootstrapServiceSystemd, bootoutServiceSystemd, installServiceUnit,
13
13
  systemdServiceRegistrationStatus,
14
14
  } from './service-linux.mjs';
15
+ import {
16
+ bootstrapServiceWindows, bootoutServiceWindows, installServiceDefinitionWindows,
17
+ windowsServiceRegistrationStatus,
18
+ } from './service-windows.mjs';
15
19
 
16
20
  // The macOS service backend for the product daemon, plus the platform
17
21
  // dispatch into the systemd user backend (service-linux.mjs). The plist pins
@@ -169,36 +173,58 @@ export function bootoutLaunchdService(paths, uid = process.getuid(), options = {
169
173
  }
170
174
  }
171
175
 
172
- // Platform dispatch (S03): the service backend is selected by the process
176
+ // Platform dispatch: the service backend is selected by the process
173
177
  // platform — launchd on macOS, the systemd user manager on Linux — behind the
174
178
  // same call surface every caller (init, start/stop, uninstall, status) already
175
179
  // uses. The macOS arms below are the verbatim launchd implementations; the
176
180
  // Linux arms carry the same idempotence/removal-confirmation contract through
177
181
  // `systemctl --user` (options.systemctl is the injectable seam, the twin of
178
182
  // options.launchctl).
179
- export function serviceRegistrationStatus(uid = process.getuid(), options = {}) {
180
- return platform() === 'darwin'
181
- ? launchdServiceRegistrationStatus(uid, options)
182
- : systemdServiceRegistrationStatus(options);
183
+ export function serviceRegistrationStatus(uid, options = {}) {
184
+ const currentPlatform = platform();
185
+ if (currentPlatform === 'darwin') {
186
+ return launchdServiceRegistrationStatus(uid ?? process.getuid(), options);
187
+ }
188
+ if (currentPlatform === 'win32') {
189
+ return windowsServiceRegistrationStatus(options);
190
+ }
191
+ return systemdServiceRegistrationStatus(options);
183
192
  }
184
193
 
185
- export function bootstrapService(paths, uid = process.getuid(), options = {}) {
186
- return platform() === 'darwin'
187
- ? bootstrapLaunchdService(paths, uid, options)
188
- : bootstrapServiceSystemd(paths, uid, options);
194
+ export function bootstrapService(paths, uid, options = {}) {
195
+ const currentPlatform = platform();
196
+ if (currentPlatform === 'darwin') {
197
+ return bootstrapLaunchdService(paths, uid ?? process.getuid(), options);
198
+ }
199
+ if (currentPlatform === 'win32') {
200
+ return bootstrapServiceWindows(paths, options);
201
+ }
202
+ return bootstrapServiceSystemd(paths, uid, options);
189
203
  }
190
204
 
191
- export function bootoutService(paths, uid = process.getuid(), options = {}) {
192
- return platform() === 'darwin'
193
- ? bootoutLaunchdService(paths, uid, options)
194
- : bootoutServiceSystemd(paths, uid, options);
205
+ export function bootoutService(paths, uid, options = {}) {
206
+ const currentPlatform = platform();
207
+ if (currentPlatform === 'darwin') {
208
+ return bootoutLaunchdService(paths, uid ?? process.getuid(), options);
209
+ }
210
+ if (currentPlatform === 'win32') {
211
+ return bootoutServiceWindows(paths, options);
212
+ }
213
+ return bootoutServiceSystemd(paths, uid, options);
195
214
  }
196
215
 
197
216
  // The service-definition installer behind init's install-launch-agent step:
198
217
  // the launchd plist on macOS, the systemd user unit (0600, absolute verified
199
218
  // payload, Restart=always) on Linux, both at paths.launchAgent.
200
219
  export function installServiceDefinition(paths, options = {}) {
201
- return platform() === 'darwin' ? installLaunchAgent(paths, options) : installServiceUnit(paths, options);
220
+ const currentPlatform = platform();
221
+ if (currentPlatform === 'darwin') {
222
+ return installLaunchAgent(paths, options);
223
+ }
224
+ if (currentPlatform === 'win32') {
225
+ return installServiceDefinitionWindows(paths, options);
226
+ }
227
+ return installServiceUnit(paths, options);
202
228
  }
203
229
 
204
230
  export { SYSTEMD_UNIT_NAME };