proteum 2.1.0-5 → 2.1.1

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 (56) hide show
  1. package/AGENTS.md +37 -49
  2. package/README.md +52 -1
  3. package/agents/framework/AGENTS.md +104 -236
  4. package/agents/project/AGENTS.md +36 -70
  5. package/cli/commands/command.ts +243 -0
  6. package/cli/commands/commandLocalRunner.js +198 -0
  7. package/cli/commands/dev.ts +95 -1
  8. package/cli/commands/doctor.ts +8 -74
  9. package/cli/commands/explain.ts +8 -194
  10. package/cli/commands/trace.ts +8 -0
  11. package/cli/compiler/artifacts/commands.ts +217 -0
  12. package/cli/compiler/artifacts/manifest.ts +17 -2
  13. package/cli/compiler/artifacts/services.ts +291 -0
  14. package/cli/compiler/client/index.ts +13 -0
  15. package/cli/compiler/common/commands.ts +175 -0
  16. package/cli/compiler/common/proteumManifest.ts +15 -124
  17. package/cli/compiler/index.ts +25 -2
  18. package/cli/compiler/server/index.ts +3 -0
  19. package/cli/presentation/commands.ts +37 -5
  20. package/cli/runtime/commands.ts +29 -1
  21. package/cli/tsconfig.json +4 -1
  22. package/cli/utils/check.ts +1 -1
  23. package/client/app/component.tsx +11 -0
  24. package/client/dev/profiler/index.tsx +1511 -0
  25. package/client/dev/profiler/noop.tsx +5 -0
  26. package/client/dev/profiler/runtime.noop.ts +116 -0
  27. package/client/dev/profiler/runtime.ts +840 -0
  28. package/client/services/router/components/router.tsx +30 -2
  29. package/client/services/router/index.tsx +25 -0
  30. package/client/services/router/request/api.ts +133 -17
  31. package/commands/proteum/diagnostics.ts +11 -0
  32. package/common/dev/commands.ts +50 -0
  33. package/common/dev/diagnostics.ts +298 -0
  34. package/common/dev/profiler.ts +91 -0
  35. package/common/dev/proteumManifest.ts +135 -0
  36. package/common/dev/requestTrace.ts +28 -1
  37. package/docs/dev-commands.md +86 -0
  38. package/docs/request-tracing.md +2 -0
  39. package/package.json +1 -2
  40. package/server/app/commands.ts +35 -370
  41. package/server/app/commandsManager.ts +393 -0
  42. package/server/app/container/console/index.ts +0 -2
  43. package/server/app/container/trace/index.ts +88 -8
  44. package/server/app/devCommands.ts +192 -0
  45. package/server/app/devDiagnostics.ts +53 -0
  46. package/server/app/index.ts +27 -4
  47. package/server/services/cron/CronTask.ts +73 -5
  48. package/server/services/cron/index.ts +34 -11
  49. package/server/services/fetch/index.ts +3 -10
  50. package/server/services/prisma/index.ts +1 -1
  51. package/server/services/router/http/index.ts +132 -21
  52. package/server/services/router/index.ts +40 -4
  53. package/server/services/router/request/api.ts +30 -1
  54. package/skills/clean-project-code/SKILL.md +7 -2
  55. package/test-results/.last-run.json +4 -0
  56. package/types/aliases.d.ts +6 -0
@@ -1,104 +1,76 @@
1
1
  # Project Guide
2
2
 
3
- This file only adds project-local rules on top of the canonical Proteum app contract.
3
+ This file adds project-local rules on top of the canonical Proteum app contract.
4
4
 
5
5
  Framework contract:
6
6
 
7
7
  - framework repo: `agents/framework/AGENTS.md`
8
8
  - installed app: `./node_modules/proteum/agents/framework/AGENTS.md`
9
9
 
10
- Coding style source of truth:
11
-
12
- - `./CODING_STYLE.md`
10
+ Coding style source of truth: `./CODING_STYLE.md`.
13
11
 
