@rigkit/cli 0.2.7 → 0.2.9

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/src/ui.ts ADDED
@@ -0,0 +1,159 @@
1
+ // Minimal design system for the rig CLI.
2
+ // Mirrors apps/website/src/components/CliShowcase.tsx: bold black for the thing
3
+ // that matters, dim for muted, blue for prompts and active items, green for ok,
4
+ // red for fail. No spinners, no log-update, no boxes. Append-only output that
5
+ // looks the same in a TTY, a CI log, or an LLM transcript.
6
+
7
+ import chalk from "chalk";
8
+
9
+ const HEX_ACCENT = "#2d4df5";
10
+ const HEX_OK = "#1f8b4c";
11
+ const HEX_WARN = "#a36b00";
12
+ const HEX_ERR = "#b91c1c";
13
+
14
+ export const accent = (text: string) => chalk.hex(HEX_ACCENT)(text);
15
+ export const ok = (text: string) => chalk.hex(HEX_OK)(text);
16
+ export const warn = (text: string) => chalk.hex(HEX_WARN)(text);
17
+ export const err = (text: string) => chalk.hex(HEX_ERR)(text);
18
+ export const dim = (text: string) => chalk.dim(text);
19
+ export const bold = (text: string) => chalk.bold(text);
20
+
21
+ export const sym = {
22
+ prompt: "$",
23
+ arrow: "›",
24
+ active: "▸",
25
+ ok: "✓",
26
+ err: "✗",
27
+ dot: "·",
28
+ ellipsis: "…",
29
+ } as const;
30
+
31
+ export function title(text: string): string {
32
+ return bold(text);
33
+ }
34
+
35
+ export function heading(text: string): string {
36
+ return bold(text);
37
+ }
38
+
39
+ export function muted(text: string): string {
40
+ return dim(text);
41
+ }
42
+
43
+ export function prompt(command: string): string {
44
+ return `${accent(sym.prompt)} ${command}`;
45
+ }
46
+
47
+ export function hint(text: string): string {
48
+ return `${dim(sym.arrow)} ${text}`;
49
+ }
50
+
51
+ export type FileStatus = "created" | "updated" | "kept" | "failed" | "pinned";
52
+
53
+ const STATUS_WORD_WIDTH = "created".length;
54
+
55
+ export function fileStatus(kind: FileStatus, label: string): string {
56
+ switch (kind) {
57
+ case "created":
58
+ return `${ok("+")} ${bold(pad("created", STATUS_WORD_WIDTH))} ${label}`;
59
+ case "updated":
60
+ return `${warn("~")} ${bold(pad("updated", STATUS_WORD_WIDTH))} ${label}`;
61
+ case "kept":
62
+ return `${dim(sym.dot)} ${dim(pad("kept", STATUS_WORD_WIDTH))} ${dim(label)}`;
63
+ case "pinned":
64
+ return `${accent("·")} ${bold(pad("pinned", STATUS_WORD_WIDTH))} ${label}`;
65
+ case "failed":
66
+ return `${err(sym.err)} ${err(pad("failed", STATUS_WORD_WIDTH))} ${label}`;
67
+ }
68
+ }
69
+
70
+ export type StepKind = "active" | "ok" | "cached" | "err";
71
+
72
+ export function stepLine(kind: StepKind, label: string, detail?: string): string {
73
+ const tail = detail ? ` ${dim(detail)}` : "";
74
+ switch (kind) {
75
+ case "active":
76
+ return `${accent(sym.active)} ${bold(label)}${tail}`;
77
+ case "ok":
78
+ return `${ok(sym.ok)} ${label}${tail}`;
79
+ case "cached":
80
+ return `${dim(sym.ok)} ${dim(label)}${tail ? ` ${dim("cached")}` : ` ${dim("cached")}`}`;
81
+ case "err":
82
+ return `${err(sym.err)} ${bold(label)}${tail}`;
83
+ }
84
+ }
85
+
86
+ type Cell = { text: string; style?: (text: string) => string };
87
+ type Row = Array<string | Cell>;
88
+
89
+ function toCell(value: string | Cell): Cell {
90
+ return typeof value === "string" ? { text: value } : value;
91
+ }
92
+
93
+ export type ColumnsOptions = {
94
+ // Bold + underline the header row. Default true.
95
+ emphasizeHeader?: boolean;
96
+ // Two-space indent in front of each row. Default true.
97
+ indent?: boolean;
98
+ };
99
+
100
+ // Replacement for the old ASCII table. Aligned columns, bold-underlined header,
101
+ // no dashed separator. Cells may carry their own style.
102
+ export function columns(headers: string[], rows: Row[], options: ColumnsOptions = {}): string {
103
+ const indent = options.indent === false ? "" : " ";
104
+ const emphasizeHeader = options.emphasizeHeader !== false;
105
+ const widths = headers.map((header, index) =>
106
+ Math.max(
107
+ header.length,
108
+ ...rows.map((row) => toCell(row[index] ?? "").text.length),
109
+ ),
110
+ );
111
+
112
+ const renderRow = (cells: Cell[], styleFallback?: (text: string) => string): string => {
113
+ const last = cells.length - 1;
114
+ return indent + cells
115
+ .map((cell, index) => {
116
+ const width = widths[index] ?? cell.text.length;
117
+ const style = cell.style ?? styleFallback;
118
+ const styled = style ? style(cell.text) : cell.text;
119
+ if (index === last) return styled;
120
+ const padding = " ".repeat(Math.max(0, width - cell.text.length));
121
+ return `${styled}${padding} `;
122
+ })
123
+ .join("")
124
+ .replace(/\s+$/u, "");
125
+ };
126
+
127
+ const headerCells = headers.map((text) => ({ text }));
128
+ const lines: string[] = [];
129
+ lines.push(renderRow(headerCells, emphasizeHeader ? (s) => chalk.bold.underline(s) : undefined));
130
+ for (const row of rows) {
131
+ lines.push(renderRow(row.map(toCell)));
132
+ }
133
+ return lines.join("\n");
134
+ }
135
+
136
+ // "key value" pairs aligned by the longest key. Keys are bold.
137
+ export function kvList(pairs: Array<[string, string]>, options: { indent?: boolean } = {}): string {
138
+ const indent = options.indent === false ? "" : " ";
139
+ const width = pairs.reduce((max, [key]) => Math.max(max, key.length), 0);
140
+ return pairs
141
+ .map(([key, value]) => `${indent}${bold(pad(key, width))} ${value}`)
142
+ .join("\n");
143
+ }
144
+
145
+ export function section(headingText: string, body: string): string {
146
+ return `${heading(headingText)}\n${body}`;
147
+ }
148
+
149
+ export function pad(text: string, width: number): string {
150
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
151
+ }
152
+
153
+ export function termWidth(stream: NodeJS.WriteStream = process.stderr): number {
154
+ return stream.columns || 80;
155
+ }
156
+
157
+ export function clip(value: string, max: number): string {
158
+ return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}${sym.ellipsis}` : value;
159
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const RIGKIT_CLI_VERSION = "0.2.7";
1
+ export const RIGKIT_CLI_VERSION = "0.2.9";
@@ -1,55 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- parseGithubProjectTarget,
4
- remoteProjectId,
5
- splitGithubProjectTarget,
6
- } from "./remote-project.ts";
7
-
8
- describe("remote GitHub project targets", () => {
9
- test("parses github owner repo targets with optional refs", () => {
10
- expect(parseGithubProjectTarget("github:freestyle-sh/rigkit")).toEqual({
11
- kind: "github",
12
- raw: "github:freestyle-sh/rigkit",
13
- owner: "freestyle-sh",
14
- repo: "rigkit",
15
- });
16
-
17
- expect(parseGithubProjectTarget("github:freestyle-sh/rigkit#feature/runtime")).toEqual({
18
- kind: "github",
19
- raw: "github:freestyle-sh/rigkit#feature/runtime",
20
- owner: "freestyle-sh",
21
- repo: "rigkit",
22
- ref: "feature/runtime",
23
- });
24
- });
25
-
26
- test("splits a run target from operation arguments", () => {
27
- const split = splitGithubProjectTarget(["github:freestyle-sh/rigkit@main", "--workflow", "smoke"]);
28
-
29
- expect(split.target).toEqual({
30
- kind: "github",
31
- raw: "github:freestyle-sh/rigkit@main",
32
- owner: "freestyle-sh",
33
- repo: "rigkit",
34
- ref: "main",
35
- });
36
- expect(split.args).toEqual(["--workflow", "smoke"]);
37
- });
38
-
39
- test("remote project ids include repo, ref, commit, and config path", () => {
40
- const id = remoteProjectId({
41
- repoUrl: "https://github.com/freestyle-sh/rigkit.git",
42
- ref: "main",
43
- commitSha: "0123456789abcdef0123456789abcdef01234567",
44
- configPath: "rig.config.ts",
45
- });
46
-
47
- expect(id).toMatch(/^github-[a-f0-9]{32}$/);
48
- expect(remoteProjectId({
49
- repoUrl: "https://github.com/freestyle-sh/rigkit.git",
50
- ref: "main",
51
- commitSha: "fedcba9876543210fedcba9876543210fedcba98",
52
- configPath: "rig.config.ts",
53
- })).not.toBe(id);
54
- });
55
- });
@@ -1,225 +0,0 @@
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
- }