create-spatius-app 0.1.0-beta.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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/cli.js +9046 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/templates.js +2856 -0
  6. package/dist/templates.js.map +1 -0
  7. package/package.json +79 -0
  8. package/schemas/result-v1.schema.json +129 -0
  9. package/schemas/result-v2.schema.json +146 -0
  10. package/templates/cloudflare-livekit/.editorconfig +15 -0
  11. package/templates/cloudflare-livekit/.prettierignore +6 -0
  12. package/templates/cloudflare-livekit/.prettierrc.json +4 -0
  13. package/templates/cloudflare-livekit/AGENTS.md +41 -0
  14. package/templates/cloudflare-livekit/README.md +37 -0
  15. package/templates/cloudflare-livekit/agent/Dockerfile.pip +31 -0
  16. package/templates/cloudflare-livekit/agent/Dockerfile.uv +29 -0
  17. package/templates/cloudflare-livekit/agent/README.md +84 -0
  18. package/templates/cloudflare-livekit/agent/dockerignore +11 -0
  19. package/templates/cloudflare-livekit/agent/env.example +8 -0
  20. package/templates/cloudflare-livekit/agent/pyproject.toml +21 -0
  21. package/templates/cloudflare-livekit/agent/python-version +1 -0
  22. package/templates/cloudflare-livekit/agent/src/__init__.py +1 -0
  23. package/templates/cloudflare-livekit/agent/src/agent.py +97 -0
  24. package/templates/cloudflare-livekit/agent/src/dispatch_metadata.py +64 -0
  25. package/templates/cloudflare-livekit/agent/tests/test_agent_startup.py +211 -0
  26. package/templates/cloudflare-livekit/agent/tests/test_dispatch_metadata.py +97 -0
  27. package/templates/cloudflare-livekit/agent/uv.lock +2272 -0
  28. package/templates/cloudflare-livekit/components.json +23 -0
  29. package/templates/cloudflare-livekit/contracts/agent-dispatch-metadata.schema.json +40 -0
  30. package/templates/cloudflare-livekit/dev.vars.example +12 -0
  31. package/templates/cloudflare-livekit/eslint.config.mjs +41 -0
  32. package/templates/cloudflare-livekit/gitignore +30 -0
  33. package/templates/cloudflare-livekit/index.html +17 -0
  34. package/templates/cloudflare-livekit/npmrc +4 -0
  35. package/templates/cloudflare-livekit/package-lock.json +6514 -0
  36. package/templates/cloudflare-livekit/package.json +66 -0
  37. package/templates/cloudflare-livekit/playwright.config.ts +29 -0
  38. package/templates/cloudflare-livekit/pnpm-lock.yaml +4012 -0
  39. package/templates/cloudflare-livekit/pnpm-workspace.yaml +8 -0
  40. package/templates/cloudflare-livekit/scripts/agent-runtime.mjs +129 -0
  41. package/templates/cloudflare-livekit/scripts/command.mjs +52 -0
  42. package/templates/cloudflare-livekit/scripts/dev-agent.mjs +77 -0
  43. package/templates/cloudflare-livekit/scripts/dev-web.mjs +18 -0
  44. package/templates/cloudflare-livekit/scripts/managed-process.mjs +144 -0
  45. package/templates/cloudflare-livekit/scripts/process-tree.mjs +111 -0
  46. package/templates/cloudflare-livekit/scripts/run-agent-python.mjs +25 -0
  47. package/templates/cloudflare-livekit/tests/dev-agent.test.mjs +327 -0
  48. package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +235 -0
  49. package/templates/cloudflare-livekit/tests/dev-supervision.test.mjs +203 -0
  50. package/templates/cloudflare-livekit/tests/e2e/conversation.spec.ts +376 -0
  51. package/templates/cloudflare-livekit/tests/e2e/fake-avatar.ts +24 -0
  52. package/templates/cloudflare-livekit/tests/e2e/fake-components.tsx +173 -0
  53. package/templates/cloudflare-livekit/tests/e2e/fake-livekit.ts +123 -0
  54. package/templates/cloudflare-livekit/tests/e2e/harness.html +12 -0
  55. package/templates/cloudflare-livekit/tests/e2e/harness.tsx +1 -0
  56. package/templates/cloudflare-livekit/tests/e2e/vite.config.ts +48 -0
  57. package/templates/cloudflare-livekit/tests/web/api.test.ts +64 -0
  58. package/templates/cloudflare-livekit/tests/web/app.test.tsx +125 -0
  59. package/templates/cloudflare-livekit/tests/web/audio-visualizer.test.tsx +53 -0
  60. package/templates/cloudflare-livekit/tests/web/avatar-session.test.ts +119 -0
  61. package/templates/cloudflare-livekit/tests/web/components.test.tsx +152 -0
  62. package/templates/cloudflare-livekit/tests/web/conversation.test.tsx +76 -0
  63. package/templates/cloudflare-livekit/tests/web/session-attempt.test.ts +178 -0
  64. package/templates/cloudflare-livekit/tsconfig.json +9 -0
  65. package/templates/cloudflare-livekit/tsconfig.node.json +23 -0
  66. package/templates/cloudflare-livekit/tsconfig.web.json +26 -0
  67. package/templates/cloudflare-livekit/tsconfig.worker.json +22 -0
  68. package/templates/cloudflare-livekit/vite.config.ts +49 -0
  69. package/templates/cloudflare-livekit/vitest.config.ts +11 -0
  70. package/templates/cloudflare-livekit/web/App.tsx +176 -0
  71. package/templates/cloudflare-livekit/web/api.ts +69 -0
  72. package/templates/cloudflare-livekit/web/assets/FONT-PROVENANCE.md +8 -0
  73. package/templates/cloudflare-livekit/web/assets/OFL.txt +92 -0
  74. package/templates/cloudflare-livekit/web/assets/inter-latin.woff2 +0 -0
  75. package/templates/cloudflare-livekit/web/avatar-session.ts +67 -0
  76. package/templates/cloudflare-livekit/web/components/agents-ui/LICENSE +201 -0
  77. package/templates/cloudflare-livekit/web/components/agents-ui/PROVENANCE.md +32 -0
  78. package/templates/cloudflare-livekit/web/components/agents-ui/agent-audio-visualizer-bar.tsx +43 -0
  79. package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-input.tsx +78 -0
  80. package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-transcript.tsx +145 -0
  81. package/templates/cloudflare-livekit/web/components/agents-ui/agent-disconnect-button.tsx +27 -0
  82. package/templates/cloudflare-livekit/web/components/agents-ui/agent-session-provider.tsx +29 -0
  83. package/templates/cloudflare-livekit/web/components/agents-ui/agent-track-toggle.tsx +51 -0
  84. package/templates/cloudflare-livekit/web/components/agents-ui/start-audio-button.tsx +13 -0
  85. package/templates/cloudflare-livekit/web/components/conversation.tsx +352 -0
  86. package/templates/cloudflare-livekit/web/components/icons.tsx +39 -0
  87. package/templates/cloudflare-livekit/web/components/transcript-panel.tsx +12 -0
  88. package/templates/cloudflare-livekit/web/main.tsx +17 -0
  89. package/templates/cloudflare-livekit/web/session-attempt.ts +110 -0
  90. package/templates/cloudflare-livekit/web/styles.css +571 -0
  91. package/templates/cloudflare-livekit/worker/dispatch-metadata.ts +22 -0
  92. package/templates/cloudflare-livekit/worker/index.test.ts +152 -0
  93. package/templates/cloudflare-livekit/worker/index.ts +61 -0
  94. package/templates/cloudflare-livekit/worker/session.ts +103 -0
  95. package/templates/cloudflare-livekit/worker-configuration.d.ts +17184 -0
  96. package/templates/cloudflare-livekit/wrangler.jsonc +27 -0
