@lizard-build/cli 0.1.0 → 0.3.30
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/.github/workflows/release.yml +90 -0
- package/AGENTS.md +113 -0
- package/README.md +41 -0
- package/dist/commands/add.js +318 -45
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +68 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +13 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/domain.d.ts +9 -0
- package/dist/commands/domain.js +195 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/git.js +175 -36
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/init.d.ts +24 -0
- package/dist/commands/init.js +128 -86
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +7 -0
- package/dist/commands/link.js +104 -33
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.js +4 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logs.js +223 -30
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/open.js +3 -2
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/port.d.ts +7 -0
- package/dist/commands/port.js +49 -0
- package/dist/commands/port.js.map +1 -0
- package/dist/commands/projects.js +36 -6
- package/dist/commands/projects.js.map +1 -1
- package/dist/commands/ps.js +32 -39
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/redeploy.js +48 -8
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/regions.js +2 -5
- package/dist/commands/regions.js.map +1 -1
- package/dist/commands/restart.js +84 -10
- package/dist/commands/restart.js.map +1 -1
- package/dist/commands/run.d.ts +9 -0
- package/dist/commands/run.js +61 -22
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/scale.d.ts +10 -0
- package/dist/commands/scale.js +166 -0
- package/dist/commands/scale.js.map +1 -0
- package/dist/commands/secrets.js +200 -89
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/service-set.d.ts +49 -0
- package/dist/commands/service-set.js +552 -0
- package/dist/commands/service-set.js.map +1 -0
- package/dist/commands/service-show.d.ts +11 -0
- package/dist/commands/service-show.js +44 -0
- package/dist/commands/service-show.js.map +1 -0
- package/dist/commands/service.d.ts +8 -0
- package/dist/commands/service.js +262 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/skill.d.ts +2 -0
- package/dist/commands/skill.js +146 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/ssh.d.ts +2 -0
- package/dist/commands/ssh.js +161 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +49 -38
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +5 -0
- package/dist/commands/unlink.js +18 -0
- package/dist/commands/unlink.js.map +1 -0
- package/dist/commands/up.d.ts +9 -0
- package/dist/commands/up.js +417 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +79 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/whoami.js +26 -6
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +36 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/index.js +209 -82
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +17 -2
- package/dist/lib/api.js +85 -51
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +3 -11
- package/dist/lib/auth.js +16 -36
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config.d.ts +36 -15
- package/dist/lib/config.js +71 -58
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.js +17 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/name.d.ts +11 -0
- package/dist/lib/name.js +26 -0
- package/dist/lib/name.js.map +1 -0
- package/dist/lib/picker.d.ts +32 -0
- package/dist/lib/picker.js +91 -0
- package/dist/lib/picker.js.map +1 -0
- package/dist/lib/resolve.d.ts +85 -0
- package/dist/lib/resolve.js +203 -0
- package/dist/lib/resolve.js.map +1 -0
- package/dist/lib/updater.d.ts +16 -0
- package/dist/lib/updater.js +102 -0
- package/dist/lib/updater.js.map +1 -0
- package/lizard-wrapper.sh +2 -0
- package/package.json +11 -3
- package/skill-data/core/SKILL.md +239 -0
- package/src/commands/add.ts +388 -56
- package/src/commands/config.ts +80 -0
- package/src/commands/docs.ts +15 -0
- package/src/commands/domain.ts +248 -0
- package/src/commands/git.ts +201 -40
- package/src/commands/init.ts +149 -100
- package/src/commands/link.ts +127 -35
- package/src/commands/login.ts +4 -3
- package/src/commands/logs.ts +283 -27
- package/src/commands/open.ts +3 -2
- package/src/commands/port.ts +57 -0
- package/src/commands/projects.ts +43 -6
- package/src/commands/ps.ts +39 -60
- package/src/commands/redeploy.ts +51 -10
- package/src/commands/regions.ts +2 -6
- package/src/commands/restart.ts +84 -10
- package/src/commands/run.ts +68 -24
- package/src/commands/scale.ts +216 -0
- package/src/commands/secrets.ts +277 -100
- package/src/commands/service-set.ts +669 -0
- package/src/commands/service-show.ts +52 -0
- package/src/commands/service.ts +298 -0
- package/src/commands/skill.ts +157 -0
- package/src/commands/ssh.ts +176 -0
- package/src/commands/status.ts +51 -46
- package/src/commands/unlink.ts +17 -0
- package/src/commands/up.ts +461 -0
- package/src/commands/upgrade.ts +87 -0
- package/src/commands/whoami.ts +34 -6
- package/src/commands/workspace.ts +44 -0
- package/src/index.ts +219 -85
- package/src/lib/api.ts +114 -51
- package/src/lib/auth.ts +22 -46
- package/src/lib/config.ts +100 -65
- package/src/lib/format.ts +18 -4
- package/src/lib/name.ts +27 -0
- package/src/lib/picker.ts +133 -0
- package/src/lib/resolve.ts +285 -0
- package/src/lib/updater.ts +106 -0
- package/test/cli.test.ts +491 -0
- package/test/fixtures/hello-app/Dockerfile +5 -0
- package/test/fixtures/hello-app/index.js +5 -0
- package/test/unit/api.test.ts +66 -0
- package/test/unit/config.test.ts +94 -0
- package/test/unit/init.test.ts +211 -0
- package/test/unit/json.test.ts +208 -0
- package/test/unit/picker.test.ts +161 -0
- package/test/unit/resolve.test.ts +124 -0
- package/test/unit/service-set.test.ts +355 -0
- package/vitest.config.ts +10 -0
- package/dist/commands/connect.d.ts +0 -2
- package/dist/commands/connect.js +0 -117
- package/dist/commands/connect.js.map +0 -1
- package/dist/commands/context.d.ts +0 -2
- package/dist/commands/context.js +0 -71
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/deploy.d.ts +0 -2
- package/dist/commands/deploy.js +0 -120
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/destroy.d.ts +0 -2
- package/dist/commands/destroy.js +0 -51
- package/dist/commands/destroy.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.js +0 -41
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/version.d.ts +0 -2
- package/dist/commands/version.js +0 -37
- package/dist/commands/version.js.map +0 -1
- package/src/commands/connect.ts +0 -145
- package/src/commands/context.ts +0 -93
- package/src/commands/deploy.ts +0 -153
- package/src/commands/destroy.ts +0 -51
- package/src/commands/update.ts +0 -44
- package/src/commands/version.ts +0 -37
package/src/lib/api.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { getToken } from "./auth.js";
|
|
2
|
+
import { CURRENT_VERSION } from "./updater.js";
|
|
3
|
+
import * as https from "node:https";
|
|
4
|
+
import * as http from "node:http";
|
|
2
5
|
|
|
3
6
|
const DEFAULT_BASE_URL = "https://lizard.build";
|
|
4
|
-
const USER_AGENT =
|
|
7
|
+
const USER_AGENT = `lizard-cli/${CURRENT_VERSION}`;
|
|
5
8
|
|
|
6
9
|
let baseURL = process.env.LIZARD_API_URL || DEFAULT_BASE_URL;
|
|
7
10
|
let _accessToken: string | null = null;
|
|
@@ -10,13 +13,56 @@ export function setBaseURL(url: string) { baseURL = url; }
|
|
|
10
13
|
export function getBaseURL() { return baseURL; }
|
|
11
14
|
export function setAccessToken(token: string) { _accessToken = token; }
|
|
12
15
|
|
|
16
|
+
// ── Scoping ───────────────────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// Every project-scoped endpoint takes `?workspaceId=…` — mirrors
|
|
19
|
+
// lizard-client's `withScope` so server-side state is shared across
|
|
20
|
+
// CLI and browser. Build URLs through these helpers, never by hand.
|
|
21
|
+
|
|
22
|
+
export interface ResourceScope {
|
|
23
|
+
workspaceId?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Workspace {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
role: "owner" | "member";
|
|
31
|
+
isPersonal?: boolean;
|
|
32
|
+
projectCount?: number;
|
|
33
|
+
createdAt?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function withQuery(
|
|
37
|
+
path: string,
|
|
38
|
+
params: Record<string, string | number | boolean | null | undefined>,
|
|
39
|
+
): string {
|
|
40
|
+
const search = new URLSearchParams();
|
|
41
|
+
for (const [key, value] of Object.entries(params)) {
|
|
42
|
+
if (value === null || value === undefined || value === "") continue;
|
|
43
|
+
search.set(key, String(value));
|
|
44
|
+
}
|
|
45
|
+
const query = search.toString();
|
|
46
|
+
if (!query) return path;
|
|
47
|
+
return `${path}${path.includes("?") ? "&" : "?"}${query}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function withScope(path: string, scope?: ResourceScope): string {
|
|
51
|
+
if (!scope) return path;
|
|
52
|
+
return withQuery(path, {
|
|
53
|
+
workspaceId: scope.workspaceId,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
13
57
|
export class APIError extends Error {
|
|
14
58
|
status: number;
|
|
15
59
|
code: string;
|
|
16
|
-
|
|
60
|
+
body: unknown;
|
|
61
|
+
constructor(status: number, message: string, code = "", body: unknown = null) {
|
|
17
62
|
super(message);
|
|
18
63
|
this.status = status;
|
|
19
64
|
this.code = code;
|
|
65
|
+
this.body = body;
|
|
20
66
|
}
|
|
21
67
|
}
|
|
22
68
|
|
|
@@ -32,12 +78,14 @@ async function request<T = any>(
|
|
|
32
78
|
method: string,
|
|
33
79
|
path: string,
|
|
34
80
|
body?: unknown,
|
|
81
|
+
extraHeaders: Record<string, string> = {},
|
|
35
82
|
): Promise<T> {
|
|
36
83
|
const url = baseURL + path;
|
|
37
84
|
const token = _accessToken || getToken();
|
|
38
85
|
|
|
39
86
|
const headers: Record<string, string> = {
|
|
40
87
|
"User-Agent": USER_AGENT,
|
|
88
|
+
...extraHeaders,
|
|
41
89
|
};
|
|
42
90
|
if (token) {
|
|
43
91
|
headers["Authorization"] = `Bearer ${token}`;
|
|
@@ -55,12 +103,14 @@ async function request<T = any>(
|
|
|
55
103
|
if (!res.ok) {
|
|
56
104
|
let msg = res.statusText;
|
|
57
105
|
let code = "";
|
|
106
|
+
let body: unknown = null;
|
|
58
107
|
try {
|
|
59
108
|
const j = (await res.json()) as any;
|
|
109
|
+
body = j;
|
|
60
110
|
msg = j.error || j.message || msg;
|
|
61
111
|
code = j.code || "";
|
|
62
112
|
} catch {}
|
|
63
|
-
throw new APIError(res.status, msg, code);
|
|
113
|
+
throw new APIError(res.status, msg, code, body);
|
|
64
114
|
}
|
|
65
115
|
|
|
66
116
|
const text = await res.text();
|
|
@@ -70,8 +120,8 @@ async function request<T = any>(
|
|
|
70
120
|
|
|
71
121
|
export const api = {
|
|
72
122
|
get: <T = any>(path: string) => request<T>("GET", path),
|
|
73
|
-
post: <T = any>(path: string, body?: unknown) =>
|
|
74
|
-
request<T>("POST", path, body),
|
|
123
|
+
post: <T = any>(path: string, body?: unknown, headers?: Record<string, string>) =>
|
|
124
|
+
request<T>("POST", path, body, headers),
|
|
75
125
|
put: <T = any>(path: string, body?: unknown) =>
|
|
76
126
|
request<T>("PUT", path, body),
|
|
77
127
|
patch: <T = any>(path: string, body?: unknown) =>
|
|
@@ -80,55 +130,68 @@ export const api = {
|
|
|
80
130
|
};
|
|
81
131
|
|
|
82
132
|
/** Stream SSE and call handler for each data line. Return false to stop. */
|
|
83
|
-
export
|
|
133
|
+
export function streamSSE(
|
|
84
134
|
path: string,
|
|
85
135
|
handler: (event: string, data: string) => boolean | void,
|
|
86
136
|
): Promise<void> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const url = new URL(baseURL + path);
|
|
139
|
+
const token = _accessToken || getToken();
|
|
140
|
+
const reqHeaders: Record<string, string> = {
|
|
141
|
+
"User-Agent": USER_AGENT,
|
|
142
|
+
Accept: "text/event-stream",
|
|
143
|
+
};
|
|
144
|
+
if (token) reqHeaders["Authorization"] = `Bearer ${token}`;
|
|
145
|
+
|
|
146
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
147
|
+
const req = transport.request(
|
|
148
|
+
{ hostname: url.hostname, port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
149
|
+
path: url.pathname + url.search, method: "GET", headers: reqHeaders },
|
|
150
|
+
(res) => {
|
|
151
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
152
|
+
let body = "";
|
|
153
|
+
res.on("data", (c: Buffer) => body += c.toString());
|
|
154
|
+
res.on("end", () => reject(new APIError(res.statusCode!, `SSE failed: ${body}`)));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
94
157
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
let buffer = "";
|
|
159
|
+
let currentEvent = "";
|
|
160
|
+
let currentData = "";
|
|
161
|
+
|
|
162
|
+
res.setEncoding("utf8");
|
|
163
|
+
res.on("data", (chunk: string) => {
|
|
164
|
+
buffer += chunk;
|
|
165
|
+
const lines = buffer.split("\n");
|
|
166
|
+
buffer = lines.pop() ?? "";
|
|
167
|
+
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
const trimmed = line.replace(/\r$/, "");
|
|
170
|
+
if (trimmed === "") {
|
|
171
|
+
if (currentData) {
|
|
172
|
+
const cont = handler(currentEvent, currentData);
|
|
173
|
+
if (cont === false) {
|
|
174
|
+
req.destroy();
|
|
175
|
+
resolve();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
currentEvent = "";
|
|
180
|
+
currentData = "";
|
|
181
|
+
} else if (trimmed.startsWith("event:")) {
|
|
182
|
+
currentEvent = trimmed.slice(6).trim();
|
|
183
|
+
} else if (trimmed.startsWith("data:")) {
|
|
184
|
+
currentData = trimmed.slice(5).trimStart();
|
|
185
|
+
}
|
|
123
186
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
res.on("end", resolve);
|
|
190
|
+
res.on("error", reject);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
req.on("error", reject);
|
|
195
|
+
req.end();
|
|
196
|
+
});
|
|
134
197
|
}
|
package/src/lib/auth.ts
CHANGED
|
@@ -1,55 +1,32 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
1
|
import open from "open";
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
saveConfig,
|
|
5
|
+
type Credentials,
|
|
6
|
+
} from "./config.js";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const CREDENTIALS_FILE = path.join(LIZARD_DIR, "credentials.json");
|
|
8
|
+
export type { Credentials } from "./config.js";
|
|
8
9
|
|
|
9
|
-
|
|
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 */
|
|
10
|
+
/** Get the active token in priority order: env → file */
|
|
26
11
|
export function getToken(): string | null {
|
|
27
|
-
if (tokenOverride) return tokenOverride;
|
|
28
12
|
if (process.env.LIZARD_TOKEN) return process.env.LIZARD_TOKEN;
|
|
29
|
-
|
|
30
|
-
return creds?.accessToken ?? null;
|
|
13
|
+
return loadCredentials()?.accessToken ?? null;
|
|
31
14
|
}
|
|
32
15
|
|
|
33
16
|
export function loadCredentials(): Credentials | null {
|
|
34
|
-
|
|
35
|
-
const data = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
36
|
-
return JSON.parse(data) as Credentials;
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
17
|
+
return loadConfig().credentials ?? null;
|
|
40
18
|
}
|
|
41
19
|
|
|
42
20
|
export function saveCredentials(creds: Credentials) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
config.credentials = creds;
|
|
23
|
+
saveConfig(config);
|
|
47
24
|
}
|
|
48
25
|
|
|
49
26
|
export function clearCredentials() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
delete config.credentials;
|
|
29
|
+
saveConfig(config);
|
|
53
30
|
}
|
|
54
31
|
|
|
55
32
|
export function isLoggedIn(): boolean {
|
|
@@ -65,10 +42,9 @@ function isTTY(): boolean {
|
|
|
65
42
|
* Returns credentials or throws.
|
|
66
43
|
*/
|
|
67
44
|
export async function requireAuth(): Promise<Credentials> {
|
|
68
|
-
|
|
69
|
-
if (tokenOverride || process.env.LIZARD_TOKEN) {
|
|
45
|
+
if (process.env.LIZARD_TOKEN) {
|
|
70
46
|
return {
|
|
71
|
-
accessToken:
|
|
47
|
+
accessToken: process.env.LIZARD_TOKEN,
|
|
72
48
|
userId: "",
|
|
73
49
|
username: "",
|
|
74
50
|
};
|
|
@@ -77,14 +53,14 @@ export async function requireAuth(): Promise<Credentials> {
|
|
|
77
53
|
const creds = loadCredentials();
|
|
78
54
|
if (creds) return creds;
|
|
79
55
|
|
|
80
|
-
// Not logged in
|
|
81
56
|
if (!isTTY()) {
|
|
82
|
-
|
|
57
|
+
const err = new Error(
|
|
83
58
|
"Not authenticated. Set LIZARD_TOKEN or run `lizard login` first.",
|
|
84
|
-
);
|
|
59
|
+
) as Error & { code: string };
|
|
60
|
+
err.code = "NOT_AUTHENTICATED";
|
|
61
|
+
throw err;
|
|
85
62
|
}
|
|
86
63
|
|
|
87
|
-
// Auto-login
|
|
88
64
|
const { performLogin } = await import("../commands/login.js");
|
|
89
65
|
return performLogin();
|
|
90
66
|
}
|
|
@@ -101,7 +77,7 @@ export async function openURL(url: string) {
|
|
|
101
77
|
!process.env.WAYLAND_DISPLAY;
|
|
102
78
|
|
|
103
79
|
if (isSSH || isCI || noDisplay) {
|
|
104
|
-
return false;
|
|
80
|
+
return false;
|
|
105
81
|
}
|
|
106
82
|
|
|
107
83
|
try {
|
package/src/lib/config.ts
CHANGED
|
@@ -2,92 +2,127 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const CONFIG_FILE = "config.json";
|
|
7
|
-
const GLOBAL_SETTINGS_FILE = path.join(os.homedir(), ".lizard", "settings.json");
|
|
5
|
+
export const DEFAULT_REGION = "us-east-1";
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
// Resolve at every call so tests (and exotic shells that mutate HOME) get a
|
|
8
|
+
// fresh path. Cost is negligible — we hit disk anyway. LIZARD_HOME lets tests
|
|
9
|
+
// (and power users) point the config dir somewhere other than ~/.lizard.
|
|
10
|
+
function configDir(): string {
|
|
11
|
+
return process.env.LIZARD_HOME
|
|
12
|
+
? path.join(process.env.LIZARD_HOME, ".lizard")
|
|
13
|
+
: path.join(os.homedir(), ".lizard");
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
defaultWorkspace?: string;
|
|
15
|
+
function configFile(): string {
|
|
16
|
+
return path.join(configDir(), "config.json");
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
19
|
+
export interface Credentials {
|
|
20
|
+
accessToken: string;
|
|
21
|
+
refreshToken?: string;
|
|
22
|
+
expiresAt?: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
username: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
avatarUrl?: string;
|
|
37
27
|
}
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
export interface ProjectLink {
|
|
30
|
+
projectId: string;
|
|
31
|
+
projectName?: string;
|
|
32
|
+
/** Workspace the project belongs to. Lazy-filled for legacy links. */
|
|
33
|
+
workspaceId?: string;
|
|
34
|
+
workspaceName?: string;
|
|
35
|
+
/** Active service for this cwd. `appId/appName` are kept as aliases for backwards compat. */
|
|
36
|
+
serviceId?: string;
|
|
37
|
+
serviceName?: string;
|
|
38
|
+
/** @deprecated use serviceId */
|
|
39
|
+
appId?: string;
|
|
40
|
+
/** @deprecated use serviceName */
|
|
41
|
+
appName?: string;
|
|
42
|
+
}
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 {}
|
|
44
|
+
export interface Config {
|
|
45
|
+
credentials?: Credentials;
|
|
46
|
+
projects?: Record<string, ProjectLink>;
|
|
58
47
|
}
|
|
59
48
|
|
|
60
|
-
export function
|
|
49
|
+
export function loadConfig(): Config {
|
|
61
50
|
try {
|
|
62
|
-
return JSON.parse(fs.readFileSync(
|
|
51
|
+
return JSON.parse(fs.readFileSync(configFile(), "utf-8")) as Config;
|
|
63
52
|
} catch {
|
|
64
53
|
return {};
|
|
65
54
|
}
|
|
66
55
|
}
|
|
67
56
|
|
|
68
|
-
export function
|
|
69
|
-
|
|
70
|
-
fs.
|
|
71
|
-
|
|
57
|
+
export function saveConfig(config: Config) {
|
|
58
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
59
|
+
fs.writeFileSync(configFile(), JSON.stringify(config, null, 2), {
|
|
60
|
+
mode: 0o600,
|
|
61
|
+
});
|
|
72
62
|
}
|
|
73
63
|
|
|
74
64
|
/**
|
|
75
|
-
*
|
|
65
|
+
* Read the link for a directory. Normalises legacy `appId/appName` into
|
|
66
|
+
* `serviceId/serviceName` so callers only have to look at one pair.
|
|
76
67
|
*/
|
|
77
|
-
export function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
export function getProjectLink(cwd: string = process.cwd()): ProjectLink | null {
|
|
69
|
+
const raw = loadConfig().projects?.[cwd];
|
|
70
|
+
if (!raw) return null;
|
|
71
|
+
return {
|
|
72
|
+
...raw,
|
|
73
|
+
serviceId: raw.serviceId ?? raw.appId,
|
|
74
|
+
serviceName: raw.serviceName ?? raw.appName,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function setProjectLink(link: ProjectLink, cwd: string = process.cwd()) {
|
|
79
|
+
const config = loadConfig();
|
|
80
|
+
config.projects ??= {};
|
|
81
|
+
// Mirror service↔app for older readers.
|
|
82
|
+
const normalised: ProjectLink = {
|
|
83
|
+
...link,
|
|
84
|
+
appId: link.serviceId ?? link.appId,
|
|
85
|
+
appName: link.serviceName ?? link.appName,
|
|
86
|
+
serviceId: link.serviceId ?? link.appId,
|
|
87
|
+
serviceName: link.serviceName ?? link.appName,
|
|
88
|
+
};
|
|
89
|
+
config.projects[cwd] = normalised;
|
|
90
|
+
saveConfig(config);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function updateProjectLink(
|
|
94
|
+
patch: Partial<ProjectLink>,
|
|
95
|
+
cwd: string = process.cwd(),
|
|
96
|
+
) {
|
|
97
|
+
const existing = getProjectLink(cwd);
|
|
98
|
+
if (!existing) return;
|
|
99
|
+
setProjectLink({ ...existing, ...patch }, cwd);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function clearProjectLink(cwd: string = process.cwd()) {
|
|
103
|
+
const config = loadConfig();
|
|
104
|
+
if (config.projects) {
|
|
105
|
+
delete config.projects[cwd];
|
|
106
|
+
saveConfig(config);
|
|
107
|
+
}
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
/**
|
|
87
|
-
* Resolve
|
|
111
|
+
* Resolve a project flag (name, slug, or ID) to an actual project ID.
|
|
112
|
+
* When no flag is given, falls back to the linked cwd. Hits the API only
|
|
113
|
+
* when a flag is provided so name/slug lookups work as advertised.
|
|
88
114
|
*/
|
|
89
|
-
export function
|
|
90
|
-
if (flagValue)
|
|
91
|
-
|
|
92
|
-
|
|
115
|
+
export async function resolveProjectId(flagValue?: string): Promise<string> {
|
|
116
|
+
if (!flagValue) {
|
|
117
|
+
const link = getProjectLink();
|
|
118
|
+
if (link?.projectId) return link.projectId;
|
|
119
|
+
throw new Error("No project linked. Run `lizard init` or pass --project <id>.");
|
|
120
|
+
}
|
|
121
|
+
const { api } = await import("./api.js");
|
|
122
|
+
const projects = await api.get<Array<{ id: string; name: string; slug: string }>>("/api/projects");
|
|
123
|
+
const match = projects.find(
|
|
124
|
+
(p) => p.id === flagValue || p.slug === flagValue || p.name === flagValue,
|
|
125
|
+
);
|
|
126
|
+
if (!match) throw new Error(`Project "${flagValue}" not found.`);
|
|
127
|
+
return match.id;
|
|
93
128
|
}
|
package/src/lib/format.ts
CHANGED
|
@@ -34,29 +34,43 @@ export function info(msg: string) {
|
|
|
34
34
|
process.stderr.write(msg + "\n");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function link(url: string, text?: string): string {
|
|
38
|
+
const label = text ?? url;
|
|
39
|
+
if (!isTTY()) return label;
|
|
40
|
+
return `\x1b]8;;${url}\x1b\\${label}\x1b]8;;\x1b\\`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
44
|
+
function visibleLength(s: string): number {
|
|
45
|
+
return s.replace(ANSI_RE, "").length;
|
|
46
|
+
}
|
|
47
|
+
function padVisible(s: string, width: number): string {
|
|
48
|
+
return s + " ".repeat(Math.max(0, width - visibleLength(s)));
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
export function table(
|
|
38
52
|
headers: string[],
|
|
39
53
|
rows: string[][],
|
|
40
54
|
) {
|
|
41
55
|
if (rows.length === 0) return;
|
|
42
56
|
|
|
43
|
-
const widths = headers.map((h) => h
|
|
57
|
+
const widths = headers.map((h) => visibleLength(h));
|
|
44
58
|
for (const row of rows) {
|
|
45
59
|
for (let i = 0; i < row.length; i++) {
|
|
46
60
|
if (i < widths.length) {
|
|
47
|
-
widths[i] = Math.max(widths[i], (row[i] || "")
|
|
61
|
+
widths[i] = Math.max(widths[i], visibleLength(row[i] || ""));
|
|
48
62
|
}
|
|
49
63
|
}
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
const header = headers
|
|
53
|
-
.map((h, i) => h.toUpperCase()
|
|
67
|
+
.map((h, i) => padVisible(h.toUpperCase(), widths[i]))
|
|
54
68
|
.join(" ");
|
|
55
69
|
console.log(chalk.dim(header));
|
|
56
70
|
|
|
57
71
|
for (const row of rows) {
|
|
58
72
|
const line = headers
|
|
59
|
-
.map((_, i) => (row[i] || ""
|
|
73
|
+
.map((_, i) => padVisible(row[i] || "", widths[i]))
|
|
60
74
|
.join(" ");
|
|
61
75
|
console.log(line);
|
|
62
76
|
}
|
package/src/lib/name.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Shared name validator. Mirrors server/src/services/var-transform.ts.
|
|
2
|
+
// LIZARD-55: 1–40 chars, [a-z0-9-], must start and end with [a-z0-9].
|
|
3
|
+
|
|
4
|
+
export const NAME_REGEX = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
5
|
+
export const NAME_VALIDATION_HINT =
|
|
6
|
+
"1–40 chars, lowercase a–z, digits, hyphens; can't start or end with a hyphen";
|
|
7
|
+
|
|
8
|
+
export function validateName(name: string): string | null {
|
|
9
|
+
if (!name) return "name is required";
|
|
10
|
+
if (name.length > 40) return "name must be 40 characters or fewer";
|
|
11
|
+
if (!NAME_REGEX.test(name)) return `invalid name (${NAME_VALIDATION_HINT})`;
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Mirrors `slugifyName` in lizard-client/src/lib/api.ts. */
|
|
16
|
+
export function slugifyName(name: string): string {
|
|
17
|
+
return name
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
20
|
+
.replace(/^-+|-+$/g, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Mirrors `addonRefName` in lizard-client — what users type inside ${{...}}. */
|
|
24
|
+
export function addonRefName(addon: { name?: string | null; type?: string; addonType?: string }): string {
|
|
25
|
+
const display = addon.name || addon.type || addon.addonType || "";
|
|
26
|
+
return slugifyName(display) || display;
|
|
27
|
+
}
|