codepiper 0.1.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/.env.example +28 -0
- package/CHANGELOG.md +10 -0
- package/LEGAL_NOTICE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/package.json +90 -0
- package/packages/cli/package.json +13 -0
- package/packages/cli/src/commands/analytics.ts +157 -0
- package/packages/cli/src/commands/attach.ts +299 -0
- package/packages/cli/src/commands/audit.ts +50 -0
- package/packages/cli/src/commands/auth.ts +261 -0
- package/packages/cli/src/commands/daemon.ts +162 -0
- package/packages/cli/src/commands/doctor.ts +303 -0
- package/packages/cli/src/commands/env-set.ts +162 -0
- package/packages/cli/src/commands/hook-forward.ts +268 -0
- package/packages/cli/src/commands/keys.ts +77 -0
- package/packages/cli/src/commands/kill.ts +19 -0
- package/packages/cli/src/commands/logs.ts +419 -0
- package/packages/cli/src/commands/model.ts +172 -0
- package/packages/cli/src/commands/policy-set.ts +185 -0
- package/packages/cli/src/commands/policy.ts +227 -0
- package/packages/cli/src/commands/providers.ts +114 -0
- package/packages/cli/src/commands/resize.ts +34 -0
- package/packages/cli/src/commands/send.ts +184 -0
- package/packages/cli/src/commands/sessions.ts +202 -0
- package/packages/cli/src/commands/slash.ts +92 -0
- package/packages/cli/src/commands/start.ts +243 -0
- package/packages/cli/src/commands/stop.ts +19 -0
- package/packages/cli/src/commands/tail.ts +137 -0
- package/packages/cli/src/commands/workflow.ts +786 -0
- package/packages/cli/src/commands/workspace.ts +127 -0
- package/packages/cli/src/lib/api.ts +78 -0
- package/packages/cli/src/lib/args.ts +72 -0
- package/packages/cli/src/lib/format.ts +93 -0
- package/packages/cli/src/main.ts +563 -0
- package/packages/core/package.json +7 -0
- package/packages/core/src/config.ts +30 -0
- package/packages/core/src/errors.ts +38 -0
- package/packages/core/src/eventBus.ts +56 -0
- package/packages/core/src/eventBusAdapter.ts +143 -0
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/sqliteEventBus.ts +336 -0
- package/packages/core/src/types.ts +63 -0
- package/packages/daemon/package.json +11 -0
- package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
- package/packages/daemon/src/api/authRoutes.ts +344 -0
- package/packages/daemon/src/api/bodyLimit.ts +133 -0
- package/packages/daemon/src/api/envSetRoutes.ts +170 -0
- package/packages/daemon/src/api/gitRoutes.ts +409 -0
- package/packages/daemon/src/api/hooks.ts +588 -0
- package/packages/daemon/src/api/inputPolicy.ts +249 -0
- package/packages/daemon/src/api/notificationRoutes.ts +532 -0
- package/packages/daemon/src/api/policyRoutes.ts +234 -0
- package/packages/daemon/src/api/policySetRoutes.ts +445 -0
- package/packages/daemon/src/api/routeUtils.ts +28 -0
- package/packages/daemon/src/api/routes.ts +1004 -0
- package/packages/daemon/src/api/server.ts +1388 -0
- package/packages/daemon/src/api/settingsRoutes.ts +367 -0
- package/packages/daemon/src/api/sqliteErrors.ts +47 -0
- package/packages/daemon/src/api/stt.ts +143 -0
- package/packages/daemon/src/api/terminalRoutes.ts +200 -0
- package/packages/daemon/src/api/validation.ts +287 -0
- package/packages/daemon/src/api/validationRoutes.ts +174 -0
- package/packages/daemon/src/api/workflowRoutes.ts +567 -0
- package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
- package/packages/daemon/src/api/ws.ts +1588 -0
- package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
- package/packages/daemon/src/auth/authMiddleware.ts +305 -0
- package/packages/daemon/src/auth/authService.ts +496 -0
- package/packages/daemon/src/auth/rateLimiter.ts +137 -0
- package/packages/daemon/src/config/pricing.ts +79 -0
- package/packages/daemon/src/crypto/encryption.ts +196 -0
- package/packages/daemon/src/db/db.ts +2745 -0
- package/packages/daemon/src/db/index.ts +16 -0
- package/packages/daemon/src/db/migrations.ts +182 -0
- package/packages/daemon/src/db/policyDb.ts +349 -0
- package/packages/daemon/src/db/schema.sql +408 -0
- package/packages/daemon/src/db/workflowDb.ts +464 -0
- package/packages/daemon/src/git/gitUtils.ts +544 -0
- package/packages/daemon/src/index.ts +6 -0
- package/packages/daemon/src/main.ts +525 -0
- package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
- package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
- package/packages/daemon/src/providers/registry.ts +111 -0
- package/packages/daemon/src/providers/types.ts +82 -0
- package/packages/daemon/src/sessions/auditLogger.ts +103 -0
- package/packages/daemon/src/sessions/policyEngine.ts +165 -0
- package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
- package/packages/daemon/src/sessions/policyTypes.ts +94 -0
- package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
- package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
- package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
- package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
- package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
- package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
- package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
- package/packages/daemon/src/workflows/contextManager.ts +83 -0
- package/packages/daemon/src/workflows/index.ts +31 -0
- package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
- package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
- package/packages/daemon/src/workflows/workflowParser.ts +217 -0
- package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
- package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
- package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
- package/packages/providers/claude-code/package.json +11 -0
- package/packages/providers/claude-code/src/index.ts +7 -0
- package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
- package/packages/providers/claude-code/src/provider.ts +311 -0
- package/packages/web/dist/android-chrome-192x192.png +0 -0
- package/packages/web/dist/android-chrome-512x512.png +0 -0
- package/packages/web/dist/apple-touch-icon.png +0 -0
- package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
- package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
- package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
- package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
- package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
- package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
- package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
- package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
- package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
- package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
- package/packages/web/dist/assets/index-hgphORiw.js +204 -0
- package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
- package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
- package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
- package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
- package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
- package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
- package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
- package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
- package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
- package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
- package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
- package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
- package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/icon.svg +1 -0
- package/packages/web/dist/index.html +29 -0
- package/packages/web/dist/manifest.json +29 -0
- package/packages/web/dist/og-image.png +0 -0
- package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
- package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
- package/packages/web/dist/originals/apple-touch-icon.png +0 -0
- package/packages/web/dist/originals/favicon.ico +0 -0
- package/packages/web/dist/piper.svg +1 -0
- package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
- package/packages/web/dist/sw.js +257 -0
- package/scripts/postinstall-link-workspaces.mjs +58 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { daemonFetch, daemonJson, daemonPost } from "../lib/api";
|
|
2
|
+
import { getOption, getPositional, getSocket } from "../lib/args";
|
|
3
|
+
import { colors, formatDate, info, success, table } from "../lib/format";
|
|
4
|
+
|
|
5
|
+
interface Workspace {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function listWorkspaces(args: string[]): Promise<void> {
|
|
14
|
+
const socket = getSocket(args);
|
|
15
|
+
const data = await daemonJson<{ workspaces: Workspace[] }>("/workspaces", { socket });
|
|
16
|
+
|
|
17
|
+
if (data.workspaces.length === 0) {
|
|
18
|
+
console.log("No workspaces found.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const rows = data.workspaces.map((w) => [
|
|
23
|
+
w.id.slice(0, 8),
|
|
24
|
+
w.name,
|
|
25
|
+
w.path,
|
|
26
|
+
formatDate(w.createdAt),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
console.log(table(["ID", "NAME", "PATH", "CREATED"], rows));
|
|
30
|
+
console.log(`\n${colors.bold}Total:${colors.reset} ${data.workspaces.length} workspace(s)`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getWorkspace(args: string[]): Promise<void> {
|
|
34
|
+
const socket = getSocket(args);
|
|
35
|
+
const id = getPositional(args, 0);
|
|
36
|
+
if (!id) throw new Error("workspace ID is required");
|
|
37
|
+
|
|
38
|
+
const data = await daemonJson<{ workspace: Workspace }>(`/workspaces/${id}`, { socket });
|
|
39
|
+
const w = data.workspace;
|
|
40
|
+
|
|
41
|
+
console.log(`${colors.bold}Workspace ${w.id.slice(0, 8)}${colors.reset}`);
|
|
42
|
+
info("ID", w.id);
|
|
43
|
+
info("Name", w.name);
|
|
44
|
+
info("Path", w.path);
|
|
45
|
+
info("Created", formatDate(w.createdAt));
|
|
46
|
+
info("Updated", formatDate(w.updatedAt));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function createWorkspace(args: string[]): Promise<void> {
|
|
50
|
+
const socket = getSocket(args);
|
|
51
|
+
const name = getOption(args, "name");
|
|
52
|
+
const wsPath = getOption(args, "path");
|
|
53
|
+
|
|
54
|
+
if (!name) throw new Error("--name is required");
|
|
55
|
+
if (!wsPath) throw new Error("--path is required");
|
|
56
|
+
|
|
57
|
+
const body: Record<string, unknown> = { name, path: wsPath };
|
|
58
|
+
|
|
59
|
+
const data = await daemonPost<{ workspace: Workspace }>("/workspaces", body, { socket });
|
|
60
|
+
|
|
61
|
+
success(
|
|
62
|
+
`Workspace created: ${colors.bold}${data.workspace.name}${colors.reset} (ID: ${data.workspace.id})`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function updateWorkspace(args: string[]): Promise<void> {
|
|
67
|
+
const socket = getSocket(args);
|
|
68
|
+
const id = getPositional(args, 0);
|
|
69
|
+
if (!id) throw new Error("workspace ID is required");
|
|
70
|
+
|
|
71
|
+
const body: Record<string, unknown> = {};
|
|
72
|
+
const name = getOption(args, "name");
|
|
73
|
+
const wsPath = getOption(args, "path");
|
|
74
|
+
|
|
75
|
+
if (name) body.name = name;
|
|
76
|
+
if (wsPath) body.path = wsPath;
|
|
77
|
+
|
|
78
|
+
if (Object.keys(body).length === 0) {
|
|
79
|
+
throw new Error("at least one field to update is required (--name, --path)");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await daemonFetch(`/workspaces/${id}`, {
|
|
83
|
+
method: "PUT",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
socket,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
success(`Workspace #${id} updated`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function deleteWorkspace(args: string[]): Promise<void> {
|
|
93
|
+
const socket = getSocket(args);
|
|
94
|
+
const id = getPositional(args, 0);
|
|
95
|
+
if (!id) throw new Error("workspace ID is required");
|
|
96
|
+
|
|
97
|
+
await daemonFetch(`/workspaces/${id}`, { method: "DELETE", socket });
|
|
98
|
+
|
|
99
|
+
success(`Workspace #${id} deleted`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runWorkspaceCommand(args: string[]): Promise<void> {
|
|
103
|
+
if (args.length === 0) {
|
|
104
|
+
throw new Error("subcommand required (list, get, create, update, delete)");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const subcommand = args[0];
|
|
108
|
+
const subArgs = args.slice(1);
|
|
109
|
+
|
|
110
|
+
switch (subcommand) {
|
|
111
|
+
case "list":
|
|
112
|
+
case "ls":
|
|
113
|
+
return listWorkspaces(subArgs);
|
|
114
|
+
case "get":
|
|
115
|
+
case "show":
|
|
116
|
+
return getWorkspace(subArgs);
|
|
117
|
+
case "create":
|
|
118
|
+
return createWorkspace(subArgs);
|
|
119
|
+
case "update":
|
|
120
|
+
return updateWorkspace(subArgs);
|
|
121
|
+
case "delete":
|
|
122
|
+
case "rm":
|
|
123
|
+
return deleteWorkspace(subArgs);
|
|
124
|
+
default:
|
|
125
|
+
throw new Error(`Unknown workspace subcommand: ${subcommand}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const DEFAULT_SOCKET = "/tmp/codepiper.sock";
|
|
2
|
+
|
|
3
|
+
export interface DaemonErrorBody {
|
|
4
|
+
error?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function readJson<T>(response: Response): Promise<T> {
|
|
9
|
+
return (await response.json()) as T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function readErrorJson(response: Response): Promise<DaemonErrorBody> {
|
|
13
|
+
return (await response.json().catch(() => ({}))) as DaemonErrorBody;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function responseErrorMessage(response: Response, data?: DaemonErrorBody): string {
|
|
17
|
+
return typeof data?.error === "string" && data.error.length > 0
|
|
18
|
+
? data.error
|
|
19
|
+
: `HTTP ${response.status}: ${response.statusText}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getSocketPath(): string {
|
|
23
|
+
return process.env.CODEPIPER_SOCKET || DEFAULT_SOCKET;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function daemonFetch(
|
|
27
|
+
path: string,
|
|
28
|
+
opts?: RequestInit & { socket?: string }
|
|
29
|
+
): Promise<Response> {
|
|
30
|
+
const socket = opts?.socket || getSocketPath();
|
|
31
|
+
const { socket: _, ...fetchOpts } = opts || {};
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(`http://localhost${path}`, {
|
|
35
|
+
unix: socket,
|
|
36
|
+
...fetchOpts,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const errorData = await readErrorJson(response);
|
|
41
|
+
throw new Error(responseErrorMessage(response, errorData));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return response;
|
|
45
|
+
} catch (error: any) {
|
|
46
|
+
if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
|
|
47
|
+
throw new Error(`Failed to connect to daemon at ${socket}. Is the daemon running?`);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function daemonJson<T>(
|
|
54
|
+
path: string,
|
|
55
|
+
opts?: RequestInit & { socket?: string }
|
|
56
|
+
): Promise<T> {
|
|
57
|
+
const response = await daemonFetch(path, opts);
|
|
58
|
+
return readJson<T>(response);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function daemonPost<T>(
|
|
62
|
+
path: string,
|
|
63
|
+
body?: unknown,
|
|
64
|
+
opts?: { socket?: string }
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
const request: RequestInit & { socket?: string } = {
|
|
67
|
+
method: "POST",
|
|
68
|
+
};
|
|
69
|
+
if (body !== undefined) {
|
|
70
|
+
request.headers = { "Content-Type": "application/json" };
|
|
71
|
+
request.body = JSON.stringify(body);
|
|
72
|
+
}
|
|
73
|
+
if (opts?.socket !== undefined) {
|
|
74
|
+
request.socket = opts.socket;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return daemonJson<T>(path, request);
|
|
78
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function getFlag(args: string[], flag: string): boolean {
|
|
2
|
+
return args.includes(`--${flag}`);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getOption(args: string[], flag: string): string | undefined {
|
|
6
|
+
for (let i = 0; i < args.length; i++) {
|
|
7
|
+
if (args[i] === `--${flag}` && i + 1 < args.length) {
|
|
8
|
+
const value = args[i + 1];
|
|
9
|
+
if (value !== undefined) {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getOptionShort(args: string[], long: string, short?: string): string | undefined {
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
if ((args[i] === `--${long}` || (short && args[i] === `-${short}`)) && i + 1 < args.length) {
|
|
20
|
+
const value = args[i + 1];
|
|
21
|
+
if (value !== undefined) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getAllOptions(args: string[], flag: string): string[] {
|
|
30
|
+
const values: string[] = [];
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
if (args[i] === `--${flag}` && i + 1 < args.length) {
|
|
33
|
+
const value = args[i + 1];
|
|
34
|
+
if (value !== undefined) {
|
|
35
|
+
values.push(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return values;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getSubcommand(args: string[]): string | undefined {
|
|
43
|
+
for (const arg of args) {
|
|
44
|
+
if (!arg.startsWith("-")) return arg;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getPositional(args: string[], index: number): string | undefined {
|
|
50
|
+
let count = 0;
|
|
51
|
+
for (const arg of args) {
|
|
52
|
+
if (!arg.startsWith("-")) {
|
|
53
|
+
if (count === index) return arg;
|
|
54
|
+
count++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getSocket(args: string[]): string {
|
|
61
|
+
return (
|
|
62
|
+
getOptionShort(args, "socket", "s") || process.env.CODEPIPER_SOCKET || "/tmp/codepiper.sock"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getRequiredValue(args: string[], index: number, flag: string): string {
|
|
67
|
+
const value = args[index + 1];
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
throw new Error(`${flag} requires a value`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ANSI color codes
|
|
2
|
+
export const colors = {
|
|
3
|
+
reset: "\x1b[0m",
|
|
4
|
+
bold: "\x1b[1m",
|
|
5
|
+
dim: "\x1b[2m",
|
|
6
|
+
green: "\x1b[32m",
|
|
7
|
+
yellow: "\x1b[33m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
blue: "\x1b[34m",
|
|
10
|
+
cyan: "\x1b[36m",
|
|
11
|
+
gray: "\x1b[90m",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function formatStatus(status: string): string {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case "RUNNING":
|
|
17
|
+
return `${colors.green}${status}${colors.reset}`;
|
|
18
|
+
case "STARTING":
|
|
19
|
+
return `${colors.yellow}${status}${colors.reset}`;
|
|
20
|
+
case "STOPPED":
|
|
21
|
+
return `${colors.gray}${status}${colors.reset}`;
|
|
22
|
+
case "CRASHED":
|
|
23
|
+
return `${colors.red}${status}${colors.reset}`;
|
|
24
|
+
case "NEEDS_PERMISSION":
|
|
25
|
+
return `${colors.blue}${status}${colors.reset}`;
|
|
26
|
+
case "NEEDS_INPUT":
|
|
27
|
+
return `${colors.cyan}${status}${colors.reset}`;
|
|
28
|
+
default:
|
|
29
|
+
return status;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatDate(iso: string): string {
|
|
34
|
+
return new Date(iso).toLocaleString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function truncate(str: string, maxLen: number): string {
|
|
38
|
+
if (str.length <= maxLen) return str;
|
|
39
|
+
return `...${str.slice(-(maxLen - 3))}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function table(headers: string[], rows: string[][]): string {
|
|
43
|
+
// Calculate column widths
|
|
44
|
+
const widths = headers.map((h, i) =>
|
|
45
|
+
Math.max(h.length, ...rows.map((r) => stripAnsi(r[i] ?? "").length))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
|
|
50
|
+
// Header
|
|
51
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i] ?? h.length)).join(" ");
|
|
52
|
+
lines.push(`${colors.bold}${headerLine}${colors.reset}`);
|
|
53
|
+
lines.push(
|
|
54
|
+
colors.dim +
|
|
55
|
+
"─".repeat(widths.reduce((a, b) => a + b, 0) + (widths.length - 1) * 2) +
|
|
56
|
+
colors.reset
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Rows
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
const line = row
|
|
62
|
+
.map((cell, i) => {
|
|
63
|
+
const value = cell ?? "";
|
|
64
|
+
const stripped = stripAnsi(value);
|
|
65
|
+
const width = widths[i] ?? stripped.length;
|
|
66
|
+
const padding = width - stripped.length;
|
|
67
|
+
return value + " ".repeat(Math.max(0, padding));
|
|
68
|
+
})
|
|
69
|
+
.join(" ");
|
|
70
|
+
lines.push(line);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape codes requires matching control chars
|
|
77
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
78
|
+
|
|
79
|
+
function stripAnsi(str: string): string {
|
|
80
|
+
return str.replace(ANSI_RE, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function success(msg: string): void {
|
|
84
|
+
console.log(`${colors.green}✓${colors.reset} ${msg}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function error(msg: string): void {
|
|
88
|
+
console.error(`${colors.red}✗${colors.reset} ${msg}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function info(label: string, value: string): void {
|
|
92
|
+
console.log(` ${colors.dim}${label}:${colors.reset} ${value}`);
|
|
93
|
+
}
|