@pikku/cli 0.12.41 → 0.12.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/console-app/assets/index-AwGnKyWe.js +254 -0
- package/console-app/assets/index-VleHndkw.css +1 -0
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +10 -0
- package/dist/.pikku/cli/pikku-cli-client.gen.js +73 -0
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +22 -26
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +2 -2
- package/dist/.pikku/function/pikku-function-types.gen.js +3 -2
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +74 -55
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +2 -2
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +6 -5
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +7 -3
- package/dist/.pikku/schemas/schemas/DeployApplyInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/DeployPlanInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricLinkOutput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricSmokeInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricSmokeOutput.schema.json +1 -0
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/deploy/build-pipeline.d.ts +2 -0
- package/dist/src/deploy/build-pipeline.js +7 -1
- package/dist/src/deploy/bundler/bundler.d.ts +2 -0
- package/dist/src/deploy/bundler/bundler.js +8 -5
- package/dist/src/fabric/fabric-commands.d.ts +67 -0
- package/dist/src/fabric/fabric-commands.js +21 -0
- package/dist/src/fabric/functions/link.function.d.ts +4 -0
- package/dist/src/fabric/functions/link.function.js +3 -1
- package/dist/src/fabric/functions/smoke.function.d.ts +93 -0
- package/dist/src/fabric/functions/smoke.function.js +860 -0
- package/dist/src/functions/commands/deploy-apply.d.ts +3 -0
- package/dist/src/functions/commands/deploy-apply.js +1 -0
- package/dist/src/functions/commands/deploy-plan.d.ts +3 -0
- package/dist/src/functions/commands/deploy-plan.js +1 -0
- package/dist/src/functions/db/local-db.js +20 -75
- package/dist/src/functions/wirings/auth/pikku-command-auth.js +6 -1
- package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +4 -1
- package/dist/src/functions/wirings/auth/serialize-auth-gen.js +36 -12
- package/dist/src/functions/wirings/functions/pikku-command-services.js +5 -4
- package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -2
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/services.d.ts +6 -1
- package/dist/src/services.js +6 -12
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/console-app/assets/index-D9Z9rySK.js +0 -233
- package/console-app/assets/index-DwUzVI5k.css +0 -1
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createServer } from 'node:net';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, relative, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
|
|
11
|
+
import { findProjectRoot, readJsonSafe, } from '../../functions/validate/workspace-validate.js';
|
|
12
|
+
import { headSha, isWorkingTreeClean } from '../lib/git.js';
|
|
13
|
+
import { added, changed, dim, removed } from '../lib/output.js';
|
|
14
|
+
const DEFAULT_YARN_VERSION = 'yarn@4.9.2';
|
|
15
|
+
const DEFAULT_STEP_TIMEOUT_SECONDS = 300;
|
|
16
|
+
const DEFAULT_STARTUP_TIMEOUT_SECONDS = 60;
|
|
17
|
+
const DEV_LOG_TAIL_BYTES = 16_000;
|
|
18
|
+
const SmokeStepSchema = z.object({
|
|
19
|
+
name: z.string(),
|
|
20
|
+
status: z.enum(['passed', 'failed', 'skipped']),
|
|
21
|
+
durationMs: z.number().int().nonnegative(),
|
|
22
|
+
detail: z.string().optional(),
|
|
23
|
+
command: z.string().optional(),
|
|
24
|
+
});
|
|
25
|
+
export const FabricSmokeInput = z.object({
|
|
26
|
+
keepTemp: z.boolean().optional(),
|
|
27
|
+
timeoutSeconds: z.number().int().positive().optional(),
|
|
28
|
+
startupTimeoutSeconds: z.number().int().positive().optional(),
|
|
29
|
+
port: z.number().int().positive().optional(),
|
|
30
|
+
});
|
|
31
|
+
export const FabricSmokeOutput = z.object({
|
|
32
|
+
ok: z.boolean(),
|
|
33
|
+
root: z.string(),
|
|
34
|
+
ref: z.string(),
|
|
35
|
+
tempDir: z.string(),
|
|
36
|
+
tempDirKept: z.boolean(),
|
|
37
|
+
notes: z.array(z.string()),
|
|
38
|
+
steps: z.array(SmokeStepSchema),
|
|
39
|
+
failure: z.string().optional(),
|
|
40
|
+
logTail: z.string().optional(),
|
|
41
|
+
});
|
|
42
|
+
function now() {
|
|
43
|
+
return Date.now();
|
|
44
|
+
}
|
|
45
|
+
function formatDuration(durationMs) {
|
|
46
|
+
if (durationMs < 1000)
|
|
47
|
+
return `${durationMs}ms`;
|
|
48
|
+
return `${(durationMs / 1000).toFixed(durationMs >= 10_000 ? 0 : 1)}s`;
|
|
49
|
+
}
|
|
50
|
+
function truncateTail(text, maxBytes = DEV_LOG_TAIL_BYTES) {
|
|
51
|
+
const trimmed = text.trim();
|
|
52
|
+
if (Buffer.byteLength(trimmed, 'utf8') <= maxBytes) {
|
|
53
|
+
return trimmed;
|
|
54
|
+
}
|
|
55
|
+
const slice = Buffer.from(trimmed, 'utf8').subarray(-maxBytes).toString('utf8');
|
|
56
|
+
const newlineIndex = slice.indexOf('\n');
|
|
57
|
+
return newlineIndex === -1 ? slice : slice.slice(newlineIndex + 1);
|
|
58
|
+
}
|
|
59
|
+
function commandLabel(command, args) {
|
|
60
|
+
return [command, ...args].join(' ');
|
|
61
|
+
}
|
|
62
|
+
async function runCommandStep(args) {
|
|
63
|
+
const startedAt = now();
|
|
64
|
+
return await new Promise((resolvePromise) => {
|
|
65
|
+
const child = spawn(args.command, args.commandArgs, {
|
|
66
|
+
cwd: args.cwd,
|
|
67
|
+
env: args.env,
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
|
+
});
|
|
70
|
+
let combined = '';
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
const append = (chunk) => {
|
|
73
|
+
combined += chunk;
|
|
74
|
+
if (combined.length > DEV_LOG_TAIL_BYTES * 4) {
|
|
75
|
+
combined = combined.slice(-DEV_LOG_TAIL_BYTES * 4);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
child.stdout.on('data', (chunk) => append(chunk.toString()));
|
|
79
|
+
child.stderr.on('data', (chunk) => append(chunk.toString()));
|
|
80
|
+
child.on('error', (error) => append(String(error)));
|
|
81
|
+
const timeout = setTimeout(() => {
|
|
82
|
+
timedOut = true;
|
|
83
|
+
child.kill('SIGTERM');
|
|
84
|
+
setTimeout(() => child.kill('SIGKILL'), 2000).unref();
|
|
85
|
+
}, args.timeoutMs);
|
|
86
|
+
child.on('close', (code) => {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
resolvePromise({
|
|
89
|
+
ok: code === 0 && !timedOut,
|
|
90
|
+
durationMs: now() - startedAt,
|
|
91
|
+
tail: truncateTail(combined),
|
|
92
|
+
timedOut,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function waitForHealth(args) {
|
|
98
|
+
const startedAt = now();
|
|
99
|
+
const fastFailAt = startedAt + 25_000;
|
|
100
|
+
while (now() - startedAt < args.timeoutMs) {
|
|
101
|
+
if (args.child.exitCode !== null) {
|
|
102
|
+
const tail = await readLogTail(args.logFile);
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
tail,
|
|
106
|
+
detail: `pikku dev exited before health-check became ready`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
for (const url of args.urls) {
|
|
111
|
+
const response = await fetch(url, {
|
|
112
|
+
signal: AbortSignal.timeout(2_000),
|
|
113
|
+
});
|
|
114
|
+
if (response.status !== 0) {
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
tail: '',
|
|
118
|
+
detail: `${url} responded ${response.status}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// keep polling
|
|
125
|
+
}
|
|
126
|
+
if (now() >= fastFailAt) {
|
|
127
|
+
const tail = await readLogTail(args.logFile);
|
|
128
|
+
if (/error TS|SyntaxError|Cannot find|Error:/i.test(tail)) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
tail,
|
|
132
|
+
detail: 'pikku dev failed to start — error detected in startup log',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await delay(1000);
|
|
137
|
+
}
|
|
138
|
+
const tail = await readLogTail(args.logFile);
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
tail,
|
|
142
|
+
detail: `pikku dev did not become healthy within ${Math.ceil(args.timeoutMs / 1000)}s`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function rewriteFrontendCommand(command) {
|
|
146
|
+
if (command.length === 0) {
|
|
147
|
+
throw new Error('frontend dev.command must not be empty');
|
|
148
|
+
}
|
|
149
|
+
if (command[0] === 'yarn') {
|
|
150
|
+
return {
|
|
151
|
+
command: 'yarn',
|
|
152
|
+
args: command.slice(1),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
command: command[0],
|
|
157
|
+
args: command.slice(1),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function startManagedProcess(args) {
|
|
161
|
+
const logStream = createWriteStream(args.logFile, { flags: 'w' });
|
|
162
|
+
const child = spawn(args.command, args.commandArgs, {
|
|
163
|
+
cwd: args.cwd,
|
|
164
|
+
env: args.env,
|
|
165
|
+
detached: true,
|
|
166
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
|
+
});
|
|
168
|
+
child.stdout?.pipe(logStream);
|
|
169
|
+
child.stderr?.pipe(logStream);
|
|
170
|
+
return { child, logStream };
|
|
171
|
+
}
|
|
172
|
+
async function readLogTail(path) {
|
|
173
|
+
if (!existsSync(path))
|
|
174
|
+
return '';
|
|
175
|
+
try {
|
|
176
|
+
return truncateTail(await readFile(path, 'utf8'));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return '';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function terminateProcessGroup(child) {
|
|
183
|
+
if (!child.pid)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
child.kill('SIGTERM');
|
|
190
|
+
}
|
|
191
|
+
await delay(1000);
|
|
192
|
+
if (child.exitCode === null) {
|
|
193
|
+
try {
|
|
194
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
child.kill('SIGKILL');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function runFrontendChecks(args) {
|
|
202
|
+
const fabricConfigPath = existsSync(join(args.root, 'pikkufabric.config.json'))
|
|
203
|
+
? join(args.root, 'pikkufabric.config.json')
|
|
204
|
+
: join(args.root, 'fabric.config.json');
|
|
205
|
+
const fabricConfig = await readJsonSafe(fabricConfigPath);
|
|
206
|
+
const frontends = fabricConfig?.frontends ?? {};
|
|
207
|
+
const entries = Object.entries(frontends).filter((entry) => Boolean(entry[1]) && typeof entry[1] === 'object');
|
|
208
|
+
if (entries.length === 0) {
|
|
209
|
+
args.steps.push({
|
|
210
|
+
name: 'frontend checks',
|
|
211
|
+
status: 'skipped',
|
|
212
|
+
durationMs: 0,
|
|
213
|
+
detail: 'no frontends declared in pikkufabric.config.json',
|
|
214
|
+
});
|
|
215
|
+
return { ok: true };
|
|
216
|
+
}
|
|
217
|
+
for (const [slug, config] of entries) {
|
|
218
|
+
const cwd = typeof config.cwd === 'string' && config.cwd.length > 0
|
|
219
|
+
? resolve(args.root, config.cwd)
|
|
220
|
+
: null;
|
|
221
|
+
if (!cwd || !existsSync(cwd) || !existsSync(join(cwd, 'package.json'))) {
|
|
222
|
+
args.steps.push({
|
|
223
|
+
name: `frontend:${slug}`,
|
|
224
|
+
status: 'skipped',
|
|
225
|
+
durationMs: 0,
|
|
226
|
+
detail: `skipping ${slug} — invalid or missing cwd`,
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const pkg = await readJsonSafe(join(cwd, 'package.json'));
|
|
231
|
+
const scripts = pkg?.scripts ?? {};
|
|
232
|
+
const scriptOrder = ['i18n', 'build', 'tsc'].filter((name) => typeof scripts[name] === 'string');
|
|
233
|
+
if (scriptOrder.length === 0) {
|
|
234
|
+
args.steps.push({
|
|
235
|
+
name: `frontend:${slug}`,
|
|
236
|
+
status: 'skipped',
|
|
237
|
+
durationMs: 0,
|
|
238
|
+
detail: `no i18n/build/tsc scripts in ${relative(args.root, cwd)}`,
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
for (const script of scriptOrder) {
|
|
243
|
+
const result = await runCommandStep({
|
|
244
|
+
name: `frontend:${slug}:${script}`,
|
|
245
|
+
command: 'yarn',
|
|
246
|
+
commandArgs: [script],
|
|
247
|
+
cwd,
|
|
248
|
+
env: args.env,
|
|
249
|
+
timeoutMs: args.timeoutMs,
|
|
250
|
+
});
|
|
251
|
+
args.steps.push({
|
|
252
|
+
name: `frontend:${slug}:${script}`,
|
|
253
|
+
status: result.ok ? 'passed' : 'failed',
|
|
254
|
+
durationMs: result.durationMs,
|
|
255
|
+
detail: result.ok
|
|
256
|
+
? `${slug} ${script} passed`
|
|
257
|
+
: `${slug} ${script} failed`,
|
|
258
|
+
command: commandLabel('yarn', [script]),
|
|
259
|
+
});
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
failure: `${slug} ${script} failed`,
|
|
264
|
+
logTail: result.tail,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { ok: true };
|
|
270
|
+
}
|
|
271
|
+
async function startFrontends(args) {
|
|
272
|
+
const fabricConfigPath = existsSync(join(args.root, 'pikkufabric.config.json'))
|
|
273
|
+
? join(args.root, 'pikkufabric.config.json')
|
|
274
|
+
: join(args.root, 'fabric.config.json');
|
|
275
|
+
const fabricConfig = await readJsonSafe(fabricConfigPath);
|
|
276
|
+
const frontends = fabricConfig?.frontends ?? {};
|
|
277
|
+
const entries = Object.entries(frontends).filter((entry) => Boolean(entry[1]) && typeof entry[1] === 'object');
|
|
278
|
+
const running = [];
|
|
279
|
+
if (entries.length === 0) {
|
|
280
|
+
args.steps.push({
|
|
281
|
+
name: 'frontend startup',
|
|
282
|
+
status: 'skipped',
|
|
283
|
+
durationMs: 0,
|
|
284
|
+
detail: 'no frontends declared in pikkufabric.config.json',
|
|
285
|
+
});
|
|
286
|
+
return { ok: true, running };
|
|
287
|
+
}
|
|
288
|
+
for (const [slug, config] of entries) {
|
|
289
|
+
const cwd = typeof config.cwd === 'string' && config.cwd.length > 0
|
|
290
|
+
? resolve(args.root, config.cwd)
|
|
291
|
+
: null;
|
|
292
|
+
const devConfig = config.dev;
|
|
293
|
+
if (!cwd || !existsSync(cwd)) {
|
|
294
|
+
args.steps.push({
|
|
295
|
+
name: `frontend:${slug}:start`,
|
|
296
|
+
status: 'failed',
|
|
297
|
+
durationMs: 0,
|
|
298
|
+
detail: `frontend cwd is missing: ${config.cwd ?? '(unset)'}`,
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
running,
|
|
303
|
+
failure: `${slug} frontend cwd is missing`,
|
|
304
|
+
logTail: '',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (!devConfig ||
|
|
308
|
+
!Array.isArray(devConfig.command) ||
|
|
309
|
+
devConfig.command.length === 0 ||
|
|
310
|
+
typeof devConfig.port !== 'number') {
|
|
311
|
+
args.steps.push({
|
|
312
|
+
name: `frontend:${slug}:start`,
|
|
313
|
+
status: 'failed',
|
|
314
|
+
durationMs: 0,
|
|
315
|
+
detail: `frontend dev config is incomplete in ${relative(args.root, fabricConfigPath)}`,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
ok: false,
|
|
319
|
+
running,
|
|
320
|
+
failure: `${slug} frontend dev config is incomplete`,
|
|
321
|
+
logTail: '',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const rewritten = rewriteFrontendCommand(devConfig.command);
|
|
325
|
+
const healthPath = typeof devConfig.healthPath === 'string' && devConfig.healthPath.length > 0
|
|
326
|
+
? devConfig.healthPath
|
|
327
|
+
: '/';
|
|
328
|
+
const normalizedHealthPath = healthPath.startsWith('/')
|
|
329
|
+
? healthPath
|
|
330
|
+
: `/${healthPath}`;
|
|
331
|
+
const logFile = join(args.root, `.pikku-fabric-smoke-${slug}.log`);
|
|
332
|
+
const startedAt = now();
|
|
333
|
+
const managed = startManagedProcess({
|
|
334
|
+
command: rewritten.command,
|
|
335
|
+
commandArgs: rewritten.args,
|
|
336
|
+
cwd,
|
|
337
|
+
env: args.env,
|
|
338
|
+
logFile,
|
|
339
|
+
});
|
|
340
|
+
running.push({ slug, child: managed.child, logStream: managed.logStream });
|
|
341
|
+
const healthResult = await waitForHealth({
|
|
342
|
+
urls: [
|
|
343
|
+
`http://localhost:${devConfig.port}${normalizedHealthPath}`,
|
|
344
|
+
`http://127.0.0.1:${devConfig.port}${normalizedHealthPath}`,
|
|
345
|
+
],
|
|
346
|
+
timeoutMs: args.timeoutMs,
|
|
347
|
+
child: managed.child,
|
|
348
|
+
logFile,
|
|
349
|
+
});
|
|
350
|
+
args.steps.push({
|
|
351
|
+
name: `frontend:${slug}:start`,
|
|
352
|
+
status: healthResult.ok ? 'passed' : 'failed',
|
|
353
|
+
durationMs: now() - startedAt,
|
|
354
|
+
detail: healthResult.detail,
|
|
355
|
+
command: commandLabel(rewritten.command, rewritten.args),
|
|
356
|
+
});
|
|
357
|
+
if (!healthResult.ok) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
running,
|
|
361
|
+
failure: `${slug} frontend failed to start`,
|
|
362
|
+
logTail: healthResult.tail,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { ok: true, running };
|
|
367
|
+
}
|
|
368
|
+
function resolvePikkuBin() {
|
|
369
|
+
const fromArgv = process.argv[1];
|
|
370
|
+
if (fromArgv && existsSync(fromArgv)) {
|
|
371
|
+
return fromArgv;
|
|
372
|
+
}
|
|
373
|
+
return fileURLToPath(new URL('../../../bin/pikku.js', import.meta.url));
|
|
374
|
+
}
|
|
375
|
+
function resolveCurrentCliPackageRoot(pikkuBin) {
|
|
376
|
+
return resolve(pikkuBin, '../../..');
|
|
377
|
+
}
|
|
378
|
+
async function vendorCurrentCliPackage(args) {
|
|
379
|
+
const startedAt = now();
|
|
380
|
+
const vendorDir = join(args.tempRoot, '.pikku-fabric-smoke', 'vendor');
|
|
381
|
+
await mkdir(vendorDir, { recursive: true });
|
|
382
|
+
const cliPackageRoot = await realpath(args.currentCliPackageRoot).catch(() => args.currentCliPackageRoot);
|
|
383
|
+
const packStep = await runCommandStep({
|
|
384
|
+
name: 'vendor @pikku/cli',
|
|
385
|
+
command: 'npm',
|
|
386
|
+
commandArgs: ['pack', cliPackageRoot, '--pack-destination', vendorDir],
|
|
387
|
+
cwd: args.tempRoot,
|
|
388
|
+
env: process.env,
|
|
389
|
+
timeoutMs: args.timeoutMs,
|
|
390
|
+
});
|
|
391
|
+
if (!packStep.ok) {
|
|
392
|
+
return {
|
|
393
|
+
durationMs: now() - startedAt,
|
|
394
|
+
detail: 'npm pack failed for @pikku/cli',
|
|
395
|
+
errorTail: packStep.tail || 'npm pack failed for @pikku/cli',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const tarballName = packStep.tail
|
|
399
|
+
.split('\n')
|
|
400
|
+
.map((line) => line.trim())
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.at(-1);
|
|
403
|
+
if (!tarballName?.endsWith('.tgz')) {
|
|
404
|
+
return {
|
|
405
|
+
durationMs: now() - startedAt,
|
|
406
|
+
detail: 'npm pack did not return a tarball name',
|
|
407
|
+
errorTail: packStep.tail || '(empty)',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const tarballRelativePath = relative(args.tempRoot, join(vendorDir, tarballName));
|
|
411
|
+
return {
|
|
412
|
+
durationMs: now() - startedAt,
|
|
413
|
+
tarballRelativePath,
|
|
414
|
+
detail: `packed ${relative(args.tempRoot, cliPackageRoot)} -> ${tarballRelativePath}`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async function rewriteLocalPikkuDependencies(args) {
|
|
418
|
+
const packageJsonPath = join(args.root, 'package.json');
|
|
419
|
+
const packageJson = await readJsonSafe(packageJsonPath);
|
|
420
|
+
if (!packageJson)
|
|
421
|
+
return [];
|
|
422
|
+
const rewrites = [];
|
|
423
|
+
const rewriteMap = (dependencies, label) => {
|
|
424
|
+
if (!dependencies)
|
|
425
|
+
return;
|
|
426
|
+
const current = dependencies['@pikku/cli'];
|
|
427
|
+
if (typeof current !== 'string' || !current.startsWith('file:')) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
dependencies['@pikku/cli'] = `file:${args.cliTarballRelativePath}`;
|
|
431
|
+
rewrites.push(`${label}.@pikku/cli: ${current} -> file:${args.cliTarballRelativePath}`);
|
|
432
|
+
};
|
|
433
|
+
rewriteMap(packageJson.dependencies, 'dependencies');
|
|
434
|
+
rewriteMap(packageJson.devDependencies, 'devDependencies');
|
|
435
|
+
if (rewrites.length === 0) {
|
|
436
|
+
return rewrites;
|
|
437
|
+
}
|
|
438
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
439
|
+
return rewrites;
|
|
440
|
+
}
|
|
441
|
+
async function createTempWorktree(root) {
|
|
442
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'pikku-fabric-smoke-'));
|
|
443
|
+
const step = await runCommandStep({
|
|
444
|
+
name: 'git worktree add',
|
|
445
|
+
command: 'git',
|
|
446
|
+
commandArgs: ['worktree', 'add', '--detach', tempDir, 'HEAD'],
|
|
447
|
+
cwd: root,
|
|
448
|
+
timeoutMs: 60_000,
|
|
449
|
+
});
|
|
450
|
+
if (step.ok) {
|
|
451
|
+
return tempDir;
|
|
452
|
+
}
|
|
453
|
+
const cloneStep = await runCommandStep({
|
|
454
|
+
name: 'git clone',
|
|
455
|
+
command: 'git',
|
|
456
|
+
commandArgs: ['clone', '--shared', root, tempDir],
|
|
457
|
+
cwd: root,
|
|
458
|
+
timeoutMs: 60_000,
|
|
459
|
+
});
|
|
460
|
+
if (!cloneStep.ok) {
|
|
461
|
+
throw new Error(`Failed to create temp checkout.\nworktree: ${step.tail || 'git worktree add failed.'}\nclone: ${cloneStep.tail || 'git clone failed.'}`);
|
|
462
|
+
}
|
|
463
|
+
const detachStep = await runCommandStep({
|
|
464
|
+
name: 'git checkout --detach',
|
|
465
|
+
command: 'git',
|
|
466
|
+
commandArgs: ['checkout', '--detach', 'HEAD'],
|
|
467
|
+
cwd: tempDir,
|
|
468
|
+
timeoutMs: 60_000,
|
|
469
|
+
});
|
|
470
|
+
if (!detachStep.ok) {
|
|
471
|
+
throw new Error(`Failed to detach temp checkout.\n${detachStep.tail || 'git checkout --detach failed.'}`);
|
|
472
|
+
}
|
|
473
|
+
return tempDir;
|
|
474
|
+
}
|
|
475
|
+
async function removeTempWorktree(root, tempDir) {
|
|
476
|
+
await runCommandStep({
|
|
477
|
+
name: 'git worktree remove',
|
|
478
|
+
command: 'git',
|
|
479
|
+
commandArgs: ['worktree', 'remove', '--force', tempDir],
|
|
480
|
+
cwd: root,
|
|
481
|
+
timeoutMs: 60_000,
|
|
482
|
+
});
|
|
483
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
484
|
+
}
|
|
485
|
+
async function stopManagedProcesses(running) {
|
|
486
|
+
for (const processHandle of running) {
|
|
487
|
+
await terminateProcessGroup(processHandle.child).catch(() => { });
|
|
488
|
+
processHandle.logStream.end();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function pickPort(preferred) {
|
|
492
|
+
if (preferred)
|
|
493
|
+
return preferred;
|
|
494
|
+
return await new Promise((resolvePromise, reject) => {
|
|
495
|
+
const server = createServer();
|
|
496
|
+
server.listen(0, '127.0.0.1', () => {
|
|
497
|
+
const address = server.address();
|
|
498
|
+
if (!address || typeof address === 'string') {
|
|
499
|
+
server.close(() => reject(new Error('Failed to allocate a free port')));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const { port } = address;
|
|
503
|
+
server.close((error) => {
|
|
504
|
+
if (error)
|
|
505
|
+
reject(error);
|
|
506
|
+
else
|
|
507
|
+
resolvePromise(port);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
server.on('error', reject);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function hasMigrations(root) {
|
|
514
|
+
return (existsSync(join(root, 'db', 'sqlite')) ||
|
|
515
|
+
existsSync(join(root, 'db', 'postgres')));
|
|
516
|
+
}
|
|
517
|
+
export const FabricSmoke = pikkuSessionlessFunc({
|
|
518
|
+
description: 'Run a clean-room Fabric smoke test: temp worktree, install, bootstrap, codegen, frontend checks, migrate, and pikku dev startup.',
|
|
519
|
+
input: FabricSmokeInput,
|
|
520
|
+
output: FabricSmokeOutput,
|
|
521
|
+
func: async (_services, input) => {
|
|
522
|
+
const root = await findProjectRoot(process.cwd());
|
|
523
|
+
const ref = await headSha(root);
|
|
524
|
+
const tempDir = await createTempWorktree(root);
|
|
525
|
+
const keepTempRequested = input.keepTemp ?? false;
|
|
526
|
+
const notes = [
|
|
527
|
+
'Smoke uses a detached temp worktree from HEAD, not your live working tree.',
|
|
528
|
+
'Frontend runtime generation/Caddy routing is not exercised here; frontend build/typecheck and pikku dev startup are.',
|
|
529
|
+
];
|
|
530
|
+
const steps = [];
|
|
531
|
+
const stepTimeoutMs = (input.timeoutSeconds ?? DEFAULT_STEP_TIMEOUT_SECONDS) * 1000;
|
|
532
|
+
const startupTimeoutMs = (input.startupTimeoutSeconds ?? DEFAULT_STARTUP_TIMEOUT_SECONDS) * 1000;
|
|
533
|
+
const port = await pickPort(input.port);
|
|
534
|
+
const packageJson = await readJsonSafe(join(tempDir, 'package.json'));
|
|
535
|
+
const packageManager = packageJson?.packageManager ?? DEFAULT_YARN_VERSION;
|
|
536
|
+
const pikkuBin = resolvePikkuBin();
|
|
537
|
+
const currentCliPackageRoot = resolveCurrentCliPackageRoot(pikkuBin);
|
|
538
|
+
const env = {
|
|
539
|
+
...process.env,
|
|
540
|
+
PIKKU_DEV_DIR: tempDir,
|
|
541
|
+
};
|
|
542
|
+
if (!(await isWorkingTreeClean(root))) {
|
|
543
|
+
notes.push('Your source worktree has uncommitted changes; this smoke run used committed HEAD only.');
|
|
544
|
+
}
|
|
545
|
+
if (!packageManager.startsWith('yarn@')) {
|
|
546
|
+
const result = {
|
|
547
|
+
ok: false,
|
|
548
|
+
root,
|
|
549
|
+
ref,
|
|
550
|
+
tempDir,
|
|
551
|
+
tempDirKept: true,
|
|
552
|
+
notes,
|
|
553
|
+
steps,
|
|
554
|
+
failure: `Unsupported packageManager for Fabric smoke: ${packageManager}`,
|
|
555
|
+
logTail: 'Fabric smoke expects package.json to declare a Yarn packageManager like "yarn@4.9.2".',
|
|
556
|
+
};
|
|
557
|
+
process.exitCode = 1;
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
let tempDirKept = true;
|
|
561
|
+
let failure;
|
|
562
|
+
let logTail;
|
|
563
|
+
let devChild = null;
|
|
564
|
+
const runningFrontends = [];
|
|
565
|
+
try {
|
|
566
|
+
const vendoredCli = await vendorCurrentCliPackage({
|
|
567
|
+
tempRoot: tempDir,
|
|
568
|
+
currentCliPackageRoot,
|
|
569
|
+
timeoutMs: stepTimeoutMs,
|
|
570
|
+
});
|
|
571
|
+
steps.push({
|
|
572
|
+
name: 'vendor @pikku/cli',
|
|
573
|
+
status: vendoredCli.tarballRelativePath ? 'passed' : 'failed',
|
|
574
|
+
durationMs: vendoredCli.durationMs,
|
|
575
|
+
detail: vendoredCli.detail,
|
|
576
|
+
});
|
|
577
|
+
if (!vendoredCli.tarballRelativePath) {
|
|
578
|
+
failure = 'vendor @pikku/cli';
|
|
579
|
+
logTail = vendoredCli.errorTail;
|
|
580
|
+
process.exitCode = 1;
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
root,
|
|
584
|
+
ref,
|
|
585
|
+
tempDir,
|
|
586
|
+
tempDirKept,
|
|
587
|
+
notes,
|
|
588
|
+
steps,
|
|
589
|
+
failure,
|
|
590
|
+
logTail,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const localDependencyRewrites = await rewriteLocalPikkuDependencies({
|
|
594
|
+
root: tempDir,
|
|
595
|
+
cliTarballRelativePath: vendoredCli.tarballRelativePath,
|
|
596
|
+
});
|
|
597
|
+
if (localDependencyRewrites.length > 0) {
|
|
598
|
+
steps.push({
|
|
599
|
+
name: 'rewrite local Pikku dependencies',
|
|
600
|
+
status: 'passed',
|
|
601
|
+
durationMs: 0,
|
|
602
|
+
detail: localDependencyRewrites.join('; '),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
for (const step of [
|
|
606
|
+
{
|
|
607
|
+
name: 'yarn install',
|
|
608
|
+
command: 'yarn',
|
|
609
|
+
commandArgs: ['install', '--mode=skip-build'],
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: 'pikku bootstrap',
|
|
613
|
+
command: process.execPath,
|
|
614
|
+
commandArgs: [pikkuBin, 'bootstrap'],
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: 'pikku all',
|
|
618
|
+
command: process.execPath,
|
|
619
|
+
commandArgs: [pikkuBin, 'all'],
|
|
620
|
+
},
|
|
621
|
+
]) {
|
|
622
|
+
const result = await runCommandStep({
|
|
623
|
+
name: step.name,
|
|
624
|
+
command: step.command,
|
|
625
|
+
commandArgs: step.commandArgs,
|
|
626
|
+
cwd: tempDir,
|
|
627
|
+
env,
|
|
628
|
+
timeoutMs: stepTimeoutMs,
|
|
629
|
+
});
|
|
630
|
+
steps.push({
|
|
631
|
+
name: step.name,
|
|
632
|
+
status: result.ok ? 'passed' : 'failed',
|
|
633
|
+
durationMs: result.durationMs,
|
|
634
|
+
detail: result.ok
|
|
635
|
+
? `${step.name} passed`
|
|
636
|
+
: result.timedOut
|
|
637
|
+
? `${step.name} timed out`
|
|
638
|
+
: `${step.name} failed`,
|
|
639
|
+
command: commandLabel(step.command, step.commandArgs),
|
|
640
|
+
});
|
|
641
|
+
if (!result.ok) {
|
|
642
|
+
failure = step.name;
|
|
643
|
+
logTail = result.tail;
|
|
644
|
+
process.exitCode = 1;
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
root,
|
|
648
|
+
ref,
|
|
649
|
+
tempDir,
|
|
650
|
+
tempDirKept,
|
|
651
|
+
notes,
|
|
652
|
+
steps,
|
|
653
|
+
failure,
|
|
654
|
+
logTail,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const frontendResult = await runFrontendChecks({
|
|
659
|
+
root: tempDir,
|
|
660
|
+
env,
|
|
661
|
+
timeoutMs: stepTimeoutMs,
|
|
662
|
+
steps,
|
|
663
|
+
});
|
|
664
|
+
if (!frontendResult.ok) {
|
|
665
|
+
failure = frontendResult.failure;
|
|
666
|
+
logTail = frontendResult.logTail;
|
|
667
|
+
process.exitCode = 1;
|
|
668
|
+
return {
|
|
669
|
+
ok: false,
|
|
670
|
+
root,
|
|
671
|
+
ref,
|
|
672
|
+
tempDir,
|
|
673
|
+
tempDirKept,
|
|
674
|
+
notes,
|
|
675
|
+
steps,
|
|
676
|
+
failure,
|
|
677
|
+
logTail,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (hasMigrations(tempDir)) {
|
|
681
|
+
const result = await runCommandStep({
|
|
682
|
+
name: 'pikku db migrate',
|
|
683
|
+
command: process.execPath,
|
|
684
|
+
commandArgs: [pikkuBin, 'db', 'migrate'],
|
|
685
|
+
cwd: tempDir,
|
|
686
|
+
env,
|
|
687
|
+
timeoutMs: stepTimeoutMs,
|
|
688
|
+
});
|
|
689
|
+
steps.push({
|
|
690
|
+
name: 'pikku db migrate',
|
|
691
|
+
status: result.ok ? 'passed' : 'failed',
|
|
692
|
+
durationMs: result.durationMs,
|
|
693
|
+
detail: result.ok
|
|
694
|
+
? 'database migrations passed'
|
|
695
|
+
: result.timedOut
|
|
696
|
+
? 'database migrations timed out'
|
|
697
|
+
: 'database migrations failed',
|
|
698
|
+
command: commandLabel(process.execPath, [pikkuBin, 'db', 'migrate']),
|
|
699
|
+
});
|
|
700
|
+
if (!result.ok) {
|
|
701
|
+
failure = 'pikku db migrate';
|
|
702
|
+
logTail = result.tail;
|
|
703
|
+
process.exitCode = 1;
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
root,
|
|
707
|
+
ref,
|
|
708
|
+
tempDir,
|
|
709
|
+
tempDirKept,
|
|
710
|
+
notes,
|
|
711
|
+
steps,
|
|
712
|
+
failure,
|
|
713
|
+
logTail,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
steps.push({
|
|
719
|
+
name: 'pikku db migrate',
|
|
720
|
+
status: 'skipped',
|
|
721
|
+
durationMs: 0,
|
|
722
|
+
detail: 'no db/sqlite or db/postgres directory found',
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
const devLogFile = join(tempDir, '.pikku-fabric-smoke-dev.log');
|
|
726
|
+
const logStream = createWriteStream(devLogFile, { flags: 'w' });
|
|
727
|
+
devChild = spawn(process.execPath, [pikkuBin, 'dev', '--hostname', 'localhost', '--port', String(port)], {
|
|
728
|
+
cwd: tempDir,
|
|
729
|
+
env,
|
|
730
|
+
detached: true,
|
|
731
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
732
|
+
});
|
|
733
|
+
devChild.stdout?.pipe(logStream);
|
|
734
|
+
devChild.stderr?.pipe(logStream);
|
|
735
|
+
const startupStartedAt = now();
|
|
736
|
+
const healthResult = await waitForHealth({
|
|
737
|
+
urls: [
|
|
738
|
+
`http://localhost:${port}/health-check`,
|
|
739
|
+
`http://127.0.0.1:${port}/health-check`,
|
|
740
|
+
],
|
|
741
|
+
timeoutMs: startupTimeoutMs,
|
|
742
|
+
child: devChild,
|
|
743
|
+
logFile: devLogFile,
|
|
744
|
+
});
|
|
745
|
+
steps.push({
|
|
746
|
+
name: 'pikku dev startup',
|
|
747
|
+
status: healthResult.ok ? 'passed' : 'failed',
|
|
748
|
+
durationMs: now() - startupStartedAt,
|
|
749
|
+
detail: healthResult.detail,
|
|
750
|
+
command: commandLabel(process.execPath, [
|
|
751
|
+
pikkuBin,
|
|
752
|
+
'dev',
|
|
753
|
+
'--hostname',
|
|
754
|
+
'localhost',
|
|
755
|
+
'--port',
|
|
756
|
+
String(port),
|
|
757
|
+
]),
|
|
758
|
+
});
|
|
759
|
+
await terminateProcessGroup(devChild);
|
|
760
|
+
logStream.end();
|
|
761
|
+
devChild = null;
|
|
762
|
+
if (!healthResult.ok) {
|
|
763
|
+
failure = 'pikku dev startup';
|
|
764
|
+
logTail = healthResult.tail;
|
|
765
|
+
process.exitCode = 1;
|
|
766
|
+
return {
|
|
767
|
+
ok: false,
|
|
768
|
+
root,
|
|
769
|
+
ref,
|
|
770
|
+
tempDir,
|
|
771
|
+
tempDirKept,
|
|
772
|
+
notes,
|
|
773
|
+
steps,
|
|
774
|
+
failure,
|
|
775
|
+
logTail,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const frontendStartResult = await startFrontends({
|
|
779
|
+
root: tempDir,
|
|
780
|
+
env,
|
|
781
|
+
timeoutMs: startupTimeoutMs,
|
|
782
|
+
steps,
|
|
783
|
+
});
|
|
784
|
+
runningFrontends.push(...frontendStartResult.running);
|
|
785
|
+
if (!frontendStartResult.ok) {
|
|
786
|
+
failure = frontendStartResult.failure;
|
|
787
|
+
logTail = frontendStartResult.logTail;
|
|
788
|
+
process.exitCode = 1;
|
|
789
|
+
return {
|
|
790
|
+
ok: false,
|
|
791
|
+
root,
|
|
792
|
+
ref,
|
|
793
|
+
tempDir,
|
|
794
|
+
tempDirKept,
|
|
795
|
+
notes,
|
|
796
|
+
steps,
|
|
797
|
+
failure,
|
|
798
|
+
logTail,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
if (!keepTempRequested) {
|
|
802
|
+
await stopManagedProcesses(runningFrontends);
|
|
803
|
+
await removeTempWorktree(root, tempDir);
|
|
804
|
+
tempDirKept = false;
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
ok: true,
|
|
808
|
+
root,
|
|
809
|
+
ref,
|
|
810
|
+
tempDir,
|
|
811
|
+
tempDirKept,
|
|
812
|
+
notes,
|
|
813
|
+
steps,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
finally {
|
|
817
|
+
await stopManagedProcesses(runningFrontends).catch(() => { });
|
|
818
|
+
if (devChild) {
|
|
819
|
+
await terminateProcessGroup(devChild).catch(() => { });
|
|
820
|
+
}
|
|
821
|
+
if (!failure && !keepTempRequested && tempDirKept) {
|
|
822
|
+
await removeTempWorktree(root, tempDir).catch(() => { });
|
|
823
|
+
tempDirKept = false;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
export const renderSmoke = (_services, output) => {
|
|
829
|
+
if (output.ok) {
|
|
830
|
+
console.log(`${added('passed')} ${dim(output.ref.slice(0, 8))} ${dim('·')} ${output.steps.length} steps`);
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
console.log(`${removed('failed')} ${dim(output.ref.slice(0, 8))} ${dim('·')} ${output.failure ?? 'smoke failed'}`);
|
|
834
|
+
}
|
|
835
|
+
for (const note of output.notes) {
|
|
836
|
+
console.log(dim(`note: ${note}`));
|
|
837
|
+
}
|
|
838
|
+
console.log(dim(`temp: ${output.tempDirKept ? output.tempDir : '(removed)'}`));
|
|
839
|
+
console.log();
|
|
840
|
+
for (const step of output.steps) {
|
|
841
|
+
const icon = step.status === 'passed'
|
|
842
|
+
? added('✓')
|
|
843
|
+
: step.status === 'failed'
|
|
844
|
+
? removed('✗')
|
|
845
|
+
: changed('•');
|
|
846
|
+
const detail = step.detail ? ` ${dim(step.detail)}` : '';
|
|
847
|
+
console.log(`${icon} ${step.name} ${dim(`(${formatDuration(step.durationMs)})`)}${detail}`);
|
|
848
|
+
if (step.command) {
|
|
849
|
+
console.log(` ${dim(step.command)}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (output.logTail) {
|
|
853
|
+
console.log();
|
|
854
|
+
console.log(dim('log tail:'));
|
|
855
|
+
console.log(output.logTail);
|
|
856
|
+
}
|
|
857
|
+
if (!output.ok) {
|
|
858
|
+
process.exitCode = 1;
|
|
859
|
+
}
|
|
860
|
+
};
|