@@ -0,0 +1,8 @@
1
+ allowBuilds:
2
+ core-js: true
3
+ esbuild: true
4
+ workerd: true
5
+ autoInstallPeers: false
6
+ preferFrozenLockfile: true
7
+ saveExact: true
8
+ strictPeerDependencies: true
@@ -0,0 +1,129 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { delimiter, join, resolve } from 'node:path';
3
+ import { parseEnv } from 'node:util';
4
+ import { resolveCommand } from './command.mjs';
5
+
6
+ export const projectDirectory = resolve(import.meta.dirname, '..');
7
+ export const agentDirectory = join(projectDirectory, 'agent');
8
+ export const usesUv = existsSync(join(agentDirectory, 'uv.lock'));
9
+ const virtualEnvironment = join(agentDirectory, '.venv');
10
+ const virtualBin = join(
11
+ virtualEnvironment,
12
+ process.platform === 'win32' ? 'Scripts' : 'bin',
13
+ );
14
+ export const python = join(
15
+ virtualBin,
16
+ process.platform === 'win32' ? 'python.exe' : 'python',
17
+ );
18
+
19
+ export function pythonEnvironment(environment = process.env) {
20
+ // lk's pip launcher resolves Python through PATH. Pin both launch paths to
21
+ // this project's environment, even when another environment is activated.
22
+ const env = { ...environment };
23
+ const pathKey =
24
+ Object.keys(env).find((key) => key.toUpperCase() === 'PATH') ?? 'PATH';
25
+ let inheritedPath = env[pathKey] ?? '';
26
+ if (process.platform === 'win32') {
27
+ // lk checks python3 before python and only recognizes POSIX venv paths.
28
+ // Exclude other python3 installations so its pip path finds our python.exe.
29
+ inheritedPath = inheritedPath
30
+ .split(delimiter)
31
+ .filter(
32
+ (directory) =>
33
+ !['.exe', '.com', '.cmd', '.bat'].some((suffix) =>
34
+ existsSync(join(directory, `python3${suffix}`)),
35
+ ),
36
+ )
37
+ .join(delimiter);
38
+ }
39
+ env[pathKey] = `${virtualBin}${delimiter}${inheritedPath}`;
40
+ env.VIRTUAL_ENV = virtualEnvironment;
41
+ env.UV_PROJECT_ENVIRONMENT = virtualEnvironment;
42
+ env.UV_PYTHON = python;
43
+ env.UV_NO_SYNC = '1';
44
+ env.UV_PYTHON_DOWNLOADS = 'never';
45
+ env.UV_OFFLINE = '1';
46
+ env.UV_NO_ENV_FILE = '1';
47
+ return env;
48
+ }
49
+
50
+ export function requirePythonEnvironment() {
51
+ if (!existsSync(python)) {
52
+ throw new Error(
53
+ 'Python dependencies are not installed in agent/.venv. Follow the Python installation instructions in README.md, then try again.',
54
+ );
55
+ }
56
+ }
57
+
58
+ export function pythonCommand(arguments_) {
59
+ return usesUv
60
+ ? {
61
+ command: resolveCommand('uv'),
62
+ arguments_: [
63
+ 'run',
64
+ '--no-sync',
65
+ '--directory',
66
+ agentDirectory,
67
+ '--extra',
68
+ 'dev',
69
+ 'python',
70
+ ...arguments_,
71
+ ],
72
+ }
73
+ : { command: python, arguments_ };
74
+ }
75
+
76
+ export function devEnvironment() {
77
+ let local = {};
78
+ try {
79
+ local = parseEnv(readFileSync(join(agentDirectory, '.env.local'), 'utf8'));
80
+ } catch (error) {
81
+ if (error.code !== 'ENOENT') {
82
+ throw new Error(
83
+ 'Could not read agent/.env.local. Check its contents and permissions.',
84
+ { cause: error },
85
+ );
86
+ }
87
+ }
88
+
89
+ // Explicit inherited values, including empty values, take precedence.
90
+ const env = { ...local, ...process.env };
91
+ const required = [
92
+ 'LIVEKIT_URL',
93
+ 'LIVEKIT_API_KEY',
94
+ 'LIVEKIT_API_SECRET',
95
+ 'SPATIUS_API_KEY',
96
+ 'SPATIUS_APP_ID',
97
+ ];
98
+ const missing = required.filter((key) => !env[key]?.trim());
99
+ if (missing.length) {
100
+ throw new Error(
101
+ `Missing agent development credentials: ${missing.join(', ')}. Set them in agent/.env.local or the environment. See README.md for credential setup.`,
102
+ );
103
+ }
104
+ const placeholders = required.filter((key) =>
105
+ /^(?:your[-_]|<)|your-project\.livekit\.cloud/i.test(env[key].trim()),
106
+ );
107
+ if (placeholders.length) {
108
+ throw new Error(
109
+ `Replace example values for ${placeholders.join(', ')} in agent/.env.local or the environment. See README.md for credential setup.`,
110
+ );
111
+ }
112
+ let validUrl = false;
113
+ try {
114
+ const url = new URL(env.LIVEKIT_URL);
115
+ validUrl =
116
+ ['https:', 'wss:'].includes(url.protocol) &&
117
+ Boolean(url.hostname) &&
118
+ !url.username &&
119
+ !url.password;
120
+ } catch {
121
+ /* Report only the variable name, never the supplied value. */
122
+ }
123
+ if (!validUrl) {
124
+ throw new Error(
125
+ 'LIVEKIT_URL must be a valid https:// or wss:// server URL without embedded credentials. Set it in agent/.env.local or the environment. See README.md for credential setup.',
126
+ );
127
+ }
128
+ return pythonEnvironment(env);
129
+ }
@@ -0,0 +1,52 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { delimiter, extname, isAbsolute, join } from 'node:path';
3
+
4
+ // Node cannot execute Windows .cmd/.bat launchers directly. Resolve PATH first
5
+ // so a missing CLI stays a bounded spawn failure instead of a shell diagnostic.
6
+ export function resolveCommand(command, env = process.env) {
7
+ if (process.platform !== 'win32') return command;
8
+ const value = (key) =>
9
+ env[Object.keys(env).find((entry) => entry.toUpperCase() === key)] ?? '';
10
+ const suffixes = extname(command)
11
+ ? ['']
12
+ : (value('PATHEXT') || '.COM;.EXE;.BAT;.CMD').split(';');
13
+ const directories = isAbsolute(command)
14
+ ? ['']
15
+ : value('PATH').split(delimiter);
16
+ let executable;
17
+ for (const directory of directories) {
18
+ for (const suffix of suffixes) {
19
+ const candidate = join(directory, `${command}${suffix}`);
20
+ if (existsSync(candidate)) {
21
+ executable = candidate;
22
+ break;
23
+ }
24
+ }
25
+ if (executable) break;
26
+ }
27
+ return executable ?? command;
28
+ }
29
+
30
+ export function commandSpec(command, arguments_, env = process.env) {
31
+ const executable = resolveCommand(command, env);
32
+ if (process.platform !== 'win32' || !/\.(cmd|bat)$/i.test(executable)) {
33
+ return { command: executable, arguments_ };
34
+ }
35
+
36
+ const escapeMeta = (text) => text.replace(/([()[\]%!^"`<>&|;, *?])/g, '^$1');
37
+ const quoteArgument = (argument) => {
38
+ const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
39
+ const escaped = escapeMeta(quoted);
40
+ return /node_modules[\\/]\.bin[\\/]/i.test(executable)
41
+ ? escapeMeta(escaped)
42
+ : escaped;
43
+ };
44
+ const line = [escapeMeta(executable), ...arguments_.map(quoteArgument)].join(
45
+ ' ',
46
+ );
47
+ return {
48
+ command: env.ComSpec || env.COMSPEC || env.comspec || 'cmd.exe',
49
+ arguments_: ['/d', '/s', '/c', `"${line}"`],
50
+ windowsVerbatimArguments: true,
51
+ };
52
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ agentDirectory,
3
+ devEnvironment,
4
+ pythonCommand,
5
+ requirePythonEnvironment,
6
+ } from './agent-runtime.mjs';
7
+ import { runProcess } from './managed-process.mjs';
8
+ import { resolveCommand } from './command.mjs';
9
+
10
+ // v2.18.2 first passes dev credentials to Python through env rather than argv.
11
+ // https://github.com/livekit/livekit-cli/releases/tag/v2.18.2
12
+ function compatibleVersion(output) {
13
+ const version =
14
+ /^lk version v?(\d+)\.(\d+)\.(\d+)(?:\+[^\s]+)?(?:\s|$)/m.exec(
15
+ output.trim(),
16
+ );
17
+ if (!version) return false;
18
+ const [major, minor, patch] = version.slice(1).map(Number);
19
+ return (
20
+ major > 2 || (major === 2 && (minor > 18 || (minor === 18 && patch >= 2)))
21
+ );
22
+ }
23
+
24
+ async function main() {
25
+ // Resolve before restricting Windows Python lookup: lk/uv may share a PATH
26
+ // directory with an unrelated global python3 installation.
27
+ const liveKitCommand = resolveCommand('lk');
28
+ const env = devEnvironment();
29
+ requirePythonEnvironment();
30
+ // Neither compatibility probe needs credentials, including inherited ones.
31
+ const probeEnv = Object.fromEntries(
32
+ Object.entries(env).filter(([key]) => !/^(LIVEKIT_|SPATIUS_)/i.test(key)),
33
+ );
34
+ const probeOptions = { cwd: agentDirectory, env: probeEnv, probe: true };
35
+ const version = await runProcess(liveKitCommand, ['--version'], probeOptions);
36
+ if (version.interrupted) return version.code;
37
+ let useLiveKit =
38
+ !version.failed && version.code === 0 && compatibleVersion(version.output);
39
+ if (useLiveKit) {
40
+ const help = await runProcess(
41
+ liveKitCommand,
42
+ ['agent', 'dev', '--help'],
43
+ probeOptions,
44
+ );
45
+ if (help.interrupted) return help.code;
46
+ useLiveKit =
47
+ !help.failed &&
48
+ help.code === 0 &&
49
+ /lk agent dev\b/.test(help.output) &&
50
+ /entrypoint/.test(help.output);
51
+ }
52
+
53
+ const { command, arguments_ } = useLiveKit
54
+ ? { command: liveKitCommand, arguments_: ['agent', 'dev', 'src/agent.py'] }
55
+ : pythonCommand(['-m', 'livekit.agents', 'start', 'src/agent.py', '--dev']);
56
+ if (!useLiveKit) {
57
+ console.error(
58
+ 'A compatible LiveKit CLI (>=2.18.2) was not found. Starting the selected Python runtime in dev mode without CLI auto-reload.',
59
+ );
60
+ }
61
+ const result = await runProcess(command, arguments_, {
62
+ cwd: agentDirectory,
63
+ env,
64
+ });
65
+ if (result.failed)
66
+ console.error(
67
+ 'Could not launch the selected agent runtime. Check its installation and try again.',
68
+ );
69
+ return result.code;
70
+ }
71
+
72
+ try {
73
+ process.exitCode = await main();
74
+ } catch (error) {
75
+ console.error(error.message);
76
+ process.exitCode = 1;
77
+ }
@@ -0,0 +1,18 @@
1
+ import { join } from 'node:path';
2
+ import { projectDirectory } from './agent-runtime.mjs';
3
+ import { runProcess } from './managed-process.mjs';
4
+
5
+ const result = await runProcess(
6
+ process.execPath,
7
+ [
8
+ join(projectDirectory, 'node_modules', 'vite', 'bin', 'vite.js'),
9
+ ...process.argv.slice(2),
10
+ ],
11
+ { cwd: projectDirectory, env: process.env },
12
+ );
13
+ if (result.failed) {
14
+ console.error(
15
+ 'Could not launch Vite. Install JavaScript dependencies and try again.',
16
+ );
17
+ }
18
+ process.exitCode = result.code;
@@ -0,0 +1,144 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { constants } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { commandSpec } from './command.mjs';
5
+ import { trackProcessTree } from './process-tree.mjs';
6
+
7
+ export function exitCode(code, signal) {
8
+ return code ?? (signal ? 128 + (constants.signals[signal] ?? 1) : 1);
9
+ }
10
+
11
+ // Runtime processes inherit output. Probes capture bounded output privately;
12
+ // never print spawn errors or captured probe data, which can contain secrets.
13
+ export function runProcess(
14
+ command,
15
+ arguments_,
16
+ { cwd, env, probe = false, timeout = 2000 } = {},
17
+ ) {
18
+ return new Promise((resolve) => {
19
+ let child;
20
+ try {
21
+ const spec = commandSpec(command, arguments_, env);
22
+ child = spawn(spec.command, spec.arguments_, {
23
+ cwd,
24
+ env,
25
+ stdio: probe ? ['ignore', 'pipe', 'pipe'] : 'inherit',
26
+ windowsHide: true,
27
+ detached: process.platform !== 'win32',
28
+ windowsVerbatimArguments: spec.windowsVerbatimArguments,
29
+ });
30
+ } catch {
31
+ resolve({ code: 1, output: '', failed: true, interrupted: false });
32
+ return;
33
+ }
34
+ let output = '';
35
+ let interrupted;
36
+ let failed = false;
37
+ let forceTimer;
38
+ let timeoutTimer;
39
+ let stopRequest;
40
+ const treeKills = [];
41
+ const tree = child.pid ? trackProcessTree(child.pid) : undefined;
42
+
43
+ const kill = (signal) => {
44
+ if (!child.pid) return;
45
+ try {
46
+ if (process.platform === 'win32') {
47
+ const killer = spawn(
48
+ join(
49
+ process.env.SystemRoot ?? 'C:\\Windows',
50
+ 'System32',
51
+ 'taskkill.exe',
52
+ ),
53
+ ['/pid', String(child.pid), '/T', '/F'],
54
+ { windowsHide: true, stdio: 'ignore' },
55
+ );
56
+ treeKills.push(
57
+ new Promise((done) => {
58
+ killer.on('error', () => {
59
+ child.kill();
60
+ });
61
+ killer.once('close', done);
62
+ }),
63
+ );
64
+ } else process.kill(-child.pid, signal);
65
+ } catch (error) {
66
+ if (error.code !== 'ESRCH') child.kill(signal);
67
+ }
68
+ };
69
+ const stop = (signal) => {
70
+ if (stopRequest) return;
71
+ stopRequest = (async () => {
72
+ // Capture and signal descendants before their parent can exit and
73
+ // reparent them. Repeated signals from npm/concurrently share teardown.
74
+ if (tree) await tree.kill(signal);
75
+ kill(signal);
76
+ if (child.exitCode === null && child.signalCode === null) {
77
+ forceTimer = setTimeout(() => {
78
+ if (tree) treeKills.push(tree.kill('SIGKILL'));
79
+ kill('SIGKILL');
80
+ }, 1500);
81
+ }
82
+ })();
83
+ treeKills.push(stopRequest);
84
+ };
85
+ const onInterrupt = () => {
86
+ interrupted ??= 'SIGINT';
87
+ stop('SIGINT');
88
+ };
89
+ const onTerminate = () => {
90
+ interrupted ??= 'SIGTERM';
91
+ stop('SIGTERM');
92
+ };
93
+ process.on('SIGINT', onInterrupt);
94
+ process.on('SIGTERM', onTerminate);
95
+
96
+ if (probe) {
97
+ const capture = (chunk) => {
98
+ if (failed) return;
99
+ if (Buffer.byteLength(output) + chunk.length > 64 * 1024) {
100
+ failed = true;
101
+ stop('SIGKILL');
102
+ return;
103
+ }
104
+ output += chunk.toString();
105
+ };
106
+ child.stdout.on('data', capture);
107
+ child.stderr.on('data', capture);
108
+ timeoutTimer = setTimeout(() => {
109
+ failed = true;
110
+ stop('SIGKILL');
111
+ }, timeout);
112
+ }
113
+ child.on('error', () => {
114
+ failed = true;
115
+ });
116
+ // 'exit' occurs before 'close': an orphan can keep inherited output pipes
117
+ // open, so waiting for 'close' to clean up would hang indefinitely.
118
+ child.once('exit', () => {
119
+ tree?.stop();
120
+ if (tree) treeKills.push(tree.kill('SIGKILL'));
121
+ if (process.platform !== 'win32') kill('SIGKILL');
122
+ });
123
+ child.once('close', async (code, signal) => {
124
+ clearTimeout(timeoutTimer);
125
+ clearTimeout(forceTimer);
126
+ process.off('SIGINT', onInterrupt);
127
+ process.off('SIGTERM', onTerminate);
128
+ tree?.stop();
129
+ // Reap remaining members of our process group when its leader exits.
130
+ if (process.platform !== 'win32') kill('SIGKILL');
131
+ await Promise.all(treeKills);
132
+ resolve({
133
+ code: interrupted
134
+ ? exitCode(null, interrupted)
135
+ : failed
136
+ ? 1
137
+ : exitCode(code, signal),
138
+ output,
139
+ failed,
140
+ interrupted: Boolean(interrupted),
141
+ });
142
+ });
143
+ });
144
+ }
@@ -0,0 +1,111 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+
4
+ // lk puts its Python subprocess in a separate process group. Remember the
5
+ // descendant identities while lk is alive so an unexpected leader exit cannot
6
+ // leave that group behind. ps reads only process IDs and start times, not argv.
7
+ export function trackProcessTree(rootPid) {
8
+ const known = new Map();
9
+ let pending;
10
+ let stopped = false;
11
+ const windows = process.platform === 'win32';
12
+
13
+ const snapshot = () => {
14
+ pending ??= new Promise((resolve) => {
15
+ execFile(
16
+ windows
17
+ ? join(
18
+ process.env.SystemRoot ?? 'C:\\Windows',
19
+ 'System32',
20
+ 'WindowsPowerShell',
21
+ 'v1.0',
22
+ 'powershell.exe',
23
+ )
24
+ : '/bin/ps',
25
+ windows
26
+ ? [
27
+ '-NoProfile',
28
+ '-NonInteractive',
29
+ '-Command',
30
+ 'Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | Where-Object { $_.CreationDate } | ForEach-Object { "{0} {1} {2}" -f $_.ProcessId,$_.ParentProcessId,$_.CreationDate.ToFileTimeUtc() }',
31
+ ]
32
+ : ['-A', '-o', 'pid=,ppid=,lstart='],
33
+ {
34
+ timeout: windows ? 3000 : 1000,
35
+ maxBuffer: 1024 * 1024,
36
+ windowsHide: true,
37
+ },
38
+ (error, stdout) => {
39
+ const rows = error
40
+ ? []
41
+ : stdout
42
+ .trim()
43
+ .split('\n')
44
+ .map((line) => {
45
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
46
+ return match
47
+ ? {
48
+ pid: Number(match[1]),
49
+ parent: Number(match[2]),
50
+ identity: match[3],
51
+ }
52
+ : null;
53
+ })
54
+ .filter(Boolean);
55
+ const descendants = new Set([rootPid]);
56
+ for (const row of rows) {
57
+ if (known.get(row.pid) === row.identity) descendants.add(row.pid);
58
+ }
59
+ let changed = true;
60
+ while (changed) {
61
+ changed = false;
62
+ for (const row of rows) {
63
+ if (descendants.has(row.parent) && !descendants.has(row.pid)) {
64
+ descendants.add(row.pid);
65
+ changed = true;
66
+ }
67
+ }
68
+ }
69
+ known.clear();
70
+ for (const row of rows) {
71
+ if (row.pid !== rootPid && descendants.has(row.pid))
72
+ known.set(row.pid, row.identity);
73
+ }
74
+ resolve([...known.keys()].reverse());
75
+ },
76
+ );
77
+ })
78
+ .catch(() => [])
79
+ .finally(() => {
80
+ pending = undefined;
81
+ });
82
+ return pending;
83
+ };
84
+
85
+ // Windows retains the creation-time ParentProcessId even after parent exit;
86
+ // one shutdown snapshot suffices. POSIX reparents orphans, requiring tracking.
87
+ const timer = windows
88
+ ? undefined
89
+ : setInterval(() => {
90
+ if (!stopped) void snapshot();
91
+ }, 100);
92
+ timer?.unref();
93
+ return {
94
+ async kill(signal) {
95
+ // A polling snapshot may predate a just-spawned grandchild. Refresh it
96
+ // before signalling its parent, while that ancestry is still observable.
97
+ if (pending) await pending;
98
+ for (const pid of await snapshot()) {
99
+ try {
100
+ process.kill(pid, signal);
101
+ } catch {
102
+ /* Already exited. */
103
+ }
104
+ }
105
+ },
106
+ stop() {
107
+ stopped = true;
108
+ clearInterval(timer);
109
+ },
110
+ };
111
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ agentDirectory,
3
+ pythonCommand,
4
+ pythonEnvironment,
5
+ requirePythonEnvironment,
6
+ } from './agent-runtime.mjs';
7
+ import { runProcess } from './managed-process.mjs';
8
+
9
+ try {
10
+ requirePythonEnvironment();
11
+ const { command, arguments_ } = pythonCommand(process.argv.slice(2));
12
+ const result = await runProcess(command, arguments_, {
13
+ cwd: agentDirectory,
14
+ env: pythonEnvironment(),
15
+ });
16
+ if (result.failed) {
17
+ console.error(
18
+ 'Could not launch the selected Python runtime. Follow the Python installation instructions in README.md.',
19
+ );
20
+ }
21
+ process.exitCode = result.code;
22
+ } catch (error) {
23
+ console.error(error.message);
24
+ process.exitCode = 1;
25
+ }