@rigkit/cli 0.1.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 +25 -0
- package/package.json +45 -0
- package/src/cli.test.ts +253 -0
- package/src/cli.ts +1911 -0
- package/src/completion.test.ts +204 -0
- package/src/completion.ts +444 -0
- package/src/init.test.ts +83 -0
- package/src/init.ts +242 -0
- package/src/interaction.test.ts +28 -0
- package/src/interaction.ts +33 -0
- package/src/project.test.ts +51 -0
- package/src/project.ts +94 -0
- package/src/remote-project.test.ts +55 -0
- package/src/remote-project.ts +225 -0
- package/src/run-presenter.ts +373 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { defaultRigkitHome } from "@rigkit/runtime-client";
|
|
15
|
+
import { DEFAULT_CONFIG_FILE } from "./project.ts";
|
|
16
|
+
|
|
17
|
+
export type GithubProjectTarget = {
|
|
18
|
+
kind: "github";
|
|
19
|
+
raw: string;
|
|
20
|
+
owner: string;
|
|
21
|
+
repo: string;
|
|
22
|
+
ref?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type MaterializedGithubProject = {
|
|
26
|
+
target: GithubProjectTarget;
|
|
27
|
+
projectId: string;
|
|
28
|
+
projectDir: string;
|
|
29
|
+
configPath: string;
|
|
30
|
+
statePath: string;
|
|
31
|
+
commitSha: string;
|
|
32
|
+
ref: string;
|
|
33
|
+
repoUrl: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type GithubRepoInfo = {
|
|
37
|
+
default_branch?: unknown;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type GithubCommitInfo = {
|
|
41
|
+
sha?: unknown;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function parseGithubProjectTarget(value: string): GithubProjectTarget | undefined {
|
|
45
|
+
const match = /^github:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:(?:#|@)(.+))?$/.exec(value);
|
|
46
|
+
if (!match) return undefined;
|
|
47
|
+
const owner = match[1]!;
|
|
48
|
+
const repo = match[2]!.replace(/\.git$/, "");
|
|
49
|
+
const ref = match[3]?.trim() || undefined;
|
|
50
|
+
if (!repo) return undefined;
|
|
51
|
+
return {
|
|
52
|
+
kind: "github",
|
|
53
|
+
raw: value,
|
|
54
|
+
owner,
|
|
55
|
+
repo,
|
|
56
|
+
...(ref ? { ref } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function splitGithubProjectTarget(args: string[]): {
|
|
61
|
+
target?: GithubProjectTarget;
|
|
62
|
+
args: string[];
|
|
63
|
+
} {
|
|
64
|
+
const first = args[0];
|
|
65
|
+
const target = first ? parseGithubProjectTarget(first) : undefined;
|
|
66
|
+
if (!target) return { args };
|
|
67
|
+
return { target, args: args.slice(1) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function materializeGithubProject(
|
|
71
|
+
target: GithubProjectTarget,
|
|
72
|
+
options: { rigkitHome?: string } = {},
|
|
73
|
+
): Promise<MaterializedGithubProject> {
|
|
74
|
+
const repoUrl = `https://github.com/${target.owner}/${target.repo}.git`;
|
|
75
|
+
const ref = target.ref ?? await readDefaultBranch(target);
|
|
76
|
+
const commitSha = await resolveCommitSha(target, ref);
|
|
77
|
+
const projectId = remoteProjectId({ repoUrl, ref, commitSha, configPath: DEFAULT_CONFIG_FILE });
|
|
78
|
+
const projectRoot = join(options.rigkitHome ?? defaultRigkitHome(), "projects", projectId);
|
|
79
|
+
const projectDir = join(projectRoot, "checkout");
|
|
80
|
+
const configPath = join(projectDir, DEFAULT_CONFIG_FILE);
|
|
81
|
+
const statePath = join(projectRoot, "state.sqlite");
|
|
82
|
+
|
|
83
|
+
if (!existsSync(configPath)) {
|
|
84
|
+
await downloadGithubTarball(target, commitSha, projectDir);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!existsSync(configPath)) {
|
|
88
|
+
throw new Error(`Remote project ${target.raw} does not contain ${DEFAULT_CONFIG_FILE}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
installProjectDependenciesIfNeeded(projectDir);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
target,
|
|
95
|
+
projectId,
|
|
96
|
+
projectDir,
|
|
97
|
+
configPath,
|
|
98
|
+
statePath,
|
|
99
|
+
commitSha,
|
|
100
|
+
ref,
|
|
101
|
+
repoUrl,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function remoteProjectId(input: {
|
|
106
|
+
repoUrl: string;
|
|
107
|
+
ref: string;
|
|
108
|
+
commitSha: string;
|
|
109
|
+
configPath: string;
|
|
110
|
+
}): string {
|
|
111
|
+
const hash = createHash("sha256").update(JSON.stringify({
|
|
112
|
+
repoUrl: input.repoUrl,
|
|
113
|
+
ref: input.ref,
|
|
114
|
+
commitSha: input.commitSha,
|
|
115
|
+
configPath: input.configPath,
|
|
116
|
+
})).digest("hex").slice(0, 32);
|
|
117
|
+
return `github-${hash}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function installProjectDependenciesIfNeeded(projectDir: string): void {
|
|
121
|
+
if (existsSync(runtimeBinPath(projectDir))) return;
|
|
122
|
+
if (!existsSync(join(projectDir, "package.json"))) {
|
|
123
|
+
throw new Error(`Remote project at ${projectDir} does not contain package.json`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const command = installCommandFor(projectDir);
|
|
127
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
128
|
+
cwd: projectDir,
|
|
129
|
+
stdio: "inherit",
|
|
130
|
+
env: process.env,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (result.error) {
|
|
134
|
+
throw new Error(`Failed to run ${command.join(" ")} in ${projectDir}: ${result.error.message}`);
|
|
135
|
+
}
|
|
136
|
+
if (result.status !== 0) {
|
|
137
|
+
throw new Error(`${command.join(" ")} failed in ${projectDir} with exit code ${result.status}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function installCommandFor(projectDir: string): string[] {
|
|
142
|
+
if (existsSync(join(projectDir, "bun.lock")) || existsSync(join(projectDir, "bun.lockb"))) return ["bun", "install"];
|
|
143
|
+
if (existsSync(join(projectDir, "pnpm-lock.yaml"))) return ["pnpm", "install"];
|
|
144
|
+
if (existsSync(join(projectDir, "package-lock.json"))) return ["npm", "install"];
|
|
145
|
+
return ["npm", "install"];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function runtimeBinPath(projectDir: string): string {
|
|
149
|
+
return join(projectDir, "node_modules", ".bin", process.platform === "win32" ? "rigkit-project-runtime.cmd" : "rigkit-project-runtime");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readDefaultBranch(target: GithubProjectTarget): Promise<string> {
|
|
153
|
+
const info = await githubJson<GithubRepoInfo>(`/repos/${target.owner}/${target.repo}`);
|
|
154
|
+
if (typeof info.default_branch !== "string" || !info.default_branch) {
|
|
155
|
+
throw new Error(`GitHub did not return a default branch for ${target.owner}/${target.repo}`);
|
|
156
|
+
}
|
|
157
|
+
return info.default_branch;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function resolveCommitSha(target: GithubProjectTarget, ref: string): Promise<string> {
|
|
161
|
+
const commit = await githubJson<GithubCommitInfo>(`/repos/${target.owner}/${target.repo}/commits/${encodeURIComponent(ref)}`);
|
|
162
|
+
if (typeof commit.sha !== "string" || !/^[a-f0-9]{40}$/i.test(commit.sha)) {
|
|
163
|
+
throw new Error(`GitHub did not return a commit SHA for ${target.owner}/${target.repo}@${ref}`);
|
|
164
|
+
}
|
|
165
|
+
return commit.sha;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function downloadGithubTarball(
|
|
169
|
+
target: GithubProjectTarget,
|
|
170
|
+
commitSha: string,
|
|
171
|
+
projectDir: string,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
const tempDir = mkdtempSync(join(tmpdir(), "rigkit-github-"));
|
|
174
|
+
const archivePath = join(tempDir, "source.tar.gz");
|
|
175
|
+
const extractDir = join(tempDir, "extract");
|
|
176
|
+
mkdirSync(extractDir, { recursive: true });
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(`https://codeload.github.com/${target.owner}/${target.repo}/tar.gz/${commitSha}`, {
|
|
180
|
+
headers: githubHeaders(),
|
|
181
|
+
});
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new Error(`GitHub archive download failed for ${target.raw}: ${response.status} ${response.statusText}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
writeFileSync(archivePath, Buffer.from(await response.arrayBuffer()));
|
|
187
|
+
|
|
188
|
+
const tar = spawnSync("tar", ["-xzf", archivePath, "-C", extractDir], {
|
|
189
|
+
stdio: "inherit",
|
|
190
|
+
});
|
|
191
|
+
if (tar.error) throw new Error(`Failed to extract GitHub archive: ${tar.error.message}`);
|
|
192
|
+
if (tar.status !== 0) throw new Error(`Failed to extract GitHub archive: tar exited ${tar.status}`);
|
|
193
|
+
|
|
194
|
+
const roots = readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
195
|
+
if (roots.length !== 1) {
|
|
196
|
+
throw new Error(`GitHub archive for ${target.raw} had ${roots.length} root directories`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
mkdirSync(dirname(projectDir), { recursive: true });
|
|
200
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
201
|
+
cpSync(join(extractDir, roots[0]!.name), projectDir, { recursive: true });
|
|
202
|
+
} finally {
|
|
203
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function githubJson<T>(path: string): Promise<T> {
|
|
208
|
+
const response = await fetch(`https://api.github.com${path}`, {
|
|
209
|
+
headers: githubHeaders(),
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const text = await response.text().catch(() => "");
|
|
213
|
+
throw new Error(`GitHub request failed for ${path}: ${response.status} ${response.statusText}${text ? `: ${text.slice(0, 200)}` : ""}`);
|
|
214
|
+
}
|
|
215
|
+
return await response.json() as T;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function githubHeaders(): Record<string, string> {
|
|
219
|
+
const headers: Record<string, string> = {
|
|
220
|
+
accept: "application/vnd.github+json",
|
|
221
|
+
"user-agent": "rigkit-cli",
|
|
222
|
+
};
|
|
223
|
+
if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
224
|
+
return headers;
|
|
225
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { createLogUpdate } from "log-update";
|
|
3
|
+
import logSymbols from "log-symbols";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
|
|
6
|
+
export type RunPresenter = {
|
|
7
|
+
render(event: { type: string; [key: string]: unknown }): void;
|
|
8
|
+
pause(): void;
|
|
9
|
+
resume(): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
};
|
|
12
|
+
|
|
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
|
+
export function createRunPresenter(operation: string): RunPresenter | undefined {
|
|
24
|
+
if (!canUsePresenter()) return undefined;
|
|
25
|
+
|
|
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
|
+
let workflow = "";
|
|
41
|
+
let phase = "Starting runtime operation";
|
|
42
|
+
let activePath: string | undefined;
|
|
43
|
+
let finalStatus: "running" | "completed" | "failed" = "running";
|
|
44
|
+
let closed = false;
|
|
45
|
+
let paused = false;
|
|
46
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
47
|
+
|
|
48
|
+
const refresh = () => {
|
|
49
|
+
if (closed || paused) return;
|
|
50
|
+
log(renderTimeline({
|
|
51
|
+
activePath,
|
|
52
|
+
finalStatus,
|
|
53
|
+
operation,
|
|
54
|
+
phase,
|
|
55
|
+
spinnerFrame: spinner.frame().trimEnd(),
|
|
56
|
+
steps,
|
|
57
|
+
totalSteps,
|
|
58
|
+
workflow,
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const startTimer = () => {
|
|
63
|
+
timer ??= setInterval(refresh, spinner.interval);
|
|
64
|
+
timer.unref?.();
|
|
65
|
+
};
|
|
66
|
+
const stopTimer = () => {
|
|
67
|
+
if (!timer) return;
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
timer = undefined;
|
|
70
|
+
};
|
|
71
|
+
startTimer();
|
|
72
|
+
refresh();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
render(event) {
|
|
76
|
+
if (closed) return;
|
|
77
|
+
switch (event.type) {
|
|
78
|
+
case "definition.loaded":
|
|
79
|
+
workflow = String(event.workflow ?? "workflow");
|
|
80
|
+
phase = `Loaded ${workflow}`;
|
|
81
|
+
break;
|
|
82
|
+
case "plan.created":
|
|
83
|
+
workflow = String(event.workflow ?? (workflow || "workflow"));
|
|
84
|
+
totalSteps = numberField(event.nodeCount) ?? totalSteps;
|
|
85
|
+
phase = `Planned ${workflow}`;
|
|
86
|
+
break;
|
|
87
|
+
case "node.cached":
|
|
88
|
+
markStep(stepFor(steps, stepsByPath, event.nodePath), "cached", compactId(event.runId));
|
|
89
|
+
break;
|
|
90
|
+
case "node.started":
|
|
91
|
+
activePath = stringField(event.nodePath);
|
|
92
|
+
markStep(stepFor(steps, stepsByPath, event.nodePath), "running", "loading");
|
|
93
|
+
phase = `Running ${activePath ?? "step"}`;
|
|
94
|
+
break;
|
|
95
|
+
case "node.completed": {
|
|
96
|
+
const step = stepFor(steps, stepsByPath, event.nodePath);
|
|
97
|
+
markStep(step, "done", compactId(event.runId));
|
|
98
|
+
if (activePath === step.path) activePath = undefined;
|
|
99
|
+
phase = `Completed ${step.path}`;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "vm.created":
|
|
103
|
+
setActiveDetail(stepsByPath, activePath, event.fromSnapshotId
|
|
104
|
+
? `VM ${String(event.vmId ?? "")} from ${String(event.fromSnapshotId)}`
|
|
105
|
+
: `VM ${String(event.vmId ?? "")} created`);
|
|
106
|
+
break;
|
|
107
|
+
case "command.started": {
|
|
108
|
+
const step = stepFor(steps, stepsByPath, event.nodePath ?? activePath);
|
|
109
|
+
step.status = "running";
|
|
110
|
+
activePath = step.path;
|
|
111
|
+
step.detail = String(event.command ?? event.commandName ?? "command");
|
|
112
|
+
phase = `Command ${String(event.commandName ?? "command")}`;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case "command.output": {
|
|
116
|
+
const step = stepFor(steps, stepsByPath, event.nodePath ?? activePath);
|
|
117
|
+
activePath = step.path;
|
|
118
|
+
appendLog(step.logs, `[${String(event.stream ?? "log")}] ${String(event.data ?? "")}`);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "command.completed":
|
|
122
|
+
setActiveDetail(
|
|
123
|
+
stepsByPath,
|
|
124
|
+
stringField(event.nodePath) ?? activePath,
|
|
125
|
+
`Command ${String(event.commandName ?? "")} exited ${String(event.exitCode ?? 0)}`,
|
|
126
|
+
);
|
|
127
|
+
break;
|
|
128
|
+
case "log.output": {
|
|
129
|
+
const step = stepFor(steps, stepsByPath, event.nodePath ?? activePath);
|
|
130
|
+
activePath = step.status === "running" ? step.path : activePath;
|
|
131
|
+
appendLog(step.logs, `[${String(event.label ?? event.stream ?? "log")}] ${String(event.data ?? "")}`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "interaction.awaiting_user":
|
|
135
|
+
phase = `Waiting for ${String(event.title ?? "user interaction")}`;
|
|
136
|
+
setActiveDetail(stepsByPath, stringField(event.nodePath) ?? activePath, String(event.url ?? ""));
|
|
137
|
+
break;
|
|
138
|
+
case "interaction.completed":
|
|
139
|
+
phase = `Completed ${String(event.title ?? "interaction")}`;
|
|
140
|
+
break;
|
|
141
|
+
case "artifact.created":
|
|
142
|
+
setActiveDetail(
|
|
143
|
+
stepsByPath,
|
|
144
|
+
stringField(event.nodePath) ?? activePath,
|
|
145
|
+
`Created ${String(event.kind ?? "artifact")}`,
|
|
146
|
+
);
|
|
147
|
+
break;
|
|
148
|
+
case "workspace.ready":
|
|
149
|
+
phase = `Workspace ${String(event.resourceId ?? event.workspaceId ?? "ready")} ready`;
|
|
150
|
+
break;
|
|
151
|
+
case "run.completed":
|
|
152
|
+
finalStatus = "completed";
|
|
153
|
+
phase = "Completed";
|
|
154
|
+
collapseActiveStep(stepsByPath, activePath);
|
|
155
|
+
activePath = undefined;
|
|
156
|
+
break;
|
|
157
|
+
case "run.failed":
|
|
158
|
+
finalStatus = "failed";
|
|
159
|
+
phase = isRecord(event.error) && typeof event.error.message === "string"
|
|
160
|
+
? event.error.message
|
|
161
|
+
: "Run failed";
|
|
162
|
+
activePath = failActiveStep(steps, stepsByPath, activePath, phase);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
refresh();
|
|
166
|
+
},
|
|
167
|
+
pause() {
|
|
168
|
+
if (closed || paused) return;
|
|
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();
|
|
182
|
+
},
|
|
183
|
+
resume() {
|
|
184
|
+
if (closed || !paused) return;
|
|
185
|
+
paused = false;
|
|
186
|
+
startTimer();
|
|
187
|
+
refresh();
|
|
188
|
+
},
|
|
189
|
+
close() {
|
|
190
|
+
if (closed) return;
|
|
191
|
+
if (paused) {
|
|
192
|
+
paused = false;
|
|
193
|
+
}
|
|
194
|
+
refresh();
|
|
195
|
+
closed = true;
|
|
196
|
+
stopTimer();
|
|
197
|
+
log.done();
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizedStderr(): NodeJS.WriteStream {
|
|
203
|
+
const stream = Object.create(process.stderr) as NodeJS.WriteStream;
|
|
204
|
+
Object.defineProperty(stream, "columns", {
|
|
205
|
+
get: () => process.stderr.columns || 80,
|
|
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");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function failActiveStep(
|
|
328
|
+
steps: RunStep[],
|
|
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);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function compactId(value: unknown): string {
|
|
356
|
+
return typeof value === "string" && value.length > 0 ? value.slice(0, 8) : "";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function stringField(value: unknown): string | undefined {
|
|
360
|
+
return typeof value === "string" ? value : undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function numberField(value: unknown): number | undefined {
|
|
364
|
+
return typeof value === "number" ? value : undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
368
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function clip(value: string, max: number): string {
|
|
372
|
+
return value.length > max ? `${value.slice(0, Math.max(0, max - 3))}...` : value;
|
|
373
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RIGKIT_CLI_VERSION = "0.1.8";
|