14
12
  ## Fast Start
15
13
 
16
- Start project inspection with:
17
-
18
- - `./.proteum/manifest.json`
19
- - `npx proteum explain`
20
- - `npx proteum doctor`
21
-
22
- For request-time issues in dev, inspect traces before adding temporary logs:
23
-
24
- - `npx proteum trace requests`
25
- - `npx proteum trace latest`
26
- - `npx proteum trace arm --capture deep`
27
-
28
- If you need to diagnose or test against a running app:
29
-
30
- - read the default port from `PORT` or `./.proteum/manifest.json`
31
- - check whether a server is already running on that port
32
- - if it is, inspect existing traces first to collect past errors and their context before reproducing the issue
14
+ - Start with `./.proteum/manifest.json`, `npx proteum explain`, and `npx proteum doctor`.
15
+ - For request-time issues in dev, inspect traces before adding logs.
16
+ - If a server is already running on the default port from `PORT` or `./.proteum/manifest.json`, inspect existing traces before reproducing the issue.
17
+ - If existing traces are insufficient, arm `npx proteum trace arm --capture deep`, reproduce once, then inspect the new request.
33
18
 
34
- ## Project Structure
19
+ ## Project Shape
35
20
 
36
- This is a full-stack monolith project using TypeScript, Node.js, Preact, and Proteum.
21
+ This is a TypeScript, Node.js, Preact, Proteum monolith:
37
22
 
38
- - `/client`
39
- - `/assets`: CSS, images, and other frontend assets
40
- - `/catalogs`: client-only catalogs and registries
41
- - `/components`: reusable components
42
- - `/pages`: page route files and page-local UI
43
- - `/hooks`
44
- - `/common`: shared functions, constants, typings, and cross-runtime catalogs
45
- - `/server`
46
- - `/catalogs`: server-only catalogs and registries
47
- - `/config`: service configuration
48
- - `/services`: backend services
49
- - `/routes`: explicit non-controller routes
50
- - `/lib`: helper functions
23
+ - `/client`: assets, catalogs, components, hooks, pages
24
+ - `/common`: shared functions, constants, types, and catalogs
25
+ - `/server`: catalogs, config, services, routes, lib
51
26
  - `/tests`
52
27
 
53
28
  ## Local Deltas
54
29
 
55
- - Always keep one class or one React/Preact component per file.
56
- - Prefer a deep tree structure grouped by business concern instead of long file names.
57
- - The default `*.ts` or `*.tsx` file is the normal implementation. Use `*.ssr.ts` or `*.ssr.tsx` only when an SSR-specific variant is actually required.
58
- - Generated files live under `./.proteum` and should never be edited by hand.
59
- - Project code should use `@generated/client/*`, `@generated/common/*`, and `@generated/server/*` for generated surfaces.
30
+ - Keep one class or one React/Preact component per file.
31
+ - Prefer a deep tree grouped by business concern instead of long file names.
32
+ - Use the default `*.ts` or `*.tsx` file unless an `*.ssr.ts` or `*.ssr.tsx` variant is truly required.
33
+ - Never edit generated files under `./.proteum`.
34
+ - Use `@generated/client/*`, `@generated/common/*`, and `@generated/server/*` for generated surfaces.
60
35
  - Client context is typically imported from `@/client/context`.
61
36
  - Prefer type inference from the explicit application class in `./server/index.ts`.
62
- - If the project already exposes shared Shadcn-based UI primitives, reuse them before creating bespoke primitives.
37
+ - Reuse shared Shadcn-based UI primitives when the project already provides them.
63
38
 
64
- ## Catalog Single Source Of Truth
39
+ ## Dependency Selection
65
40
 
