@rigkit/cli 0.2.7 → 0.2.8
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/README.md +12 -3
- package/package.json +4 -7
- package/src/cli.test.ts +139 -7
- package/src/cli.ts +699 -235
- package/src/completion.test.ts +139 -10
- package/src/completion.ts +427 -72
- package/src/init.ts +6 -4
- package/src/project.test.ts +33 -3
- package/src/project.ts +99 -11
- package/src/run-logger.test.ts +92 -0
- package/src/run-logger.ts +203 -0
- package/src/run-presenter.ts +176 -299
- package/src/ui.ts +159 -0
- package/src/version.ts +1 -1
- package/src/remote-project.test.ts +0 -55
- package/src/remote-project.ts +0 -225
package/src/run-presenter.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// Append-only run presenter. Each runtime event prints zero or one line to
|
|
2
|
+
// stderr. No spinners, no re-renders. Looks the same in a TTY, a CI log, and an
|
|
3
|
+
// LLM transcript — and it pipes cleanly.
|
|
4
|
+
|
|
5
|
+
import { accent, bold, clip, dim, err, ok, sym, termWidth, warn } from "./ui.ts";
|
|
5
6
|
|
|
6
7
|
export type RunPresenter = {
|
|
7
8
|
render(event: { type: string; [key: string]: unknown }): void;
|
|
@@ -10,346 +11,209 @@ export type RunPresenter = {
|
|
|
10
11
|
close(): void;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
type StepStatus = "running" | "done" | "cached" | "failed";
|
|
14
|
-
|
|
15
|
-
type RunStep = {
|
|
16
|
-
index: number;
|
|
17
|
-
path: string;
|
|
18
|
-
status: StepStatus;
|
|
19
|
-
detail: string;
|
|
20
|
-
logs: string[];
|
|
21
|
-
};
|
|
22
|
-
|
|
23
14
|
export function createRunPresenter(operation: string): RunPresenter | undefined {
|
|
24
|
-
if (
|
|
15
|
+
if (process.env.RIGKIT_RENDER === "0") return undefined;
|
|
25
16
|
|
|
26
|
-
const log = createLogUpdate(normalizedStderr(), {
|
|
27
|
-
defaultHeight: 24,
|
|
28
|
-
defaultWidth: 80,
|
|
29
|
-
});
|
|
30
|
-
const spinner = ora({
|
|
31
|
-
color: "cyan",
|
|
32
|
-
discardStdin: false,
|
|
33
|
-
isEnabled: true,
|
|
34
|
-
spinner: "dots",
|
|
35
|
-
stream: process.stderr,
|
|
36
|
-
});
|
|
37
|
-
const steps: RunStep[] = [];
|
|
38
|
-
const stepsByPath = new Map<string, RunStep>();
|
|
39
|
-
let totalSteps = 0;
|
|
40
17
|
let workflow = "";
|
|
41
|
-
let
|
|
42
|
-
let
|
|
43
|
-
|
|
18
|
+
let totalNodes = 0;
|
|
19
|
+
let cachedNodes = 0;
|
|
20
|
+
const seenNodes = new Set<string>();
|
|
21
|
+
const completedNodes = new Set<string>();
|
|
22
|
+
let lastNodePath: string | undefined;
|
|
44
23
|
let closed = false;
|
|
45
|
-
let paused = false;
|
|
46
|
-
let timer: ReturnType<typeof setInterval> | undefined;
|
|
47
24
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
log(renderTimeline({
|
|
51
|
-
activePath,
|
|
52
|
-
finalStatus,
|
|
53
|
-
operation,
|
|
54
|
-
phase,
|
|
55
|
-
spinnerFrame: spinner.frame().trimEnd(),
|
|
56
|
-
steps,
|
|
57
|
-
totalSteps,
|
|
58
|
-
workflow,
|
|
59
|
-
}));
|
|
25
|
+
const write = (line: string): void => {
|
|
26
|
+
process.stderr.write(`${line}\n`);
|
|
60
27
|
};
|
|
61
28
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
timer = undefined;
|
|
29
|
+
const indent = (line: string): string => ` ${line}`;
|
|
30
|
+
|
|
31
|
+
const trim = (value: string): string => clip(value, Math.max(40, termWidth() - 4));
|
|
32
|
+
|
|
33
|
+
const headerLine = (): string => {
|
|
34
|
+
const left = workflow ? `${bold(operation)} ${dim(workflow)}` : bold(operation);
|
|
35
|
+
return `${accent(sym.active)} ${left}`;
|
|
70
36
|
};
|
|
71
|
-
startTimer();
|
|
72
|
-
refresh();
|
|
73
37
|
|
|
74
38
|
return {
|
|
75
39
|
render(event) {
|
|
76
40
|
if (closed) return;
|
|
77
41
|
switch (event.type) {
|
|
78
|
-
case "definition.loaded":
|
|
79
|
-
workflow =
|
|
80
|
-
phase = `Loaded ${workflow}`;
|
|
42
|
+
case "definition.loaded": {
|
|
43
|
+
workflow = stringField(event.workflow) ?? workflow;
|
|
81
44
|
break;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
45
|
+
}
|
|
46
|
+
case "plan.created": {
|
|
47
|
+
workflow = stringField(event.workflow) ?? workflow;
|
|
48
|
+
totalNodes = numberField(event.nodeCount) ?? totalNodes;
|
|
49
|
+
cachedNodes = numberField(event.cachedNodeCount) ?? cachedNodes;
|
|
50
|
+
write(headerLine());
|
|
51
|
+
write(indent(dim(planSummary(totalNodes, cachedNodes))));
|
|
86
52
|
break;
|
|
87
|
-
|
|
88
|
-
|
|
53
|
+
}
|
|
54
|
+
case "workflow.apply.started": {
|
|
55
|
+
workflow = stringField(event.workflow) ?? workflow;
|
|
56
|
+
write(`${accent(sym.active)} workflow ${bold(workflow || operation)}`);
|
|
89
57
|
break;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
58
|
+
}
|
|
59
|
+
case "workflow.apply.completed": {
|
|
60
|
+
const name = stringField(event.workflow) ?? workflow;
|
|
61
|
+
totalNodes = numberField(event.nodeCount) ?? totalNodes;
|
|
62
|
+
cachedNodes = numberField(event.cachedNodeCount) ?? cachedNodes;
|
|
63
|
+
const summary = totalNodes > 0 ? ` ${dim(planSummary(totalNodes, cachedNodes))}` : "";
|
|
64
|
+
write(`${ok(sym.ok)} ${bold(name || "workflow")} ${dim("prepared")}${summary}`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "node.cached": {
|
|
68
|
+
const path = stringField(event.nodePath);
|
|
69
|
+
if (!path || completedNodes.has(path)) break;
|
|
70
|
+
completedNodes.add(path);
|
|
71
|
+
seenNodes.add(path);
|
|
72
|
+
write(indent(`${dim(sym.ok)} ${dim(path)} ${dim("cached")}`));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "node.started": {
|
|
76
|
+
const path = stringField(event.nodePath);
|
|
77
|
+
if (!path || seenNodes.has(path)) break;
|
|
78
|
+
seenNodes.add(path);
|
|
79
|
+
lastNodePath = path;
|
|
80
|
+
write(indent(`${accent(sym.active)} ${bold(path)}`));
|
|
94
81
|
break;
|
|
82
|
+
}
|
|
95
83
|
case "node.completed": {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
const path = stringField(event.nodePath);
|
|
85
|
+
if (!path || completedNodes.has(path)) break;
|
|
86
|
+
completedNodes.add(path);
|
|
87
|
+
if (lastNodePath === path) lastNodePath = undefined;
|
|
88
|
+
const detail = compactId(event.runId);
|
|
89
|
+
const tail = detail ? ` ${dim(detail)}` : "";
|
|
90
|
+
write(indent(`${ok(sym.ok)} ${path}${tail}`));
|
|
100
91
|
break;
|
|
101
92
|
}
|
|
102
|
-
case "vm.created":
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
case "vm.created": {
|
|
94
|
+
const vmId = stringField(event.vmId);
|
|
95
|
+
const from = stringField(event.fromSnapshotId);
|
|
96
|
+
const label = from ? `vm ${vmId ?? ""} from ${from}` : `vm ${vmId ?? "ready"}`;
|
|
97
|
+
write(indent(` ${dim(label)}`));
|
|
106
98
|
break;
|
|
99
|
+
}
|
|
107
100
|
case "command.started": {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
activePath = step.path;
|
|
111
|
-
step.detail = String(event.command ?? event.commandName ?? "command");
|
|
112
|
-
phase = `Command ${String(event.commandName ?? "command")}`;
|
|
101
|
+
const command = stringField(event.command) ?? stringField(event.commandName);
|
|
102
|
+
if (command) write(indent(` ${dim(`$ ${trim(command)}`)}`));
|
|
113
103
|
break;
|
|
114
104
|
}
|
|
115
105
|
case "command.output": {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
const data = stringField(event.data);
|
|
107
|
+
if (!data) break;
|
|
108
|
+
for (const line of data.replace(/\r/g, "").split("\n")) {
|
|
109
|
+
if (!line) continue;
|
|
110
|
+
write(indent(` ${dim(trim(line))}`));
|
|
111
|
+
}
|
|
119
112
|
break;
|
|
120
113
|
}
|
|
121
|
-
case "command.completed":
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
stringField(event.
|
|
125
|
-
`
|
|
126
|
-
|
|
114
|
+
case "command.completed": {
|
|
115
|
+
const exitCode = numberField(event.exitCode) ?? 0;
|
|
116
|
+
if (exitCode !== 0) {
|
|
117
|
+
const name = stringField(event.commandName) ?? "command";
|
|
118
|
+
write(indent(` ${err(`${name} exited ${exitCode}`)}`));
|
|
119
|
+
}
|
|
127
120
|
break;
|
|
121
|
+
}
|
|
128
122
|
case "log.output": {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
const data = stringField(event.data);
|
|
124
|
+
if (!data) break;
|
|
125
|
+
const stream = stringField(event.stream);
|
|
126
|
+
// console.debug is intentionally silent at the terminal — it lives
|
|
127
|
+
// in the run log file only. Surface it live with RIGKIT_DEBUG=1.
|
|
128
|
+
if (stream === "debug" && process.env.RIGKIT_DEBUG !== "1") break;
|
|
129
|
+
const label = stringField(event.label);
|
|
130
|
+
const { icon, style } = logStreamPresentation(stream);
|
|
131
|
+
for (const line of data.replace(/\r/g, "").split("\n")) {
|
|
132
|
+
if (!line) continue;
|
|
133
|
+
const prefix = label ? `${dim(`[${label}]`)} ` : "";
|
|
134
|
+
write(indent(` ${icon} ${prefix}${style(trim(line))}`));
|
|
135
|
+
}
|
|
132
136
|
break;
|
|
133
137
|
}
|
|
134
|
-
case "interaction.awaiting_user":
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
case "interaction.awaiting_user": {
|
|
139
|
+
const label = stringField(event.title) ?? stringField(event.label) ?? "user interaction";
|
|
140
|
+
const url = stringField(event.url);
|
|
141
|
+
write(indent(`${accent(sym.arrow)} waiting on ${bold(label)}`));
|
|
142
|
+
if (url) write(indent(` ${dim(url)}`));
|
|
137
143
|
break;
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
}
|
|
145
|
+
case "interaction.completed": {
|
|
146
|
+
const label = stringField(event.title) ?? stringField(event.label) ?? "interaction";
|
|
147
|
+
write(indent(`${ok(sym.ok)} ${dim(`${label} completed`)}`));
|
|
140
148
|
break;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
149
|
+
}
|
|
150
|
+
case "artifact.created": {
|
|
151
|
+
const kind = stringField(event.kind) ?? "artifact";
|
|
152
|
+
const provider = stringField(event.providerId);
|
|
153
|
+
const label = provider ? `${provider}:${kind}` : kind;
|
|
154
|
+
write(indent(` ${dim(`+ ${label}`)}`));
|
|
147
155
|
break;
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
}
|
|
157
|
+
case "workspace.create.started": {
|
|
158
|
+
const name = stringField(event.workspaceName) ?? "workspace";
|
|
159
|
+
write(`${accent(sym.active)} creating workspace ${bold(name)}`);
|
|
150
160
|
break;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
activePath = undefined;
|
|
161
|
+
}
|
|
162
|
+
case "workspace.ready": {
|
|
163
|
+
const id = stringField(event.workspaceId) ?? "workspace";
|
|
164
|
+
write(`${ok(sym.ok)} ${bold(id)} ${dim("ready")}`);
|
|
156
165
|
break;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
}
|
|
167
|
+
case "workspace.remove.started": {
|
|
168
|
+
const name = stringField(event.workspaceName) ?? "workspace";
|
|
169
|
+
write(`${accent(sym.active)} removing workspace ${bold(name)}`);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "workspace.remove.completed": {
|
|
173
|
+
const name = stringField(event.workspaceName) ?? "workspace";
|
|
174
|
+
write(`${ok(sym.ok)} removed ${bold(name)}`);
|
|
163
175
|
break;
|
|
176
|
+
}
|
|
177
|
+
case "workspace.operation.started": {
|
|
178
|
+
const name = stringField(event.workspaceName) ?? "workspace";
|
|
179
|
+
const op = stringField(event.operationId) ?? "operation";
|
|
180
|
+
write(`${accent(sym.active)} running ${bold(op)} on ${bold(name)}`);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case "workspace.operation.completed": {
|
|
184
|
+
const name = stringField(event.workspaceName) ?? "workspace";
|
|
185
|
+
const op = stringField(event.operationId) ?? "operation";
|
|
186
|
+
write(`${ok(sym.ok)} ran ${bold(op)} on ${bold(name)}`);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "run.completed":
|
|
190
|
+
case "run.failed": {
|
|
191
|
+
// Run terminal events are owned by the CLI's failure renderer so the
|
|
192
|
+
// structured failure block stays in one place.
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
164
195
|
}
|
|
165
|
-
refresh();
|
|
166
196
|
},
|
|
167
197
|
pause() {
|
|
168
|
-
|
|
169
|
-
log(renderTimeline({
|
|
170
|
-
activePath,
|
|
171
|
-
finalStatus,
|
|
172
|
-
operation,
|
|
173
|
-
phase,
|
|
174
|
-
spinnerFrame: spinner.frame().trimEnd(),
|
|
175
|
-
steps,
|
|
176
|
-
totalSteps,
|
|
177
|
-
workflow,
|
|
178
|
-
}));
|
|
179
|
-
paused = true;
|
|
180
|
-
stopTimer();
|
|
181
|
-
log.done();
|
|
198
|
+
// No-op — output is already append-only.
|
|
182
199
|
},
|
|
183
200
|
resume() {
|
|
184
|
-
|
|
185
|
-
paused = false;
|
|
186
|
-
startTimer();
|
|
187
|
-
refresh();
|
|
201
|
+
// No-op — output is already append-only.
|
|
188
202
|
},
|
|
189
203
|
close() {
|
|
190
|
-
if (closed) return;
|
|
191
|
-
if (paused) {
|
|
192
|
-
paused = false;
|
|
193
|
-
}
|
|
194
|
-
refresh();
|
|
195
204
|
closed = true;
|
|
196
|
-
stopTimer();
|
|
197
|
-
log.done();
|
|
198
205
|
},
|
|
199
206
|
};
|
|
200
207
|
}
|
|
201
208
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
});
|
|
207
|
-
Object.defineProperty(stream, "rows", {
|
|
208
|
-
get: () => process.stderr.rows || 24,
|
|
209
|
-
});
|
|
210
|
-
stream.write = process.stderr.write.bind(process.stderr) as typeof process.stderr.write;
|
|
211
|
-
return stream;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function canUsePresenter(): boolean {
|
|
215
|
-
return Boolean(
|
|
216
|
-
process.env.RIGKIT_RENDER !== "0" &&
|
|
217
|
-
process.stderr.isTTY,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function renderTimeline(input: {
|
|
222
|
-
activePath: string | undefined;
|
|
223
|
-
finalStatus: "running" | "completed" | "failed";
|
|
224
|
-
operation: string;
|
|
225
|
-
phase: string;
|
|
226
|
-
spinnerFrame: string;
|
|
227
|
-
steps: RunStep[];
|
|
228
|
-
totalSteps: number;
|
|
229
|
-
workflow: string;
|
|
230
|
-
}): string {
|
|
231
|
-
const done = input.steps.filter((step) => step.status === "done" || step.status === "cached").length;
|
|
232
|
-
const total = input.totalSteps || input.steps.length;
|
|
233
|
-
const progress = total > 0 ? `${done}/${total} steps` : `${input.steps.length} steps`;
|
|
234
|
-
const title = input.workflow ? `${input.operation} ${input.workflow}` : input.operation;
|
|
235
|
-
const lines = [
|
|
236
|
-
`${statusPrefix(input.finalStatus, input.spinnerFrame)} ${chalk.bold(title)} ${chalk.dim(input.phase)} ${chalk.dim(`(${progress})`)}`,
|
|
237
|
-
"",
|
|
238
|
-
...timelineLines(input),
|
|
239
|
-
];
|
|
240
|
-
return lines.join("\n");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function timelineLines(input: {
|
|
244
|
-
activePath: string | undefined;
|
|
245
|
-
spinnerFrame: string;
|
|
246
|
-
steps: RunStep[];
|
|
247
|
-
}): string[] {
|
|
248
|
-
const visible = input.steps.length > 8 ? input.steps.slice(-8) : input.steps;
|
|
249
|
-
const lines: string[] = [];
|
|
250
|
-
if (visible.length < input.steps.length) {
|
|
251
|
-
lines.push(chalk.dim(`... ${input.steps.length - visible.length} earlier steps`));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const active = input.activePath ? input.steps.find((step) => step.path === input.activePath) : undefined;
|
|
255
|
-
for (const step of visible) {
|
|
256
|
-
lines.push(stepLine(step, input.spinnerFrame));
|
|
257
|
-
if (step.path === active?.path && (step.status === "running" || step.status === "failed")) {
|
|
258
|
-
if (step.detail) lines.push(chalk.dim(` ${clip(step.detail, 100)}`));
|
|
259
|
-
for (const line of step.logs.slice(-4)) lines.push(chalk.dim(` ${clip(line, 100)}`));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return lines.length > 0 ? lines : [chalk.dim("waiting for runtime events")];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function stepLine(step: RunStep, spinnerFrame: string): string {
|
|
267
|
-
const index = String(step.index).padStart(2, " ");
|
|
268
|
-
switch (step.status) {
|
|
269
|
-
case "running":
|
|
270
|
-
return `${chalk.dim(index)} ${spinnerFrame} ${chalk.bold(step.path)}`;
|
|
271
|
-
case "cached":
|
|
272
|
-
return `${chalk.dim(index)} ${logSymbols.success} ${chalk.dim(step.path)}${step.detail ? chalk.dim(` cached ${step.detail}`) : ""}`;
|
|
273
|
-
case "done":
|
|
274
|
-
return `${chalk.dim(index)} ${logSymbols.success} ${chalk.dim(step.path)}${step.detail ? chalk.dim(` ${step.detail}`) : ""}`;
|
|
275
|
-
case "failed":
|
|
276
|
-
return `${chalk.dim(index)} ${logSymbols.error} ${chalk.bold(step.path)}`;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function statusPrefix(status: "running" | "completed" | "failed", spinnerFrame: string): string {
|
|
281
|
-
if (status === "completed") return logSymbols.success;
|
|
282
|
-
if (status === "failed") return logSymbols.error;
|
|
283
|
-
return spinnerFrame;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function stepFor(
|
|
287
|
-
steps: RunStep[],
|
|
288
|
-
stepsByPath: Map<string, RunStep>,
|
|
289
|
-
rawPath: unknown,
|
|
290
|
-
): RunStep {
|
|
291
|
-
const path = typeof rawPath === "string" && rawPath.length > 0 ? rawPath : "runtime";
|
|
292
|
-
const existing = stepsByPath.get(path);
|
|
293
|
-
if (existing) return existing;
|
|
294
|
-
const step: RunStep = {
|
|
295
|
-
index: steps.length + 1,
|
|
296
|
-
path,
|
|
297
|
-
status: "running",
|
|
298
|
-
detail: "loading",
|
|
299
|
-
logs: [],
|
|
300
|
-
};
|
|
301
|
-
steps.push(step);
|
|
302
|
-
stepsByPath.set(path, step);
|
|
303
|
-
return step;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function markStep(step: RunStep, status: StepStatus, detail: string): void {
|
|
307
|
-
step.status = status;
|
|
308
|
-
step.detail = detail;
|
|
309
|
-
if (status === "done" || status === "cached") {
|
|
310
|
-
step.logs = [];
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function setActiveDetail(stepsByPath: Map<string, RunStep>, path: string | undefined, detail: string): void {
|
|
315
|
-
if (!path) return;
|
|
316
|
-
const step = stepsByPath.get(path);
|
|
317
|
-
if (!step) return;
|
|
318
|
-
step.detail = detail;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function collapseActiveStep(stepsByPath: Map<string, RunStep>, path: string | undefined): void {
|
|
322
|
-
if (!path) return;
|
|
323
|
-
const step = stepsByPath.get(path);
|
|
324
|
-
if (step?.status === "running") markStep(step, "done", "completed");
|
|
209
|
+
function planSummary(total: number, cached: number): string {
|
|
210
|
+
if (total === 0) return "no nodes";
|
|
211
|
+
if (cached === 0) return `${total} ${noun("node", total)}`;
|
|
212
|
+
return `${cached}/${total} cached`;
|
|
325
213
|
}
|
|
326
214
|
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
stepsByPath: Map<string, RunStep>,
|
|
330
|
-
path: string | undefined,
|
|
331
|
-
message: string,
|
|
332
|
-
): string | undefined {
|
|
333
|
-
const step = path ? stepsByPath.get(path) : lastStepToFail(steps);
|
|
334
|
-
if (!step) return undefined;
|
|
335
|
-
markStep(step, "failed", message);
|
|
336
|
-
return step.path;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function lastStepToFail(steps: RunStep[]): RunStep | undefined {
|
|
340
|
-
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
341
|
-
const step = steps[index]!;
|
|
342
|
-
if (step.status === "running") return step;
|
|
343
|
-
}
|
|
344
|
-
return steps[steps.length - 1];
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function appendLog(lines: string[], value: string): void {
|
|
348
|
-
for (const line of value.replace(/\r/g, "").split("\n")) {
|
|
349
|
-
if (line.length === 0) continue;
|
|
350
|
-
lines.push(line);
|
|
351
|
-
}
|
|
352
|
-
if (lines.length > 200) lines.splice(0, lines.length - 200);
|
|
215
|
+
function noun(singular: string, count: number): string {
|
|
216
|
+
return count === 1 ? singular : `${singular}s`;
|
|
353
217
|
}
|
|
354
218
|
|
|
355
219
|
function compactId(value: unknown): string {
|
|
@@ -357,17 +221,30 @@ function compactId(value: unknown): string {
|
|
|
357
221
|
}
|
|
358
222
|
|
|
359
223
|
function stringField(value: unknown): string | undefined {
|
|
360
|
-
return typeof value === "string" ? value : undefined;
|
|
224
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
361
225
|
}
|
|
362
226
|
|
|
363
227
|
function numberField(value: unknown): number | undefined {
|
|
364
228
|
return typeof value === "number" ? value : undefined;
|
|
365
229
|
}
|
|
366
230
|
|
|
367
|
-
function
|
|
368
|
-
|
|
231
|
+
function logStreamPresentation(stream: string | undefined): {
|
|
232
|
+
icon: string;
|
|
233
|
+
style: (text: string) => string;
|
|
234
|
+
} {
|
|
235
|
+
switch (stream) {
|
|
236
|
+
case "warn":
|
|
237
|
+
return { icon: warn("!"), style: warn };
|
|
238
|
+
case "stderr":
|
|
239
|
+
case "error":
|
|
240
|
+
return { icon: err(sym.err), style: err };
|
|
241
|
+
case "debug":
|
|
242
|
+
return { icon: dim("·"), style: (text: string) => dim(`${dim("debug")} ${text}`) };
|
|
243
|
+
case "stdout":
|
|
244
|
+
case "info":
|
|
245
|
+
case "log":
|
|
246
|
+
default:
|
|
247
|
+
return { icon: dim(sym.dot), style: dim };
|
|
248
|
+
}
|
|
369
249
|
}
|
|
370
250
|
|
|
371
|
-
function clip(value: string, max: number): string {
|
|
372
|
-
return value.length > max ? `${value.slice(0, Math.max(0, max - 3))}...` : value;
|
|
373
|
-
}
|