@lizard-build/cli 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/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +72 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.js +117 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/context.js +71 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +120 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/destroy.d.ts +2 -0
- package/dist/commands/destroy.js +51 -0
- package/dist/commands/destroy.js.map +1 -0
- package/dist/commands/git.d.ts +2 -0
- package/dist/commands/git.js +67 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +107 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +50 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +92 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +16 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +28 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/ps.d.ts +2 -0
- package/dist/commands/ps.js +52 -0
- package/dist/commands/ps.js.map +1 -0
- package/dist/commands/redeploy.d.ts +2 -0
- package/dist/commands/redeploy.js +69 -0
- package/dist/commands/redeploy.js.map +1 -0
- package/dist/commands/regions.d.ts +2 -0
- package/dist/commands/regions.js +23 -0
- package/dist/commands/regions.js.map +1 -0
- package/dist/commands/restart.d.ts +2 -0
- package/dist/commands/restart.js +21 -0
- package/dist/commands/restart.js.map +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +33 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +138 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +41 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +37 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +21 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +19 -0
- package/dist/lib/api.js +114 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/auth.d.ts +23 -0
- package/dist/lib/auth.js +89 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/config.d.ts +23 -0
- package/dist/lib/config.js +77 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/format.d.ts +11 -0
- package/dist/lib/format.js +86 -0
- package/dist/lib/format.js.map +1 -0
- package/install.sh +59 -0
- package/package.json +35 -0
- package/src/commands/add.ts +100 -0
- package/src/commands/connect.ts +145 -0
- package/src/commands/context.ts +93 -0
- package/src/commands/deploy.ts +153 -0
- package/src/commands/destroy.ts +51 -0
- package/src/commands/git.ts +80 -0
- package/src/commands/init.ts +139 -0
- package/src/commands/link.ts +63 -0
- package/src/commands/login.ts +158 -0
- package/src/commands/logout.ts +17 -0
- package/src/commands/logs.ts +104 -0
- package/src/commands/open.ts +17 -0
- package/src/commands/projects.ts +45 -0
- package/src/commands/ps.ts +80 -0
- package/src/commands/redeploy.ts +74 -0
- package/src/commands/regions.ts +38 -0
- package/src/commands/restart.ts +24 -0
- package/src/commands/run.ts +43 -0
- package/src/commands/secrets.ts +175 -0
- package/src/commands/status.ts +65 -0
- package/src/commands/update.ts +44 -0
- package/src/commands/version.ts +37 -0
- package/src/commands/whoami.ts +27 -0
- package/src/index.ts +168 -0
- package/src/lib/api.ts +134 -0
- package/src/lib/auth.ts +113 -0
- package/src/lib/config.ts +93 -0
- package/src/lib/format.ts +95 -0
- package/tsconfig.json +17 -0
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getToken } from "./auth.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BASE_URL = "https://lizard.build";
|
|
4
|
+
const USER_AGENT = "lizard-cli/0.1";
|
|
5
|
+
|
|
6
|
+
let baseURL = process.env.LIZARD_API_URL || DEFAULT_BASE_URL;
|
|
7
|
+
let _accessToken: string | null = null;
|
|
8
|
+
|
|
9
|
+
export function setBaseURL(url: string) { baseURL = url; }
|
|
10
|
+
export function getBaseURL() { return baseURL; }
|
|
11
|
+
export function setAccessToken(token: string) { _accessToken = token; }
|
|
12
|
+
|
|
13
|
+
export class APIError extends Error {
|
|
14
|
+
status: number;
|
|
15
|
+
code: string;
|
|
16
|
+
constructor(status: number, message: string, code = "") {
|
|
17
|
+
super(message);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isNotFound(err: unknown): boolean {
|
|
24
|
+
return err instanceof APIError && err.status === 404;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isAuthError(err: unknown): boolean {
|
|
28
|
+
return err instanceof APIError && (err.status === 401 || err.status === 403);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function request<T = any>(
|
|
32
|
+
method: string,
|
|
33
|
+
path: string,
|
|
34
|
+
body?: unknown,
|
|
35
|
+
): Promise<T> {
|
|
36
|
+
const url = baseURL + path;
|
|
37
|
+
const token = _accessToken || getToken();
|
|
38
|
+
|
|
39
|
+
const headers: Record<string, string> = {
|
|
40
|
+
"User-Agent": USER_AGENT,
|
|
41
|
+
};
|
|
42
|
+
if (token) {
|
|
43
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
44
|
+
}
|
|
45
|
+
if (body !== undefined) {
|
|
46
|
+
headers["Content-Type"] = "application/json";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const res = await fetch(url, {
|
|
50
|
+
method,
|
|
51
|
+
headers,
|
|
52
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
let msg = res.statusText;
|
|
57
|
+
let code = "";
|
|
58
|
+
try {
|
|
59
|
+
const j = (await res.json()) as any;
|
|
60
|
+
msg = j.error || j.message || msg;
|
|
61
|
+
code = j.code || "";
|
|
62
|
+
} catch {}
|
|
63
|
+
throw new APIError(res.status, msg, code);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
if (!text) return undefined as T;
|
|
68
|
+
return JSON.parse(text) as T;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const api = {
|
|
72
|
+
get: <T = any>(path: string) => request<T>("GET", path),
|
|
73
|
+
post: <T = any>(path: string, body?: unknown) =>
|
|
74
|
+
request<T>("POST", path, body),
|
|
75
|
+
put: <T = any>(path: string, body?: unknown) =>
|
|
76
|
+
request<T>("PUT", path, body),
|
|
77
|
+
patch: <T = any>(path: string, body?: unknown) =>
|
|
78
|
+
request<T>("PATCH", path, body),
|
|
79
|
+
delete: <T = any>(path: string) => request<T>("DELETE", path),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Stream SSE and call handler for each data line. Return false to stop. */
|
|
83
|
+
export async function streamSSE(
|
|
84
|
+
path: string,
|
|
85
|
+
handler: (event: string, data: string) => boolean | void,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const url = baseURL + path;
|
|
88
|
+
const token = _accessToken || getToken();
|
|
89
|
+
const headers: Record<string, string> = {
|
|
90
|
+
"User-Agent": USER_AGENT,
|
|
91
|
+
Accept: "text/event-stream",
|
|
92
|
+
};
|
|
93
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
94
|
+
|
|
95
|
+
const res = await fetch(url, { headers });
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new APIError(res.status, `SSE failed: ${res.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
if (!res.body) return;
|
|
100
|
+
|
|
101
|
+
const reader = res.body.getReader();
|
|
102
|
+
const decoder = new TextDecoder();
|
|
103
|
+
let buffer = "";
|
|
104
|
+
let currentEvent = "";
|
|
105
|
+
let currentData = "";
|
|
106
|
+
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) break;
|
|
110
|
+
|
|
111
|
+
buffer += decoder.decode(value, { stream: true });
|
|
112
|
+
const lines = buffer.split("\n");
|
|
113
|
+
buffer = lines.pop() || "";
|
|
114
|
+
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const trimmed = line.replace(/\r$/, "");
|
|
117
|
+
if (trimmed === "") {
|
|
118
|
+
if (currentData) {
|
|
119
|
+
const cont = handler(currentEvent, currentData);
|
|
120
|
+
if (cont === false) {
|
|
121
|
+
reader.cancel();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
currentEvent = "";
|
|
126
|
+
currentData = "";
|
|
127
|
+
} else if (trimmed.startsWith("event:")) {
|
|
128
|
+
currentEvent = trimmed.slice(6).trim();
|
|
129
|
+
} else if (trimmed.startsWith("data:")) {
|
|
130
|
+
currentData = trimmed.slice(5).trimStart();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import open from "open";
|
|
5
|
+
|
|
6
|
+
const LIZARD_DIR = path.join(os.homedir(), ".lizard");
|
|
7
|
+
const CREDENTIALS_FILE = path.join(LIZARD_DIR, "credentials.json");
|
|
8
|
+
|
|
9
|
+
export interface Credentials {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
refreshToken?: string;
|
|
12
|
+
expiresAt?: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
username: string;
|
|
15
|
+
email?: string;
|
|
16
|
+
avatarUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let tokenOverride: string | null = null;
|
|
20
|
+
|
|
21
|
+
export function setTokenOverride(token: string) {
|
|
22
|
+
tokenOverride = token;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Get the active token in priority order: override → env → file */
|
|
26
|
+
export function getToken(): string | null {
|
|
27
|
+
if (tokenOverride) return tokenOverride;
|
|
28
|
+
if (process.env.LIZARD_TOKEN) return process.env.LIZARD_TOKEN;
|
|
29
|
+
const creds = loadCredentials();
|
|
30
|
+
return creds?.accessToken ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function loadCredentials(): Credentials | null {
|
|
34
|
+
try {
|
|
35
|
+
const data = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
36
|
+
return JSON.parse(data) as Credentials;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function saveCredentials(creds: Credentials) {
|
|
43
|
+
fs.mkdirSync(LIZARD_DIR, { recursive: true });
|
|
44
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), {
|
|
45
|
+
mode: 0o600,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearCredentials() {
|
|
50
|
+
try {
|
|
51
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isLoggedIn(): boolean {
|
|
56
|
+
return getToken() !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isTTY(): boolean {
|
|
60
|
+
return Boolean(process.stdout.isTTY);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Ensure the user is authenticated. If not logged in and TTY, auto-login.
|
|
65
|
+
* Returns credentials or throws.
|
|
66
|
+
*/
|
|
67
|
+
export async function requireAuth(): Promise<Credentials> {
|
|
68
|
+
// Token override or env var — we don't have full Credentials, fake it
|
|
69
|
+
if (tokenOverride || process.env.LIZARD_TOKEN) {
|
|
70
|
+
return {
|
|
71
|
+
accessToken: (tokenOverride || process.env.LIZARD_TOKEN)!,
|
|
72
|
+
userId: "",
|
|
73
|
+
username: "",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const creds = loadCredentials();
|
|
78
|
+
if (creds) return creds;
|
|
79
|
+
|
|
80
|
+
// Not logged in
|
|
81
|
+
if (!isTTY()) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"Not authenticated. Set LIZARD_TOKEN or run `lizard login` first.",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Auto-login
|
|
88
|
+
const { performLogin } = await import("../commands/login.js");
|
|
89
|
+
return performLogin();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Open a URL in the default browser, or print it if headless. */
|
|
93
|
+
export async function openURL(url: string) {
|
|
94
|
+
const isSSH = Boolean(
|
|
95
|
+
process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION,
|
|
96
|
+
);
|
|
97
|
+
const isCI = Boolean(process.env.CI);
|
|
98
|
+
const noDisplay =
|
|
99
|
+
process.platform === "linux" &&
|
|
100
|
+
!process.env.DISPLAY &&
|
|
101
|
+
!process.env.WAYLAND_DISPLAY;
|
|
102
|
+
|
|
103
|
+
if (isSSH || isCI || noDisplay) {
|
|
104
|
+
return false; // caller should show URL manually
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await open(url);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = ".lizard";
|
|
6
|
+
const CONFIG_FILE = "config.json";
|
|
7
|
+
const GLOBAL_SETTINGS_FILE = path.join(os.homedir(), ".lizard", "settings.json");
|
|
8
|
+
|
|
9
|
+
export interface ProjectConfig {
|
|
10
|
+
workspaceId?: string;
|
|
11
|
+
projectId: string;
|
|
12
|
+
projectName?: string;
|
|
13
|
+
environment?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GlobalSettings {
|
|
17
|
+
defaultWorkspace?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Find project config by walking up from cwd */
|
|
21
|
+
export function findProjectConfig(): ProjectConfig | null {
|
|
22
|
+
let dir = process.cwd();
|
|
23
|
+
while (true) {
|
|
24
|
+
const configPath = path.join(dir, CONFIG_DIR, CONFIG_FILE);
|
|
25
|
+
if (fs.existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const parent = path.dirname(dir);
|
|
33
|
+
if (parent === dir) break;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Save project config in cwd */
|
|
40
|
+
export function saveProjectConfig(config: ProjectConfig) {
|
|
41
|
+
const dir = path.join(process.cwd(), CONFIG_DIR);
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(path.join(dir, CONFIG_FILE), JSON.stringify(config, null, 2));
|
|
44
|
+
|
|
45
|
+
// Add .lizard/ to .gitignore if not already there
|
|
46
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
47
|
+
try {
|
|
48
|
+
const existing = fs.existsSync(gitignorePath)
|
|
49
|
+
? fs.readFileSync(gitignorePath, "utf-8")
|
|
50
|
+
: "";
|
|
51
|
+
if (!existing.includes(".lizard/")) {
|
|
52
|
+
fs.appendFileSync(
|
|
53
|
+
gitignorePath,
|
|
54
|
+
(existing.endsWith("\n") ? "" : "\n") + ".lizard/\n",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadGlobalSettings(): GlobalSettings {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(fs.readFileSync(GLOBAL_SETTINGS_FILE, "utf-8"));
|
|
63
|
+
} catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function saveGlobalSettings(settings: GlobalSettings) {
|
|
69
|
+
const dir = path.dirname(GLOBAL_SETTINGS_FILE);
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
fs.writeFileSync(GLOBAL_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve projectId from: --project flag → .lizard/config.json → error
|
|
76
|
+
*/
|
|
77
|
+
export function resolveProjectId(flagValue?: string): string {
|
|
78
|
+
if (flagValue) return flagValue;
|
|
79
|
+
const config = findProjectConfig();
|
|
80
|
+
if (config?.projectId) return config.projectId;
|
|
81
|
+
throw new Error(
|
|
82
|
+
"No project linked. Run `lizard init` or `lizard link` first, or use --project <id>.",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve environment from: --environment flag → .lizard/config.json → "production"
|
|
88
|
+
*/
|
|
89
|
+
export function resolveEnvironment(flagValue?: string): string {
|
|
90
|
+
if (flagValue) return flagValue;
|
|
91
|
+
const config = findProjectConfig();
|
|
92
|
+
return config?.environment ?? "production";
|
|
93
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export function isTTY(): boolean {
|
|
4
|
+
return Boolean(process.stdout.isTTY);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let jsonMode = false;
|
|
8
|
+
export function setJSONMode(on: boolean) {
|
|
9
|
+
jsonMode = on;
|
|
10
|
+
}
|
|
11
|
+
export function isJSONMode(): boolean {
|
|
12
|
+
return jsonMode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function printJSON(data: unknown) {
|
|
16
|
+
console.log(JSON.stringify(data, null, 2));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function success(msg: string) {
|
|
20
|
+
if (jsonMode) return;
|
|
21
|
+
process.stderr.write(chalk.green("✓") + " " + msg + "\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function error(msg: string) {
|
|
25
|
+
process.stderr.write(chalk.red("Error:") + " " + msg + "\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function warn(msg: string) {
|
|
29
|
+
process.stderr.write(chalk.yellow("Warning:") + " " + msg + "\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function info(msg: string) {
|
|
33
|
+
if (jsonMode) return;
|
|
34
|
+
process.stderr.write(msg + "\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function table(
|
|
38
|
+
headers: string[],
|
|
39
|
+
rows: string[][],
|
|
40
|
+
) {
|
|
41
|
+
if (rows.length === 0) return;
|
|
42
|
+
|
|
43
|
+
const widths = headers.map((h) => h.length);
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
for (let i = 0; i < row.length; i++) {
|
|
46
|
+
if (i < widths.length) {
|
|
47
|
+
widths[i] = Math.max(widths[i], (row[i] || "").length);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const header = headers
|
|
53
|
+
.map((h, i) => h.toUpperCase().padEnd(widths[i]))
|
|
54
|
+
.join(" ");
|
|
55
|
+
console.log(chalk.dim(header));
|
|
56
|
+
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const line = headers
|
|
59
|
+
.map((_, i) => (row[i] || "").padEnd(widths[i]))
|
|
60
|
+
.join(" ");
|
|
61
|
+
console.log(line);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function statusColor(status: string): string {
|
|
66
|
+
switch (status) {
|
|
67
|
+
case "running":
|
|
68
|
+
return chalk.green(status);
|
|
69
|
+
case "failed":
|
|
70
|
+
case "error":
|
|
71
|
+
return chalk.red(status);
|
|
72
|
+
case "building":
|
|
73
|
+
case "deploying":
|
|
74
|
+
case "restarting":
|
|
75
|
+
case "pending":
|
|
76
|
+
return chalk.yellow(status);
|
|
77
|
+
case "deleting":
|
|
78
|
+
return chalk.dim(status);
|
|
79
|
+
default:
|
|
80
|
+
return status;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function timeAgo(ts: number | string): string {
|
|
85
|
+
const ms = typeof ts === "string" ? Date.parse(ts) : ts;
|
|
86
|
+
const diff = Date.now() - ms;
|
|
87
|
+
const secs = Math.floor(diff / 1000);
|
|
88
|
+
if (secs < 60) return `${secs}s ago`;
|
|
89
|
+
const mins = Math.floor(secs / 60);
|
|
90
|
+
if (mins < 60) return `${mins}m ago`;
|
|
91
|
+
const hours = Math.floor(mins / 60);
|
|
92
|
+
if (hours < 24) return `${hours}h ago`;
|
|
93
|
+
const days = Math.floor(hours / 24);
|
|
94
|
+
return `${days}d ago`;
|
|
95
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"]
|
|
17
|
+
}
|