66
- When a feature depends on a curated list, keep one canonical catalog or registry file and import it everywhere else.
41
+ - Before implementing a feature or change, first check whether the repo already includes a suitable dependency.
42
+ - If not, search npm before building a new utility, abstraction, component primitive, parser, formatter, or integration from scratch.
43
+ - Prefer the most popular, flexible, maintained packages that fit the project constraints.
44
+ - Only reinvent the wheel when existing packages are clearly inadequate on bundle size, SSR behavior, performance, typing quality, flexibility, licensing, or maintenance risk.
45
+ - When you choose custom over a package, explain the reason briefly.
67
46
 
68
- - client-only catalogs live in `/client/catalogs/**`
69
- - server-only catalogs live in `/server/catalogs/**`
70
- - shared catalogs live in `/common/catalogs/**`
71
- - do not create nested `catalogs/` folders under pages, components, services, tests, or other feature folders
47
+ ## Catalogs And Typing
72
48
 
73
- ## Typings
74
-
75
- - Keep strong, consistent TypeScript typings across the whole project.
49
+ - Keep one canonical catalog or registry file and import it everywhere else.
50
+ - Client-only catalogs live in `/client/catalogs/**`, server-only catalogs in `/server/catalogs/**`, and shared catalogs in `/common/catalogs/**`.
51
+ - Do not create nested `catalogs/` folders under pages, components, services, tests, or other feature folders.
52
+ - Keep strong TypeScript typings across the project.
76
53
  - Do not introduce `any` or `unknown`, including through casts, helper aliases, or fallback generic defaults.
77
- - Fix typing issues only on the code you wrote.
78
- - Never cast with `as any` or `as unknown`. Fix the contract or introduce an explicit typed adapter instead.
54
+ - Fix typing issues only on code you wrote.
55
+ - Never cast with `as any` or `as unknown`; fix the contract or add an explicit typed adapter.
79
56
 
80
57
  ## Workflow
81
58
 
82
- - Every time I input error messages without any instructions, do not implement fixes. Instead, investigate the potential causes and, for each one:
83
- 1. evaluate or quantify the probability
84
- 2. explain why
85
- 3. suggest how to fix it
86
- - When the issue is request-time behavior in dev, first check whether a server is already running on the default port from `PORT` or `./.proteum/manifest.json`. If it is, prefer `npx proteum trace` to inspect past errors and their context before reproducing the issue or adding logs.
87
- - When you have finished your work, summarize in one top-level short sentence the changes you made since the beginning of the conversation. Output as `Commit message`.
59
+ - If the user pastes raw errors without asking for a fix, do not implement changes. List likely causes and, for each one, give probability, why, and how to fix it.
60
+ - For request-time behavior in dev, check whether a server is already running on the default port and prefer `npx proteum trace` before reproducing the issue or adding logs.
61
+ - End your work with `Commit message`: one short top-level sentence.
88
62
 
89
63
  ## High-Impact Files
90
64
 
65
+ Edit these only when required, and keep changes minimal and explicit:
66
+
91
67
  - `tsconfig*.json`
92
68
  - `PORT`, `ENV_*`, `URL`, and `TRACE_*` env setup
93
69
  - Prisma-generated files
94
70
  - symbolic links
95
71
 
96
- Edit those files only when the task actually requires it, and keep the change minimal and explicit.
97
-
98
72
  ## Commands Not To Run
99
73
 
100
- Do not run:
101
-
102
74
  - `git restore`
103
75
  - `git reset`
104
76
  - `prisma *`
@@ -106,10 +78,4 @@ Do not run:
106
78
 
107
79
  ## Product And UX Docs
108
80
 
