@blinkdotnew/cli 0.3.6 → 0.4.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 +152 -321
- package/dist/cli.js +1548 -85
- package/package.json +28 -2
- package/src/cli.ts +88 -1
- package/src/commands/auth-config.ts +129 -0
- package/src/commands/backend.ts +175 -0
- package/src/commands/billing.ts +56 -0
- package/src/commands/domains.ts +211 -0
- package/src/commands/env.ts +215 -0
- package/src/commands/functions.ts +136 -0
- package/src/commands/hosting.ts +117 -0
- package/src/commands/init.ts +77 -0
- package/src/commands/linkedin.ts +5 -5
- package/src/commands/security.ts +136 -0
- package/src/commands/tokens.ts +98 -0
- package/src/commands/versions.ts +95 -0
- package/src/commands/workspace.ts +130 -0
- package/src/lib/api-app.ts +7 -1
- package/src/lib/api-resources.ts +11 -1
- package/src/lib/auth.ts +6 -1
- package/src/lib/config.ts +2 -2
- package/src/lib/project.ts +7 -1
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,14 @@ function resolveProjectId(explicitId) {
|
|
|
50
50
|
function requireProjectId(explicitId) {
|
|
51
51
|
const id = resolveProjectId(explicitId);
|
|
52
52
|
if (!id) {
|
|
53
|
-
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
`Error: No project context.
|
|
55
|
+
1. blink link <project_id> Link to current directory
|
|
56
|
+
2. export BLINK_ACTIVE_PROJECT=proj_xxx Set env var (CI/agents)
|
|
57
|
+
3. Pass project_id as argument e.g. blink db query proj_xxx "SELECT 1"
|
|
58
|
+
Don't have a project yet? Run: blink init
|
|
59
|
+
`
|
|
60
|
+
);
|
|
54
61
|
process.exit(1);
|
|
55
62
|
}
|
|
56
63
|
return id;
|
|
@@ -136,10 +143,10 @@ function readConfig(profile = "default") {
|
|
|
136
143
|
return sections[profile];
|
|
137
144
|
}
|
|
138
145
|
function writeConfig(data, profile = "default") {
|
|
139
|
-
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
146
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
140
147
|
const existing = existsSync(CONFIG_FILE) ? parseToml(readFileSync(CONFIG_FILE, "utf-8")) : {};
|
|
141
148
|
existing[profile] = { ...existing[profile], ...data };
|
|
142
|
-
writeFileSync(CONFIG_FILE, serializeToml(existing));
|
|
149
|
+
writeFileSync(CONFIG_FILE, serializeToml(existing), { mode: 384 });
|
|
143
150
|
}
|
|
144
151
|
function clearConfig(profile = "default") {
|
|
145
152
|
if (!existsSync(CONFIG_FILE)) return;
|
|
@@ -166,7 +173,9 @@ function resolveToken() {
|
|
|
166
173
|
function requireToken() {
|
|
167
174
|
const token = resolveToken();
|
|
168
175
|
if (!token) {
|
|
169
|
-
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
"Error: Not authenticated.\n Get your API key at: blink.new/settings?tab=api-keys (starts with blnk_ak_)\n Then: blink login --interactive Save to ~/.config/blink/config.toml\n or: export BLINK_API_KEY=blnk_ak_... Set env var (CI/agents)\n"
|
|
178
|
+
);
|
|
170
179
|
process.exit(1);
|
|
171
180
|
}
|
|
172
181
|
return token;
|
|
@@ -197,9 +206,17 @@ async function resourcesRequest(path, opts = {}) {
|
|
|
197
206
|
const errText = await res.text();
|
|
198
207
|
let errMsg = `HTTP ${res.status}`;
|
|
199
208
|
try {
|
|
200
|
-
|
|
209
|
+
const parsed = JSON.parse(errText);
|
|
210
|
+
const err = parsed.error;
|
|
211
|
+
if (typeof err === "string") errMsg = err;
|
|
212
|
+
else if (err?.message) errMsg = err.message;
|
|
213
|
+
else if (parsed.message) errMsg = parsed.message;
|
|
214
|
+
else if (err) errMsg = JSON.stringify(err);
|
|
201
215
|
} catch {
|
|
202
216
|
}
|
|
217
|
+
if (res.status === 401) errMsg += " \u2014 check your API key (blink login --interactive)";
|
|
218
|
+
if (res.status === 403) errMsg += " \u2014 check project permissions or workspace tier";
|
|
219
|
+
if (res.status === 404) errMsg += " \u2014 resource not found (check project ID)";
|
|
203
220
|
throw new Error(errMsg);
|
|
204
221
|
}
|
|
205
222
|
const ct = res.headers.get("content-type") ?? "";
|
|
@@ -212,6 +229,13 @@ import chalk from "chalk";
|
|
|
212
229
|
import Table from "cli-table3";
|
|
213
230
|
var isJsonMode = () => process.argv.includes("--json");
|
|
214
231
|
var isCI = () => !process.stdout.isTTY;
|
|
232
|
+
function printSuccess(msg) {
|
|
233
|
+
if (!isJsonMode()) console.log(chalk.green("\u2713") + " " + msg);
|
|
234
|
+
}
|
|
235
|
+
function printError(msg, hint) {
|
|
236
|
+
console.error(chalk.red("Error:") + " " + msg);
|
|
237
|
+
if (hint) console.error(" " + chalk.dim(hint));
|
|
238
|
+
}
|
|
215
239
|
function printUrl(label, url) {
|
|
216
240
|
if (!isJsonMode()) console.log(chalk.bold(label.padEnd(12)) + chalk.cyan(url));
|
|
217
241
|
}
|
|
@@ -1391,7 +1415,7 @@ async function liExec(method, httpMethod, params, agentId) {
|
|
|
1391
1415
|
return result.data;
|
|
1392
1416
|
}
|
|
1393
1417
|
async function getPersonId(agentId) {
|
|
1394
|
-
const data = await liExec("
|
|
1418
|
+
const data = await liExec("userinfo", "GET", {}, agentId);
|
|
1395
1419
|
const id = data?.sub ?? data?.id;
|
|
1396
1420
|
if (!id) throw new Error("Could not resolve LinkedIn person ID");
|
|
1397
1421
|
return id;
|
|
@@ -1436,7 +1460,7 @@ Examples:
|
|
|
1436
1460
|
const agentId = requireAgentId(opts.agent);
|
|
1437
1461
|
const data = await withSpinner(
|
|
1438
1462
|
"Fetching LinkedIn profile...",
|
|
1439
|
-
() => liExec("
|
|
1463
|
+
() => liExec("userinfo", "GET", {}, agentId)
|
|
1440
1464
|
);
|
|
1441
1465
|
if (isJsonMode()) return printJson(data);
|
|
1442
1466
|
const name = data?.name ?? [data?.given_name, data?.family_name].filter(Boolean).join(" ");
|
|
@@ -1515,7 +1539,7 @@ Examples:
|
|
|
1515
1539
|
const encoded = encodeURIComponent(postUrn);
|
|
1516
1540
|
const data = await withSpinner(
|
|
1517
1541
|
"Liking post...",
|
|
1518
|
-
() => liExec(`
|
|
1542
|
+
() => liExec(`socialActions/${encoded}/likes`, "POST", {
|
|
1519
1543
|
actor: `urn:li:person:${personId}`
|
|
1520
1544
|
}, agentId)
|
|
1521
1545
|
);
|
|
@@ -1537,7 +1561,7 @@ Examples:
|
|
|
1537
1561
|
const encodedPerson = encodeURIComponent(`urn:li:person:${personId}`);
|
|
1538
1562
|
await withSpinner(
|
|
1539
1563
|
"Unliking post...",
|
|
1540
|
-
() => liExec(`
|
|
1564
|
+
() => liExec(`socialActions/${encodedPost}/likes/${encodedPerson}`, "DELETE", {}, agentId)
|
|
1541
1565
|
);
|
|
1542
1566
|
if (isJsonMode()) return printJson({ unliked: true });
|
|
1543
1567
|
console.log(chalk7.green("\u2713 Post unliked"));
|
|
@@ -1558,7 +1582,7 @@ Examples:
|
|
|
1558
1582
|
const encoded = encodeURIComponent(postUrn);
|
|
1559
1583
|
const data = await withSpinner(
|
|
1560
1584
|
"Adding comment...",
|
|
1561
|
-
() => liExec(`
|
|
1585
|
+
() => liExec(`socialActions/${encoded}/comments`, "POST", {
|
|
1562
1586
|
actor: `urn:li:person:${personId}`,
|
|
1563
1587
|
message: { text }
|
|
1564
1588
|
}, agentId)
|
|
@@ -1797,9 +1821,13 @@ async function appRequest(path, opts = {}) {
|
|
|
1797
1821
|
const errText = await res.text();
|
|
1798
1822
|
let errMsg = `HTTP ${res.status}`;
|
|
1799
1823
|
try {
|
|
1800
|
-
|
|
1824
|
+
const parsed = JSON.parse(errText);
|
|
1825
|
+
errMsg = parsed.error ?? parsed.message ?? errMsg;
|
|
1801
1826
|
} catch {
|
|
1802
1827
|
}
|
|
1828
|
+
if (res.status === 401) errMsg += " \u2014 check your API key (blink login --interactive)";
|
|
1829
|
+
if (res.status === 403) errMsg += " \u2014 check project permissions or workspace tier";
|
|
1830
|
+
if (res.status === 404) errMsg += " \u2014 resource not found (check project ID)";
|
|
1803
1831
|
throw new Error(errMsg);
|
|
1804
1832
|
}
|
|
1805
1833
|
const ct = res.headers.get("content-type") ?? "";
|
|
@@ -2298,87 +2326,1511 @@ Examples:
|
|
|
2298
2326
|
});
|
|
2299
2327
|
}
|
|
2300
2328
|
|
|
2301
|
-
// src/
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2329
|
+
// src/commands/auth-config.ts
|
|
2330
|
+
init_project();
|
|
2331
|
+
import chalk13 from "chalk";
|
|
2332
|
+
function printAuthConfig(auth) {
|
|
2333
|
+
printKv("Mode", auth.mode ?? "redirect");
|
|
2334
|
+
console.log();
|
|
2335
|
+
console.log(chalk13.bold("Providers:"));
|
|
2336
|
+
for (const [name, cfg] of Object.entries(auth.providers ?? {})) {
|
|
2337
|
+
const status = cfg.enabled ? chalk13.green("enabled") : chalk13.dim("disabled");
|
|
2338
|
+
console.log(` ${name.padEnd(12)} ${status}`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
function registerAuthConfigCommands(program2) {
|
|
2342
|
+
const authConfig = program2.command("auth-config").description("Configure Blink Auth \u2014 providers, mode, and BYOC credentials").addHelpText("after", `
|
|
2343
|
+
Manage authentication settings for your Blink project.
|
|
2344
|
+
Configure OAuth providers (Google, GitHub, Microsoft, Apple, Email)
|
|
2345
|
+
and set managed/headless mode.
|
|
2315
2346
|
|
|
2316
|
-
|
|
2317
|
-
$ blink
|
|
2318
|
-
$ blink
|
|
2319
|
-
$ blink
|
|
2320
|
-
$ blink
|
|
2347
|
+
Examples:
|
|
2348
|
+
$ blink auth-config get Show current auth config
|
|
2349
|
+
$ blink auth-config set --provider google --enabled true
|
|
2350
|
+
$ blink auth-config set --mode managed
|
|
2351
|
+
$ blink auth-config byoc-set --provider google --client-id xxx --client-secret yyy
|
|
2352
|
+
$ blink auth-config byoc-remove --provider google
|
|
2353
|
+
`);
|
|
2354
|
+
authConfig.command("get [project_id]").description("Show current auth configuration").addHelpText("after", `
|
|
2355
|
+
Examples:
|
|
2356
|
+
$ blink auth-config get Show auth config for linked project
|
|
2357
|
+
$ blink auth-config get proj_xxx Show auth config for specific project
|
|
2358
|
+
$ blink auth-config get --json Machine-readable output
|
|
2359
|
+
`).action(async (projectArg) => {
|
|
2360
|
+
requireToken();
|
|
2361
|
+
const projectId = requireProjectId(projectArg);
|
|
2362
|
+
const result = await withSpinner(
|
|
2363
|
+
"Loading auth config...",
|
|
2364
|
+
() => appRequest(`/api/projects/${projectId}/auth`)
|
|
2365
|
+
);
|
|
2366
|
+
if (isJsonMode()) return printJson(result);
|
|
2367
|
+
const auth = result?.auth ?? result;
|
|
2368
|
+
printAuthConfig(auth);
|
|
2369
|
+
});
|
|
2370
|
+
authConfig.command("set [project_id]").description("Update auth provider or mode").option("--provider <name>", "Provider to update (google/github/microsoft/apple/email)").option("--enabled <bool>", "Enable or disable provider (true/false)").option("--mode <mode>", "Auth mode (managed/headless)").addHelpText("after", `
|
|
2371
|
+
Examples:
|
|
2372
|
+
$ blink auth-config set --provider google --enabled true
|
|
2373
|
+
$ blink auth-config set --provider email --enabled false
|
|
2374
|
+
$ blink auth-config set --mode managed
|
|
2375
|
+
$ blink auth-config set proj_xxx --provider github --enabled true
|
|
2376
|
+
`).action(async (projectArg, opts) => {
|
|
2377
|
+
requireToken();
|
|
2378
|
+
const projectId = requireProjectId(projectArg);
|
|
2379
|
+
const current = await appRequest(`/api/projects/${projectId}/auth`);
|
|
2380
|
+
const auth = current?.auth ?? current ?? {};
|
|
2381
|
+
if (opts.mode) auth.mode = opts.mode;
|
|
2382
|
+
if (opts.provider && opts.enabled !== void 0) {
|
|
2383
|
+
if (!auth.providers) auth.providers = {};
|
|
2384
|
+
const existing = auth.providers[opts.provider];
|
|
2385
|
+
const providerConfig = typeof existing === "object" && existing ? existing : {};
|
|
2386
|
+
auth.providers[opts.provider] = { ...providerConfig, enabled: opts.enabled === "true" };
|
|
2387
|
+
}
|
|
2388
|
+
const result = await withSpinner(
|
|
2389
|
+
"Updating auth config...",
|
|
2390
|
+
() => appRequest(`/api/projects/${projectId}/auth`, { method: "PUT", body: auth })
|
|
2391
|
+
);
|
|
2392
|
+
if (isJsonMode()) return printJson(result);
|
|
2393
|
+
printSuccess("Auth config updated");
|
|
2394
|
+
});
|
|
2395
|
+
registerByocCommands(authConfig);
|
|
2396
|
+
}
|
|
2397
|
+
function registerByocCommands(authConfig) {
|
|
2398
|
+
authConfig.command("byoc-set [project_id]").description("Set BYOC (Bring Your Own Credentials) for an OAuth provider").requiredOption("--provider <name>", "Provider (google/github/microsoft/apple)").requiredOption("--client-id <id>", "OAuth client ID").requiredOption("--client-secret <secret>", "OAuth client secret").addHelpText("after", `
|
|
2399
|
+
Examples:
|
|
2400
|
+
$ blink auth-config byoc-set --provider google --client-id xxx --client-secret yyy
|
|
2401
|
+
$ blink auth-config byoc-set proj_xxx --provider github --client-id xxx --client-secret yyy
|
|
2402
|
+
`).action(async (projectArg, opts) => {
|
|
2403
|
+
requireToken();
|
|
2404
|
+
const projectId = requireProjectId(projectArg);
|
|
2405
|
+
const body = { provider: opts.provider, client_id: opts.clientId, client_secret: opts.clientSecret };
|
|
2406
|
+
await withSpinner(
|
|
2407
|
+
"Setting BYOC credentials...",
|
|
2408
|
+
() => appRequest(`/api/projects/${projectId}/auth/byoc`, { body })
|
|
2409
|
+
);
|
|
2410
|
+
if (isJsonMode()) return printJson({ status: "ok", provider: opts.provider });
|
|
2411
|
+
printSuccess(`BYOC credentials set for ${opts.provider}`);
|
|
2412
|
+
});
|
|
2413
|
+
authConfig.command("byoc-remove [project_id]").description("Remove BYOC credentials for an OAuth provider").requiredOption("--provider <name>", "Provider (google/github/microsoft/apple)").addHelpText("after", `
|
|
2414
|
+
Examples:
|
|
2415
|
+
$ blink auth-config byoc-remove --provider google
|
|
2416
|
+
$ blink auth-config byoc-remove proj_xxx --provider github
|
|
2417
|
+
`).action(async (projectArg, opts) => {
|
|
2418
|
+
requireToken();
|
|
2419
|
+
const projectId = requireProjectId(projectArg);
|
|
2420
|
+
await withSpinner(
|
|
2421
|
+
"Removing BYOC credentials...",
|
|
2422
|
+
() => appRequest(`/api/projects/${projectId}/auth/byoc`, {
|
|
2423
|
+
method: "DELETE",
|
|
2424
|
+
body: { provider: opts.provider }
|
|
2425
|
+
})
|
|
2426
|
+
);
|
|
2427
|
+
if (isJsonMode()) return printJson({ status: "ok", provider: opts.provider });
|
|
2428
|
+
printSuccess(`BYOC credentials removed for ${opts.provider}`);
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2321
2431
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2432
|
+
// src/commands/domains.ts
|
|
2433
|
+
init_project();
|
|
2434
|
+
import chalk14 from "chalk";
|
|
2435
|
+
function printDomainRow(d) {
|
|
2436
|
+
const status = d.verified ? chalk14.green("verified") : chalk14.yellow("pending");
|
|
2437
|
+
return [d.id ?? "-", d.domain, status, d.dns_type ?? "-"];
|
|
2438
|
+
}
|
|
2439
|
+
function registerDomainsCommands(program2) {
|
|
2440
|
+
const domains = program2.command("domains").description("Custom domain management \u2014 add, verify, remove, purchase, and connect").addHelpText("after", `
|
|
2441
|
+
Manage custom domains for your Blink projects.
|
|
2442
|
+
Search and purchase domains directly, or connect existing ones.
|
|
2328
2443
|
|
|
2329
|
-
|
|
2330
|
-
$ blink
|
|
2331
|
-
$ blink
|
|
2332
|
-
$ blink
|
|
2333
|
-
$ blink
|
|
2444
|
+
Examples:
|
|
2445
|
+
$ blink domains list List domains on linked project
|
|
2446
|
+
$ blink domains add example.com Add a custom domain
|
|
2447
|
+
$ blink domains verify dom_xxx Trigger DNS verification
|
|
2448
|
+
$ blink domains remove dom_xxx --yes Remove a domain
|
|
2449
|
+
$ blink domains search cool-app Search available domains
|
|
2450
|
+
$ blink domains purchase cool-app.com Purchase a domain
|
|
2451
|
+
$ blink domains my List your purchased domains
|
|
2452
|
+
$ blink domains connect example.com Connect a purchased domain to project
|
|
2453
|
+
`);
|
|
2454
|
+
domains.command("list [project_id]").description("List all domains on a project").addHelpText("after", `
|
|
2455
|
+
Examples:
|
|
2456
|
+
$ blink domains list List domains for linked project
|
|
2457
|
+
$ blink domains list proj_xxx List domains for specific project
|
|
2458
|
+
$ blink domains list --json Machine-readable output
|
|
2459
|
+
`).action(async (projectArg) => {
|
|
2460
|
+
requireToken();
|
|
2461
|
+
const projectId = requireProjectId(projectArg);
|
|
2462
|
+
const result = await withSpinner(
|
|
2463
|
+
"Loading domains...",
|
|
2464
|
+
() => appRequest(`/api/project/${projectId}/domains`)
|
|
2465
|
+
);
|
|
2466
|
+
if (isJsonMode()) return printJson(result);
|
|
2467
|
+
const items = result?.domains ?? result ?? [];
|
|
2468
|
+
if (!items.length) {
|
|
2469
|
+
console.log(chalk14.dim("(no domains \u2014 use `blink domains add <domain>`)"));
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
const table = createTable(["ID", "Domain", "Status", "DNS"]);
|
|
2473
|
+
for (const d of items) table.push(printDomainRow(d));
|
|
2474
|
+
console.log(table.toString());
|
|
2475
|
+
});
|
|
2476
|
+
domains.command("add <domain> [project_id]").description("Add a custom domain to a project").addHelpText("after", `
|
|
2477
|
+
Examples:
|
|
2478
|
+
$ blink domains add example.com Add to linked project
|
|
2479
|
+
$ blink domains add example.com proj_xxx Add to specific project
|
|
2480
|
+
`).action(async (domain, projectArg) => {
|
|
2481
|
+
requireToken();
|
|
2482
|
+
const projectId = requireProjectId(projectArg);
|
|
2483
|
+
const result = await withSpinner(
|
|
2484
|
+
`Adding ${domain}...`,
|
|
2485
|
+
() => appRequest(`/api/project/${projectId}/domains`, { body: { domain } })
|
|
2486
|
+
);
|
|
2487
|
+
if (isJsonMode()) return printJson(result);
|
|
2488
|
+
printSuccess(`Domain ${domain} added`);
|
|
2489
|
+
const domainId = result?.domain?.id ?? result?.id;
|
|
2490
|
+
if (domainId) printKv("Domain ID", domainId);
|
|
2491
|
+
console.log(chalk14.dim("Run `blink domains verify " + (domainId ?? "<domain_id>") + "` after configuring DNS"));
|
|
2492
|
+
});
|
|
2493
|
+
registerDomainActions(domains);
|
|
2494
|
+
registerGlobalDomainCommands(domains);
|
|
2495
|
+
}
|
|
2496
|
+
function registerDomainActions(domains) {
|
|
2497
|
+
domains.command("verify <domain_id> [project_id]").description("Trigger DNS verification for a domain").addHelpText("after", `
|
|
2498
|
+
Examples:
|
|
2499
|
+
$ blink domains verify dom_xxx
|
|
2500
|
+
$ blink domains verify dom_xxx proj_xxx
|
|
2501
|
+
`).action(async (domainId, projectArg) => {
|
|
2502
|
+
requireToken();
|
|
2503
|
+
const projectId = requireProjectId(projectArg);
|
|
2504
|
+
const result = await withSpinner(
|
|
2505
|
+
"Verifying DNS...",
|
|
2506
|
+
() => appRequest(`/api/project/${projectId}/domains/${domainId}`, { method: "POST" })
|
|
2507
|
+
);
|
|
2508
|
+
if (isJsonMode()) return printJson(result);
|
|
2509
|
+
const verified = result?.hostname_status === "active";
|
|
2510
|
+
if (verified) printSuccess("Domain verified");
|
|
2511
|
+
else console.log(chalk14.yellow("!") + " DNS not yet propagated \u2014 try again in a few minutes");
|
|
2512
|
+
});
|
|
2513
|
+
domains.command("remove <domain_id> [project_id]").description("Remove a domain from a project").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
2514
|
+
Examples:
|
|
2515
|
+
$ blink domains remove dom_xxx --yes
|
|
2516
|
+
$ blink domains remove dom_xxx proj_xxx
|
|
2517
|
+
`).action(async (domainId, projectArg, opts) => {
|
|
2518
|
+
requireToken();
|
|
2519
|
+
const projectId = requireProjectId(projectArg);
|
|
2520
|
+
if (!shouldSkipConfirm(opts)) {
|
|
2521
|
+
const { confirm } = await import("@clack/prompts");
|
|
2522
|
+
const ok = await confirm({ message: `Remove domain ${domainId}?` });
|
|
2523
|
+
if (!ok) {
|
|
2524
|
+
console.log("Cancelled.");
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
await withSpinner(
|
|
2529
|
+
"Removing domain...",
|
|
2530
|
+
() => appRequest(`/api/project/${projectId}/domains/${domainId}`, { method: "DELETE" })
|
|
2531
|
+
);
|
|
2532
|
+
if (isJsonMode()) return printJson({ status: "ok", domain_id: domainId });
|
|
2533
|
+
printSuccess("Domain removed");
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
function registerGlobalDomainCommands(domains) {
|
|
2537
|
+
domains.command("search <query>").description("Search available domains for purchase (no auth needed)").addHelpText("after", `
|
|
2538
|
+
Examples:
|
|
2539
|
+
$ blink domains search cool-app
|
|
2540
|
+
$ blink domains search my-startup --json
|
|
2541
|
+
`).action(async (query) => {
|
|
2542
|
+
const result = await withSpinner(
|
|
2543
|
+
"Searching domains...",
|
|
2544
|
+
() => appRequest(`/api/domains/search?q=${encodeURIComponent(query)}`)
|
|
2545
|
+
);
|
|
2546
|
+
if (isJsonMode()) return printJson(result);
|
|
2547
|
+
const items = result?.results ?? result ?? [];
|
|
2548
|
+
if (!items.length) {
|
|
2549
|
+
console.log(chalk14.dim("No results"));
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const table = createTable(["Domain", "Available", "Price"]);
|
|
2553
|
+
for (const d of items) {
|
|
2554
|
+
const avail = d.available ? chalk14.green("yes") : chalk14.red("no");
|
|
2555
|
+
table.push([d.domain, avail, d.price ?? "-"]);
|
|
2556
|
+
}
|
|
2557
|
+
console.log(table.toString());
|
|
2558
|
+
});
|
|
2559
|
+
domains.command("purchase <domain>").description("Purchase a domain").option("--period <years>", "Registration period in years", "1").addHelpText("after", `
|
|
2560
|
+
Examples:
|
|
2561
|
+
$ blink domains purchase cool-app.com
|
|
2562
|
+
$ blink domains purchase cool-app.com --period 2
|
|
2563
|
+
$ blink domains purchase cool-app.com --json
|
|
2564
|
+
`).option("--yes", "Skip confirmation").action(async (domain, opts) => {
|
|
2565
|
+
requireToken();
|
|
2566
|
+
if (!shouldSkipConfirm(opts)) {
|
|
2567
|
+
const { confirm } = await import("@clack/prompts");
|
|
2568
|
+
const ok = await confirm({ message: `Purchase ${domain} for ${opts.period} year(s)? This will charge your account.` });
|
|
2569
|
+
if (!ok) {
|
|
2570
|
+
console.log("Cancelled.");
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
const result = await withSpinner(
|
|
2575
|
+
`Purchasing ${domain}...`,
|
|
2576
|
+
() => appRequest("/api/domains/purchase", { body: { domain, period: Number(opts.period) } })
|
|
2577
|
+
);
|
|
2578
|
+
if (isJsonMode()) return printJson(result);
|
|
2579
|
+
printSuccess(`Domain ${domain} purchased`);
|
|
2580
|
+
if (result?.domain_id) printKv("Domain ID", result.domain_id);
|
|
2581
|
+
});
|
|
2582
|
+
domains.command("connect <domain> [project_id]").description("Connect a purchased domain to a project").addHelpText("after", `
|
|
2583
|
+
Examples:
|
|
2584
|
+
$ blink domains connect example.com Connect to linked project
|
|
2585
|
+
$ blink domains connect example.com proj_xxx Connect to specific project
|
|
2586
|
+
`).action(async (domain, projectArg) => {
|
|
2587
|
+
requireToken();
|
|
2588
|
+
const projectId = requireProjectId(projectArg);
|
|
2589
|
+
const result = await withSpinner(
|
|
2590
|
+
`Connecting ${domain}...`,
|
|
2591
|
+
() => appRequest("/api/domains/connect", { body: { domainId: domain, projectId } })
|
|
2592
|
+
);
|
|
2593
|
+
if (isJsonMode()) return printJson(result);
|
|
2594
|
+
printSuccess(`${domain} connected to ${projectId}`);
|
|
2595
|
+
});
|
|
2596
|
+
domains.command("my").description("List your purchased domains").addHelpText("after", `
|
|
2597
|
+
Examples:
|
|
2598
|
+
$ blink domains my
|
|
2599
|
+
$ blink domains my --json
|
|
2600
|
+
`).action(async () => {
|
|
2601
|
+
requireToken();
|
|
2602
|
+
const result = await withSpinner(
|
|
2603
|
+
"Loading your domains...",
|
|
2604
|
+
() => appRequest("/api/domains/my")
|
|
2605
|
+
);
|
|
2606
|
+
if (isJsonMode()) return printJson(result);
|
|
2607
|
+
const items = result?.domains ?? result ?? [];
|
|
2608
|
+
if (!items.length) {
|
|
2609
|
+
console.log(chalk14.dim("(no purchased domains)"));
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const table = createTable(["Domain", "Status", "Expires"]);
|
|
2613
|
+
for (const d of items) table.push([d.domain, d.status ?? "-", d.expiration_date ?? "-"]);
|
|
2614
|
+
console.log(table.toString());
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
function shouldSkipConfirm(opts) {
|
|
2618
|
+
return opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
2619
|
+
}
|
|
2334
2620
|
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2621
|
+
// src/commands/hosting.ts
|
|
2622
|
+
init_project();
|
|
2623
|
+
import chalk15 from "chalk";
|
|
2624
|
+
var STATE_COLORS = {
|
|
2625
|
+
active: chalk15.green,
|
|
2626
|
+
inactive: chalk15.dim,
|
|
2627
|
+
grace: chalk15.yellow,
|
|
2628
|
+
offline: chalk15.red
|
|
2629
|
+
};
|
|
2630
|
+
function printHostingStatus(data) {
|
|
2631
|
+
const state = String(data.status ?? data.state ?? "unknown");
|
|
2632
|
+
const colorFn = STATE_COLORS[state] ?? chalk15.white;
|
|
2633
|
+
printKv("State", colorFn(state));
|
|
2634
|
+
if (data.hosting_tier) printKv("Tier", String(data.hosting_tier));
|
|
2635
|
+
if (data.hosting_prod_url) printKv("URL", String(data.hosting_prod_url));
|
|
2636
|
+
}
|
|
2637
|
+
function registerHostingCommands(program2) {
|
|
2638
|
+
const hosting = program2.command("hosting").description("Hosting management \u2014 activate, deactivate, and check status").addHelpText("after", `
|
|
2639
|
+
Manage hosting for your Blink project.
|
|
2640
|
+
Hosting gives your project a live URL on blinkpowered.com or a custom domain.
|
|
2341
2641
|
|
|
2342
|
-
|
|
2343
|
-
$ blink
|
|
2344
|
-
$ blink
|
|
2345
|
-
$ blink
|
|
2346
|
-
$ blink
|
|
2347
|
-
|
|
2642
|
+
Examples:
|
|
2643
|
+
$ blink hosting status Check hosting state and URLs
|
|
2644
|
+
$ blink hosting activate Activate hosting
|
|
2645
|
+
$ blink hosting deactivate --yes Deactivate hosting
|
|
2646
|
+
$ blink hosting reactivate Reactivate after deactivation
|
|
2647
|
+
`);
|
|
2648
|
+
hosting.command("status [project_id]").description("Show hosting state, tier, and URLs").addHelpText("after", `
|
|
2649
|
+
Examples:
|
|
2650
|
+
$ blink hosting status Status for linked project
|
|
2651
|
+
$ blink hosting status proj_xxx Status for specific project
|
|
2652
|
+
$ blink hosting status --json Machine-readable output
|
|
2653
|
+
`).action(async (projectArg) => {
|
|
2654
|
+
requireToken();
|
|
2655
|
+
const projectId = requireProjectId(projectArg);
|
|
2656
|
+
const result = await withSpinner(
|
|
2657
|
+
"Loading hosting status...",
|
|
2658
|
+
() => appRequest(`/api/project/${projectId}/hosting/status`)
|
|
2659
|
+
);
|
|
2660
|
+
if (isJsonMode()) return printJson(result);
|
|
2661
|
+
printHostingStatus(result);
|
|
2662
|
+
});
|
|
2663
|
+
hosting.command("activate [project_id]").description("Activate hosting for a project").addHelpText("after", `
|
|
2664
|
+
Examples:
|
|
2665
|
+
$ blink hosting activate
|
|
2666
|
+
$ blink hosting activate proj_xxx
|
|
2667
|
+
`).action(async (projectArg) => {
|
|
2668
|
+
requireToken();
|
|
2669
|
+
const projectId = requireProjectId(projectArg);
|
|
2670
|
+
const result = await withSpinner(
|
|
2671
|
+
"Activating hosting...",
|
|
2672
|
+
() => appRequest(`/api/project/${projectId}/hosting/activate`, { method: "POST" })
|
|
2673
|
+
);
|
|
2674
|
+
if (isJsonMode()) return printJson(result);
|
|
2675
|
+
printSuccess("Hosting activated");
|
|
2676
|
+
if (result?.hosting_prod_url) printKv("URL", result.hosting_prod_url);
|
|
2677
|
+
});
|
|
2678
|
+
hosting.command("deactivate [project_id]").description("Deactivate hosting for a project").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
2679
|
+
Examples:
|
|
2680
|
+
$ blink hosting deactivate --yes
|
|
2681
|
+
$ blink hosting deactivate proj_xxx
|
|
2682
|
+
`).action(async (projectArg, opts) => {
|
|
2683
|
+
requireToken();
|
|
2684
|
+
const projectId = requireProjectId(projectArg);
|
|
2685
|
+
if (!shouldSkipConfirm2(opts)) {
|
|
2686
|
+
const { confirm } = await import("@clack/prompts");
|
|
2687
|
+
const ok = await confirm({ message: "Deactivate hosting? Your site will go offline." });
|
|
2688
|
+
if (!ok) {
|
|
2689
|
+
console.log("Cancelled.");
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
await withSpinner(
|
|
2694
|
+
"Deactivating hosting...",
|
|
2695
|
+
() => appRequest(`/api/project/${projectId}/hosting/deactivate`, { method: "POST" })
|
|
2696
|
+
);
|
|
2697
|
+
if (isJsonMode()) return printJson({ status: "ok", project_id: projectId });
|
|
2698
|
+
printSuccess("Hosting deactivated");
|
|
2699
|
+
});
|
|
2700
|
+
hosting.command("reactivate [project_id]").description("Reactivate hosting after deactivation").addHelpText("after", `
|
|
2701
|
+
Examples:
|
|
2702
|
+
$ blink hosting reactivate
|
|
2703
|
+
$ blink hosting reactivate proj_xxx
|
|
2704
|
+
`).action(async (projectArg) => {
|
|
2705
|
+
requireToken();
|
|
2706
|
+
const projectId = requireProjectId(projectArg);
|
|
2707
|
+
const result = await withSpinner(
|
|
2708
|
+
"Reactivating hosting...",
|
|
2709
|
+
() => appRequest(`/api/project/${projectId}/hosting/reactivate`, { method: "POST" })
|
|
2710
|
+
);
|
|
2711
|
+
if (isJsonMode()) return printJson(result);
|
|
2712
|
+
printSuccess("Hosting reactivated");
|
|
2713
|
+
if (result?.hosting_prod_url) printKv("URL", result.hosting_prod_url);
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
function shouldSkipConfirm2(opts) {
|
|
2717
|
+
return opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
2718
|
+
}
|
|
2348
2719
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2720
|
+
// src/commands/security.ts
|
|
2721
|
+
init_project();
|
|
2722
|
+
import chalk16 from "chalk";
|
|
2723
|
+
function printSecurityPolicy(data) {
|
|
2724
|
+
const modules = data.modules ?? data;
|
|
2725
|
+
const table = createTable(["Module", "Require Auth"]);
|
|
2726
|
+
for (const [name, cfg] of Object.entries(modules)) {
|
|
2727
|
+
const status = cfg.require_auth ? chalk16.green("yes") : chalk16.dim("no");
|
|
2728
|
+
table.push([name, status]);
|
|
2729
|
+
}
|
|
2730
|
+
console.log(table.toString());
|
|
2731
|
+
}
|
|
2732
|
+
function printCorsOrigins(origins) {
|
|
2733
|
+
if (!origins.length) {
|
|
2734
|
+
console.log(chalk16.dim("(no CORS origins configured \u2014 all origins allowed)"));
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
for (const o of origins) console.log(` ${o}`);
|
|
2738
|
+
console.log(chalk16.dim(`
|
|
2739
|
+
${origins.length} origin${origins.length === 1 ? "" : "s"}`));
|
|
2740
|
+
}
|
|
2741
|
+
function registerSecurityCommands(program2) {
|
|
2742
|
+
registerSecurityGroup(program2);
|
|
2743
|
+
registerCorsGroup(program2);
|
|
2744
|
+
}
|
|
2745
|
+
function registerSecurityGroup(program2) {
|
|
2746
|
+
const security = program2.command("security").description("Security policy \u2014 require auth per module (db/ai/storage/realtime/rag)").addHelpText("after", `
|
|
2747
|
+
Control which backend modules require user authentication.
|
|
2748
|
+
When require_auth is true, unauthenticated requests are rejected.
|
|
2353
2749
|
|
|
2354
|
-
|
|
2355
|
-
$ blink
|
|
2356
|
-
$ blink
|
|
2357
|
-
$ blink
|
|
2358
|
-
|
|
2750
|
+
Examples:
|
|
2751
|
+
$ blink security get Show security policy
|
|
2752
|
+
$ blink security set --module db --require-auth true
|
|
2753
|
+
$ blink security set --module ai --require-auth false
|
|
2754
|
+
`);
|
|
2755
|
+
security.command("get [project_id]").description("Show current security policy").addHelpText("after", `
|
|
2756
|
+
Examples:
|
|
2757
|
+
$ blink security get Show policy for linked project
|
|
2758
|
+
$ blink security get proj_xxx Show policy for specific project
|
|
2759
|
+
$ blink security get --json Machine-readable output
|
|
2760
|
+
`).action(async (projectArg) => {
|
|
2761
|
+
requireToken();
|
|
2762
|
+
const projectId = requireProjectId(projectArg);
|
|
2763
|
+
const result = await withSpinner(
|
|
2764
|
+
"Loading security policy...",
|
|
2765
|
+
() => appRequest(`/api/project/${projectId}/security`)
|
|
2766
|
+
);
|
|
2767
|
+
if (isJsonMode()) return printJson(result);
|
|
2768
|
+
printSecurityPolicy(result?.policy ?? result);
|
|
2769
|
+
});
|
|
2770
|
+
security.command("set [project_id]").description("Update security policy for a module").requiredOption("--module <name>", "Module (db/ai/storage/realtime/rag)").requiredOption("--require-auth <bool>", "Require auth (true/false)").addHelpText("after", `
|
|
2771
|
+
Examples:
|
|
2772
|
+
$ blink security set --module db --require-auth true
|
|
2773
|
+
$ blink security set --module ai --require-auth false
|
|
2774
|
+
$ blink security set proj_xxx --module storage --require-auth true
|
|
2775
|
+
`).action(async (projectArg, opts) => {
|
|
2776
|
+
requireToken();
|
|
2777
|
+
const projectId = requireProjectId(projectArg);
|
|
2778
|
+
const requireAuth = opts.requireAuth === "true";
|
|
2779
|
+
const body = { policy: { modules: { [opts.module]: { require_auth: requireAuth } } } };
|
|
2780
|
+
await withSpinner(
|
|
2781
|
+
"Updating security policy...",
|
|
2782
|
+
() => appRequest(`/api/project/${projectId}/security`, { method: "PUT", body })
|
|
2783
|
+
);
|
|
2784
|
+
if (isJsonMode()) return printJson({ status: "ok", module: opts.module, require_auth: requireAuth });
|
|
2785
|
+
printSuccess(`${opts.module}: require_auth = ${requireAuth}`);
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
function registerCorsGroup(program2) {
|
|
2789
|
+
const cors = program2.command("cors").description("CORS origin management \u2014 control which origins can access your project APIs").addHelpText("after", `
|
|
2790
|
+
Manage allowed CORS origins for your project.
|
|
2791
|
+
When no origins are configured, all origins are allowed.
|
|
2359
2792
|
|
|
2360
|
-
|
|
2361
|
-
$ blink
|
|
2362
|
-
$ blink
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
$ blink
|
|
2367
|
-
$ blink
|
|
2368
|
-
$ blink
|
|
2369
|
-
|
|
2370
|
-
|
|
2793
|
+
Examples:
|
|
2794
|
+
$ blink cors get Show allowed origins
|
|
2795
|
+
$ blink cors set --origins https://example.com https://app.example.com
|
|
2796
|
+
`);
|
|
2797
|
+
cors.command("get [project_id]").description("Show allowed CORS origins").addHelpText("after", `
|
|
2798
|
+
Examples:
|
|
2799
|
+
$ blink cors get Show origins for linked project
|
|
2800
|
+
$ blink cors get proj_xxx Show origins for specific project
|
|
2801
|
+
$ blink cors get --json Machine-readable output
|
|
2802
|
+
`).action(async (projectArg) => {
|
|
2803
|
+
requireToken();
|
|
2804
|
+
const projectId = requireProjectId(projectArg);
|
|
2805
|
+
const result = await withSpinner(
|
|
2806
|
+
"Loading CORS config...",
|
|
2807
|
+
() => appRequest(`/api/project/${projectId}/cors`)
|
|
2808
|
+
);
|
|
2809
|
+
if (isJsonMode()) return printJson(result);
|
|
2810
|
+
const origins = result?.custom_origins ?? result?.origins ?? [];
|
|
2811
|
+
printCorsOrigins(origins);
|
|
2812
|
+
});
|
|
2813
|
+
cors.command("set [project_id]").description("Set allowed CORS origins").requiredOption("--origins <urls...>", "Allowed origins (space-separated)").addHelpText("after", `
|
|
2814
|
+
Examples:
|
|
2815
|
+
$ blink cors set --origins https://example.com
|
|
2816
|
+
$ blink cors set --origins https://example.com https://app.example.com
|
|
2817
|
+
$ blink cors set proj_xxx --origins https://example.com
|
|
2818
|
+
`).action(async (projectArg, opts) => {
|
|
2819
|
+
requireToken();
|
|
2820
|
+
const projectId = requireProjectId(projectArg);
|
|
2821
|
+
const origins = opts.origins;
|
|
2822
|
+
await withSpinner(
|
|
2823
|
+
"Updating CORS origins...",
|
|
2824
|
+
() => appRequest(`/api/project/${projectId}/cors`, { method: "PUT", body: { custom_origins: origins } })
|
|
2825
|
+
);
|
|
2826
|
+
if (isJsonMode()) return printJson({ status: "ok", origins });
|
|
2827
|
+
printSuccess(`CORS origins set: ${origins.join(", ")}`);
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2371
2830
|
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2831
|
+
// src/commands/env.ts
|
|
2832
|
+
init_project();
|
|
2833
|
+
import { readFileSync as readFileSync9, existsSync as existsSync4 } from "fs";
|
|
2834
|
+
import chalk17 from "chalk";
|
|
2835
|
+
async function resolveSecretId(projectId, keyName) {
|
|
2836
|
+
const result = await appRequest(`/api/projects/${projectId}/secrets`);
|
|
2837
|
+
const secrets = result?.secrets ?? result ?? [];
|
|
2838
|
+
const match = secrets.find((s) => s.key === keyName);
|
|
2839
|
+
if (!match) {
|
|
2840
|
+
printError(`Env var "${keyName}" not found in project ${projectId}`);
|
|
2841
|
+
process.exit(1);
|
|
2842
|
+
}
|
|
2843
|
+
return match.id;
|
|
2844
|
+
}
|
|
2845
|
+
function parseEnvFile(filePath) {
|
|
2846
|
+
if (!existsSync4(filePath)) {
|
|
2847
|
+
printError(`File not found: ${filePath}`);
|
|
2848
|
+
process.exit(1);
|
|
2849
|
+
}
|
|
2850
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2851
|
+
const entries = [];
|
|
2852
|
+
for (const line of content.split("\n")) {
|
|
2853
|
+
const trimmed = line.trim();
|
|
2854
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2855
|
+
const eqIdx = trimmed.indexOf("=");
|
|
2856
|
+
if (eqIdx === -1) continue;
|
|
2857
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
2858
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
2859
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2860
|
+
value = value.slice(1, -1);
|
|
2861
|
+
}
|
|
2862
|
+
if (key) entries.push({ key, value });
|
|
2863
|
+
}
|
|
2864
|
+
return entries;
|
|
2865
|
+
}
|
|
2866
|
+
function registerEnvCommands(program2) {
|
|
2867
|
+
const env = program2.command("env").description("Manage project environment variables (secrets)").addHelpText("after", `
|
|
2868
|
+
Environment variables are encrypted key-value pairs for your project.
|
|
2869
|
+
They are injected at runtime in backend functions and edge workers.
|
|
2870
|
+
Use "env pull > .env.local" to export as a file.
|
|
2871
|
+
|
|
2872
|
+
Note: For Claw agent secrets, use "blink secrets" instead.
|
|
2873
|
+
|
|
2874
|
+
Examples:
|
|
2875
|
+
$ blink env list List env vars
|
|
2876
|
+
$ blink env set DATABASE_URL postgres://...
|
|
2877
|
+
$ blink env delete OLD_KEY
|
|
2878
|
+
$ blink env push .env Bulk import from .env file
|
|
2879
|
+
$ blink env pull > .env.local Export all vars to stdout
|
|
2880
|
+
`);
|
|
2881
|
+
registerEnvList(env);
|
|
2882
|
+
registerEnvSet(env);
|
|
2883
|
+
registerEnvDelete(env);
|
|
2884
|
+
registerEnvPush(env);
|
|
2885
|
+
registerEnvPull(env);
|
|
2886
|
+
}
|
|
2887
|
+
function registerEnvList(env) {
|
|
2888
|
+
env.command("list [project_id]").description("List environment variables").addHelpText("after", `
|
|
2889
|
+
Examples:
|
|
2890
|
+
$ blink env list List for linked project
|
|
2891
|
+
$ blink env list proj_xxx List for specific project
|
|
2892
|
+
$ blink env list --json Machine-readable output
|
|
2893
|
+
`).action(async (projectArg) => {
|
|
2894
|
+
requireToken();
|
|
2895
|
+
const projectId = requireProjectId(projectArg);
|
|
2896
|
+
const result = await withSpinner(
|
|
2897
|
+
"Loading env vars...",
|
|
2898
|
+
() => appRequest(`/api/projects/${projectId}/secrets`)
|
|
2899
|
+
);
|
|
2900
|
+
if (isJsonMode()) return printJson(result);
|
|
2901
|
+
const secrets = result?.secrets ?? result ?? [];
|
|
2902
|
+
if (!secrets.length) {
|
|
2903
|
+
console.log(chalk17.dim("(no env vars \u2014 use `blink env set KEY value`)"));
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
const table = createTable(["Key", "Value"]);
|
|
2907
|
+
for (const s of secrets) table.push([chalk17.bold(s.key), s.value ?? ""]);
|
|
2908
|
+
console.log(table.toString());
|
|
2909
|
+
console.log(chalk17.dim(`
|
|
2910
|
+
${secrets.length} variable${secrets.length === 1 ? "" : "s"}`));
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
function registerEnvSet(env) {
|
|
2914
|
+
env.command("set <key> <value>").description("Create or update an environment variable").option("--project <id>", "Project ID (defaults to linked project)").addHelpText("after", `
|
|
2915
|
+
Examples:
|
|
2916
|
+
$ blink env set DATABASE_URL postgres://user:pass@host/db
|
|
2917
|
+
$ blink env set STRIPE_KEY sk_live_xxx
|
|
2918
|
+
$ blink env set API_SECRET mysecret --project proj_xxx
|
|
2919
|
+
`).action(async (key, value, opts) => {
|
|
2920
|
+
requireToken();
|
|
2921
|
+
const projectId = requireProjectId(opts.project);
|
|
2922
|
+
await withSpinner(
|
|
2923
|
+
`Setting ${key}...`,
|
|
2924
|
+
() => appRequest(`/api/projects/${projectId}/secrets`, {
|
|
2925
|
+
method: "POST",
|
|
2926
|
+
body: { key, value }
|
|
2927
|
+
})
|
|
2928
|
+
);
|
|
2929
|
+
if (isJsonMode()) return printJson({ status: "ok", key, project_id: projectId });
|
|
2930
|
+
printSuccess(`${key} saved to project ${projectId}`);
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
function registerEnvDelete(env) {
|
|
2934
|
+
env.command("delete <key>").description("Delete an environment variable").option("--project <id>", "Project ID (defaults to linked project)").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
2935
|
+
Examples:
|
|
2936
|
+
$ blink env delete OLD_KEY
|
|
2937
|
+
$ blink env delete OLD_KEY --yes
|
|
2938
|
+
$ blink env delete OLD_KEY --project proj_xxx
|
|
2939
|
+
`).action(async (key, opts) => {
|
|
2940
|
+
requireToken();
|
|
2941
|
+
const projectId = requireProjectId(opts.project);
|
|
2942
|
+
const skipConfirm = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y");
|
|
2943
|
+
if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
|
|
2944
|
+
const { confirm } = await import("@clack/prompts");
|
|
2945
|
+
const ok = await confirm({ message: `Delete env var "${key}" from project ${projectId}?` });
|
|
2946
|
+
if (!ok) {
|
|
2947
|
+
console.log("Cancelled.");
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const secretId = await resolveSecretId(projectId, key);
|
|
2952
|
+
await withSpinner(
|
|
2953
|
+
`Deleting ${key}...`,
|
|
2954
|
+
() => appRequest(`/api/projects/${projectId}/secrets/${secretId}`, { method: "DELETE" })
|
|
2955
|
+
);
|
|
2956
|
+
if (isJsonMode()) return printJson({ status: "ok", key, project_id: projectId });
|
|
2957
|
+
printSuccess(`Deleted ${key}`);
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
function registerEnvPush(env) {
|
|
2961
|
+
env.command("push <file>").description("Bulk import environment variables from a .env file").option("--project <id>", "Project ID (defaults to linked project)").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
2962
|
+
Reads KEY=VALUE pairs from the file and upserts each one.
|
|
2963
|
+
Lines starting with # and blank lines are skipped.
|
|
2964
|
+
|
|
2965
|
+
Examples:
|
|
2966
|
+
$ blink env push .env
|
|
2967
|
+
$ blink env push .env.production --project proj_xxx
|
|
2968
|
+
$ blink env push .env --yes
|
|
2969
|
+
`).action(async (file, opts) => {
|
|
2970
|
+
requireToken();
|
|
2971
|
+
const projectId = requireProjectId(opts.project);
|
|
2972
|
+
const entries = parseEnvFile(file);
|
|
2973
|
+
if (!entries.length) {
|
|
2974
|
+
printError(`No KEY=VALUE pairs found in ${file}`);
|
|
2975
|
+
process.exit(1);
|
|
2976
|
+
}
|
|
2977
|
+
const skipConfirm = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode() || !process.stdout.isTTY;
|
|
2978
|
+
if (!skipConfirm) {
|
|
2979
|
+
console.log(chalk17.dim(` Will upsert ${entries.length} vars: ${entries.map((e) => e.key).join(", ")}`));
|
|
2980
|
+
const { confirm } = await import("@clack/prompts");
|
|
2981
|
+
const ok = await confirm({ message: `Push ${entries.length} env vars to project ${projectId}?` });
|
|
2982
|
+
if (!ok) {
|
|
2983
|
+
console.log("Cancelled.");
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
await withSpinner(`Pushing ${entries.length} vars from ${file}...`, async () => {
|
|
2988
|
+
for (const { key, value } of entries) {
|
|
2989
|
+
await appRequest(`/api/projects/${projectId}/secrets`, {
|
|
2990
|
+
method: "POST",
|
|
2991
|
+
body: { key, value }
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
if (isJsonMode()) return printJson({ status: "ok", count: entries.length, project_id: projectId });
|
|
2996
|
+
printSuccess(`Pushed ${entries.length} env var${entries.length === 1 ? "" : "s"} to ${projectId}`);
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
function registerEnvPull(env) {
|
|
3000
|
+
env.command("pull [project_id]").description("Export all environment variables as KEY=VALUE to stdout").addHelpText("after", `
|
|
3001
|
+
Prints all env vars as KEY=VALUE lines to stdout.
|
|
3002
|
+
Pipe to a file to create a local .env:
|
|
3003
|
+
|
|
3004
|
+
Examples:
|
|
3005
|
+
$ blink env pull > .env.local
|
|
3006
|
+
$ blink env pull proj_xxx > .env.production
|
|
3007
|
+
$ blink env pull --json
|
|
3008
|
+
`).action(async (projectArg) => {
|
|
3009
|
+
requireToken();
|
|
3010
|
+
const projectId = requireProjectId(projectArg);
|
|
3011
|
+
const result = await appRequest(`/api/projects/${projectId}/secrets`);
|
|
3012
|
+
const secrets = result?.secrets ?? result ?? [];
|
|
3013
|
+
if (isJsonMode()) return printJson(secrets);
|
|
3014
|
+
if (process.stdout.isTTY) {
|
|
3015
|
+
process.stderr.write(chalk17.yellow("\u26A0 Printing secret values to terminal. Pipe to file: blink env pull > .env.local\n"));
|
|
3016
|
+
}
|
|
3017
|
+
for (const s of secrets) {
|
|
3018
|
+
process.stdout.write(`${s.key}=${s.value ?? ""}
|
|
3019
|
+
`);
|
|
3020
|
+
}
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// src/commands/backend.ts
|
|
3025
|
+
init_project();
|
|
3026
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync10, statSync } from "fs";
|
|
3027
|
+
import { join as join4, relative as relative2 } from "path";
|
|
3028
|
+
import chalk18 from "chalk";
|
|
3029
|
+
var EXCLUDED_FILES = /* @__PURE__ */ new Set(["package-lock.json", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"]);
|
|
3030
|
+
function collectBackendFiles(dir) {
|
|
3031
|
+
const files = [];
|
|
3032
|
+
function walk(current) {
|
|
3033
|
+
for (const entry of readdirSync2(current, { withFileTypes: true })) {
|
|
3034
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
3035
|
+
const fullPath = join4(current, entry.name);
|
|
3036
|
+
if (entry.isDirectory()) {
|
|
3037
|
+
walk(fullPath);
|
|
3038
|
+
continue;
|
|
3039
|
+
}
|
|
3040
|
+
if (EXCLUDED_FILES.has(entry.name)) continue;
|
|
3041
|
+
const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
3042
|
+
if (!["ts", "js", "tsx", "jsx", "json"].includes(ext)) continue;
|
|
3043
|
+
files.push({
|
|
3044
|
+
path: join4("backend", relative2(dir, fullPath)),
|
|
3045
|
+
source_code: readFileSync10(fullPath, "utf-8")
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
walk(dir);
|
|
3050
|
+
return files;
|
|
3051
|
+
}
|
|
3052
|
+
function resolveProjectAndDir(arg1, arg2) {
|
|
3053
|
+
if (arg2) return { projectId: requireProjectId(arg1), dir: arg2 };
|
|
3054
|
+
if (arg1?.startsWith("proj_")) return { projectId: requireProjectId(arg1), dir: "backend" };
|
|
3055
|
+
return { projectId: requireProjectId(), dir: arg1 ?? "backend" };
|
|
3056
|
+
}
|
|
3057
|
+
function registerBackendCommands(program2) {
|
|
3058
|
+
const backend = program2.command("backend").description("Manage backend (CF Workers for Platforms) for your project").addHelpText("after", `
|
|
3059
|
+
Deploy, monitor, and manage your project's backend worker.
|
|
3060
|
+
The backend is a Hono server running on Cloudflare Workers for Platforms.
|
|
3061
|
+
|
|
3062
|
+
Examples:
|
|
3063
|
+
$ blink backend deploy Deploy backend/ folder
|
|
3064
|
+
$ blink backend deploy ./server Deploy from custom dir
|
|
3065
|
+
$ blink backend status Check deployment status
|
|
3066
|
+
$ blink backend logs View recent logs
|
|
3067
|
+
$ blink backend delete --yes Remove backend worker
|
|
3068
|
+
`);
|
|
3069
|
+
registerBackendDeploy(backend);
|
|
3070
|
+
registerBackendStatus(backend);
|
|
3071
|
+
registerBackendLogs(backend);
|
|
3072
|
+
registerBackendDelete(backend);
|
|
3073
|
+
}
|
|
3074
|
+
function registerBackendDeploy(backend) {
|
|
3075
|
+
backend.command("deploy [project_id] [dir]").description("Deploy backend files to CF Workers for Platforms").addHelpText("after", `
|
|
3076
|
+
Reads all .ts/.js/.json files from the backend/ directory (or specified dir),
|
|
3077
|
+
bundles and deploys them as a Cloudflare Worker.
|
|
3078
|
+
|
|
3079
|
+
Examples:
|
|
3080
|
+
$ blink backend deploy Deploy backend/ for linked project
|
|
3081
|
+
$ blink backend deploy ./server Deploy from custom directory
|
|
3082
|
+
$ blink backend deploy proj_xxx Deploy backend/ for specific project
|
|
3083
|
+
$ blink backend deploy proj_xxx ./api Deploy ./api for specific project
|
|
3084
|
+
`).action(async (arg1, arg2) => {
|
|
3085
|
+
requireToken();
|
|
3086
|
+
const { projectId, dir } = resolveProjectAndDir(arg1, arg2);
|
|
3087
|
+
if (!statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
3088
|
+
printError(`Directory not found: ${dir}`, `Create a backend/ folder with index.ts`);
|
|
3089
|
+
process.exit(1);
|
|
3090
|
+
}
|
|
3091
|
+
const files = collectBackendFiles(dir);
|
|
3092
|
+
if (!files.length) {
|
|
3093
|
+
printError(`No .ts/.js files found in ${dir}`);
|
|
3094
|
+
process.exit(1);
|
|
3095
|
+
}
|
|
3096
|
+
if (!isJsonMode()) console.log(chalk18.dim(` ${files.length} file${files.length === 1 ? "" : "s"} in ${dir}`));
|
|
3097
|
+
const result = await withSpinner(
|
|
3098
|
+
"Deploying backend...",
|
|
3099
|
+
() => appRequest(`/api/v1/projects/${projectId}/backend/deploy`, { body: { files } })
|
|
3100
|
+
);
|
|
3101
|
+
if (isJsonMode()) return printJson(result);
|
|
3102
|
+
printSuccess("Backend deployed");
|
|
3103
|
+
if (result?.url) printUrl("URL", result.url);
|
|
3104
|
+
printKv("Project", projectId);
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
function registerBackendStatus(backend) {
|
|
3108
|
+
backend.command("status [project_id]").description("Check backend deployment status").addHelpText("after", `
|
|
3109
|
+
Examples:
|
|
3110
|
+
$ blink backend status
|
|
3111
|
+
$ blink backend status proj_xxx
|
|
3112
|
+
$ blink backend status --json
|
|
3113
|
+
`).action(async (projectArg) => {
|
|
3114
|
+
requireToken();
|
|
3115
|
+
const projectId = requireProjectId(projectArg);
|
|
3116
|
+
const result = await withSpinner(
|
|
3117
|
+
"Checking status...",
|
|
3118
|
+
() => appRequest(`/api/v1/projects/${projectId}/backend/status`)
|
|
3119
|
+
);
|
|
3120
|
+
if (isJsonMode()) return printJson(result);
|
|
3121
|
+
printKv("Project", projectId);
|
|
3122
|
+
printKv("Enabled", result?.enabled ? "yes" : "no");
|
|
3123
|
+
printKv("Tier", result?.tier ?? "-");
|
|
3124
|
+
printKv("Requests", String(result?.requests_this_month ?? 0));
|
|
3125
|
+
if (result?.enabled) printUrl("URL", `https://${projectId.slice(-8)}.backend.blink.new`);
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
function registerBackendLogs(backend) {
|
|
3129
|
+
backend.command("logs [project_id]").description("View recent backend logs").option("--minutes <n>", "Minutes of logs to fetch", "30").option("--slug <name>", "Function slug", "index").addHelpText("after", `
|
|
3130
|
+
Examples:
|
|
3131
|
+
$ blink backend logs Last 30 min of logs
|
|
3132
|
+
$ blink backend logs --minutes 60 Last hour
|
|
3133
|
+
$ blink backend logs proj_xxx --minutes 5
|
|
3134
|
+
$ blink backend logs --json
|
|
3135
|
+
`).action(async (projectArg, opts) => {
|
|
3136
|
+
requireToken();
|
|
3137
|
+
const projectId = requireProjectId(projectArg);
|
|
3138
|
+
const slug = opts.slug ?? "index";
|
|
3139
|
+
const since = new Date(Date.now() - parseInt(opts.minutes ?? "30") * 60 * 1e3).toISOString();
|
|
3140
|
+
const result = await withSpinner(
|
|
3141
|
+
"Fetching logs...",
|
|
3142
|
+
() => appRequest(`/api/v1/projects/${projectId}/functions/${slug}/logs?since=${since}`)
|
|
3143
|
+
);
|
|
3144
|
+
if (isJsonMode()) return printJson(result);
|
|
3145
|
+
const logs = result?.logs ?? result ?? [];
|
|
3146
|
+
if (!logs.length) {
|
|
3147
|
+
console.log(chalk18.dim("(no logs)"));
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
for (const log of logs) {
|
|
3151
|
+
const ts = log.timestamp ? chalk18.dim(new Date(log.timestamp).toLocaleTimeString()) + " " : "";
|
|
3152
|
+
const err = log.error ? chalk18.red(` ${log.error}`) : "";
|
|
3153
|
+
console.log(`${ts}${log.method ?? ""} ${log.path ?? ""} ${log.status_code ?? ""} ${log.latency_ms ?? ""}ms${err}`);
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
function registerBackendDelete(backend) {
|
|
3158
|
+
backend.command("delete [project_id]").description("Delete the backend worker").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
3159
|
+
Examples:
|
|
3160
|
+
$ blink backend delete --yes
|
|
3161
|
+
$ blink backend delete proj_xxx --yes
|
|
3162
|
+
`).action(async (projectArg, opts) => {
|
|
3163
|
+
requireToken();
|
|
3164
|
+
const projectId = requireProjectId(projectArg);
|
|
3165
|
+
const skipConfirm = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y");
|
|
3166
|
+
if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
|
|
3167
|
+
const { confirm } = await import("@clack/prompts");
|
|
3168
|
+
const ok = await confirm({ message: `Delete backend for project ${projectId}? This cannot be undone.` });
|
|
3169
|
+
if (!ok) {
|
|
3170
|
+
console.log("Cancelled.");
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
await withSpinner(
|
|
3175
|
+
"Deleting backend...",
|
|
3176
|
+
() => appRequest(`/api/project/${projectId}/backend`, { method: "DELETE" })
|
|
3177
|
+
);
|
|
3178
|
+
if (isJsonMode()) return printJson({ status: "ok", project_id: projectId });
|
|
3179
|
+
printSuccess(`Backend deleted for ${projectId}`);
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/commands/functions.ts
|
|
3184
|
+
init_project();
|
|
3185
|
+
import chalk19 from "chalk";
|
|
3186
|
+
var toDate = (ts) => new Date(typeof ts === "number" && ts < 1e11 ? ts * 1e3 : ts);
|
|
3187
|
+
function registerFunctionsCommands(program2) {
|
|
3188
|
+
const fns = program2.command("functions").description("Manage edge functions (legacy) for your project").addHelpText("after", `
|
|
3189
|
+
List, inspect, and manage edge functions deployed to your project.
|
|
3190
|
+
For the newer backend worker (CF Workers for Platforms), use "blink backend".
|
|
3191
|
+
|
|
3192
|
+
Examples:
|
|
3193
|
+
$ blink functions list List all functions
|
|
3194
|
+
$ blink functions get index Get function details
|
|
3195
|
+
$ blink functions logs index View function logs
|
|
3196
|
+
$ blink functions delete old-fn --yes Delete a function
|
|
3197
|
+
`);
|
|
3198
|
+
registerFnList(fns);
|
|
3199
|
+
registerFnGet(fns);
|
|
3200
|
+
registerFnDelete(fns);
|
|
3201
|
+
registerFnLogs(fns);
|
|
3202
|
+
}
|
|
3203
|
+
function registerFnList(fns) {
|
|
3204
|
+
fns.command("list [project_id]").description("List all edge functions").addHelpText("after", `
|
|
3205
|
+
Examples:
|
|
3206
|
+
$ blink functions list
|
|
3207
|
+
$ blink functions list proj_xxx
|
|
3208
|
+
$ blink functions list --json
|
|
3209
|
+
`).action(async (projectArg) => {
|
|
3210
|
+
requireToken();
|
|
3211
|
+
const projectId = requireProjectId(projectArg);
|
|
3212
|
+
const result = await withSpinner(
|
|
3213
|
+
"Loading functions...",
|
|
3214
|
+
() => appRequest(`/api/v1/projects/${projectId}/functions`)
|
|
3215
|
+
);
|
|
3216
|
+
const functions = result?.functions ?? result ?? [];
|
|
3217
|
+
if (isJsonMode()) return printJson(functions);
|
|
3218
|
+
if (!functions.length) {
|
|
3219
|
+
console.log(chalk19.dim("(no functions)"));
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
const table = createTable(["Slug", "Status", "Created"]);
|
|
3223
|
+
for (const fn of functions) {
|
|
3224
|
+
table.push([fn.slug, fn.status ?? "-", fn.created_at ? toDate(fn.created_at).toLocaleDateString() : "-"]);
|
|
3225
|
+
}
|
|
3226
|
+
console.log(table.toString());
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
function registerFnGet(fns) {
|
|
3230
|
+
fns.command("get <slug> [project_id]").description("Get details of a specific function").addHelpText("after", `
|
|
3231
|
+
Examples:
|
|
3232
|
+
$ blink functions get index
|
|
3233
|
+
$ blink functions get my-api proj_xxx
|
|
3234
|
+
$ blink functions get index --json
|
|
3235
|
+
`).action(async (slug, projectArg) => {
|
|
3236
|
+
requireToken();
|
|
3237
|
+
const projectId = requireProjectId(projectArg);
|
|
3238
|
+
const result = await withSpinner(
|
|
3239
|
+
`Loading ${slug}...`,
|
|
3240
|
+
() => appRequest(`/api/v1/projects/${projectId}/functions/${slug}`)
|
|
3241
|
+
);
|
|
3242
|
+
if (isJsonMode()) return printJson(result);
|
|
3243
|
+
const fn = result?.function ?? result;
|
|
3244
|
+
console.log(chalk19.bold("Slug: ") + (fn?.slug ?? slug));
|
|
3245
|
+
if (fn?.status) console.log(chalk19.bold("Status: ") + fn.status);
|
|
3246
|
+
if (fn?.url) console.log(chalk19.bold("URL: ") + chalk19.cyan(fn.url));
|
|
3247
|
+
if (fn?.created_at) console.log(chalk19.bold("Created: ") + toDate(fn.created_at).toLocaleString());
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
function registerFnDelete(fns) {
|
|
3251
|
+
fns.command("delete <slug> [project_id]").description("Delete an edge function").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
3252
|
+
Examples:
|
|
3253
|
+
$ blink functions delete old-fn --yes
|
|
3254
|
+
$ blink functions delete my-api proj_xxx --yes
|
|
3255
|
+
`).action(async (slug, projectArg, opts) => {
|
|
3256
|
+
requireToken();
|
|
3257
|
+
const projectId = requireProjectId(projectArg);
|
|
3258
|
+
const skipConfirm = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y");
|
|
3259
|
+
if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
|
|
3260
|
+
const { confirm } = await import("@clack/prompts");
|
|
3261
|
+
const ok = await confirm({ message: `Delete function "${slug}" from project ${projectId}?` });
|
|
3262
|
+
if (!ok) {
|
|
3263
|
+
console.log("Cancelled.");
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
await withSpinner(
|
|
3268
|
+
`Deleting ${slug}...`,
|
|
3269
|
+
() => appRequest(`/api/v1/projects/${projectId}/functions/${slug}`, { method: "DELETE" })
|
|
3270
|
+
);
|
|
3271
|
+
if (isJsonMode()) return printJson({ status: "ok", slug, project_id: projectId });
|
|
3272
|
+
printSuccess(`Deleted function ${slug}`);
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
function registerFnLogs(fns) {
|
|
3276
|
+
fns.command("logs <slug> [project_id]").description("View logs for a specific function").option("--minutes <n>", "Minutes of logs to fetch", "30").option("--limit <n>", "Max number of log entries").addHelpText("after", `
|
|
3277
|
+
Examples:
|
|
3278
|
+
$ blink functions logs index Last 30 min
|
|
3279
|
+
$ blink functions logs my-api --minutes 60 Last hour
|
|
3280
|
+
$ blink functions logs index proj_xxx --limit 100
|
|
3281
|
+
$ blink functions logs index --json
|
|
3282
|
+
`).action(async (slug, projectArg, opts) => {
|
|
3283
|
+
requireToken();
|
|
3284
|
+
const projectId = requireProjectId(projectArg);
|
|
3285
|
+
const since = new Date(Date.now() - parseInt(opts.minutes ?? "30") * 60 * 1e3).toISOString();
|
|
3286
|
+
const params = new URLSearchParams({ since });
|
|
3287
|
+
if (opts.limit) params.set("limit", opts.limit);
|
|
3288
|
+
const result = await withSpinner(
|
|
3289
|
+
"Fetching logs...",
|
|
3290
|
+
() => appRequest(`/api/v1/projects/${projectId}/functions/${slug}/logs?${params}`)
|
|
3291
|
+
);
|
|
3292
|
+
if (isJsonMode()) return printJson(result);
|
|
3293
|
+
const logs = result?.logs ?? result ?? [];
|
|
3294
|
+
if (!logs.length) {
|
|
3295
|
+
console.log(chalk19.dim("(no logs)"));
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
for (const log of logs) {
|
|
3299
|
+
const ts = log.timestamp ? chalk19.dim(new Date(log.timestamp).toLocaleTimeString()) + " " : "";
|
|
3300
|
+
const level = log.level ? chalk19.yellow(`[${log.level}] `) : "";
|
|
3301
|
+
console.log(`${ts}${level}${log.message ?? JSON.stringify(log)}`);
|
|
3302
|
+
}
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// src/commands/init.ts
|
|
3307
|
+
init_project();
|
|
3308
|
+
import { basename as basename3 } from "path";
|
|
3309
|
+
import chalk20 from "chalk";
|
|
3310
|
+
function registerInitCommands(program2) {
|
|
3311
|
+
program2.command("init").description("Initialize a new Blink project and link it to the current directory").option("--name <name>", "Project name (defaults to current directory name)").option("--from <project_id>", "Create a new project named after an existing one").addHelpText("after", `
|
|
3312
|
+
Creates a new Blink project and writes .blink/project.json in the current directory.
|
|
3313
|
+
After init, all commands work without specifying a project_id.
|
|
3314
|
+
|
|
3315
|
+
Examples:
|
|
3316
|
+
$ blink init Create project named after current dir
|
|
3317
|
+
$ blink init --name "My SaaS App" Create with custom name
|
|
3318
|
+
$ blink init --from proj_xxx Clone from existing project
|
|
3319
|
+
$ blink init --json Machine-readable output
|
|
3320
|
+
|
|
3321
|
+
After init:
|
|
3322
|
+
$ blink deploy ./dist --prod
|
|
3323
|
+
$ blink db query "SELECT * FROM users"
|
|
3324
|
+
$ blink env set DATABASE_URL postgres://...
|
|
3325
|
+
`).action(async (opts) => {
|
|
3326
|
+
requireToken();
|
|
3327
|
+
if (opts.from) {
|
|
3328
|
+
await initFromExisting(opts.from);
|
|
3329
|
+
} else {
|
|
3330
|
+
await initNew(opts.name);
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
async function initNew(nameOpt) {
|
|
3335
|
+
const name = nameOpt ?? basename3(process.cwd());
|
|
3336
|
+
const result = await withSpinner(
|
|
3337
|
+
`Creating project "${name}"...`,
|
|
3338
|
+
() => appRequest("/api/projects/create", { method: "POST", body: { prompt: name } })
|
|
3339
|
+
);
|
|
3340
|
+
const proj = result?.project ?? result;
|
|
3341
|
+
if (!proj?.id) {
|
|
3342
|
+
printError("Failed to create project \u2014 no ID returned");
|
|
3343
|
+
process.exit(1);
|
|
3344
|
+
}
|
|
3345
|
+
writeProjectConfig({ projectId: proj.id });
|
|
3346
|
+
if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name });
|
|
3347
|
+
printSuccess(`Project created and linked`);
|
|
3348
|
+
printKv("ID", proj.id);
|
|
3349
|
+
printKv("Name", proj.name ?? name);
|
|
3350
|
+
console.log(chalk20.dim("\n Run `blink deploy ./dist --prod` to deploy"));
|
|
3351
|
+
}
|
|
3352
|
+
async function initFromExisting(sourceId) {
|
|
3353
|
+
const source = await withSpinner(
|
|
3354
|
+
"Loading source project...",
|
|
3355
|
+
() => appRequest(`/api/projects/${sourceId}`)
|
|
3356
|
+
);
|
|
3357
|
+
const sourceName = source?.project?.name ?? source?.name ?? sourceId;
|
|
3358
|
+
const name = `${sourceName} (copy)`;
|
|
3359
|
+
const result = await withSpinner(
|
|
3360
|
+
`Creating project "${name}"...`,
|
|
3361
|
+
() => appRequest("/api/projects/create", { method: "POST", body: { prompt: name } })
|
|
3362
|
+
);
|
|
3363
|
+
const proj = result?.project ?? result;
|
|
3364
|
+
if (!proj?.id) {
|
|
3365
|
+
printError("Failed to create project \u2014 no ID returned");
|
|
3366
|
+
process.exit(1);
|
|
3367
|
+
}
|
|
3368
|
+
writeProjectConfig({ projectId: proj.id });
|
|
3369
|
+
if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name, from: sourceId });
|
|
3370
|
+
printSuccess(`Project created from ${sourceId} and linked`);
|
|
3371
|
+
printKv("ID", proj.id);
|
|
3372
|
+
printKv("Name", proj.name ?? name);
|
|
3373
|
+
printKv("From", sourceId);
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// src/commands/workspace.ts
|
|
3377
|
+
import chalk21 from "chalk";
|
|
3378
|
+
function printWorkspaceTable(workspaces) {
|
|
3379
|
+
const table = createTable(["ID", "Name", "Slug", "Tier", "Role"]);
|
|
3380
|
+
for (const w of workspaces) table.push([w.id, w.name, w.slug ?? "-", w.tier ?? "-", w.role ?? "-"]);
|
|
3381
|
+
console.log(table.toString());
|
|
3382
|
+
}
|
|
3383
|
+
function printMemberTable(members) {
|
|
3384
|
+
const table = createTable(["Name", "Email", "Role"]);
|
|
3385
|
+
for (const m of members) table.push([m.name ?? "-", m.email, m.role ?? "-"]);
|
|
3386
|
+
console.log(table.toString());
|
|
3387
|
+
}
|
|
3388
|
+
function registerWorkspaceCommands(program2) {
|
|
3389
|
+
const workspace = program2.command("workspace").description("Manage workspaces, members, and invitations").addHelpText("after", `
|
|
3390
|
+
Examples:
|
|
3391
|
+
$ blink workspace list
|
|
3392
|
+
$ blink workspace create "My Team"
|
|
3393
|
+
$ blink workspace switch wsp_xxx
|
|
3394
|
+
$ blink workspace members wsp_xxx
|
|
3395
|
+
$ blink workspace invite user@example.com wsp_xxx --role admin
|
|
3396
|
+
`);
|
|
3397
|
+
workspace.command("list").description("List all workspaces you belong to").addHelpText("after", `
|
|
3398
|
+
Examples:
|
|
3399
|
+
$ blink workspace list
|
|
3400
|
+
$ blink workspace list --json
|
|
3401
|
+
`).action(async () => {
|
|
3402
|
+
requireToken();
|
|
3403
|
+
const result = await withSpinner("Loading workspaces...", () => appRequest("/api/workspaces"));
|
|
3404
|
+
const workspaces = result?.workspaces ?? result ?? [];
|
|
3405
|
+
if (isJsonMode()) return printJson(workspaces);
|
|
3406
|
+
if (!workspaces.length) return console.log(chalk21.dim("No workspaces found."));
|
|
3407
|
+
printWorkspaceTable(workspaces);
|
|
3408
|
+
});
|
|
3409
|
+
workspace.command("create <name>").description("Create a new workspace").addHelpText("after", `
|
|
3410
|
+
Examples:
|
|
3411
|
+
$ blink workspace create "My Team"
|
|
3412
|
+
$ blink workspace create "Acme Corp" --json
|
|
3413
|
+
`).action(async (name) => {
|
|
3414
|
+
requireToken();
|
|
3415
|
+
const result = await withSpinner(
|
|
3416
|
+
`Creating "${name}"...`,
|
|
3417
|
+
() => appRequest("/api/workspaces", { method: "POST", body: { name } })
|
|
3418
|
+
);
|
|
3419
|
+
if (isJsonMode()) return printJson(result);
|
|
3420
|
+
const ws = result?.workspace ?? result;
|
|
3421
|
+
printSuccess(`Created workspace: ${ws.name} (${ws.id})`);
|
|
3422
|
+
});
|
|
3423
|
+
workspace.command("switch <workspace_id>").description("Switch your active workspace").addHelpText("after", `
|
|
3424
|
+
Examples:
|
|
3425
|
+
$ blink workspace switch wsp_xxx
|
|
3426
|
+
`).action(async (workspaceId) => {
|
|
3427
|
+
requireToken();
|
|
3428
|
+
await withSpinner(
|
|
3429
|
+
"Switching workspace...",
|
|
3430
|
+
() => appRequest("/api/workspaces/switch", { method: "POST", body: { workspace_id: workspaceId } })
|
|
3431
|
+
);
|
|
3432
|
+
if (isJsonMode()) return printJson({ status: "ok", workspace_id: workspaceId });
|
|
3433
|
+
printSuccess(`Switched to workspace ${workspaceId}`);
|
|
3434
|
+
});
|
|
3435
|
+
workspace.command("members [workspace_id]").description("List members of a workspace").option("--workspace <id>", "Workspace ID").addHelpText("after", `
|
|
3436
|
+
Examples:
|
|
3437
|
+
$ blink workspace members wsp_xxx
|
|
3438
|
+
$ blink workspace members --workspace wsp_xxx
|
|
3439
|
+
$ blink workspace members wsp_xxx --json
|
|
3440
|
+
`).action(async (workspaceIdArg, opts) => {
|
|
3441
|
+
requireToken();
|
|
3442
|
+
const workspaceId = workspaceIdArg ?? opts.workspace;
|
|
3443
|
+
if (!workspaceId) {
|
|
3444
|
+
printError("Workspace ID required", "blink workspace members wsp_xxx");
|
|
3445
|
+
process.exit(1);
|
|
3446
|
+
}
|
|
3447
|
+
const result = await withSpinner(
|
|
3448
|
+
"Loading members...",
|
|
3449
|
+
() => appRequest(`/api/workspaces/${workspaceId}/members`)
|
|
3450
|
+
);
|
|
3451
|
+
const members = result?.members ?? result ?? [];
|
|
3452
|
+
if (isJsonMode()) return printJson(members);
|
|
3453
|
+
if (!members.length) return console.log(chalk21.dim("No members found."));
|
|
3454
|
+
printMemberTable(members);
|
|
3455
|
+
});
|
|
3456
|
+
workspace.command("invite <email> [workspace_id]").description("Invite a member to a workspace").option("--workspace <id>", "Workspace ID").option("--role <role>", "Role: admin, member, viewer", "member").addHelpText("after", `
|
|
3457
|
+
Examples:
|
|
3458
|
+
$ blink workspace invite user@example.com wsp_xxx
|
|
3459
|
+
$ blink workspace invite user@example.com --workspace wsp_xxx --role admin
|
|
3460
|
+
$ blink workspace invite user@example.com wsp_xxx --role viewer
|
|
3461
|
+
`).action(async (email, workspaceIdArg, opts) => {
|
|
3462
|
+
requireToken();
|
|
3463
|
+
const workspaceId = workspaceIdArg ?? opts.workspace;
|
|
3464
|
+
if (!workspaceId) {
|
|
3465
|
+
printError("Workspace ID required", "blink workspace invite user@example.com wsp_xxx");
|
|
3466
|
+
process.exit(1);
|
|
3467
|
+
}
|
|
3468
|
+
const result = await withSpinner(
|
|
3469
|
+
`Inviting ${email}...`,
|
|
3470
|
+
() => appRequest(`/api/workspaces/${workspaceId}/invites`, {
|
|
3471
|
+
method: "POST",
|
|
3472
|
+
body: { emails: [email], role: opts.role }
|
|
3473
|
+
})
|
|
3474
|
+
);
|
|
3475
|
+
if (isJsonMode()) return printJson(result);
|
|
3476
|
+
printSuccess(`Invited ${email} as ${opts.role} to ${workspaceId}`);
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
// src/commands/versions.ts
|
|
3481
|
+
init_project();
|
|
3482
|
+
import chalk22 from "chalk";
|
|
3483
|
+
function printVersionTable(versions) {
|
|
3484
|
+
const table = createTable(["ID", "Description", "Created"]);
|
|
3485
|
+
for (const v of versions) {
|
|
3486
|
+
table.push([v.id, v.description ?? v.message ?? "-", new Date(v.created_at).toLocaleString()]);
|
|
3487
|
+
}
|
|
3488
|
+
console.log(table.toString());
|
|
3489
|
+
}
|
|
3490
|
+
function registerVersionCommands(program2) {
|
|
3491
|
+
const versions = program2.command("versions").description("Manage project version snapshots").addHelpText("after", `
|
|
3492
|
+
Examples:
|
|
3493
|
+
$ blink versions list
|
|
3494
|
+
$ blink versions save --message "before refactor"
|
|
3495
|
+
$ blink versions restore ver_xxx
|
|
3496
|
+
`);
|
|
3497
|
+
versions.command("list [project_id]").description("List saved versions for a project").addHelpText("after", `
|
|
3498
|
+
Examples:
|
|
3499
|
+
$ blink versions list
|
|
3500
|
+
$ blink versions list proj_xxx
|
|
3501
|
+
$ blink versions list --json
|
|
3502
|
+
`).action(async (projectIdArg) => {
|
|
3503
|
+
requireToken();
|
|
3504
|
+
const projectId = requireProjectId(projectIdArg);
|
|
3505
|
+
const result = await withSpinner(
|
|
3506
|
+
"Loading versions...",
|
|
3507
|
+
() => appRequest(`/api/versions?projectId=${projectId}`)
|
|
3508
|
+
);
|
|
3509
|
+
const versionsList = result?.versions ?? result ?? [];
|
|
3510
|
+
if (isJsonMode()) return printJson(versionsList);
|
|
3511
|
+
if (!versionsList.length) return console.log(chalk22.dim("No versions saved yet."));
|
|
3512
|
+
printVersionTable(versionsList);
|
|
3513
|
+
});
|
|
3514
|
+
versions.command("save [project_id]").description("Save a version snapshot of the current project state").requiredOption("--message <msg>", "Version description (required)").addHelpText("after", `
|
|
3515
|
+
Examples:
|
|
3516
|
+
$ blink versions save --message "stable v1"
|
|
3517
|
+
$ blink versions save proj_xxx --message "pre-deploy"
|
|
3518
|
+
$ blink versions save --json
|
|
3519
|
+
`).action(async (projectIdArg, opts) => {
|
|
3520
|
+
requireToken();
|
|
3521
|
+
const projectId = requireProjectId(projectIdArg);
|
|
3522
|
+
const result = await withSpinner(
|
|
3523
|
+
"Saving version...",
|
|
3524
|
+
() => appRequest("/api/save-version", {
|
|
3525
|
+
method: "POST",
|
|
3526
|
+
body: { projectId, description: opts.message }
|
|
3527
|
+
})
|
|
3528
|
+
);
|
|
3529
|
+
if (isJsonMode()) return printJson(result);
|
|
3530
|
+
const ver = result?.version ?? result;
|
|
3531
|
+
printSuccess(`Version saved: ${ver?.id ?? "ok"}`);
|
|
3532
|
+
});
|
|
3533
|
+
versions.command("restore <version_id> [project_id]").description("Restore a project to a saved version").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
3534
|
+
Examples:
|
|
3535
|
+
$ blink versions restore ver_xxx
|
|
3536
|
+
$ blink versions restore ver_xxx proj_xxx --yes
|
|
3537
|
+
`).action(async (versionId, projectIdArg, opts) => {
|
|
3538
|
+
requireToken();
|
|
3539
|
+
const projectId = requireProjectId(projectIdArg);
|
|
3540
|
+
const skipConfirm = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y");
|
|
3541
|
+
if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
|
|
3542
|
+
const { confirm } = await import("@clack/prompts");
|
|
3543
|
+
const ok = await confirm({ message: `Restore project ${projectId} to version ${versionId}?` });
|
|
3544
|
+
if (!ok) {
|
|
3545
|
+
console.log("Cancelled.");
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
const result = await withSpinner(
|
|
3550
|
+
"Restoring version...",
|
|
3551
|
+
() => appRequest("/api/versions/restore", {
|
|
3552
|
+
method: "POST",
|
|
3553
|
+
body: { projectId, identifier: versionId }
|
|
3554
|
+
})
|
|
3555
|
+
);
|
|
3556
|
+
if (isJsonMode()) return printJson(result);
|
|
3557
|
+
printSuccess(`Restored to version ${versionId}`);
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
// src/commands/billing.ts
|
|
3562
|
+
import chalk23 from "chalk";
|
|
3563
|
+
function printUsage(data) {
|
|
3564
|
+
console.log();
|
|
3565
|
+
printKv("Total", `${data.total ?? 0} credits used`);
|
|
3566
|
+
const breakdown = data.breakdown ?? [];
|
|
3567
|
+
if (breakdown.length) {
|
|
3568
|
+
console.log();
|
|
3569
|
+
console.log(chalk23.bold("Breakdown:"));
|
|
3570
|
+
for (const item of breakdown) printKv(` ${item.category}`, `${item.amount}`);
|
|
3571
|
+
}
|
|
3572
|
+
console.log();
|
|
3573
|
+
}
|
|
3574
|
+
function registerBillingCommands(program2) {
|
|
3575
|
+
program2.command("credits").description("Check your credit balance and tier").addHelpText("after", `
|
|
3576
|
+
Examples:
|
|
3577
|
+
$ blink credits
|
|
3578
|
+
$ blink credits --json
|
|
3579
|
+
`).action(async () => {
|
|
3580
|
+
requireToken();
|
|
3581
|
+
const result = await withSpinner(
|
|
3582
|
+
"Loading usage...",
|
|
3583
|
+
() => appRequest("/api/usage/summary?period=month")
|
|
3584
|
+
);
|
|
3585
|
+
if (isJsonMode()) return printJson(result);
|
|
3586
|
+
const data = result ?? {};
|
|
3587
|
+
console.log();
|
|
3588
|
+
printKv("Used", `${data.total ?? 0} credits this month`);
|
|
3589
|
+
console.log();
|
|
3590
|
+
});
|
|
3591
|
+
program2.command("usage").description("Show usage summary for the current billing period").option("--period <granularity>", "Period granularity: hour, day, week, month", "day").addHelpText("after", `
|
|
3592
|
+
Examples:
|
|
3593
|
+
$ blink usage
|
|
3594
|
+
$ blink usage --period month
|
|
3595
|
+
$ blink usage --json
|
|
3596
|
+
`).action(async (opts) => {
|
|
3597
|
+
requireToken();
|
|
3598
|
+
const result = await withSpinner(
|
|
3599
|
+
"Loading usage...",
|
|
3600
|
+
() => appRequest(`/api/usage/summary?period=${opts.period}`)
|
|
3601
|
+
);
|
|
3602
|
+
if (isJsonMode()) return printJson(result);
|
|
3603
|
+
printUsage(result);
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
// src/commands/tokens.ts
|
|
3608
|
+
import chalk24 from "chalk";
|
|
3609
|
+
function printTokenTable(tokens) {
|
|
3610
|
+
const table = createTable(["ID", "Name", "Prefix", "Created", "Last Used"]);
|
|
3611
|
+
for (const t of tokens) {
|
|
3612
|
+
table.push([
|
|
3613
|
+
t.id,
|
|
3614
|
+
t.name ?? "-",
|
|
3615
|
+
t.key_prefix ?? "-",
|
|
3616
|
+
t.created_at ? new Date(t.created_at).toLocaleDateString() : "-",
|
|
3617
|
+
t.last_used_at ? new Date(t.last_used_at).toLocaleDateString() : "never"
|
|
3618
|
+
]);
|
|
3619
|
+
}
|
|
3620
|
+
console.log(table.toString());
|
|
3621
|
+
}
|
|
3622
|
+
function printNewToken(token) {
|
|
3623
|
+
console.log();
|
|
3624
|
+
printSuccess(`Token created: ${token.name}`);
|
|
3625
|
+
console.log();
|
|
3626
|
+
console.log(chalk24.bold(" Token: ") + chalk24.green(token.key));
|
|
3627
|
+
console.log();
|
|
3628
|
+
console.log(chalk24.yellow(" \u26A0 Save this token now \u2014 it will not be shown again!"));
|
|
3629
|
+
console.log(chalk24.dim(" Use as: export BLINK_API_KEY=" + token.key));
|
|
3630
|
+
console.log();
|
|
3631
|
+
}
|
|
3632
|
+
function registerTokenCommands(program2) {
|
|
3633
|
+
const tokens = program2.command("tokens").description("Manage personal access tokens (API keys)").addHelpText("after", `
|
|
3634
|
+
Examples:
|
|
3635
|
+
$ blink tokens list
|
|
3636
|
+
$ blink tokens create --name "CI deploy key"
|
|
3637
|
+
$ blink tokens revoke tok_xxx --yes
|
|
3638
|
+
`);
|
|
3639
|
+
tokens.command("list").description("List all personal access tokens").addHelpText("after", `
|
|
3640
|
+
Examples:
|
|
3641
|
+
$ blink tokens list
|
|
3642
|
+
$ blink tokens list --json
|
|
3643
|
+
`).action(async () => {
|
|
3644
|
+
requireToken();
|
|
3645
|
+
const result = await withSpinner("Loading tokens...", () => appRequest("/api/tokens"));
|
|
3646
|
+
const tokensList = result?.tokens ?? result ?? [];
|
|
3647
|
+
if (isJsonMode()) return printJson(tokensList);
|
|
3648
|
+
if (!tokensList.length) return console.log(chalk24.dim('No tokens found. Create one with `blink tokens create --name "my key"`.'));
|
|
3649
|
+
printTokenTable(tokensList);
|
|
3650
|
+
});
|
|
3651
|
+
tokens.command("create").description("Create a new personal access token").requiredOption("--name <name>", 'Name for the token (e.g. "CI deploy key")').addHelpText("after", `
|
|
3652
|
+
Examples:
|
|
3653
|
+
$ blink tokens create --name "CI deploy key"
|
|
3654
|
+
$ blink tokens create --name "staging" --json
|
|
3655
|
+
`).action(async (opts) => {
|
|
3656
|
+
requireToken();
|
|
3657
|
+
const result = await withSpinner(
|
|
3658
|
+
"Creating token...",
|
|
3659
|
+
() => appRequest("/api/tokens", { method: "POST", body: { name: opts.name } })
|
|
3660
|
+
);
|
|
3661
|
+
if (isJsonMode()) return printJson(result);
|
|
3662
|
+
const token = result?.token ?? result;
|
|
3663
|
+
printNewToken(token);
|
|
3664
|
+
});
|
|
3665
|
+
tokens.command("revoke <token_id>").description("Revoke (delete) a personal access token").option("--yes", "Skip confirmation").addHelpText("after", `
|
|
3666
|
+
Examples:
|
|
3667
|
+
$ blink tokens revoke tok_xxx
|
|
3668
|
+
$ blink tokens revoke tok_xxx --yes
|
|
3669
|
+
`).action(async (tokenId, opts) => {
|
|
3670
|
+
requireToken();
|
|
3671
|
+
const skipConfirm = opts.yes || process.argv.includes("--yes") || process.argv.includes("-y");
|
|
3672
|
+
if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
|
|
3673
|
+
const { confirm } = await import("@clack/prompts");
|
|
3674
|
+
const ok = await confirm({ message: `Revoke token ${tokenId}? This cannot be undone.` });
|
|
3675
|
+
if (!ok) {
|
|
3676
|
+
console.log("Cancelled.");
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
await withSpinner(
|
|
3681
|
+
"Revoking token...",
|
|
3682
|
+
() => appRequest(`/api/tokens/${tokenId}`, { method: "DELETE" })
|
|
3683
|
+
);
|
|
3684
|
+
if (isJsonMode()) return printJson({ status: "ok", token_id: tokenId });
|
|
3685
|
+
printSuccess(`Revoked token ${tokenId}`);
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
// src/cli.ts
|
|
3690
|
+
var require2 = createRequire(import.meta.url);
|
|
3691
|
+
var pkg = require2("../package.json");
|
|
3692
|
+
var program = new Command();
|
|
3693
|
+
program.name("blink").description("Blink platform CLI \u2014 build, deploy, and manage AI-powered apps").version(pkg.version).option("--token <key>", "Override API token for this command").option("--json", "Machine-readable JSON output (no colors, great for scripting)").option("-y, --yes", "Skip confirmation prompts").option("--debug", "Verbose request logging").option("--profile <name>", "Use named config profile from ~/.config/blink/config.toml").addHelpText("after", `
|
|
3694
|
+
Auth:
|
|
3695
|
+
API key lives at blink.new \u2192 Settings \u2192 API Keys (starts with blnk_ak_)
|
|
3696
|
+
$ blink login --interactive Save your API key to ~/.config/blink/config.toml
|
|
3697
|
+
$ export BLINK_API_KEY=blnk_ak_... Or set env var directly (used in CI/agents)
|
|
3698
|
+
|
|
3699
|
+
Quick Start:
|
|
3700
|
+
$ blink login --interactive Authenticate
|
|
3701
|
+
$ blink link Link current dir to a project (interactive picker)
|
|
3702
|
+
$ npm run build && blink deploy ./dist --prod Build then deploy to production
|
|
3703
|
+
|
|
3704
|
+
Deploy:
|
|
3705
|
+
$ blink deploy ./dist --prod Deploy build output to production
|
|
3706
|
+
$ blink deploy ./dist Preview deployment (safe, non-destructive)
|
|
3707
|
+
$ blink deployments List past deployments
|
|
3708
|
+
$ blink rollback Rollback production to previous version
|
|
3709
|
+
|
|
3710
|
+
Database (requires linked project or project_id):
|
|
3711
|
+
$ blink db query "SELECT * FROM users LIMIT 10"
|
|
3712
|
+
$ blink db query proj_xxx "SELECT count(*) FROM orders"
|
|
3713
|
+
$ blink db exec schema.sql Run a SQL file
|
|
3714
|
+
$ blink db list Show all tables
|
|
3715
|
+
$ blink db list users Show rows in the users table
|
|
3716
|
+
|
|
3717
|
+
Storage:
|
|
3718
|
+
$ blink storage upload ./photo.jpg
|
|
3719
|
+
$ blink storage list
|
|
3720
|
+
$ blink storage url images/photo.jpg
|
|
3721
|
+
$ blink storage download images/photo.jpg ./local.jpg
|
|
3722
|
+
|
|
3723
|
+
AI (no project needed \u2014 workspace-scoped):
|
|
3724
|
+
$ blink ai image "a futuristic city at sunset"
|
|
3725
|
+
$ blink ai text "Summarize this: ..." --model anthropic/claude-sonnet-4.5
|
|
3726
|
+
$ blink ai video "ocean waves at sunset" --duration 10s
|
|
3727
|
+
$ blink ai speech "Hello world" --voice nova --output hello.mp3
|
|
3728
|
+
$ blink ai transcribe ./meeting.mp3
|
|
3729
|
+
|
|
3730
|
+
Web & Data:
|
|
3731
|
+
$ blink search "latest AI news" --count 10
|
|
3732
|
+
$ blink fetch https://api.github.com/users/octocat
|
|
3733
|
+
$ blink fetch https://api.example.com --method POST --body '{"key":"val"}'
|
|
3734
|
+
$ blink scrape https://example.com --text Clean text (no HTML)
|
|
3735
|
+
$ blink scrape https://example.com --extract "prices" AI-extract specific data
|
|
3736
|
+
|
|
3737
|
+
Realtime / RAG / Notify:
|
|
3738
|
+
$ blink realtime publish updates '{"type":"refresh"}'
|
|
3739
|
+
$ blink rag search "how does billing work" --ai
|
|
3740
|
+
$ blink notify email user@example.com "Subject" "Body"
|
|
3741
|
+
|
|
3742
|
+
Phone Numbers (10 credits/month per number):
|
|
3743
|
+
$ blink phone list List all workspace phone numbers
|
|
3744
|
+
$ blink phone buy --label Sales Buy a new number (US, UK, CA, AU)
|
|
3745
|
+
$ blink phone label <id> Sales Update label
|
|
3746
|
+
$ blink phone release <id> Release a number
|
|
3747
|
+
|
|
3748
|
+
Connectors (38 OAuth providers \u2014 GitHub, Notion, Slack, Stripe, Shopify, Jira, Linear, and more):
|
|
3749
|
+
$ blink connector providers List all 38 providers
|
|
3750
|
+
$ blink connector status Show all connected accounts
|
|
3751
|
+
$ blink connector status github Check a specific provider
|
|
3752
|
+
$ blink connector exec github /user/repos GET Call any REST endpoint
|
|
3753
|
+
$ blink connector exec notion /search POST '{"query":"notes"}' Notion search
|
|
3754
|
+
$ blink connector exec slack /chat.postMessage POST '{"channel":"C123","text":"hi"}'
|
|
3755
|
+
$ blink connector exec stripe /customers GET '{"limit":5}'
|
|
3756
|
+
$ blink connector exec jira /search GET '{"jql":"assignee=currentUser()"}'
|
|
3757
|
+
$ blink connector exec linear '{ viewer { id name } }' POST GraphQL (Linear)
|
|
3758
|
+
Connect accounts at: blink.new/settings?tab=connectors
|
|
3759
|
+
|
|
3760
|
+
LinkedIn (dedicated commands for the LinkedIn connector):
|
|
3761
|
+
$ blink linkedin me Show your LinkedIn profile
|
|
3762
|
+
$ blink linkedin posts List your 10 most recent posts
|
|
3763
|
+
$ blink linkedin post "Hello LinkedIn!" Publish a text post
|
|
3764
|
+
$ blink linkedin comments "urn:li:ugcPost:123" Read comments on a post
|
|
3765
|
+
$ blink linkedin comment "urn:li:ugcPost:123" "Nice!" Add a comment
|
|
3766
|
+
$ blink linkedin like "urn:li:ugcPost:123" Like a post
|
|
2379
3767
|
$ blink linkedin unlike "urn:li:ugcPost:123" Unlike a post
|
|
2380
3768
|
Link LinkedIn at: blink.new/claw (Agent Integrations tab)
|
|
2381
3769
|
|
|
3770
|
+
Environment Variables (project secrets):
|
|
3771
|
+
$ blink env list List env var names
|
|
3772
|
+
$ blink env set STRIPE_KEY sk_live_xxx Set an env var
|
|
3773
|
+
$ blink env delete OLD_KEY Remove an env var
|
|
3774
|
+
$ blink env push .env Bulk import from .env file
|
|
3775
|
+
$ blink env pull > .env.local Export as KEY=VALUE
|
|
3776
|
+
|
|
3777
|
+
Backend (Hono server on CF Workers):
|
|
3778
|
+
$ blink backend deploy ./backend Deploy backend/ to Cloudflare Workers
|
|
3779
|
+
$ blink backend status Check backend status + URL
|
|
3780
|
+
$ blink backend logs Tail backend request logs
|
|
3781
|
+
$ blink backend delete --yes Remove backend
|
|
3782
|
+
|
|
3783
|
+
Auth Config (Blink Auth providers):
|
|
3784
|
+
$ blink auth-config get Show auth configuration
|
|
3785
|
+
$ blink auth-config set --provider google --enabled true
|
|
3786
|
+
$ blink auth-config byoc-set --provider google --client-id X --client-secret Y
|
|
3787
|
+
|
|
3788
|
+
Domains:
|
|
3789
|
+
$ blink domains list List custom domains
|
|
3790
|
+
$ blink domains add myapp.com Add a custom domain
|
|
3791
|
+
$ blink domains verify <domain_id> Verify DNS
|
|
3792
|
+
$ blink domains search "myapp" Search available domains
|
|
3793
|
+
$ blink domains purchase myapp.com Buy a domain
|
|
3794
|
+
|
|
3795
|
+
Hosting:
|
|
3796
|
+
$ blink hosting status Check hosting state + URLs
|
|
3797
|
+
$ blink hosting activate Activate production hosting
|
|
3798
|
+
$ blink hosting deactivate --yes Deactivate hosting
|
|
3799
|
+
|
|
3800
|
+
Security & CORS:
|
|
3801
|
+
$ blink security get Show per-module auth policy
|
|
3802
|
+
$ blink security set --module db --require-auth true
|
|
3803
|
+
$ blink cors get Show allowed CORS origins
|
|
3804
|
+
$ blink cors set --origins https://example.com
|
|
3805
|
+
|
|
3806
|
+
Functions (legacy edge functions):
|
|
3807
|
+
$ blink functions list List edge functions
|
|
3808
|
+
$ blink functions logs index View function logs
|
|
3809
|
+
$ blink functions delete old-fn --yes Delete a function
|
|
3810
|
+
|
|
3811
|
+
Versions:
|
|
3812
|
+
$ blink versions list List saved versions
|
|
3813
|
+
$ blink versions save --message "v1.0" Save a snapshot
|
|
3814
|
+
$ blink versions restore ver_xxx --yes Restore to a version
|
|
3815
|
+
|
|
3816
|
+
Workspace:
|
|
3817
|
+
$ blink workspace list List workspaces
|
|
3818
|
+
$ blink workspace create "My Team" Create workspace
|
|
3819
|
+
$ blink workspace members List members + roles
|
|
3820
|
+
|
|
3821
|
+
Billing:
|
|
3822
|
+
$ blink credits Check credit balance
|
|
3823
|
+
$ blink usage Usage breakdown
|
|
3824
|
+
|
|
3825
|
+
Tokens (Personal Access Tokens):
|
|
3826
|
+
$ blink tokens list List PATs
|
|
3827
|
+
$ blink tokens create --name "CI" Create PAT (shown once!)
|
|
3828
|
+
$ blink tokens revoke <id> --yes Revoke a PAT
|
|
3829
|
+
|
|
3830
|
+
Init:
|
|
3831
|
+
$ blink init Create project + link to current dir
|
|
3832
|
+
$ blink init --name "My App" Create with custom name
|
|
3833
|
+
|
|
2382
3834
|
Agents (Claw \u2014 zero config on Fly machines, BLINK_AGENT_ID is already set):
|
|
2383
3835
|
$ blink agent list List all agents in workspace
|
|
2384
3836
|
$ blink agent status Show current agent details
|
|
@@ -2388,7 +3840,6 @@ Secrets (encrypted agent vault \u2014 values never shown):
|
|
|
2388
3840
|
$ blink secrets list List secret key names
|
|
2389
3841
|
$ blink secrets set GITHUB_TOKEN ghp_xxx Add/update a secret
|
|
2390
3842
|
$ blink secrets delete OLD_KEY Remove a secret
|
|
2391
|
-
$ blink secrets set --agent clw_other KEY v Set secret on another agent (agent manager pattern)
|
|
2392
3843
|
|
|
2393
3844
|
Tip \u2014 check full context (agent + project + auth):
|
|
2394
3845
|
$ blink status
|
|
@@ -2420,6 +3871,18 @@ registerConnectorCommands(program);
|
|
|
2420
3871
|
registerLinkedInCommands(program);
|
|
2421
3872
|
registerPhoneCommands(program);
|
|
2422
3873
|
registerSmsCommands(program);
|
|
3874
|
+
registerAuthConfigCommands(program);
|
|
3875
|
+
registerDomainsCommands(program);
|
|
3876
|
+
registerHostingCommands(program);
|
|
3877
|
+
registerSecurityCommands(program);
|
|
3878
|
+
registerEnvCommands(program);
|
|
3879
|
+
registerBackendCommands(program);
|
|
3880
|
+
registerFunctionsCommands(program);
|
|
3881
|
+
registerInitCommands(program);
|
|
3882
|
+
registerWorkspaceCommands(program);
|
|
3883
|
+
registerVersionCommands(program);
|
|
3884
|
+
registerBillingCommands(program);
|
|
3885
|
+
registerTokenCommands(program);
|
|
2423
3886
|
program.command("use <project_id>").description("Set active project for this shell session (alternative to blink link)").option("--export", "Output a shell export statement \u2014 use with eval to actually set it").addHelpText("after", `
|
|
2424
3887
|
Examples:
|
|
2425
3888
|
$ blink use proj_xxx Shows the export command to run
|
|
@@ -2435,10 +3898,10 @@ After setting:
|
|
|
2435
3898
|
process.stdout.write(`export BLINK_ACTIVE_PROJECT=${projectId}
|
|
2436
3899
|
`);
|
|
2437
3900
|
} else {
|
|
2438
|
-
const { default:
|
|
2439
|
-
console.log(
|
|
2440
|
-
console.log(
|
|
2441
|
-
console.log(
|
|
3901
|
+
const { default: chalk25 } = await import("chalk");
|
|
3902
|
+
console.log(chalk25.bold("Active project: ") + projectId);
|
|
3903
|
+
console.log(chalk25.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
|
|
3904
|
+
console.log(chalk25.dim(`Or: eval $(blink use ${projectId} --export)`));
|
|
2442
3905
|
}
|
|
2443
3906
|
});
|
|
2444
3907
|
program.action(async () => {
|