@checkstack/scripts 0.3.4 → 0.4.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.
- package/package.json +15 -5
- package/src/commands/create.ts +16 -23
- package/src/commands/plugin-pack.ts +17 -28
- package/src/dev-tui/App.render.test.tsx +135 -0
- package/src/dev-tui/App.smoke.test.tsx +142 -0
- package/src/dev-tui/App.tsx +522 -0
- package/src/dev-tui/alert-buffer.test.ts +62 -0
- package/src/dev-tui/alert-buffer.ts +51 -0
- package/src/dev-tui/alt-screen.test.ts +66 -0
- package/src/dev-tui/alt-screen.ts +65 -0
- package/src/dev-tui/cli.tsx +89 -0
- package/src/dev-tui/fake-supervisor.ts +76 -0
- package/src/dev-tui/graceful-shutdown.test.ts +61 -0
- package/src/dev-tui/graceful-shutdown.ts +32 -0
- package/src/dev-tui/kill-tree.test.ts +47 -0
- package/src/dev-tui/kill-tree.ts +64 -0
- package/src/dev-tui/layout.test.ts +89 -0
- package/src/dev-tui/layout.ts +126 -0
- package/src/dev-tui/log-level.test.ts +94 -0
- package/src/dev-tui/log-level.ts +104 -0
- package/src/dev-tui/plain-runner.ts +60 -0
- package/src/dev-tui/process-config.test.ts +42 -0
- package/src/dev-tui/process-config.ts +61 -0
- package/src/dev-tui/readiness.test.ts +54 -0
- package/src/dev-tui/readiness.ts +44 -0
- package/src/dev-tui/scrollback.test.ts +83 -0
- package/src/dev-tui/scrollback.ts +82 -0
- package/src/dev-tui/supervisor.ts +231 -0
- package/src/dev-tui/text.test.ts +72 -0
- package/src/dev-tui/text.ts +101 -0
- package/src/dev-tui/types.ts +29 -0
- package/src/scaffold/index.ts +22 -0
- package/src/scaffold/resolve-versions.test.ts +49 -0
- package/src/scaffold/resolve-versions.ts +55 -0
- package/src/scaffold/rewrite-workspace-versions.test.ts +102 -0
- package/src/scaffold/rewrite-workspace-versions.ts +111 -0
- package/src/scaffold/scaffold-plugin.test.ts +209 -0
- package/src/scaffold/scaffold-plugin.ts +309 -0
- package/src/templates/backend/.changeset/initial.md.hbs +1 -1
- package/src/templates/backend/drizzle/0000_init.sql +7 -0
- package/src/templates/backend/drizzle/meta/0000_snapshot.json +65 -0
- package/src/templates/backend/drizzle/meta/_journal.json +13 -0
- package/src/templates/backend/drizzle.config.ts.hbs +5 -1
- package/src/templates/backend/package.json.hbs +7 -3
- package/src/templates/backend/src/index.ts.hbs +1 -1
- package/src/templates/backend/src/router.ts.hbs +1 -1
- package/src/templates/backend/src/service.ts.hbs +1 -1
- package/src/templates/common/.changeset/initial.md.hbs +1 -1
- package/src/templates/common/README.md.hbs +28 -11
- package/src/templates/common/package.json.hbs +1 -1
- package/src/templates/common/src/plugin-metadata.ts.hbs +1 -1
- package/src/templates/frontend/.changeset/initial.md.hbs +1 -1
- package/src/templates/frontend/package.json.hbs +2 -2
- package/src/templates/frontend/src/api.ts.hbs +2 -2
- package/src/templates/frontend/src/components/{{pluginNamePascal}}ListPage.tsx.hbs +1 -1
- package/src/templates/frontend/src/index.tsx.hbs +10 -4
- package/src/templates/standalone-root/.changeset/config.json.hbs +11 -0
- package/src/templates/standalone-root/.changeset/initial.md.hbs +9 -0
- package/src/templates/standalone-root/README.md.hbs +75 -0
- package/src/templates/standalone-root/eslint.config.mjs.hbs +37 -0
- package/src/templates/standalone-root/package.json.hbs +27 -0
- package/src/templates/standalone-root/tsconfig.json.hbs +13 -0
- package/src/templates.test.ts +20 -0
- package/src/tui/components.test.tsx +28 -0
- package/src/tui/components.tsx +159 -0
- package/src/tui/index.ts +31 -0
- package/src/tui/theme.test.ts +54 -0
- package/src/tui/theme.ts +60 -0
- package/src/utils/template.ts +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/scripts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Checkstack tooling: plugin scaffolding, codegen, and the plugin-pack CLI used by external plugin authors.",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"bin": {
|
|
11
11
|
"checkstack-scripts": "./src/cli.ts"
|
|
12
12
|
},
|
|
13
|
+
"exports": {
|
|
14
|
+
"./scaffold": {
|
|
15
|
+
"import": "./src/scaffold/index.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
13
18
|
"files": [
|
|
14
19
|
"src",
|
|
15
20
|
"README.md"
|
|
@@ -22,15 +27,20 @@
|
|
|
22
27
|
"typecheck": "tsgo -b"
|
|
23
28
|
},
|
|
24
29
|
"dependencies": {
|
|
25
|
-
"@checkstack/common": "0.
|
|
26
|
-
"
|
|
30
|
+
"@checkstack/common": "0.12.0",
|
|
31
|
+
"ansi-escapes": "^7.3.0",
|
|
27
32
|
"handlebars": "^4.7.8",
|
|
28
|
-
"
|
|
33
|
+
"ink": "^7.0.5",
|
|
34
|
+
"inquirer": "^13.4.1",
|
|
35
|
+
"react": "^19.2.7",
|
|
36
|
+
"tar": "^7.5.16",
|
|
37
|
+
"zod": "^4.4.3"
|
|
29
38
|
},
|
|
30
39
|
"devDependencies": {
|
|
31
40
|
"@checkstack/tsconfig": "0.0.7",
|
|
32
|
-
"@types/inquirer": "^8.2.10",
|
|
33
41
|
"@types/handlebars": "^4.1.0",
|
|
42
|
+
"@types/inquirer": "^8.2.10",
|
|
43
|
+
"@types/react": "^19.2.16",
|
|
34
44
|
"typescript": "^5.0.0"
|
|
35
45
|
}
|
|
36
46
|
}
|
package/src/commands/create.ts
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import inquirer from "inquirer";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
4
|
import {
|
|
6
5
|
validatePluginName,
|
|
7
6
|
pluginExists,
|
|
8
7
|
packageExists,
|
|
9
8
|
extractBaseName,
|
|
10
9
|
} from "../utils/validation";
|
|
10
|
+
import { registerHelpers, prepareTemplateData } from "../utils/template";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "../
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
|
|
18
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
scaffoldPlugin,
|
|
13
|
+
refreshMonorepoReferences,
|
|
14
|
+
type ScaffoldMode,
|
|
15
|
+
} from "../scaffold/scaffold-plugin";
|
|
19
16
|
|
|
20
17
|
interface PluginTypeChoice {
|
|
21
18
|
name: string;
|
|
@@ -191,18 +188,18 @@ export async function createCommand() {
|
|
|
191
188
|
`\n📦 Creating ${pluginType} ${locationLabel}: ${templateData.pluginName}`
|
|
192
189
|
);
|
|
193
190
|
|
|
194
|
-
const
|
|
195
|
-
|
|
191
|
+
const mode: ScaffoldMode = {
|
|
192
|
+
kind: "monorepo",
|
|
196
193
|
rootDir,
|
|
197
|
-
packageLocation,
|
|
198
|
-
|
|
199
|
-
);
|
|
194
|
+
location: packageLocation,
|
|
195
|
+
};
|
|
200
196
|
|
|
201
197
|
try {
|
|
202
|
-
const createdFiles =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
198
|
+
const { targetDir, createdFiles } = await scaffoldPlugin({
|
|
199
|
+
mode,
|
|
200
|
+
baseName: pluginBaseName,
|
|
201
|
+
description,
|
|
202
|
+
pluginType,
|
|
206
203
|
});
|
|
207
204
|
|
|
208
205
|
console.log(
|
|
@@ -222,12 +219,8 @@ export async function createCommand() {
|
|
|
222
219
|
// solution tsconfig so the new package is wired into the typecheck
|
|
223
220
|
// graph. Affects only tsconfig.json files; safe to rerun any time.
|
|
224
221
|
console.log("\n🔗 Refreshing TypeScript project references...");
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
["run", "typecheck:references:generate"],
|
|
228
|
-
{ stdio: "inherit" },
|
|
229
|
-
);
|
|
230
|
-
if (refResult.status !== 0) {
|
|
222
|
+
const refStatus = refreshMonorepoReferences({ mode });
|
|
223
|
+
if (refStatus !== 0) {
|
|
231
224
|
console.warn(
|
|
232
225
|
"⚠️ Failed to refresh references automatically. " +
|
|
233
226
|
"Run `bun run typecheck:references:generate` manually before typechecking.",
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
type InstallPackageMetadata,
|
|
11
11
|
type PluginBundleManifest,
|
|
12
12
|
} from "@checkstack/common";
|
|
13
|
+
import {
|
|
14
|
+
rewriteWorkspaceVersions,
|
|
15
|
+
type RewritablePackageJson,
|
|
16
|
+
} from "../scaffold/rewrite-workspace-versions";
|
|
17
|
+
import { createWorkspaceMapResolver } from "../scaffold/resolve-versions";
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* `plugin-pack` CLI.
|
|
@@ -315,35 +320,19 @@ async function packPackage({
|
|
|
315
320
|
}): Promise<string> {
|
|
316
321
|
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
317
322
|
const original = fs.readFileSync(pkgJsonPath, "utf8");
|
|
318
|
-
const pkg = JSON.parse(original) as InstallPackageMetadata &
|
|
319
|
-
|
|
320
|
-
peerDependencies?: Record<string, string>;
|
|
321
|
-
};
|
|
323
|
+
const pkg = JSON.parse(original) as InstallPackageMetadata &
|
|
324
|
+
RewritablePackageJson;
|
|
322
325
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const targetDir = workspaceMap.get(name);
|
|
334
|
-
if (!targetDir) {
|
|
335
|
-
throw new Error(
|
|
336
|
-
`Cannot resolve workspace dep '${name}' (declared in '${pkg.name}'). ` +
|
|
337
|
-
`Either move the package into the workspace or replace the workspace range with a concrete version.`,
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
const targetPkg = readJson<{ version: string }>(
|
|
341
|
-
path.join(targetDir, "package.json"),
|
|
342
|
-
);
|
|
343
|
-
block[name] = `^${targetPkg.version}`;
|
|
344
|
-
rewritten = true;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
326
|
+
const { rewritten, unresolved } = await rewriteWorkspaceVersions({
|
|
327
|
+
pkg,
|
|
328
|
+
resolveVersion: createWorkspaceMapResolver({ workspaceMap }),
|
|
329
|
+
});
|
|
330
|
+
if (unresolved.length > 0) {
|
|
331
|
+
const name = unresolved[0];
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Cannot resolve workspace dep '${name}' (declared in '${pkg.name}'). ` +
|
|
334
|
+
`Either move the package into the workspace or replace the workspace range with a concrete version.`,
|
|
335
|
+
);
|
|
347
336
|
}
|
|
348
337
|
|
|
349
338
|
try {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { renderToString } from "ink";
|
|
4
|
+
import { App } from "./App.tsx";
|
|
5
|
+
import { createFakeSupervisor } from "./fake-supervisor.ts";
|
|
6
|
+
import { computeLayout } from "./layout.ts";
|
|
7
|
+
import type { ScrollbackLine } from "./scrollback.ts";
|
|
8
|
+
|
|
9
|
+
const ALERTS_CAPACITY = 8;
|
|
10
|
+
|
|
11
|
+
/** Strip ANSI SGR sequences so we can measure printable width and content. */
|
|
12
|
+
function stripAnsi(value: string): string {
|
|
13
|
+
// Build the ESC (0x1b) prefix without a literal control char in the source,
|
|
14
|
+
// so we avoid both a control-char regex and any lint suppression.
|
|
15
|
+
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
16
|
+
return value.replace(ansiPattern, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Seed the focused (backend) pane with MANY lines that are each LONGER than the
|
|
21
|
+
* terminal width, so the only way the frame can stay within bounds is the
|
|
22
|
+
* height clamp plus per-line truncation working together.
|
|
23
|
+
*/
|
|
24
|
+
function manyLongLines(count: number, columns: number): ScrollbackLine[] {
|
|
25
|
+
const lines: ScrollbackLine[] = [];
|
|
26
|
+
for (let index = 0; index < count; index += 1) {
|
|
27
|
+
lines.push({
|
|
28
|
+
level: "info",
|
|
29
|
+
text: `line-${index}-`.padEnd(columns * 2, "x"),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("App height clamp (DOM-free render test)", () => {
|
|
36
|
+
const columns = 80;
|
|
37
|
+
// More lines than any tested terminal is tall, each wider than `columns`.
|
|
38
|
+
const seededLineCount = 200;
|
|
39
|
+
|
|
40
|
+
for (const rows of [24, 40]) {
|
|
41
|
+
it(`never overflows ${rows} rows when the focused pane floods output`, () => {
|
|
42
|
+
const supervisor = createFakeSupervisor();
|
|
43
|
+
const frame = renderToString(
|
|
44
|
+
<App
|
|
45
|
+
supervisor={supervisor}
|
|
46
|
+
size={{ rows, columns }}
|
|
47
|
+
initialFocused="backend"
|
|
48
|
+
preloadLines={{ backend: manyLongLines(seededLineCount, columns) }}
|
|
49
|
+
/>,
|
|
50
|
+
{ columns },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const lines = frame.split("\n");
|
|
54
|
+
|
|
55
|
+
// 1. The frame must never be taller than the terminal. This is the hard
|
|
56
|
+
// invariant the clamp guarantees.
|
|
57
|
+
expect(lines.length).toBeLessThanOrEqual(rows);
|
|
58
|
+
|
|
59
|
+
// 2. The clamp fills the terminal exactly (no overflow, no wasted band).
|
|
60
|
+
expect(lines.length).toBe(rows);
|
|
61
|
+
|
|
62
|
+
// 3. The pinned chrome must still be on-screen (not pushed off the bottom).
|
|
63
|
+
const plain = stripAnsi(frame);
|
|
64
|
+
expect(plain).toContain("quit");
|
|
65
|
+
expect(plain).toContain("alerts");
|
|
66
|
+
|
|
67
|
+
// 4. Per-line truncation holds: no rendered row exceeds the width.
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
expect(stripAnsi(line).length).toBeLessThanOrEqual(columns);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 5. The focused pane shows EXACTLY the computed log-row budget worth of
|
|
73
|
+
// seeded lines. The seeded lines start with "line-<n>-", so counting
|
|
74
|
+
// those rows tells us how many log rows actually rendered. The pre-fix
|
|
75
|
+
// `rows - 16` reserve under-filled this (e.g. 8 rows at rows=24 instead
|
|
76
|
+
// of the correct 15), so this assertion discriminates the fix.
|
|
77
|
+
const expectedLogRows = computeLayout({
|
|
78
|
+
size: { rows, columns },
|
|
79
|
+
alertCount: 0,
|
|
80
|
+
alertCapacity: ALERTS_CAPACITY,
|
|
81
|
+
}).logRows;
|
|
82
|
+
const logRowsRendered = lines.filter((line) =>
|
|
83
|
+
stripAnsi(line).includes("line-"),
|
|
84
|
+
).length;
|
|
85
|
+
expect(logRowsRendered).toBe(expectedLogRows);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
it("keeps the footer and alerts visible when alerts fill to capacity and logs flood", () => {
|
|
90
|
+
const supervisor = createFakeSupervisor();
|
|
91
|
+
const seededAlerts = Array.from({ length: 8 }, (_, index) => ({
|
|
92
|
+
source: "backend" as const,
|
|
93
|
+
level: "error" as const,
|
|
94
|
+
text: `alert-${index}-`.padEnd(columns * 2, "y"),
|
|
95
|
+
seq: index,
|
|
96
|
+
}));
|
|
97
|
+
const frame = renderToString(
|
|
98
|
+
<App
|
|
99
|
+
supervisor={supervisor}
|
|
100
|
+
size={{ rows: 24, columns }}
|
|
101
|
+
initialFocused="backend"
|
|
102
|
+
preloadLines={{ backend: manyLongLines(seededLineCount, columns) }}
|
|
103
|
+
initialAlerts={seededAlerts}
|
|
104
|
+
/>,
|
|
105
|
+
{ columns },
|
|
106
|
+
);
|
|
107
|
+
const lines = frame.split("\n");
|
|
108
|
+
expect(lines.length).toBeLessThanOrEqual(24);
|
|
109
|
+
const plain = stripAnsi(frame);
|
|
110
|
+
expect(plain).toContain("quit");
|
|
111
|
+
expect(plain).toContain("alerts (8)");
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
expect(stripAnsi(line).length).toBeLessThanOrEqual(columns);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("renders the seeded flood as the visible content of the focused pane", () => {
|
|
118
|
+
const supervisor = createFakeSupervisor();
|
|
119
|
+
const frame = renderToString(
|
|
120
|
+
<App
|
|
121
|
+
supervisor={supervisor}
|
|
122
|
+
size={{ rows: 24, columns }}
|
|
123
|
+
initialFocused="backend"
|
|
124
|
+
preloadLines={{ backend: manyLongLines(seededLineCount, columns) }}
|
|
125
|
+
/>,
|
|
126
|
+
{ columns },
|
|
127
|
+
);
|
|
128
|
+
// The seam actually seeds the focused pane: at least one of the flooded
|
|
129
|
+
// backend lines is visible (so the clamp is being exercised against real
|
|
130
|
+
// content, not an empty placeholder).
|
|
131
|
+
const plain = stripAnsi(frame);
|
|
132
|
+
expect(plain).toContain("line-");
|
|
133
|
+
expect(plain).not.toContain("Waiting for output...");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { renderToString } from "ink";
|
|
4
|
+
import { App, ShutdownScreen } from "./App.tsx";
|
|
5
|
+
import { SPINNER_FRAMES } from "../tui/index.ts";
|
|
6
|
+
import { createFakeSupervisor } from "./fake-supervisor.ts";
|
|
7
|
+
import { runPlainStreaming } from "./plain-runner.ts";
|
|
8
|
+
|
|
9
|
+
describe("App initial frame (DOM-free smoke test)", () => {
|
|
10
|
+
it("renders the chrome without throwing and starts the supervisor", () => {
|
|
11
|
+
const supervisor = createFakeSupervisor();
|
|
12
|
+
const frame = renderToString(<App supervisor={supervisor} />, {
|
|
13
|
+
columns: 100,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// App name + footer hints + every process label render in the first frame.
|
|
17
|
+
expect(frame).toContain("checkstack dev");
|
|
18
|
+
expect(frame).toContain("backend");
|
|
19
|
+
expect(frame).toContain("frontend");
|
|
20
|
+
expect(frame).toContain("deps");
|
|
21
|
+
expect(frame).toContain("alerts");
|
|
22
|
+
expect(frame).toContain("quit");
|
|
23
|
+
|
|
24
|
+
// The mount effect kicks off the supervisor.
|
|
25
|
+
expect(supervisor.started).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("ShutdownScreen", () => {
|
|
30
|
+
it("shows the shutdown heading + a spinner while services stop", () => {
|
|
31
|
+
const frame = renderToString(
|
|
32
|
+
<ShutdownScreen
|
|
33
|
+
width={80}
|
|
34
|
+
height={20}
|
|
35
|
+
frame={0}
|
|
36
|
+
statuses={{ deps: "ready", backend: "starting", frontend: "starting" }}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
expect(frame).toContain("Shutting down checkstack dev");
|
|
40
|
+
// Long-running services are listed and still spinning (not yet stopped).
|
|
41
|
+
expect(frame).toContain("backend");
|
|
42
|
+
expect(frame).toContain("frontend");
|
|
43
|
+
expect(frame).toContain(SPINNER_FRAMES[0]);
|
|
44
|
+
// The one-shot deps process is left running, so it is not listed.
|
|
45
|
+
expect(frame).not.toContain("✓");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("marks a stopped service with a check", () => {
|
|
49
|
+
const frame = renderToString(
|
|
50
|
+
<ShutdownScreen
|
|
51
|
+
width={80}
|
|
52
|
+
height={20}
|
|
53
|
+
frame={0}
|
|
54
|
+
statuses={{ deps: "ready", backend: "stopped", frontend: "starting" }}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
expect(frame).toContain("✓");
|
|
58
|
+
expect(frame).toContain("stopped");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("treats a non-zero exit during shutdown as stopped, not an error", () => {
|
|
62
|
+
// We killed the process on purpose; dev servers (e.g. Vite) often exit 143/1
|
|
63
|
+
// on SIGTERM, which the supervisor records as 'errored'. During teardown
|
|
64
|
+
// that is expected - show a check, never a cross.
|
|
65
|
+
const frame = renderToString(
|
|
66
|
+
<ShutdownScreen
|
|
67
|
+
width={80}
|
|
68
|
+
height={20}
|
|
69
|
+
frame={0}
|
|
70
|
+
statuses={{ deps: "ready", backend: "stopped", frontend: "errored" }}
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
expect(frame).not.toContain("✗");
|
|
74
|
+
// Both long-running services are shown as stopped (two checks).
|
|
75
|
+
expect(frame.match(/✓/g) ?? []).toHaveLength(2);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("runPlainStreaming (non-TTY fallback)", () => {
|
|
80
|
+
it("starts the supervisor and prefixes each line with its source", () => {
|
|
81
|
+
const supervisor = createFakeSupervisor();
|
|
82
|
+
const out: string[] = [];
|
|
83
|
+
runPlainStreaming({
|
|
84
|
+
supervisor,
|
|
85
|
+
write: (text) => out.push(text),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(supervisor.started).toBe(true);
|
|
89
|
+
|
|
90
|
+
supervisor.emitLine({
|
|
91
|
+
source: "backend",
|
|
92
|
+
level: "info",
|
|
93
|
+
text: "21:02:49 info: hello",
|
|
94
|
+
seq: 1,
|
|
95
|
+
});
|
|
96
|
+
expect(out.join("")).toContain("[backend] 21:02:49 info: hello");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("marks warn/error lines with a leading bang so they stay greppable", () => {
|
|
100
|
+
const supervisor = createFakeSupervisor();
|
|
101
|
+
const out: string[] = [];
|
|
102
|
+
runPlainStreaming({ supervisor, write: (text) => out.push(text) });
|
|
103
|
+
|
|
104
|
+
supervisor.emitLine({
|
|
105
|
+
source: "backend",
|
|
106
|
+
level: "warn",
|
|
107
|
+
text: "sandbox not ready",
|
|
108
|
+
seq: 1,
|
|
109
|
+
});
|
|
110
|
+
expect(out.some((chunk) => chunk.startsWith("!"))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("emits status transitions", () => {
|
|
114
|
+
const supervisor = createFakeSupervisor();
|
|
115
|
+
const out: string[] = [];
|
|
116
|
+
runPlainStreaming({ supervisor, write: (text) => out.push(text) });
|
|
117
|
+
|
|
118
|
+
supervisor.emitStatus({ id: "frontend", status: "ready" });
|
|
119
|
+
expect(out.join("")).toContain("[frontend] status: ready");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("shuts down the supervisor and invokes the completion callback", async () => {
|
|
123
|
+
const supervisor = createFakeSupervisor();
|
|
124
|
+
const out: string[] = [];
|
|
125
|
+
let completed = false;
|
|
126
|
+
const shutdown = runPlainStreaming({
|
|
127
|
+
supervisor,
|
|
128
|
+
write: (text) => out.push(text),
|
|
129
|
+
onShutdownComplete: () => {
|
|
130
|
+
completed = true;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
shutdown();
|
|
135
|
+
// Allow the shutdown microtask to resolve.
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
137
|
+
|
|
138
|
+
expect(supervisor.didShutdown).toBe(true);
|
|
139
|
+
expect(completed).toBe(true);
|
|
140
|
+
expect(out.join("")).toContain("shutting down");
|
|
141
|
+
});
|
|
142
|
+
});
|