109
- If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, read the relevant files under `./docs/` first.
110
-
111
- Prefer these when they exist:
112
-
113
- - `docs/PERSONAS.md`
114
- - `docs/PRODUCT.md`
115
- - `docs/MARKETING.md`
81
+ If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, read the relevant files under `./docs/` first, especially `docs/PERSONAS.md`, `docs/PRODUCT.md`, and `docs/MARKETING.md` when they exist.
@@ -0,0 +1,243 @@
1
+ import got from 'got';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { UsageError } from 'clipanion';
5
+
6
+ import cli from '..';
7
+ import app from '../app';
8
+ import {
9
+ normalizeDevCommandPath,
10
+ type TDevCommandErrorResponse,
11
+ type TDevCommandExecution,
12
+ type TDevCommandRunResponse,
13
+ } from '../../common/dev/commands';
14
+
15
+ const localCommandResultMarker = '__PROTEUM_COMMAND_RESULT__';
16
+
17
+ const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
18
+
19
+ const getRouterPortFromManifest = () => {
20
+ const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
21
+ if (!require('fs-extra').existsSync(manifestFilepath)) return undefined;
22
+
23
+ const manifest = require('fs-extra').readJsonSync(manifestFilepath, { throws: false }) as
24
+ | { env?: { resolved?: { routerPort?: number } } }
25
+ | undefined;
26
+ const port = manifest?.env?.resolved?.routerPort;
27
+
28
+ if (typeof port !== 'number' || port <= 0) return undefined;
29
+
30
+ return String(port);
31
+ };
32
+
33
+ const getRouterPort = () => {
34
+ const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
35
+ if (overridePort) return overridePort;
36
+
37
+ const envPort = process.env.PORT?.trim();
38
+ if (envPort) return envPort;
39
+
40
+ const manifestPort = getRouterPortFromManifest();
41
+ if (manifestPort) return manifestPort;
42
+
43
+ throw new UsageError(
44
+ `Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
45
+ );
46
+ };
47
+
48
+ const getRouterBaseUrls = () => {
49
+ const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
50
+ if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
51
+
52
+ const port = getRouterPort();
53
+ return [...new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`])];
54
+ };
55
+
56
+ const getCommandErrorMessage = (body: TDevCommandErrorResponse | object | string | undefined, statusCode: number) => {
57
+ if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
58
+ return body.error;
59
+ }
60
+
61
+ return `Command request failed with status ${statusCode}.`;
62
+ };
63
+
64
+ const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
65
+ const attempts: string[] = [];
66
+
67
+ for (const baseUrl of getRouterBaseUrls()) {
68
+ try {
69
+ const response = await got(`${baseUrl}${pathname}`, {
70
+ method: options?.method || 'GET',
71
+ json: options?.json,
72
+ responseType: 'json',
73
+ throwHttpErrors: false,
74
+ retry: { limit: 0 },
75
+ });
76
+
77
+ if (response.statusCode >= 400) {
78
+ throw new UsageError(
79
+ getCommandErrorMessage(response.body as TDevCommandErrorResponse | object | string | undefined, response.statusCode),
80
+ );
81
+ }
82
+
83
+ return response.body as TResponse;
84
+ } catch (error) {
85
+ if (error instanceof UsageError) throw error;
86
+
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ attempts.push(`${baseUrl}${pathname}: ${message}`);
89
+ }
90
+ }
91
+
92
+ throw new UsageError(
93
+ [
94
+ 'Could not reach the Proteum command server.',
95
+ ...attempts.map((attempt) => `- ${attempt}`),
96
+ 'Make sure the app is running with `proteum dev`, or omit --port/--url to run the command locally.',
97
+ ].join('\n'),
98
+ );
99
+ };
100
+
101
+ const printJson = (value: object) => {
102
+ console.log(JSON.stringify(value, null, 2));
103
+ };
104
+
105
+ const renderExecution = (execution: TDevCommandExecution) => {
106
+ const lines = [
107
+ `Command ${execution.command.path}`,
108
+ `- status=${execution.status} durationMs=${execution.durationMs}`,
109
+ `- source=${execution.command.filepath}:${execution.command.sourceLocation.line}:${execution.command.sourceLocation.column}`,
110
+ ];
111
+
112
+ if (execution.errorMessage) {
113
+ lines.push(`- error=${execution.errorMessage}`);
114
+ } else if (execution.result?.json !== undefined) {
115
+ lines.push('Result');
116
+ lines.push(JSON.stringify(execution.result.json, null, 2));
117
+ } else if (execution.result?.summary !== undefined) {
118
+ lines.push('Result');
119
+ lines.push(JSON.stringify(execution.result.summary, null, 2));
120
+ } else {
121
+ lines.push('- result=undefined');
122
+ }
123
+
124
+ return lines.join('\n');
125
+ };
126
+
127
+ const runLocalCommand = async (commandPath: string) => {
128
+ if (app.env.profile !== 'dev') {
129
+ throw new UsageError(`Proteum commands are only available when ENV_PROFILE=dev. Current profile: ${app.env.profile}.`);
130
+ }
131
+
132
+ const runnerFilepath = path.join(cli.paths.core.root, 'cli', 'commands', 'commandLocalRunner.js');
133
+
134
+ return await new Promise<TDevCommandExecution>((resolve, reject) => {
135
+ const stdoutChunks: Buffer[] = [];
136
+ const stderrChunks: Buffer[] = [];
137
+ const child = spawn(process.execPath, [runnerFilepath, app.paths.root, commandPath], {
138
+ cwd: app.paths.root,
139
+ env: { ...process.env },
140
+ stdio: ['ignore', 'pipe', 'pipe'],
141
+ });
142
+
143
+ child.stdout.on('data', (chunk: Buffer | string) => {
144
+ stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
145
+ });
146
+
147
+ child.stderr.on('data', (chunk: Buffer | string) => {
148
+ stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
149
+ });
150
+
151
+ child.on('error', (error) => reject(error));
152
+ child.on('close', (exitCode) => {
153
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
154
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
155
+ const markerLine = stdout
156
+ .split(/\r?\n/)
157
+ .find((line) => line.startsWith(localCommandResultMarker));
158
+
159
+ if (stderr.trim()) {
160
+ process.stderr.write(stderr.endsWith('\n') ? stderr : `${stderr}\n`);
161
+ }
162
+
163
+ if (!markerLine) {
164
+ reject(
165
+ new Error(
166
+ [
167
+ `Local command runner exited without returning a structured result (exit code ${exitCode ?? 'unknown'}).`,
168
+ stdout.trim() ? `stdout:\n${stdout.trim()}` : undefined,
169
+ ]
170
+ .filter(Boolean)
171
+ .join('\n\n'),
172
+ ),
173
+ );
174
+ return;
175
+ }
176
+
177
+ const payload = JSON.parse(markerLine.slice(localCommandResultMarker.length)) as
178
+ | { execution: TDevCommandExecution }
179
+ | { error: string };
180
+
181
+ if ('execution' in payload) {
182
+ if (exitCode && exitCode !== 0) {
183
+ const error = new Error(payload.execution.errorMessage || `Command "${commandPath}" failed.`) as Error & {
184
+ execution?: TDevCommandExecution;
185
+ };
186
+
187
+ error.execution = payload.execution;
188
+ reject(error);
189
+ return;
190
+ }
191
+
192
+ resolve(payload.execution);
193
+ return;
194
+ }
195
+
196
+ reject(new Error(payload.error || `Command "${commandPath}" failed.`));
197
+ });
198
+ });
199
+ };
200
+
201
+ export const run = async () => {
202
+ const rawPath = typeof cli.args.path === 'string' ? cli.args.path : '';
203
+ const commandPath = normalizeDevCommandPath(rawPath);
204
+ const shouldPrintJson = cli.args.json === true;
205
+ const shouldUseRemoteServer =
206
+ (typeof cli.args.port === 'string' && cli.args.port.length > 0) ||
207
+ (typeof cli.args.url === 'string' && cli.args.url.length > 0);
208
+
209
+ if (!commandPath) {
210
+ throw new UsageError('A command path is required. Example: proteum command diagnostics/ping');
211
+ }
212
+
213
+ try {
214
+ const execution = shouldUseRemoteServer
215
+ ? (await requestJson<TDevCommandRunResponse>('/__proteum/commands/run', {
216
+ method: 'POST',
217
+ json: { path: commandPath },
218
+ })).execution
219
+ : await runLocalCommand(commandPath);
220
+
221
+ if (shouldPrintJson) {
222
+ printJson({ execution });
223
+ return;
224
+ }
225
+
226
+ console.log(renderExecution(execution));
227
+ } catch (error) {
228
+ if (error instanceof Error && 'execution' in error && typeof error.execution === 'object' && error.execution) {
229
+ const execution = error.execution as TDevCommandExecution;
230
+
231
+ if (shouldPrintJson) {
232
+ printJson({ execution });
233
+ } else {
234
+ console.log(renderExecution(execution));
235
+ }
236
+
237
+ process.exitCode = 1;
238
+ return;
239
+ }
240
+
241
+ throw error;
242
+ }
243
+ };
@@ -0,0 +1,198 @@
1
+ const path = require('path');
2
+ const net = require('net');
3
+ const { spawn } = require('child_process');
4
+ const tsNode = require('ts-node');
5
+
6
+ const resultMarker = '__PROTEUM_COMMAND_RESULT__';
7
+
8
+ const printPayload = (payload) => {
9
+ process.stdout.write(`${resultMarker}${JSON.stringify(payload)}\n`);
10
+ };
11
+
12
+ const fail = (error, execution) => {
13
+ if (execution) {
14
+ printPayload({ execution });
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+
19
+ printPayload({ error: error instanceof Error ? error.message : String(error) });
20
+ process.exitCode = 1;
21
+ };
22
+
23
+ const getAvailablePort = async () =>
24
+ await new Promise((resolve, reject) => {
25
+ const server = net.createServer();
26
+
27
+ server.once('error', reject);
28
+ server.listen(0, '127.0.0.1', () => {
29
+ const address = server.address();
30
+
31
+ if (!address || typeof address === 'string') {
32
+ server.close(() => reject(new Error('Could not determine a local port for the command runner.')));
33
+ return;
34
+ }
35
+
36
+ server.close((error) => {
37
+ if (error) {
38
+ reject(error);
39
+ return;
40
+ }
41
+
42
+ resolve(address.port);
43
+ });
44
+ });
45
+ });
46
+
47
+ const closeMultiCompiler = async (multiCompiler) =>
48
+ await new Promise((resolve, reject) => {
49
+ multiCompiler.close((error) => {
50
+ if (error) {
51
+ reject(error);
52
+ return;
53
+ }
54
+
55
+ resolve();
56
+ });
57
+ });
58
+
59
+ const runCompiler = async (compiler) => {
60
+ const multiCompiler = await compiler.create();
61
+
62
+ try {
63
+ await new Promise((resolve, reject) => {
64
+ multiCompiler.run((error, stats) => {
65
+ if (error) {
66
+ reject(error);
67
+ return;
68
+ }
69
+
70
+ if (stats && stats.hasErrors()) {
71
+ reject(new Error('Compilation failed for the local dev command runner.'));
72
+ return;
73
+ }
74
+
75
+ resolve();
76
+ });
77
+ });
78
+ } finally {
79
+ compiler.dispose();
80
+ await closeMultiCompiler(multiCompiler);
81
+ }
82
+ };
83
+
84
+ const waitForServerReady = async (child) =>
85
+ await new Promise((resolve, reject) => {
86
+ let settled = false;
87
+
88
+ const finish = (callback, value) => {
89
+ if (settled) return;
90
+ settled = true;
91
+ clearTimeout(timeout);
92
+ child.off('message', onMessage);
93
+ child.off('error', onError);
94
+ child.off('exit', onExit);
95
+ callback(value);
96
+ };
97
+
98
+ const onMessage = (message) => {
99
+ if (!message || message.type !== 'proteum:server-ready' || typeof message.publicUrl !== 'string') return;
100
+ finish(resolve, message.publicUrl);
101
+ };
102
+
103
+ const onError = (error) => finish(reject, error);
104
+ const onExit = (code, signal) =>
105
+ finish(reject, new Error(`Local command server exited before becoming ready (code=${code}, signal=${signal}).`));
106
+ const timeout = setTimeout(
107
+ () => finish(reject, new Error('Timed out while waiting for the local command server to become ready.')),
108
+ 30000,
109
+ );
110
+
111
+ child.on('message', onMessage);
112
+ child.once('error', onError);
113
+ child.once('exit', onExit);
114
+ });
115
+
116
+ const stopServerProcess = async (child) => {
117
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
118
+
119
+ child.kill('SIGTERM');
120
+
121
+ await new Promise((resolve) => {
122
+ const timeout = setTimeout(() => {
123
+ if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
124
+ }, 5000);
125
+
126
+ child.once('exit', () => {
127
+ clearTimeout(timeout);
128
+ resolve();
129
+ });
130
+ });
131
+ };
132
+
133
+ const requestCommand = async (baseUrl, commandPath) => {
134
+ const response = await fetch(`${baseUrl}/__proteum/commands/run`, {
135
+ method: 'POST',
136
+ headers: { 'content-type': 'application/json' },
137
+ body: JSON.stringify({ path: commandPath }),
138
+ });
139
+ const body = await response.json();
140
+
141
+ if (body && typeof body === 'object' && body.execution) {
142
+ return { statusCode: response.status, execution: body.execution };
143
+ }
144
+
145
+ throw new Error((body && body.error) || `Command request failed with status ${response.status}.`);
146
+ };
147
+
148
+ (async () => {
149
+ const [, , appRootArg = '', commandPath = ''] = process.argv;
150
+ const appRoot = path.resolve(appRootArg);
151
+
152
+ if (!appRootArg || !commandPath) {
153
+ fail(new Error('commandLocalRunner requires <appRoot> and <commandPath>.'));
154
+ return;
155
+ }
156
+
157
+ process.chdir(appRoot);
158
+
159
+ tsNode.register({
160
+ transpileOnly: true,
161
+ project: path.join(__dirname, '..', 'tsconfig.json'),
162
+ files: true,
163
+ });
164
+
165
+ const port = await getAvailablePort();
166
+ const cli = require('../context.ts').default;
167
+ cli.setArgs({ workdir: appRoot, path: commandPath, port: String(port), url: '', json: true });
168
+
169
+ const app = require('../app/index.ts').default;
170
+ const Compiler = require('../compiler/index.ts').default;
171
+
172
+ if (app.env.profile !== 'dev') {
173
+ fail(new Error(`Proteum commands are only available when ENV_PROFILE=dev. Current profile: ${app.env.profile}.`));
174
+ return;
175
+ }
176
+
177
+ const compiler = new Compiler('dev');
178
+ await runCompiler(compiler);
179
+
180
+ const serverProcess = spawn(process.execPath, ['--preserve-symlinks', path.join(app.outputPath('dev'), 'server.js')], {
181
+ cwd: app.paths.root,
182
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
183
+ });
184
+
185
+ try {
186
+ const publicUrl = await waitForServerReady(serverProcess);
187
+ const { statusCode, execution } = await requestCommand(publicUrl, commandPath);
188
+
189
+ printPayload({ execution });
190
+ if (statusCode >= 400 || execution.status === 'error') {
191
+ process.exitCode = 1;
192
+ }
193
+ } finally {
194
+ await stopServerProcess(serverProcess);
195
+ }
196
+ })().catch((error) => {
197
+ fail(error);
198
+ });