@groundfloorcloud/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/README.md +201 -0
- package/dist/index.js +1345 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/auth/store.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
var CONFIG_DIR = path.join(os.homedir(), ".groundfloor");
|
|
11
|
+
var AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
12
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
13
|
+
async function ensureDir() {
|
|
14
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
15
|
+
}
|
|
16
|
+
async function readJson(file) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(file, "utf8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code === "ENOENT") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readAuth() {
|
|
28
|
+
return readJson(AUTH_FILE);
|
|
29
|
+
}
|
|
30
|
+
async function writeAuth(session) {
|
|
31
|
+
await ensureDir();
|
|
32
|
+
await fs.writeFile(AUTH_FILE, `${JSON.stringify(session, null, 2)}
|
|
33
|
+
`, {
|
|
34
|
+
mode: 384
|
|
35
|
+
});
|
|
36
|
+
await fs.chmod(AUTH_FILE, 384);
|
|
37
|
+
}
|
|
38
|
+
async function clearAuth() {
|
|
39
|
+
try {
|
|
40
|
+
await fs.unlink(AUTH_FILE);
|
|
41
|
+
return true;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code === "ENOENT") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function readConfig() {
|
|
50
|
+
return await readJson(CONFIG_FILE) ?? {};
|
|
51
|
+
}
|
|
52
|
+
async function writeConfig(config) {
|
|
53
|
+
await ensureDir();
|
|
54
|
+
await fs.writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
|
|
55
|
+
`, {
|
|
56
|
+
mode: 384
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function updateConfig(patch) {
|
|
60
|
+
const current = await readConfig();
|
|
61
|
+
const next = { ...current, ...patch };
|
|
62
|
+
await writeConfig(next);
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/auth/config.ts
|
|
67
|
+
var DEFAULTS = {
|
|
68
|
+
issuer: "https://auth.dev.groundfloor.cloud/realms/groundfloor_dev",
|
|
69
|
+
clientId: "groundfloor-cli",
|
|
70
|
+
apiUrl: "http://localhost:8088"
|
|
71
|
+
};
|
|
72
|
+
function trimTrailingSlash(value) {
|
|
73
|
+
return value.replace(/\/+$/, "");
|
|
74
|
+
}
|
|
75
|
+
async function resolveConfig(overrides = {}) {
|
|
76
|
+
const file = await readConfig();
|
|
77
|
+
const issuer = overrides.issuer ?? process.env.GROUNDFLOOR_ISSUER ?? file.issuer ?? DEFAULTS.issuer;
|
|
78
|
+
const clientId = overrides.clientId ?? process.env.GROUNDFLOOR_CLIENT_ID ?? file.clientId ?? DEFAULTS.clientId;
|
|
79
|
+
const apiUrl = overrides.apiUrl ?? process.env.GROUNDFLOOR_API_URL ?? file.apiUrl ?? DEFAULTS.apiUrl;
|
|
80
|
+
const workspaceId = overrides.workspaceId ?? process.env.GROUNDFLOOR_WORKSPACE_ID ?? file.workspaceId;
|
|
81
|
+
return {
|
|
82
|
+
issuer: trimTrailingSlash(issuer),
|
|
83
|
+
clientId,
|
|
84
|
+
apiUrl: trimTrailingSlash(apiUrl),
|
|
85
|
+
workspaceId: workspaceId || void 0
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/auth/oauth.ts
|
|
90
|
+
import crypto from "crypto";
|
|
91
|
+
import http from "http";
|
|
92
|
+
import open from "open";
|
|
93
|
+
var SCOPE = "openid profile email offline_access";
|
|
94
|
+
function endpoints(issuer) {
|
|
95
|
+
const base = issuer.replace(/\/+$/, "");
|
|
96
|
+
return {
|
|
97
|
+
authorization: `${base}/protocol/openid-connect/auth`,
|
|
98
|
+
token: `${base}/protocol/openid-connect/token`,
|
|
99
|
+
deviceAuthorization: `${base}/protocol/openid-connect/auth/device`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function base64url(input) {
|
|
103
|
+
return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
104
|
+
}
|
|
105
|
+
function createPkcePair() {
|
|
106
|
+
const verifier = base64url(crypto.randomBytes(32));
|
|
107
|
+
const challenge = base64url(
|
|
108
|
+
crypto.createHash("sha256").update(verifier).digest()
|
|
109
|
+
);
|
|
110
|
+
return { verifier, challenge };
|
|
111
|
+
}
|
|
112
|
+
async function postForm(url, form) {
|
|
113
|
+
const res = await fetch(url, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
116
|
+
body: new URLSearchParams(form).toString()
|
|
117
|
+
});
|
|
118
|
+
let body = {};
|
|
119
|
+
try {
|
|
120
|
+
body = await res.json();
|
|
121
|
+
} catch {
|
|
122
|
+
body = {};
|
|
123
|
+
}
|
|
124
|
+
return { ok: res.ok, status: res.status, body };
|
|
125
|
+
}
|
|
126
|
+
function tokenResponseFrom(body) {
|
|
127
|
+
return {
|
|
128
|
+
access_token: String(body.access_token ?? ""),
|
|
129
|
+
refresh_token: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
|
|
130
|
+
id_token: typeof body.id_token === "string" ? body.id_token : void 0,
|
|
131
|
+
expires_in: typeof body.expires_in === "number" ? body.expires_in : void 0,
|
|
132
|
+
token_type: typeof body.token_type === "string" ? body.token_type : void 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
var SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Groundfloor CLI</title></head>
|
|
136
|
+
<body style="font-family:system-ui,sans-serif;background:#0b0d12;color:#e6e8ec;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
137
|
+
<div style="text-align:center"><h1 style="font-size:1.4rem">You're signed in</h1>
|
|
138
|
+
<p style="opacity:.7">You can close this tab and return to your terminal.</p></div></body></html>`;
|
|
139
|
+
async function loginWithBrowser(opts) {
|
|
140
|
+
const { issuer, clientId } = opts;
|
|
141
|
+
const host = opts.redirectHost ?? "127.0.0.1";
|
|
142
|
+
const listenPort = opts.redirectPort ?? 0;
|
|
143
|
+
const autoOpen = opts.autoOpen ?? true;
|
|
144
|
+
const ep = endpoints(issuer);
|
|
145
|
+
const { verifier, challenge } = createPkcePair();
|
|
146
|
+
const state = base64url(crypto.randomBytes(16));
|
|
147
|
+
return new Promise((resolve2, reject) => {
|
|
148
|
+
const server = http.createServer((req, res) => {
|
|
149
|
+
if (!req.url) return;
|
|
150
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
151
|
+
if (url.pathname !== "/callback") {
|
|
152
|
+
res.statusCode = 404;
|
|
153
|
+
res.end("Not found");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const error = url.searchParams.get("error");
|
|
157
|
+
const code = url.searchParams.get("code");
|
|
158
|
+
const returnedState = url.searchParams.get("state");
|
|
159
|
+
res.statusCode = 200;
|
|
160
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
161
|
+
res.end(SUCCESS_HTML);
|
|
162
|
+
const finish = (fn) => {
|
|
163
|
+
server.close();
|
|
164
|
+
fn();
|
|
165
|
+
};
|
|
166
|
+
if (error) {
|
|
167
|
+
finish(
|
|
168
|
+
() => reject(
|
|
169
|
+
new Error(
|
|
170
|
+
`Authorization failed: ${error} ${url.searchParams.get("error_description") ?? ""}`.trim()
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (returnedState !== state) {
|
|
177
|
+
finish(() => reject(new Error("State mismatch; aborting login.")));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!code) {
|
|
181
|
+
finish(() => reject(new Error("No authorization code returned.")));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const port = server.address().port;
|
|
185
|
+
const redirectUri = `http://${host}:${port}/callback`;
|
|
186
|
+
postForm(ep.token, {
|
|
187
|
+
grant_type: "authorization_code",
|
|
188
|
+
code,
|
|
189
|
+
redirect_uri: redirectUri,
|
|
190
|
+
client_id: clientId,
|
|
191
|
+
code_verifier: verifier
|
|
192
|
+
}).then(({ ok, status, body }) => {
|
|
193
|
+
if (!ok) {
|
|
194
|
+
finish(
|
|
195
|
+
() => reject(
|
|
196
|
+
new Error(
|
|
197
|
+
`Token exchange failed (${status}): ${body.error_description ?? body.error ?? "unknown error"}`
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
finish(() => resolve2(tokenResponseFrom(body)));
|
|
204
|
+
}).catch((err) => finish(() => reject(err)));
|
|
205
|
+
});
|
|
206
|
+
server.on("error", reject);
|
|
207
|
+
server.listen(listenPort, host, () => {
|
|
208
|
+
const port = server.address().port;
|
|
209
|
+
const redirectUri = `http://${host}:${port}/callback`;
|
|
210
|
+
const authUrl = new URL(ep.authorization);
|
|
211
|
+
authUrl.searchParams.set("response_type", "code");
|
|
212
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
213
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
214
|
+
authUrl.searchParams.set("scope", SCOPE);
|
|
215
|
+
authUrl.searchParams.set("state", state);
|
|
216
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
217
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
218
|
+
const display = authUrl.toString();
|
|
219
|
+
if (autoOpen) {
|
|
220
|
+
process.stderr.write(
|
|
221
|
+
`
|
|
222
|
+
Opening your browser to sign in...
|
|
223
|
+
If it does not open, visit:
|
|
224
|
+
${display}
|
|
225
|
+
|
|
226
|
+
`
|
|
227
|
+
);
|
|
228
|
+
void open(display).catch(() => {
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
process.stderr.write(
|
|
232
|
+
`
|
|
233
|
+
Open this URL to sign in (e.g. in a fresh incognito window):
|
|
234
|
+
${display}
|
|
235
|
+
|
|
236
|
+
`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
setTimeout(
|
|
241
|
+
() => {
|
|
242
|
+
server.close();
|
|
243
|
+
reject(new Error("Login timed out after 5 minutes."));
|
|
244
|
+
},
|
|
245
|
+
5 * 60 * 1e3
|
|
246
|
+
).unref();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async function loginWithDeviceCode(opts) {
|
|
250
|
+
const { issuer, clientId } = opts;
|
|
251
|
+
const ep = endpoints(issuer);
|
|
252
|
+
const start = await postForm(ep.deviceAuthorization, {
|
|
253
|
+
client_id: clientId,
|
|
254
|
+
scope: SCOPE
|
|
255
|
+
});
|
|
256
|
+
if (!start.ok) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Device authorization failed (${start.status}): ${start.body.error_description ?? start.body.error ?? "unknown error"}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const deviceCode = String(start.body.device_code ?? "");
|
|
262
|
+
const userCode = String(start.body.user_code ?? "");
|
|
263
|
+
const verificationUri = String(start.body.verification_uri ?? "");
|
|
264
|
+
const verificationUriComplete = typeof start.body.verification_uri_complete === "string" ? start.body.verification_uri_complete : void 0;
|
|
265
|
+
let interval = typeof start.body.interval === "number" ? start.body.interval : 5;
|
|
266
|
+
const expiresIn = typeof start.body.expires_in === "number" ? start.body.expires_in : 600;
|
|
267
|
+
process.stderr.write(
|
|
268
|
+
`
|
|
269
|
+
To sign in, open:
|
|
270
|
+
${verificationUri}
|
|
271
|
+
and enter code:
|
|
272
|
+
${userCode}
|
|
273
|
+
|
|
274
|
+
`
|
|
275
|
+
);
|
|
276
|
+
if (verificationUriComplete) {
|
|
277
|
+
void open(verificationUriComplete).catch(() => {
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
281
|
+
while (true) {
|
|
282
|
+
if (Date.now() > deadline) {
|
|
283
|
+
throw new Error("Device login timed out.");
|
|
284
|
+
}
|
|
285
|
+
await new Promise((r) => setTimeout(r, interval * 1e3));
|
|
286
|
+
const poll = await postForm(ep.token, {
|
|
287
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
288
|
+
device_code: deviceCode,
|
|
289
|
+
client_id: clientId
|
|
290
|
+
});
|
|
291
|
+
if (poll.ok) {
|
|
292
|
+
return tokenResponseFrom(poll.body);
|
|
293
|
+
}
|
|
294
|
+
const err = poll.body.error;
|
|
295
|
+
if (err === "authorization_pending") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (err === "slow_down") {
|
|
299
|
+
interval += 5;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Device login failed: ${poll.body.error_description ?? err ?? "unknown error"}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function refreshTokens(opts) {
|
|
308
|
+
const ep = endpoints(opts.issuer);
|
|
309
|
+
const { ok, status, body } = await postForm(ep.token, {
|
|
310
|
+
grant_type: "refresh_token",
|
|
311
|
+
client_id: opts.clientId,
|
|
312
|
+
refresh_token: opts.refreshToken
|
|
313
|
+
});
|
|
314
|
+
if (!ok) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Token refresh failed (${status}): ${body.error_description ?? body.error ?? "session expired"}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return tokenResponseFrom(body);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/auth/session.ts
|
|
323
|
+
var EXPIRY_SKEW_MS = 3e4;
|
|
324
|
+
var NotLoggedInError = class extends Error {
|
|
325
|
+
constructor() {
|
|
326
|
+
super("Not logged in. Run `gf login` first.");
|
|
327
|
+
this.name = "NotLoggedInError";
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
function decodeJwt(token) {
|
|
331
|
+
const parts = token.split(".");
|
|
332
|
+
if (parts.length < 2) return null;
|
|
333
|
+
try {
|
|
334
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
335
|
+
return JSON.parse(json);
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function sessionFromTokens(tokens, ctx) {
|
|
341
|
+
const expiresInMs = (tokens.expires_in ?? 300) * 1e3;
|
|
342
|
+
return {
|
|
343
|
+
issuer: ctx.issuer,
|
|
344
|
+
clientId: ctx.clientId,
|
|
345
|
+
apiUrl: ctx.apiUrl,
|
|
346
|
+
accessToken: tokens.access_token,
|
|
347
|
+
refreshToken: tokens.refresh_token,
|
|
348
|
+
idToken: tokens.id_token,
|
|
349
|
+
expiresAt: Date.now() + expiresInMs
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function isFresh(session) {
|
|
353
|
+
return Date.now() < session.expiresAt - EXPIRY_SKEW_MS;
|
|
354
|
+
}
|
|
355
|
+
async function getValidAccessToken() {
|
|
356
|
+
const session = await readAuth();
|
|
357
|
+
if (!session || !session.accessToken) {
|
|
358
|
+
throw new NotLoggedInError();
|
|
359
|
+
}
|
|
360
|
+
if (isFresh(session)) {
|
|
361
|
+
return session.accessToken;
|
|
362
|
+
}
|
|
363
|
+
if (!session.refreshToken) {
|
|
364
|
+
throw new NotLoggedInError();
|
|
365
|
+
}
|
|
366
|
+
const refreshed = await refreshTokens({
|
|
367
|
+
issuer: session.issuer,
|
|
368
|
+
clientId: session.clientId,
|
|
369
|
+
refreshToken: session.refreshToken
|
|
370
|
+
});
|
|
371
|
+
const next = sessionFromTokens(refreshed, {
|
|
372
|
+
issuer: session.issuer,
|
|
373
|
+
clientId: session.clientId,
|
|
374
|
+
apiUrl: session.apiUrl
|
|
375
|
+
});
|
|
376
|
+
if (!next.refreshToken) {
|
|
377
|
+
next.refreshToken = session.refreshToken;
|
|
378
|
+
}
|
|
379
|
+
await writeAuth(next);
|
|
380
|
+
return next.accessToken;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/commands/login.ts
|
|
384
|
+
async function loginCommand(opts) {
|
|
385
|
+
const cfg = await resolveConfig({
|
|
386
|
+
issuer: opts.issuer,
|
|
387
|
+
clientId: opts.clientId,
|
|
388
|
+
apiUrl: opts.apiUrl
|
|
389
|
+
});
|
|
390
|
+
process.stderr.write(
|
|
391
|
+
`Signing in to ${cfg.issuer}
|
|
392
|
+
client: ${cfg.clientId}
|
|
393
|
+
api: ${cfg.apiUrl}
|
|
394
|
+
`
|
|
395
|
+
);
|
|
396
|
+
const redirectPort = opts.redirectPort ? Number.parseInt(opts.redirectPort, 10) : void 0;
|
|
397
|
+
if (redirectPort !== void 0 && Number.isNaN(redirectPort)) {
|
|
398
|
+
throw new Error(`Invalid --redirect-port "${opts.redirectPort}".`);
|
|
399
|
+
}
|
|
400
|
+
const tokens = opts.device ? await loginWithDeviceCode({ issuer: cfg.issuer, clientId: cfg.clientId }) : await loginWithBrowser({
|
|
401
|
+
issuer: cfg.issuer,
|
|
402
|
+
clientId: cfg.clientId,
|
|
403
|
+
redirectHost: opts.redirectHost,
|
|
404
|
+
redirectPort,
|
|
405
|
+
autoOpen: opts.open
|
|
406
|
+
});
|
|
407
|
+
if (!tokens.access_token) {
|
|
408
|
+
throw new Error("No access token returned from the identity provider.");
|
|
409
|
+
}
|
|
410
|
+
const session = sessionFromTokens(tokens, {
|
|
411
|
+
issuer: cfg.issuer,
|
|
412
|
+
clientId: cfg.clientId,
|
|
413
|
+
apiUrl: cfg.apiUrl
|
|
414
|
+
});
|
|
415
|
+
await writeAuth(session);
|
|
416
|
+
await updateConfig({
|
|
417
|
+
issuer: cfg.issuer,
|
|
418
|
+
clientId: cfg.clientId,
|
|
419
|
+
apiUrl: cfg.apiUrl
|
|
420
|
+
});
|
|
421
|
+
const claims = decodeJwt(tokens.access_token);
|
|
422
|
+
const who = claims?.email || claims?.preferred_username || claims?.sub || "unknown user";
|
|
423
|
+
process.stdout.write(`Logged in as ${who}
|
|
424
|
+
`);
|
|
425
|
+
if (!tokens.refresh_token) {
|
|
426
|
+
process.stderr.write(
|
|
427
|
+
"Note: no refresh token was issued; you may need to log in again when the token expires.\n"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/commands/logout.ts
|
|
433
|
+
async function logoutCommand() {
|
|
434
|
+
const removed = await clearAuth();
|
|
435
|
+
process.stdout.write(
|
|
436
|
+
removed ? "Logged out.\n" : "No active session to clear.\n"
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/cp.ts
|
|
441
|
+
var CpError = class extends Error {
|
|
442
|
+
constructor(status, message) {
|
|
443
|
+
super(message);
|
|
444
|
+
this.status = status;
|
|
445
|
+
this.name = "CpError";
|
|
446
|
+
}
|
|
447
|
+
status;
|
|
448
|
+
};
|
|
449
|
+
async function parseError(res) {
|
|
450
|
+
let detail = res.statusText;
|
|
451
|
+
try {
|
|
452
|
+
const body = await res.json();
|
|
453
|
+
if (body?.detail) detail = String(body.detail);
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
return `${res.status} ${detail}`;
|
|
457
|
+
}
|
|
458
|
+
async function cpGet(apiUrl, path4) {
|
|
459
|
+
const token = await getValidAccessToken();
|
|
460
|
+
const res = await fetch(`${apiUrl}${path4}`, {
|
|
461
|
+
headers: {
|
|
462
|
+
Authorization: `Bearer ${token}`,
|
|
463
|
+
Accept: "application/json"
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
if (!res.ok) {
|
|
467
|
+
throw new CpError(res.status, await parseError(res));
|
|
468
|
+
}
|
|
469
|
+
return await res.json();
|
|
470
|
+
}
|
|
471
|
+
async function cpPostJson(apiUrl, path4, body) {
|
|
472
|
+
const token = await getValidAccessToken();
|
|
473
|
+
const res = await fetch(`${apiUrl}${path4}`, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: {
|
|
476
|
+
Authorization: `Bearer ${token}`,
|
|
477
|
+
Accept: "application/json",
|
|
478
|
+
"Content-Type": "application/json"
|
|
479
|
+
},
|
|
480
|
+
body: JSON.stringify(body)
|
|
481
|
+
});
|
|
482
|
+
if (!res.ok) {
|
|
483
|
+
throw new CpError(res.status, await parseError(res));
|
|
484
|
+
}
|
|
485
|
+
return await res.json();
|
|
486
|
+
}
|
|
487
|
+
async function cpDelete(apiUrl, path4) {
|
|
488
|
+
const token = await getValidAccessToken();
|
|
489
|
+
const res = await fetch(`${apiUrl}${path4}`, {
|
|
490
|
+
method: "DELETE",
|
|
491
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
|
|
492
|
+
});
|
|
493
|
+
if (!res.ok) {
|
|
494
|
+
throw new CpError(res.status, await parseError(res));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function listWorkspaces(apiUrl) {
|
|
498
|
+
return cpGet(apiUrl, "/v1/workspaces");
|
|
499
|
+
}
|
|
500
|
+
async function listCoderunners(apiUrl, workspaceId) {
|
|
501
|
+
const data = await cpGet(
|
|
502
|
+
apiUrl,
|
|
503
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/coderunners`
|
|
504
|
+
);
|
|
505
|
+
if (Array.isArray(data)) return data;
|
|
506
|
+
return data.coderunners ?? [];
|
|
507
|
+
}
|
|
508
|
+
function createCoderunner(apiUrl, workspaceId, payload) {
|
|
509
|
+
return cpPostJson(
|
|
510
|
+
apiUrl,
|
|
511
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/coderunners`,
|
|
512
|
+
payload
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
async function uploadZip(apiUrl, workspaceId, coderunnerId, zip, description) {
|
|
516
|
+
const token = await getValidAccessToken();
|
|
517
|
+
const form = new FormData();
|
|
518
|
+
form.append(
|
|
519
|
+
"file",
|
|
520
|
+
new Blob([zip], { type: "application/zip" }),
|
|
521
|
+
"code.zip"
|
|
522
|
+
);
|
|
523
|
+
form.append("description", description);
|
|
524
|
+
const res = await fetch(
|
|
525
|
+
`${apiUrl}/v1/workspaces/${encodeURIComponent(
|
|
526
|
+
workspaceId
|
|
527
|
+
)}/coderunners/${encodeURIComponent(coderunnerId)}/upload`,
|
|
528
|
+
{
|
|
529
|
+
method: "POST",
|
|
530
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
531
|
+
body: form
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
if (!res.ok) {
|
|
535
|
+
throw new CpError(res.status, await parseError(res));
|
|
536
|
+
}
|
|
537
|
+
return await res.json();
|
|
538
|
+
}
|
|
539
|
+
function getVersion(apiUrl, workspaceId, coderunnerId, versionId) {
|
|
540
|
+
return cpGet(
|
|
541
|
+
apiUrl,
|
|
542
|
+
`/v1/workspaces/${encodeURIComponent(
|
|
543
|
+
workspaceId
|
|
544
|
+
)}/coderunners/${encodeURIComponent(
|
|
545
|
+
coderunnerId
|
|
546
|
+
)}/versions/${encodeURIComponent(versionId)}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
function deployCoderunner(apiUrl, workspaceId, coderunnerId, payload) {
|
|
550
|
+
return cpPostJson(
|
|
551
|
+
apiUrl,
|
|
552
|
+
`/v1/workspaces/${encodeURIComponent(
|
|
553
|
+
workspaceId
|
|
554
|
+
)}/coderunners/${encodeURIComponent(coderunnerId)}/deploy`,
|
|
555
|
+
payload
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
async function getDeployment(apiUrl, workspaceId, coderunnerId) {
|
|
559
|
+
const token = await getValidAccessToken();
|
|
560
|
+
const res = await fetch(
|
|
561
|
+
`${apiUrl}/v1/workspaces/${encodeURIComponent(
|
|
562
|
+
workspaceId
|
|
563
|
+
)}/coderunners/${encodeURIComponent(coderunnerId)}/deployment`,
|
|
564
|
+
{
|
|
565
|
+
headers: {
|
|
566
|
+
Authorization: `Bearer ${token}`,
|
|
567
|
+
Accept: "application/json"
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
if (res.status === 204) return null;
|
|
572
|
+
if (!res.ok) {
|
|
573
|
+
throw new CpError(res.status, await parseError(res));
|
|
574
|
+
}
|
|
575
|
+
return await res.json();
|
|
576
|
+
}
|
|
577
|
+
function getCoderunnerStatus(apiUrl, workspaceId, coderunnerId) {
|
|
578
|
+
return cpGet(
|
|
579
|
+
apiUrl,
|
|
580
|
+
`/v1/workspaces/${encodeURIComponent(
|
|
581
|
+
workspaceId
|
|
582
|
+
)}/coderunners/${encodeURIComponent(coderunnerId)}/status`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
function runCoderunner(apiUrl, workspaceId, coderunnerId, payload) {
|
|
586
|
+
return cpPostJson(
|
|
587
|
+
apiUrl,
|
|
588
|
+
`/v1/workspaces/${encodeURIComponent(
|
|
589
|
+
workspaceId
|
|
590
|
+
)}/coderunners/${encodeURIComponent(coderunnerId)}/run`,
|
|
591
|
+
payload
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
function domainsBase(workspaceId, coderunnerId) {
|
|
595
|
+
return `/v1/workspaces/${encodeURIComponent(
|
|
596
|
+
workspaceId
|
|
597
|
+
)}/coderunners/${encodeURIComponent(coderunnerId)}/domains`;
|
|
598
|
+
}
|
|
599
|
+
function listDomains(apiUrl, workspaceId, coderunnerId) {
|
|
600
|
+
return cpGet(
|
|
601
|
+
apiUrl,
|
|
602
|
+
domainsBase(workspaceId, coderunnerId)
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
function addDomain(apiUrl, workspaceId, coderunnerId, domain) {
|
|
606
|
+
return cpPostJson(
|
|
607
|
+
apiUrl,
|
|
608
|
+
domainsBase(workspaceId, coderunnerId),
|
|
609
|
+
{ domain }
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
function removeDomain(apiUrl, workspaceId, coderunnerId, domain) {
|
|
613
|
+
return cpDelete(
|
|
614
|
+
apiUrl,
|
|
615
|
+
`${domainsBase(workspaceId, coderunnerId)}/${encodeURIComponent(domain)}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
function verifyDomain(apiUrl, workspaceId, coderunnerId, domain) {
|
|
619
|
+
return cpPostJson(
|
|
620
|
+
apiUrl,
|
|
621
|
+
`${domainsBase(workspaceId, coderunnerId)}/${encodeURIComponent(
|
|
622
|
+
domain
|
|
623
|
+
)}/verify`,
|
|
624
|
+
{}
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/commands/whoami.ts
|
|
629
|
+
async function whoamiCommand() {
|
|
630
|
+
const cfg = await resolveConfig();
|
|
631
|
+
const token = await getValidAccessToken();
|
|
632
|
+
const claims = decodeJwt(token) ?? {};
|
|
633
|
+
const email = claims.email || claims.preferred_username || "(no email claim)";
|
|
634
|
+
const subject = claims.sub || "(no subject)";
|
|
635
|
+
process.stdout.write(`User: ${email}
|
|
636
|
+
`);
|
|
637
|
+
process.stdout.write(`Subject: ${subject}
|
|
638
|
+
`);
|
|
639
|
+
process.stdout.write(`Issuer: ${cfg.issuer}
|
|
640
|
+
`);
|
|
641
|
+
process.stdout.write(`API: ${cfg.apiUrl}
|
|
642
|
+
`);
|
|
643
|
+
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
644
|
+
process.stdout.write(
|
|
645
|
+
`Session verified: Control Plane returned ${workspaces2.length} workspace(s).
|
|
646
|
+
`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/commands/token.ts
|
|
651
|
+
async function tokenCommand() {
|
|
652
|
+
const token = await getValidAccessToken();
|
|
653
|
+
process.stdout.write(`${token}
|
|
654
|
+
`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/commands/env.ts
|
|
658
|
+
async function envCommand() {
|
|
659
|
+
const cfg = await resolveConfig();
|
|
660
|
+
const token = await getValidAccessToken();
|
|
661
|
+
const lines = [
|
|
662
|
+
`export GROUNDFLOOR_TOKEN=${token}`,
|
|
663
|
+
`export CONTROLPLANE_URL=${cfg.apiUrl}`,
|
|
664
|
+
`export NEXT_PUBLIC_API_URL=${cfg.apiUrl}`
|
|
665
|
+
];
|
|
666
|
+
if (cfg.workspaceId) {
|
|
667
|
+
lines.push(`export GROUNDFLOOR_WORKSPACE_ID=${cfg.workspaceId}`);
|
|
668
|
+
}
|
|
669
|
+
process.stdout.write(`${lines.join("\n")}
|
|
670
|
+
`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/commands/workspaces.ts
|
|
674
|
+
async function workspacesListCommand(opts) {
|
|
675
|
+
const cfg = await resolveConfig();
|
|
676
|
+
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
677
|
+
if (opts.json) {
|
|
678
|
+
process.stdout.write(`${JSON.stringify(workspaces2, null, 2)}
|
|
679
|
+
`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (workspaces2.length === 0) {
|
|
683
|
+
process.stdout.write("No workspaces found for this user.\n");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const file = await readConfig();
|
|
687
|
+
const selected = cfg.workspaceId ?? file.workspaceId;
|
|
688
|
+
for (const ws of workspaces2) {
|
|
689
|
+
const marker = ws.id === selected ? "*" : " ";
|
|
690
|
+
const name = ws.name ?? ws.slug ?? "";
|
|
691
|
+
process.stdout.write(`${marker} ${ws.id} ${name}
|
|
692
|
+
`);
|
|
693
|
+
}
|
|
694
|
+
if (selected) {
|
|
695
|
+
process.stdout.write(`
|
|
696
|
+
(* = default workspace)
|
|
697
|
+
`);
|
|
698
|
+
} else {
|
|
699
|
+
process.stdout.write(
|
|
700
|
+
`
|
|
701
|
+
Tip: run \`gf workspaces use <id>\` to set a default.
|
|
702
|
+
`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function workspacesUseCommand(workspaceId) {
|
|
707
|
+
await updateConfig({ workspaceId });
|
|
708
|
+
process.stdout.write(`Default workspace set to ${workspaceId}
|
|
709
|
+
`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/commands/coderunner.ts
|
|
713
|
+
async function coderunnerListCommand(opts) {
|
|
714
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
715
|
+
if (!cfg.workspaceId) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const coderunners = await listCoderunners(cfg.apiUrl, cfg.workspaceId);
|
|
721
|
+
if (opts.json) {
|
|
722
|
+
process.stdout.write(`${JSON.stringify(coderunners, null, 2)}
|
|
723
|
+
`);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (coderunners.length === 0) {
|
|
727
|
+
process.stdout.write("No coderunners in this workspace.\n");
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
for (const cr of coderunners) {
|
|
731
|
+
const name = cr.name ?? cr.slug ?? "";
|
|
732
|
+
const status = cr.status ?? "";
|
|
733
|
+
process.stdout.write(`${cr.id} ${name} [${status}]
|
|
734
|
+
`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/commands/deploy.ts
|
|
739
|
+
import { promises as fs3 } from "fs";
|
|
740
|
+
import path3 from "path";
|
|
741
|
+
|
|
742
|
+
// src/deploy/package.ts
|
|
743
|
+
import { execFile } from "child_process";
|
|
744
|
+
import { promises as fs2 } from "fs";
|
|
745
|
+
import os2 from "os";
|
|
746
|
+
import path2 from "path";
|
|
747
|
+
import { promisify } from "util";
|
|
748
|
+
import AdmZip from "adm-zip";
|
|
749
|
+
|
|
750
|
+
// src/deploy/git-ref.ts
|
|
751
|
+
function isGitCommitSha(ref) {
|
|
752
|
+
return /^[0-9a-f]{7,40}$/i.test(ref);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/deploy/package.ts
|
|
756
|
+
var exec = promisify(execFile);
|
|
757
|
+
var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
|
|
758
|
+
".git",
|
|
759
|
+
"node_modules",
|
|
760
|
+
"dist",
|
|
761
|
+
"build",
|
|
762
|
+
".next",
|
|
763
|
+
".turbo",
|
|
764
|
+
".cache",
|
|
765
|
+
"__pycache__",
|
|
766
|
+
".venv",
|
|
767
|
+
"venv",
|
|
768
|
+
".mypy_cache",
|
|
769
|
+
".pytest_cache",
|
|
770
|
+
".DS_Store"
|
|
771
|
+
]);
|
|
772
|
+
async function isGitRepo(dir) {
|
|
773
|
+
try {
|
|
774
|
+
await exec("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"]);
|
|
775
|
+
return true;
|
|
776
|
+
} catch {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async function gitFileList(dir) {
|
|
781
|
+
const { stdout } = await exec(
|
|
782
|
+
"git",
|
|
783
|
+
["-C", dir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
784
|
+
{ maxBuffer: 64 * 1024 * 1024 }
|
|
785
|
+
);
|
|
786
|
+
return stdout.split("\0").map((f) => f.trim()).filter(Boolean);
|
|
787
|
+
}
|
|
788
|
+
async function walkFileList(dir) {
|
|
789
|
+
const out = [];
|
|
790
|
+
async function walk(current, rel) {
|
|
791
|
+
const entries = await fs2.readdir(current, { withFileTypes: true });
|
|
792
|
+
for (const entry of entries) {
|
|
793
|
+
if (DEFAULT_IGNORES.has(entry.name)) continue;
|
|
794
|
+
const abs = path2.join(current, entry.name);
|
|
795
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
796
|
+
if (entry.isDirectory()) {
|
|
797
|
+
await walk(abs, relPath);
|
|
798
|
+
} else if (entry.isFile()) {
|
|
799
|
+
out.push(relPath);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
await walk(dir, "");
|
|
804
|
+
return out;
|
|
805
|
+
}
|
|
806
|
+
async function buildZip(sourceDir, files) {
|
|
807
|
+
const zip = new AdmZip();
|
|
808
|
+
for (const rel of files) {
|
|
809
|
+
const abs = path2.join(sourceDir, rel);
|
|
810
|
+
try {
|
|
811
|
+
const data = await fs2.readFile(abs);
|
|
812
|
+
zip.addFile(rel.split(path2.sep).join("/"), data);
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return zip.toBuffer();
|
|
817
|
+
}
|
|
818
|
+
async function cloneRepo(url, ref) {
|
|
819
|
+
const tmp = await fs2.mkdtemp(path2.join(os2.tmpdir(), "gf-deploy-"));
|
|
820
|
+
try {
|
|
821
|
+
if (ref && isGitCommitSha(ref)) {
|
|
822
|
+
await exec("git", ["init"], { cwd: tmp });
|
|
823
|
+
await exec("git", ["remote", "add", "origin", url], { cwd: tmp });
|
|
824
|
+
await exec(
|
|
825
|
+
"git",
|
|
826
|
+
["fetch", "--depth", "1", "origin", ref],
|
|
827
|
+
{ cwd: tmp, maxBuffer: 64 * 1024 * 1024 }
|
|
828
|
+
);
|
|
829
|
+
await exec("git", ["checkout", "FETCH_HEAD"], { cwd: tmp });
|
|
830
|
+
} else {
|
|
831
|
+
const args = ["clone", "--depth", "1"];
|
|
832
|
+
if (ref) args.push("--branch", ref);
|
|
833
|
+
args.push(url, tmp);
|
|
834
|
+
await exec("git", args, { maxBuffer: 64 * 1024 * 1024 });
|
|
835
|
+
}
|
|
836
|
+
} catch (err) {
|
|
837
|
+
await fs2.rm(tmp, { recursive: true, force: true });
|
|
838
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
839
|
+
throw new Error(`git clone failed: ${message}`);
|
|
840
|
+
}
|
|
841
|
+
return tmp;
|
|
842
|
+
}
|
|
843
|
+
async function packageSource(opts) {
|
|
844
|
+
let baseDir;
|
|
845
|
+
let cleanup = null;
|
|
846
|
+
if (opts.git) {
|
|
847
|
+
baseDir = await cloneRepo(opts.git, opts.ref);
|
|
848
|
+
cleanup = baseDir;
|
|
849
|
+
} else {
|
|
850
|
+
baseDir = path2.resolve(opts.path ?? process.cwd());
|
|
851
|
+
}
|
|
852
|
+
const sourceDir = opts.subdir ? path2.join(baseDir, opts.subdir) : baseDir;
|
|
853
|
+
try {
|
|
854
|
+
const stat = await fs2.stat(sourceDir);
|
|
855
|
+
if (!stat.isDirectory()) {
|
|
856
|
+
throw new Error(`Not a directory: ${sourceDir}`);
|
|
857
|
+
}
|
|
858
|
+
const useGit = await isGitRepo(sourceDir);
|
|
859
|
+
const files = useGit ? await gitFileList(sourceDir) : await walkFileList(sourceDir);
|
|
860
|
+
if (files.length === 0) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`No files to package in ${sourceDir} (everything ignored or empty).`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const zip = await buildZip(sourceDir, files);
|
|
866
|
+
let gitSha = null;
|
|
867
|
+
const shaRoot = cleanup ?? (useGit ? sourceDir : null);
|
|
868
|
+
if (shaRoot) {
|
|
869
|
+
try {
|
|
870
|
+
const { stdout } = await exec("git", ["-C", shaRoot, "rev-parse", "HEAD"]);
|
|
871
|
+
gitSha = stdout.trim() || null;
|
|
872
|
+
} catch {
|
|
873
|
+
gitSha = null;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return { zip, fileCount: files.length, sourceDir, cleanup, gitSha };
|
|
877
|
+
} catch (err) {
|
|
878
|
+
if (cleanup) {
|
|
879
|
+
await fs2.rm(cleanup, { recursive: true, force: true }).catch(() => {
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
throw err;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function readProjectManifest(dir) {
|
|
886
|
+
try {
|
|
887
|
+
const raw = await fs2.readFile(path2.join(dir, "groundfloor.json"), "utf8");
|
|
888
|
+
return JSON.parse(raw);
|
|
889
|
+
} catch {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function detectRuntime(dir) {
|
|
894
|
+
const has = async (name) => {
|
|
895
|
+
try {
|
|
896
|
+
await fs2.access(path2.join(dir, name));
|
|
897
|
+
return true;
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
if (await has("package.json")) return "node";
|
|
903
|
+
if (await has("requirements.txt")) return "python";
|
|
904
|
+
if (await has("pyproject.toml")) return "python";
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/commands/deploy.ts
|
|
909
|
+
var RUNTIMES = /* @__PURE__ */ new Set(["python", "node", "dotnet-script"]);
|
|
910
|
+
var WORKLOAD_TYPES = /* @__PURE__ */ new Set(["function", "service", "job", "schedule"]);
|
|
911
|
+
var VERSION_POLL_INTERVAL_MS = 2e3;
|
|
912
|
+
var VERSION_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
913
|
+
var DEPLOY_POLL_INTERVAL_MS = 2e3;
|
|
914
|
+
var DEPLOY_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
915
|
+
function slugify(input) {
|
|
916
|
+
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
917
|
+
return slug || "app";
|
|
918
|
+
}
|
|
919
|
+
function nameFromGitUrl(url) {
|
|
920
|
+
const cleaned = url.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
921
|
+
const last = cleaned.split(/[/:]/).pop();
|
|
922
|
+
return last || "app";
|
|
923
|
+
}
|
|
924
|
+
function parseEnvPairs(pairs) {
|
|
925
|
+
const out = {};
|
|
926
|
+
for (const pair of pairs ?? []) {
|
|
927
|
+
const eq = pair.indexOf("=");
|
|
928
|
+
if (eq === -1) {
|
|
929
|
+
throw new Error(`Invalid --env "${pair}" (expected KEY=VALUE).`);
|
|
930
|
+
}
|
|
931
|
+
out[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
932
|
+
}
|
|
933
|
+
return out;
|
|
934
|
+
}
|
|
935
|
+
var log = (msg) => process.stderr.write(`${msg}
|
|
936
|
+
`);
|
|
937
|
+
async function deployCommand(opts) {
|
|
938
|
+
const cfg = await resolveConfig({
|
|
939
|
+
apiUrl: opts.apiUrl,
|
|
940
|
+
workspaceId: opts.workspace
|
|
941
|
+
});
|
|
942
|
+
if (!cfg.workspaceId) {
|
|
943
|
+
throw new Error(
|
|
944
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
const ws = cfg.workspaceId;
|
|
948
|
+
log(
|
|
949
|
+
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path3.resolve(opts.path ?? process.cwd())}...`
|
|
950
|
+
);
|
|
951
|
+
const pkg = await packageSource({
|
|
952
|
+
path: opts.path,
|
|
953
|
+
git: opts.git,
|
|
954
|
+
ref: opts.ref,
|
|
955
|
+
subdir: opts.subdir
|
|
956
|
+
});
|
|
957
|
+
try {
|
|
958
|
+
const manifest = await readProjectManifest(pkg.sourceDir) ?? {};
|
|
959
|
+
const sizeKb = (pkg.zip.byteLength / 1024).toFixed(1);
|
|
960
|
+
log(`Packaged ${pkg.fileCount} file(s), ${sizeKb} KB`);
|
|
961
|
+
const detected = await detectRuntime(pkg.sourceDir);
|
|
962
|
+
const runtime = opts.runtime ?? manifest.runtime ?? detected ?? "python";
|
|
963
|
+
if (!RUNTIMES.has(runtime)) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
`Invalid runtime "${runtime}" (expected: ${[...RUNTIMES].join(", ")}).`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const workloadType = opts.workloadType ?? manifest.workloadType ?? manifest.workload_type ?? "function";
|
|
969
|
+
if (!WORKLOAD_TYPES.has(workloadType)) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`Invalid workload type "${workloadType}" (expected: ${[...WORKLOAD_TYPES].join(", ")}).`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
let coderunnerId = opts.coderunner;
|
|
975
|
+
if (!coderunnerId) {
|
|
976
|
+
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path3.basename(path3.resolve(opts.path ?? process.cwd())));
|
|
977
|
+
const slug = manifest.slug ?? slugify(name);
|
|
978
|
+
const existing = await listCoderunners(cfg.apiUrl, ws);
|
|
979
|
+
const match = existing.find((c) => c.slug === slug || c.name === name);
|
|
980
|
+
if (match) {
|
|
981
|
+
coderunnerId = match.id;
|
|
982
|
+
log(`Using existing coderunner ${slug} (${coderunnerId})`);
|
|
983
|
+
} else {
|
|
984
|
+
log(`Creating coderunner ${slug} (runtime=${runtime}, type=${workloadType})...`);
|
|
985
|
+
const created = await createCoderunner(cfg.apiUrl, ws, {
|
|
986
|
+
title: name,
|
|
987
|
+
slug,
|
|
988
|
+
runtime,
|
|
989
|
+
workload_type: workloadType,
|
|
990
|
+
...opts.appId ? { app_id: opts.appId } : {}
|
|
991
|
+
});
|
|
992
|
+
coderunnerId = created.id;
|
|
993
|
+
log(`Created coderunner ${created.id}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const description = opts.message ?? (pkg.gitSha ? `git_sha=${pkg.gitSha} gf deploy ${(/* @__PURE__ */ new Date()).toISOString()}` : `gf deploy ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
997
|
+
if (pkg.gitSha) {
|
|
998
|
+
log(`git_sha ${pkg.gitSha.slice(0, 12)}`);
|
|
999
|
+
}
|
|
1000
|
+
log("Uploading code...");
|
|
1001
|
+
const uploaded = await uploadZip(
|
|
1002
|
+
cfg.apiUrl,
|
|
1003
|
+
ws,
|
|
1004
|
+
coderunnerId,
|
|
1005
|
+
pkg.zip,
|
|
1006
|
+
description
|
|
1007
|
+
);
|
|
1008
|
+
log(`Uploaded version ${uploaded.version_id}`);
|
|
1009
|
+
log("Waiting for build...");
|
|
1010
|
+
const deadline = Date.now() + VERSION_POLL_TIMEOUT_MS;
|
|
1011
|
+
let built = false;
|
|
1012
|
+
while (Date.now() < deadline) {
|
|
1013
|
+
const version = await getVersion(
|
|
1014
|
+
cfg.apiUrl,
|
|
1015
|
+
ws,
|
|
1016
|
+
coderunnerId,
|
|
1017
|
+
uploaded.version_id
|
|
1018
|
+
);
|
|
1019
|
+
const status = version.status ?? 0;
|
|
1020
|
+
if (status === 3) {
|
|
1021
|
+
built = true;
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
if (status === 4) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`Build failed for version ${uploaded.version_id} (status=4).`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
process.stderr.write(".");
|
|
1030
|
+
await new Promise((r) => setTimeout(r, VERSION_POLL_INTERVAL_MS));
|
|
1031
|
+
}
|
|
1032
|
+
process.stderr.write("\n");
|
|
1033
|
+
if (!built) {
|
|
1034
|
+
throw new Error("Timed out waiting for the build to complete.");
|
|
1035
|
+
}
|
|
1036
|
+
log("Build completed.");
|
|
1037
|
+
if (opts.skipDeploy) {
|
|
1038
|
+
process.stdout.write(
|
|
1039
|
+
`Uploaded (not deployed). coderunner=${coderunnerId} version=${uploaded.version_id}
|
|
1040
|
+
`
|
|
1041
|
+
);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const envVariable = {
|
|
1045
|
+
...manifest.env ?? {},
|
|
1046
|
+
...parseEnvPairs(opts.env)
|
|
1047
|
+
};
|
|
1048
|
+
log("Deploying...");
|
|
1049
|
+
const result = await deployCoderunner(cfg.apiUrl, ws, coderunnerId, {
|
|
1050
|
+
version_id: uploaded.version_id,
|
|
1051
|
+
cpu: opts.cpu ?? manifest.cpu,
|
|
1052
|
+
memory: opts.memory ?? manifest.memory,
|
|
1053
|
+
envVariable: Object.keys(envVariable).length ? envVariable : void 0
|
|
1054
|
+
});
|
|
1055
|
+
log("Waiting for deployment...");
|
|
1056
|
+
const deployDeadline = Date.now() + DEPLOY_POLL_TIMEOUT_MS;
|
|
1057
|
+
let deployOk = false;
|
|
1058
|
+
while (Date.now() < deployDeadline) {
|
|
1059
|
+
const deployment = await getDeployment(cfg.apiUrl, ws, coderunnerId);
|
|
1060
|
+
const dStatus = deployment?.status ?? 0;
|
|
1061
|
+
if (dStatus === 3) {
|
|
1062
|
+
deployOk = true;
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
if (dStatus === 4) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Deployment failed for coderunner ${coderunnerId} (status=4).`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
process.stderr.write(".");
|
|
1071
|
+
await new Promise((r) => setTimeout(r, DEPLOY_POLL_INTERVAL_MS));
|
|
1072
|
+
}
|
|
1073
|
+
process.stderr.write("\n");
|
|
1074
|
+
if (!deployOk) {
|
|
1075
|
+
throw new Error("Timed out waiting for the deployment to complete.");
|
|
1076
|
+
}
|
|
1077
|
+
log("Deployment completed.");
|
|
1078
|
+
process.stdout.write(`
|
|
1079
|
+
Deployed coderunner ${coderunnerId}
|
|
1080
|
+
`);
|
|
1081
|
+
process.stdout.write(` version: ${uploaded.version_id}
|
|
1082
|
+
`);
|
|
1083
|
+
process.stdout.write(` deployment: ${result.deployment_id}
|
|
1084
|
+
`);
|
|
1085
|
+
if (result.public_host) {
|
|
1086
|
+
process.stdout.write(` url: https://${result.public_host}
|
|
1087
|
+
`);
|
|
1088
|
+
}
|
|
1089
|
+
if (result.deployment_url) {
|
|
1090
|
+
const label = result.public_host ? "upstream: " : "url: ";
|
|
1091
|
+
process.stdout.write(` ${label}${result.deployment_url}
|
|
1092
|
+
`);
|
|
1093
|
+
}
|
|
1094
|
+
} finally {
|
|
1095
|
+
if (pkg.cleanup) {
|
|
1096
|
+
await fs3.rm(pkg.cleanup, { recursive: true, force: true }).catch(() => {
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// src/commands/run.ts
|
|
1103
|
+
async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
|
|
1104
|
+
const list = await listCoderunners(apiUrl, workspaceId);
|
|
1105
|
+
const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
|
|
1106
|
+
if (!match) {
|
|
1107
|
+
throw new Error(`Coderunner not found: ${idOrSlug}`);
|
|
1108
|
+
}
|
|
1109
|
+
return match.id;
|
|
1110
|
+
}
|
|
1111
|
+
async function runCommand(opts) {
|
|
1112
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1113
|
+
if (!cfg.workspaceId) {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
let payload = {};
|
|
1119
|
+
if (opts.payload) {
|
|
1120
|
+
try {
|
|
1121
|
+
payload = JSON.parse(opts.payload);
|
|
1122
|
+
} catch {
|
|
1123
|
+
throw new Error(`Invalid --payload JSON: ${opts.payload}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const coderunnerId = await resolveCoderunnerId(
|
|
1127
|
+
cfg.apiUrl,
|
|
1128
|
+
cfg.workspaceId,
|
|
1129
|
+
opts.coderunner
|
|
1130
|
+
);
|
|
1131
|
+
const result = await runCoderunner(cfg.apiUrl, cfg.workspaceId, coderunnerId, {
|
|
1132
|
+
payload
|
|
1133
|
+
});
|
|
1134
|
+
if (opts.json) {
|
|
1135
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1136
|
+
`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1140
|
+
`);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/commands/status.ts
|
|
1144
|
+
async function resolveCoderunnerId2(apiUrl, workspaceId, idOrSlug) {
|
|
1145
|
+
const list = await listCoderunners(apiUrl, workspaceId);
|
|
1146
|
+
const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
|
|
1147
|
+
if (!match) {
|
|
1148
|
+
throw new Error(`Coderunner not found: ${idOrSlug}`);
|
|
1149
|
+
}
|
|
1150
|
+
return match.id;
|
|
1151
|
+
}
|
|
1152
|
+
async function statusCommand(opts) {
|
|
1153
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1154
|
+
if (!cfg.workspaceId) {
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
const coderunnerId = await resolveCoderunnerId2(
|
|
1160
|
+
cfg.apiUrl,
|
|
1161
|
+
cfg.workspaceId,
|
|
1162
|
+
opts.coderunner
|
|
1163
|
+
);
|
|
1164
|
+
const [status, deployment] = await Promise.all([
|
|
1165
|
+
getCoderunnerStatus(cfg.apiUrl, cfg.workspaceId, coderunnerId),
|
|
1166
|
+
getDeployment(cfg.apiUrl, cfg.workspaceId, coderunnerId)
|
|
1167
|
+
]);
|
|
1168
|
+
const out = { coderunner_id: coderunnerId, status, deployment };
|
|
1169
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}
|
|
1170
|
+
`);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/commands/domains.ts
|
|
1174
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1175
|
+
async function resolve(opts) {
|
|
1176
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1177
|
+
if (!cfg.workspaceId) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const ref = opts.coderunner;
|
|
1183
|
+
if (!ref) {
|
|
1184
|
+
throw new Error(
|
|
1185
|
+
"Specify the coderunner with --coderunner <id|slug>."
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
let coderunnerId = ref;
|
|
1189
|
+
if (!UUID_RE.test(ref)) {
|
|
1190
|
+
const all = await listCoderunners(cfg.apiUrl, cfg.workspaceId);
|
|
1191
|
+
const match = all.find((c) => c.slug === ref || c.name === ref);
|
|
1192
|
+
if (!match) {
|
|
1193
|
+
throw new Error(`No coderunner matching "${ref}" in this workspace.`);
|
|
1194
|
+
}
|
|
1195
|
+
coderunnerId = match.id;
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
apiUrl: cfg.apiUrl,
|
|
1199
|
+
workspaceId: cfg.workspaceId,
|
|
1200
|
+
coderunnerId
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function printInstructions(item) {
|
|
1204
|
+
const out = process.stdout;
|
|
1205
|
+
out.write(`
|
|
1206
|
+
Added ${item.domain} (status: ${item.status})
|
|
1207
|
+
`);
|
|
1208
|
+
out.write("\nCreate these DNS records at your registrar:\n");
|
|
1209
|
+
if (item.cname_target) {
|
|
1210
|
+
out.write(` CNAME ${item.domain} \u2192 ${item.cname_target}
|
|
1211
|
+
`);
|
|
1212
|
+
}
|
|
1213
|
+
if (item.verification_record) {
|
|
1214
|
+
const r = item.verification_record;
|
|
1215
|
+
out.write(` ${r.type} ${r.name} = ${r.value}
|
|
1216
|
+
`);
|
|
1217
|
+
}
|
|
1218
|
+
out.write(`
|
|
1219
|
+
Then run: gf domains verify ${item.domain} --coderunner <id|slug>
|
|
1220
|
+
`);
|
|
1221
|
+
}
|
|
1222
|
+
async function domainsAddCommand(domain, opts) {
|
|
1223
|
+
const { apiUrl, workspaceId, coderunnerId } = await resolve(opts);
|
|
1224
|
+
const item = await addDomain(apiUrl, workspaceId, coderunnerId, domain);
|
|
1225
|
+
if (opts.json) {
|
|
1226
|
+
process.stdout.write(`${JSON.stringify(item, null, 2)}
|
|
1227
|
+
`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
printInstructions(item);
|
|
1231
|
+
}
|
|
1232
|
+
async function domainsListCommand(opts) {
|
|
1233
|
+
const { apiUrl, workspaceId, coderunnerId } = await resolve(opts);
|
|
1234
|
+
const result = await listDomains(apiUrl, workspaceId, coderunnerId);
|
|
1235
|
+
if (opts.json) {
|
|
1236
|
+
process.stdout.write(`${JSON.stringify(result.domains, null, 2)}
|
|
1237
|
+
`);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (result.domains.length === 0) {
|
|
1241
|
+
process.stdout.write("No custom domains.\n");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
for (const d of result.domains) {
|
|
1245
|
+
const target = d.cname_target ? ` \u2192 ${d.cname_target}` : "";
|
|
1246
|
+
process.stdout.write(`${d.domain} [${d.status}]${target}
|
|
1247
|
+
`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async function domainsRemoveCommand(domain, opts) {
|
|
1251
|
+
const { apiUrl, workspaceId, coderunnerId } = await resolve(opts);
|
|
1252
|
+
await removeDomain(apiUrl, workspaceId, coderunnerId, domain);
|
|
1253
|
+
process.stdout.write(`Removed ${domain}
|
|
1254
|
+
`);
|
|
1255
|
+
}
|
|
1256
|
+
async function domainsVerifyCommand(domain, opts) {
|
|
1257
|
+
const { apiUrl, workspaceId, coderunnerId } = await resolve(opts);
|
|
1258
|
+
const result = await verifyDomain(apiUrl, workspaceId, coderunnerId, domain);
|
|
1259
|
+
if (opts.json) {
|
|
1260
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1261
|
+
`);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const mark = result.verified ? "verified" : "not verified";
|
|
1265
|
+
process.stdout.write(`${result.domain}: ${mark} [${result.status}]
|
|
1266
|
+
`);
|
|
1267
|
+
process.stdout.write(`${result.detail}
|
|
1268
|
+
`);
|
|
1269
|
+
if (!result.verified) {
|
|
1270
|
+
process.exitCode = 1;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/index.ts
|
|
1275
|
+
function collect(value, previous) {
|
|
1276
|
+
return previous.concat([value]);
|
|
1277
|
+
}
|
|
1278
|
+
function run(fn) {
|
|
1279
|
+
fn().catch((err) => {
|
|
1280
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1281
|
+
process.stderr.write(`Error: ${message}
|
|
1282
|
+
`);
|
|
1283
|
+
process.exitCode = 1;
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
var program = new Command();
|
|
1287
|
+
program.name("gf").description(
|
|
1288
|
+
"Groundfloor CLI \u2014 log in to the Control Plane and reuse the session during local development."
|
|
1289
|
+
).version("0.1.0");
|
|
1290
|
+
program.command("login").description("Log in via the browser (or --device) and cache the session").option("--issuer <url>", "Identity provider issuer URL").option("--client-id <id>", "OIDC client id").option("--api-url <url>", "Control Plane base URL").option("--device", "Use the device-code flow (headless / no local browser)").option(
|
|
1291
|
+
"--redirect-host <host>",
|
|
1292
|
+
"Loopback host for the redirect URI (default 127.0.0.1; try localhost)"
|
|
1293
|
+
).option(
|
|
1294
|
+
"--redirect-port <port>",
|
|
1295
|
+
"Fixed loopback port for the redirect URI (default: random)"
|
|
1296
|
+
).option("--no-open", "Do not auto-open the browser; print the URL instead").action((opts) => run(() => loginCommand(opts)));
|
|
1297
|
+
program.command("logout").description("Clear the cached session").action(() => run(() => logoutCommand()));
|
|
1298
|
+
program.command("whoami").description("Show the current identity and verify the Control Plane session").action(() => run(() => whoamiCommand()));
|
|
1299
|
+
program.command("token").description("Print a valid (auto-refreshed) access token to stdout").action(() => run(() => tokenCommand()));
|
|
1300
|
+
program.command("env").description('Print shell exports for `eval "$(gf env)"`').action(() => run(() => envCommand()));
|
|
1301
|
+
var workspaces = program.command("workspaces").alias("ws").description("List workspaces or set the default workspace");
|
|
1302
|
+
workspaces.command("list", { isDefault: true }).description("List workspaces visible to your user").option("--json", "Output raw JSON").action((opts) => run(() => workspacesListCommand(opts)));
|
|
1303
|
+
workspaces.command("use <workspaceId>").description("Set the default workspace").action(
|
|
1304
|
+
(workspaceId) => run(() => workspacesUseCommand(workspaceId))
|
|
1305
|
+
);
|
|
1306
|
+
var coderunner = program.command("coderunner").alias("cr").description("Coderunner commands");
|
|
1307
|
+
coderunner.command("ls").alias("list").description("List coderunners in the default (or --workspace) workspace").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => coderunnerListCommand(opts)));
|
|
1308
|
+
coderunner.command("status").description("Show coderunner status + current deployment JSON").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => statusCommand(opts)));
|
|
1309
|
+
coderunner.command("run").description("Invoke a deployed function/job (smoke run)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--payload <json>", "JSON payload object", "{}").option("--json", "Output raw JSON").action(
|
|
1310
|
+
(opts) => run(
|
|
1311
|
+
() => runCommand({
|
|
1312
|
+
workspace: opts.workspace,
|
|
1313
|
+
coderunner: opts.coderunner,
|
|
1314
|
+
payload: opts.payload,
|
|
1315
|
+
json: opts.json
|
|
1316
|
+
})
|
|
1317
|
+
)
|
|
1318
|
+
);
|
|
1319
|
+
var domains = program.command("domains").description("Manage custom domains (CNAME) for a coderunner service");
|
|
1320
|
+
domains.command("add <domain>").description("Add a custom domain and print the DNS records to create").requiredOption("-c, --coderunner <id|slug>", "Target coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((domain, opts) => run(() => domainsAddCommand(domain, opts)));
|
|
1321
|
+
domains.command("ls").alias("list").description("List custom domains for a coderunner service").requiredOption("-c, --coderunner <id|slug>", "Target coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => domainsListCommand(opts)));
|
|
1322
|
+
domains.command("rm <domain>").alias("remove").description("Remove a custom domain from a coderunner service").requiredOption("-c, --coderunner <id|slug>", "Target coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").action(
|
|
1323
|
+
(domain, opts) => run(() => domainsRemoveCommand(domain, opts))
|
|
1324
|
+
);
|
|
1325
|
+
domains.command("verify <domain>").description("Re-check the DNS records for a custom domain").requiredOption("-c, --coderunner <id|slug>", "Target coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action(
|
|
1326
|
+
(domain, opts) => run(() => domainsVerifyCommand(domain, opts))
|
|
1327
|
+
);
|
|
1328
|
+
program.command("deploy").description(
|
|
1329
|
+
"Package code (local folder or --git repo) and deploy as a standalone coderunner (D-061; not an App)"
|
|
1330
|
+
).option("-w, --workspace <id>", "Workspace id override").option("--api-url <url>", "Control Plane base URL").option(
|
|
1331
|
+
"-c, --coderunner <id>",
|
|
1332
|
+
"Deploy into an existing coderunner id (skip find/create)"
|
|
1333
|
+
).option("-n, --name <name>", "Coderunner name (default: folder or repo name)").option("-p, --path <dir>", "Local directory to package (default: cwd)").option("--git <url>", "Clone and deploy a remote git/bitbucket repo").option("--ref <ref>", "Branch/tag/commit for --git").option("--subdir <dir>", "Subdirectory within the source to package").option("--runtime <runtime>", "python | node | dotnet-script").option(
|
|
1334
|
+
"--workload-type <type>",
|
|
1335
|
+
"function | service | job | schedule"
|
|
1336
|
+
).option(
|
|
1337
|
+
"--app-id <id>",
|
|
1338
|
+
"Bind newly created coderunner as a helper of this App (optional)"
|
|
1339
|
+
).option("--cpu <cpu>", "CPU request, e.g. 250m").option("--memory <memory>", "Memory request, e.g. 256Mi").option("-e, --env <KEY=VALUE>", "Environment variable (repeatable)", collect, []).option("-m, --message <text>", "Version description").option("--skip-deploy", "Upload a new version but do not deploy it").action((opts) => run(() => deployCommand(opts)));
|
|
1340
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1341
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1342
|
+
process.stderr.write(`Error: ${message}
|
|
1343
|
+
`);
|
|
1344
|
+
process.exitCode = 1;
|
|
1345
|
+
});
|