doable-cli 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +606 -542
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import os from 'os';
|
|
|
4
4
|
import { readFile } from 'fs/promises';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import path7 from 'path';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import chalk5 from 'chalk';
|
|
8
|
+
import ora10 from 'ora';
|
|
9
9
|
import open from 'open';
|
|
10
10
|
import Conf from 'conf';
|
|
11
11
|
import fs6 from 'fs';
|
|
@@ -88,6 +88,9 @@ function getToken() {
|
|
|
88
88
|
function setToken(token) {
|
|
89
89
|
config.set("token", token);
|
|
90
90
|
}
|
|
91
|
+
function clearToken() {
|
|
92
|
+
config.delete("token");
|
|
93
|
+
}
|
|
91
94
|
function getApiUrl() {
|
|
92
95
|
return config.get("apiUrl") || "http://localhost:3200";
|
|
93
96
|
}
|
|
@@ -242,6 +245,14 @@ function devicePoll(deviceCode) {
|
|
|
242
245
|
device_code: deviceCode
|
|
243
246
|
});
|
|
244
247
|
}
|
|
248
|
+
async function authLogout() {
|
|
249
|
+
try {
|
|
250
|
+
await request("POST", "/v1/auth/logout");
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
245
256
|
function listProjects() {
|
|
246
257
|
return request("GET", "/v1/projects");
|
|
247
258
|
}
|
|
@@ -417,20 +428,20 @@ function listAddonTables(addonId) {
|
|
|
417
428
|
// src/commands/login.ts
|
|
418
429
|
function registerLoginCommand(program2) {
|
|
419
430
|
program2.command("login").description("Authenticate with Doable").action(async () => {
|
|
420
|
-
const spinner =
|
|
431
|
+
const spinner = ora10("Starting login flow...").start();
|
|
421
432
|
let startResp;
|
|
422
433
|
try {
|
|
423
434
|
startResp = await deviceStart();
|
|
424
435
|
} catch (err) {
|
|
425
436
|
if (err instanceof ApiError && err.status === 0) {
|
|
426
437
|
spinner.fail("Cannot connect to Doable API.");
|
|
427
|
-
console.error(
|
|
438
|
+
console.error(chalk5.red(err.message));
|
|
428
439
|
} else {
|
|
429
440
|
spinner.fail("Failed to start login flow.");
|
|
430
441
|
if (err instanceof ApiError || err instanceof Error) {
|
|
431
|
-
console.error(
|
|
442
|
+
console.error(chalk5.red(err.message));
|
|
432
443
|
} else {
|
|
433
|
-
console.error(
|
|
444
|
+
console.error(chalk5.red(String(err)));
|
|
434
445
|
}
|
|
435
446
|
}
|
|
436
447
|
process.exit(1);
|
|
@@ -438,21 +449,32 @@ function registerLoginCommand(program2) {
|
|
|
438
449
|
spinner.stop();
|
|
439
450
|
const { device_code, user_code, verification_url, expires_in, poll_interval } = startResp;
|
|
440
451
|
console.log();
|
|
441
|
-
console.log(
|
|
452
|
+
console.log(chalk5.bold(" Login to Doable"));
|
|
442
453
|
console.log();
|
|
443
|
-
console.log(` Your code: ${
|
|
454
|
+
console.log(` Your code: ${chalk5.cyan.bold(user_code)}`);
|
|
444
455
|
console.log();
|
|
445
456
|
console.log(` Open this URL to authenticate:`);
|
|
446
|
-
console.log(` ${
|
|
457
|
+
console.log(` ${chalk5.underline(verification_url)}`);
|
|
447
458
|
console.log();
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
459
|
+
if (process.stdin.isTTY && !process.env.DOABLE_NO_BROWSER) {
|
|
460
|
+
process.stdout.write(
|
|
461
|
+
chalk5.dim(" Press Enter to open your browser, or Ctrl+C to abort...")
|
|
462
|
+
);
|
|
463
|
+
await waitForEnter();
|
|
464
|
+
process.stdout.write("\r\x1B[2K");
|
|
465
|
+
}
|
|
466
|
+
if (process.env.DOABLE_NO_BROWSER) {
|
|
467
|
+
console.log(chalk5.dim(" (Open the URL above in your browser)"));
|
|
468
|
+
} else {
|
|
469
|
+
try {
|
|
470
|
+
await open(verification_url);
|
|
471
|
+
console.log(chalk5.dim(" (Browser opened)"));
|
|
472
|
+
} catch {
|
|
473
|
+
console.log(chalk5.dim(" (Open the URL above in your browser)"));
|
|
474
|
+
}
|
|
453
475
|
}
|
|
454
476
|
console.log();
|
|
455
|
-
const pollSpinner =
|
|
477
|
+
const pollSpinner = ora10("Waiting for authentication...").start();
|
|
456
478
|
const pollIntervalMs = (poll_interval || 2) * 1e3;
|
|
457
479
|
const deadline = Date.now() + expires_in * 1e3;
|
|
458
480
|
while (Date.now() < deadline) {
|
|
@@ -461,30 +483,30 @@ function registerLoginCommand(program2) {
|
|
|
461
483
|
const pollResp = await devicePoll(device_code);
|
|
462
484
|
if (pollResp.status === "complete" && pollResp.token) {
|
|
463
485
|
setToken(pollResp.token);
|
|
464
|
-
pollSpinner.succeed(
|
|
486
|
+
pollSpinner.succeed(chalk5.green("Logged in."));
|
|
465
487
|
try {
|
|
466
488
|
const me = await getMe();
|
|
467
489
|
const first = firstNameOrHandle(me.user.name, me.user.email);
|
|
468
490
|
console.log();
|
|
469
|
-
console.log(
|
|
491
|
+
console.log(chalk5.bold(` Welcome, ${first}.`));
|
|
470
492
|
if (me.account?.name) {
|
|
471
|
-
console.log(
|
|
493
|
+
console.log(chalk5.dim(` Account: ${me.account.name}`));
|
|
472
494
|
}
|
|
473
495
|
console.log();
|
|
474
|
-
console.log(
|
|
496
|
+
console.log(chalk5.dim(" Next:"));
|
|
475
497
|
console.log(
|
|
476
|
-
|
|
477
|
-
" " +
|
|
498
|
+
chalk5.dim(
|
|
499
|
+
" " + chalk5.cyan("doable deploy") + " from inside a project"
|
|
478
500
|
)
|
|
479
501
|
);
|
|
480
502
|
console.log(
|
|
481
|
-
|
|
482
|
-
" " +
|
|
503
|
+
chalk5.dim(
|
|
504
|
+
" " + chalk5.cyan("doable setup") + " configure Claude Code / Cursor / Codex"
|
|
483
505
|
)
|
|
484
506
|
);
|
|
485
507
|
console.log(
|
|
486
|
-
|
|
487
|
-
" " +
|
|
508
|
+
chalk5.dim(
|
|
509
|
+
" " + chalk5.cyan("doable status") + " see all your projects"
|
|
488
510
|
)
|
|
489
511
|
);
|
|
490
512
|
} catch {
|
|
@@ -507,9 +529,9 @@ function registerLoginCommand(program2) {
|
|
|
507
529
|
}
|
|
508
530
|
pollSpinner.fail("Login failed.");
|
|
509
531
|
if (err instanceof ApiError || err instanceof Error) {
|
|
510
|
-
console.error(
|
|
532
|
+
console.error(chalk5.red(err.message));
|
|
511
533
|
} else {
|
|
512
|
-
console.error(
|
|
534
|
+
console.error(chalk5.red(String(err)));
|
|
513
535
|
}
|
|
514
536
|
process.exit(1);
|
|
515
537
|
}
|
|
@@ -518,6 +540,21 @@ function registerLoginCommand(program2) {
|
|
|
518
540
|
process.exit(1);
|
|
519
541
|
});
|
|
520
542
|
}
|
|
543
|
+
function waitForEnter() {
|
|
544
|
+
return new Promise((resolve) => {
|
|
545
|
+
if (!process.stdin.isTTY) {
|
|
546
|
+
resolve();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const onData = () => {
|
|
550
|
+
process.stdin.removeListener("data", onData);
|
|
551
|
+
process.stdin.pause();
|
|
552
|
+
resolve();
|
|
553
|
+
};
|
|
554
|
+
process.stdin.resume();
|
|
555
|
+
process.stdin.once("data", onData);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
521
558
|
function sleep(ms) {
|
|
522
559
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
523
560
|
}
|
|
@@ -535,6 +572,32 @@ function firstNameOrHandle(name, email) {
|
|
|
535
572
|
}
|
|
536
573
|
return "friend";
|
|
537
574
|
}
|
|
575
|
+
function registerLogoutCommand(program2) {
|
|
576
|
+
program2.command("logout").description("Sign out of Doable on this machine").action(async () => {
|
|
577
|
+
const token = getToken();
|
|
578
|
+
if (!token) {
|
|
579
|
+
console.log(chalk5.dim("Already signed out."));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const spinner = ora10("Signing out...").start();
|
|
583
|
+
const revoked = await authLogout();
|
|
584
|
+
clearToken();
|
|
585
|
+
if (revoked) {
|
|
586
|
+
spinner.succeed(chalk5.green("Signed out."));
|
|
587
|
+
} else {
|
|
588
|
+
spinner.warn(
|
|
589
|
+
chalk5.yellow(
|
|
590
|
+
"Signed out locally, but couldn't reach Doable to revoke the token server-side."
|
|
591
|
+
)
|
|
592
|
+
);
|
|
593
|
+
console.error(
|
|
594
|
+
chalk5.dim(
|
|
595
|
+
" The token on this machine is gone. If you're worried about the dangling server-side token, rotate it from the dashboard once you're back online."
|
|
596
|
+
)
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
538
601
|
var POSTGRES_NODE_PACKAGES = [
|
|
539
602
|
"pg",
|
|
540
603
|
"postgres",
|
|
@@ -680,10 +743,10 @@ async function promptOne(projectId, detection) {
|
|
|
680
743
|
const SKIP = "skip";
|
|
681
744
|
console.log();
|
|
682
745
|
console.log(
|
|
683
|
-
|
|
746
|
+
chalk5.bold(` Detected ${kindLabel} usage`) + chalk5.dim(` (${detection.source})`)
|
|
684
747
|
);
|
|
685
748
|
console.log(
|
|
686
|
-
|
|
749
|
+
chalk5.dim(
|
|
687
750
|
` No ${detection.envKey} is set on this project, and no native addon is attached yet.`
|
|
688
751
|
)
|
|
689
752
|
);
|
|
@@ -710,7 +773,7 @@ async function promptOne(projectId, detection) {
|
|
|
710
773
|
]);
|
|
711
774
|
if (choice === SKIP) {
|
|
712
775
|
console.log(
|
|
713
|
-
|
|
776
|
+
chalk5.dim(
|
|
714
777
|
` OK \u2014 continuing the deploy. Set ${detection.envKey} anytime with \`doable addon attach\`.`
|
|
715
778
|
)
|
|
716
779
|
);
|
|
@@ -726,7 +789,7 @@ async function promptOne(projectId, detection) {
|
|
|
726
789
|
validate: (v) => v.trim().length > 0 ? true : "required"
|
|
727
790
|
}
|
|
728
791
|
]);
|
|
729
|
-
const spinner2 =
|
|
792
|
+
const spinner2 = ora10(`Saving ${detection.envKey}...`).start();
|
|
730
793
|
try {
|
|
731
794
|
await putEnvVars(projectId, { [detection.envKey]: url.trim() });
|
|
732
795
|
spinner2.succeed(`${detection.envKey} attached.`);
|
|
@@ -739,8 +802,8 @@ async function promptOne(projectId, detection) {
|
|
|
739
802
|
const engine = detection.kind;
|
|
740
803
|
const version = engine === "postgres" ? "16" : "7";
|
|
741
804
|
console.log();
|
|
742
|
-
console.log(
|
|
743
|
-
const creating =
|
|
805
|
+
console.log(chalk5.bold(` Provisioning Doable-managed ${kindLabel}`));
|
|
806
|
+
const creating = ora10("Requesting addon...").start();
|
|
744
807
|
let addon;
|
|
745
808
|
try {
|
|
746
809
|
addon = await createAddon(projectId, {
|
|
@@ -753,7 +816,7 @@ async function promptOne(projectId, detection) {
|
|
|
753
816
|
throw err;
|
|
754
817
|
}
|
|
755
818
|
creating.succeed(`Addon ${addon.id.slice(0, 8)} created.`);
|
|
756
|
-
const spinner =
|
|
819
|
+
const spinner = ora10("Waiting for container to start...").start();
|
|
757
820
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
758
821
|
let current = addon;
|
|
759
822
|
while (Date.now() < deadline) {
|
|
@@ -774,10 +837,10 @@ async function promptOne(projectId, detection) {
|
|
|
774
837
|
if (current.status === "failed") {
|
|
775
838
|
spinner.fail("Provisioning failed.");
|
|
776
839
|
if (current.statusMessage) {
|
|
777
|
-
console.log(
|
|
840
|
+
console.log(chalk5.red(` ${current.statusMessage}`));
|
|
778
841
|
}
|
|
779
842
|
console.log(
|
|
780
|
-
|
|
843
|
+
chalk5.dim(
|
|
781
844
|
" Continuing deploy without the addon. You can retry with `doable addon attach --native`."
|
|
782
845
|
)
|
|
783
846
|
);
|
|
@@ -1135,7 +1198,7 @@ function registerPreviewCommand(program2) {
|
|
|
1135
1198
|
await runPreview(opts);
|
|
1136
1199
|
} catch (err) {
|
|
1137
1200
|
if (err instanceof Error) {
|
|
1138
|
-
console.error(
|
|
1201
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
1139
1202
|
}
|
|
1140
1203
|
process.exit(1);
|
|
1141
1204
|
}
|
|
@@ -1152,14 +1215,14 @@ async function runPreview(opts) {
|
|
|
1152
1215
|
}
|
|
1153
1216
|
const apiUrl = getApiUrl();
|
|
1154
1217
|
console.log();
|
|
1155
|
-
console.log(
|
|
1218
|
+
console.log(chalk5.bold(" Instant preview deploy. No account needed."));
|
|
1156
1219
|
console.log(
|
|
1157
|
-
|
|
1220
|
+
chalk5.dim(
|
|
1158
1221
|
" Heads up: previews are read-only. To update code, add env vars, or\n connect a domain, claim it into an account first (link at the end)."
|
|
1159
1222
|
)
|
|
1160
1223
|
);
|
|
1161
1224
|
console.log();
|
|
1162
|
-
const spinner =
|
|
1225
|
+
const spinner = ora10("Scanning project...").start();
|
|
1163
1226
|
const detected = await detectRuntime(projectDir, nodeDetectFS);
|
|
1164
1227
|
spinner.succeed(`Detected: ${detected.framework || detected.runtime}`);
|
|
1165
1228
|
let email = opts.email;
|
|
@@ -1175,7 +1238,7 @@ async function runPreview(opts) {
|
|
|
1175
1238
|
email = inputEmail;
|
|
1176
1239
|
}
|
|
1177
1240
|
}
|
|
1178
|
-
const createSpinner =
|
|
1241
|
+
const createSpinner = ora10("Creating preview...").start();
|
|
1179
1242
|
const createRes = await fetch(`${apiUrl}/v1/preview`, {
|
|
1180
1243
|
method: "POST",
|
|
1181
1244
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1192,7 +1255,7 @@ async function runPreview(opts) {
|
|
|
1192
1255
|
}
|
|
1193
1256
|
const preview = await createRes.json();
|
|
1194
1257
|
createSpinner.succeed("Preview created");
|
|
1195
|
-
const bundleSpinner =
|
|
1258
|
+
const bundleSpinner = ora10("Bundling project...").start();
|
|
1196
1259
|
const tar2 = await import('tar');
|
|
1197
1260
|
const { tmpdir } = await import('os');
|
|
1198
1261
|
const bundlePath = path7.join(tmpdir(), `doable-preview-${Date.now()}.tar.gz`);
|
|
@@ -1252,7 +1315,7 @@ async function runPreview(opts) {
|
|
|
1252
1315
|
throw new Error(`Bundle too large (${sizeMB} MB). Preview limit is 100 MB.`);
|
|
1253
1316
|
}
|
|
1254
1317
|
bundleSpinner.succeed(`Bundled ${sizeMB} MB`);
|
|
1255
|
-
const uploadSpinner =
|
|
1318
|
+
const uploadSpinner = ora10("Uploading...").start();
|
|
1256
1319
|
const bundleBuffer = fs6.readFileSync(bundlePath);
|
|
1257
1320
|
fs6.unlinkSync(bundlePath);
|
|
1258
1321
|
const uploadRes = await fetch(preview.uploadUrl, {
|
|
@@ -1265,7 +1328,7 @@ async function runPreview(opts) {
|
|
|
1265
1328
|
throw new Error(`Upload failed: HTTP ${uploadRes.status}`);
|
|
1266
1329
|
}
|
|
1267
1330
|
uploadSpinner.succeed("Uploaded");
|
|
1268
|
-
const buildSpinner =
|
|
1331
|
+
const buildSpinner = ora10("Triggering build...").start();
|
|
1269
1332
|
const completeRes = await fetch(`${apiUrl}/v1/preview/complete`, {
|
|
1270
1333
|
method: "POST",
|
|
1271
1334
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1281,7 +1344,7 @@ async function runPreview(opts) {
|
|
|
1281
1344
|
}
|
|
1282
1345
|
const deploy = await completeRes.json();
|
|
1283
1346
|
buildSpinner.succeed(`Build #${deploy.number} queued`);
|
|
1284
|
-
const pollSpinner =
|
|
1347
|
+
const pollSpinner = ora10("Building and deploying\u2026").start();
|
|
1285
1348
|
let finalStatus = "queued";
|
|
1286
1349
|
let lastLabel = "";
|
|
1287
1350
|
for (let i = 0; i < 300; i++) {
|
|
@@ -1310,17 +1373,17 @@ async function runPreview(opts) {
|
|
|
1310
1373
|
}
|
|
1311
1374
|
console.log();
|
|
1312
1375
|
if (finalStatus === "healthy") {
|
|
1313
|
-
console.log(
|
|
1376
|
+
console.log(chalk5.green.bold(" Preview live!"));
|
|
1314
1377
|
console.log();
|
|
1315
|
-
console.log(` URL: ${
|
|
1316
|
-
console.log(` Share: ${
|
|
1317
|
-
console.log(` Expires: ${
|
|
1378
|
+
console.log(` URL: ${chalk5.underline.cyan(preview.previewUrl)}`);
|
|
1379
|
+
console.log(` Share: ${chalk5.cyan(`https://doable.do/s/${deploy.deploymentId}`)}`);
|
|
1380
|
+
console.log(` Expires: ${chalk5.dim("in 8 hours")}`);
|
|
1318
1381
|
console.log();
|
|
1319
|
-
console.log(
|
|
1382
|
+
console.log(chalk5.dim(" Share either link. Anyone can see your app."));
|
|
1320
1383
|
console.log();
|
|
1321
1384
|
console.log(` To update this app (new deploy, env vars, domain): claim it first.`);
|
|
1322
|
-
console.log(` ${
|
|
1323
|
-
console.log(
|
|
1385
|
+
console.log(` ${chalk5.cyan(`https://doable.do/signup#claim=${preview.claimToken}`)}`);
|
|
1386
|
+
console.log(chalk5.dim(` Keep this link private. Anyone who opens it can claim the project.`));
|
|
1324
1387
|
console.log();
|
|
1325
1388
|
try {
|
|
1326
1389
|
const projectName = path7.basename(projectDir);
|
|
@@ -1338,8 +1401,8 @@ async function runPreview(opts) {
|
|
|
1338
1401
|
} catch {
|
|
1339
1402
|
}
|
|
1340
1403
|
} else {
|
|
1341
|
-
console.log(
|
|
1342
|
-
console.log(
|
|
1404
|
+
console.log(chalk5.red(` Preview deploy ${finalStatus}.`));
|
|
1405
|
+
console.log(chalk5.dim(` Check status: ${apiUrl}/v1/preview/${preview.claimToken}/status`));
|
|
1343
1406
|
console.log();
|
|
1344
1407
|
}
|
|
1345
1408
|
const previewJsonPath = path7.join(projectDir, ".doable-preview.json");
|
|
@@ -1621,7 +1684,7 @@ async function promptAmbiguousDeployRoot(projectDir, nonInteractive) {
|
|
|
1621
1684
|
if (choices.length <= 1) return;
|
|
1622
1685
|
if (nonInteractive) {
|
|
1623
1686
|
console.warn(
|
|
1624
|
-
|
|
1687
|
+
chalk5.yellow(
|
|
1625
1688
|
` Multiple deployable app roots found (${choices.join(", ")}). Add {"root":"apps/web"} to .doable.json or run interactively once.`
|
|
1626
1689
|
)
|
|
1627
1690
|
);
|
|
@@ -1638,7 +1701,7 @@ async function promptAmbiguousDeployRoot(projectDir, nonInteractive) {
|
|
|
1638
1701
|
]);
|
|
1639
1702
|
const payload = { ...existing, root: answer.root };
|
|
1640
1703
|
fs6.writeFileSync(path7.join(projectDir, ".doable.json"), JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1641
|
-
console.log(
|
|
1704
|
+
console.log(chalk5.dim(` Saved deploy root to .doable.json (${answer.root}).`));
|
|
1642
1705
|
}
|
|
1643
1706
|
async function resolveTargetOption(target) {
|
|
1644
1707
|
const trimmed = target?.trim();
|
|
@@ -1676,7 +1739,7 @@ async function promptTargetOption(explicitTarget, nonInteractive) {
|
|
|
1676
1739
|
message: "Where should this project run?",
|
|
1677
1740
|
choices: [
|
|
1678
1741
|
{
|
|
1679
|
-
name:
|
|
1742
|
+
name: chalk5.green("Doable Cloud") + chalk5.dim(" (recommended for first deploy)"),
|
|
1680
1743
|
value: CLOUD
|
|
1681
1744
|
},
|
|
1682
1745
|
new inquirer3.Separator(" \u2500\u2500 your servers \u2500\u2500"),
|
|
@@ -1700,16 +1763,16 @@ function registerDeployCommand(program2) {
|
|
|
1700
1763
|
} catch (err) {
|
|
1701
1764
|
if (err instanceof ApiError) {
|
|
1702
1765
|
if (err.status === 401) {
|
|
1703
|
-
console.error(
|
|
1766
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
1704
1767
|
} else if (err.status === 0) {
|
|
1705
|
-
console.error(
|
|
1768
|
+
console.error(chalk5.red(`Connection error: ${err.message}`));
|
|
1706
1769
|
} else {
|
|
1707
|
-
console.error(
|
|
1770
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
1708
1771
|
}
|
|
1709
1772
|
} else if (err instanceof Error) {
|
|
1710
|
-
console.error(
|
|
1773
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
1711
1774
|
} else {
|
|
1712
|
-
console.error(
|
|
1775
|
+
console.error(chalk5.red(`Unexpected error: ${String(err)}`));
|
|
1713
1776
|
}
|
|
1714
1777
|
process.exit(1);
|
|
1715
1778
|
}
|
|
@@ -1725,10 +1788,10 @@ async function runDeploy(opts) {
|
|
|
1725
1788
|
const existingToken = getToken();
|
|
1726
1789
|
if (!existingToken && process.stdin.isTTY) {
|
|
1727
1790
|
console.log();
|
|
1728
|
-
console.log(
|
|
1791
|
+
console.log(chalk5.bold(" You're not signed in to Doable."));
|
|
1729
1792
|
console.log();
|
|
1730
1793
|
console.log(
|
|
1731
|
-
|
|
1794
|
+
chalk5.dim(
|
|
1732
1795
|
" Both options are free. Pick based on what you want:"
|
|
1733
1796
|
)
|
|
1734
1797
|
);
|
|
@@ -1742,13 +1805,13 @@ async function runDeploy(opts) {
|
|
|
1742
1805
|
message: "How do you want to deploy?",
|
|
1743
1806
|
choices: [
|
|
1744
1807
|
{
|
|
1745
|
-
name:
|
|
1808
|
+
name: chalk5.green("Sign in / sign up ") + chalk5.dim.italic(
|
|
1746
1809
|
"(recommended \u2014 you own the project, can redeploy, add env vars / domain)"
|
|
1747
1810
|
),
|
|
1748
1811
|
value: SIGN_IN
|
|
1749
1812
|
},
|
|
1750
1813
|
{
|
|
1751
|
-
name:
|
|
1814
|
+
name: chalk5.yellow("8-hour preview ") + chalk5.dim.italic(
|
|
1752
1815
|
"(no signup; can't be modified after deploy unless claimed first)"
|
|
1753
1816
|
),
|
|
1754
1817
|
value: QUICK_PREVIEW
|
|
@@ -1761,9 +1824,9 @@ async function runDeploy(opts) {
|
|
|
1761
1824
|
return;
|
|
1762
1825
|
}
|
|
1763
1826
|
console.log();
|
|
1764
|
-
console.log(
|
|
1765
|
-
console.log(` 1. Run ${
|
|
1766
|
-
console.log(` 2. Re-run ${
|
|
1827
|
+
console.log(chalk5.bold(" Next:"));
|
|
1828
|
+
console.log(` 1. Run ${chalk5.cyan("doable login")} (opens your browser)`);
|
|
1829
|
+
console.log(` 2. Re-run ${chalk5.cyan("doable deploy")}`);
|
|
1767
1830
|
console.log();
|
|
1768
1831
|
return;
|
|
1769
1832
|
}
|
|
@@ -1781,7 +1844,7 @@ async function runDeploy(opts) {
|
|
|
1781
1844
|
if (projectId) cameFromDoableJson = true;
|
|
1782
1845
|
} catch {
|
|
1783
1846
|
console.warn(
|
|
1784
|
-
|
|
1847
|
+
chalk5.yellow(`Warning: Could not parse ${doableJsonPath}. Ignoring.`)
|
|
1785
1848
|
);
|
|
1786
1849
|
}
|
|
1787
1850
|
}
|
|
@@ -1796,10 +1859,10 @@ async function runDeploy(opts) {
|
|
|
1796
1859
|
const estimate = formatBuildEstimate(detected.runtime);
|
|
1797
1860
|
const runtimeLabel = detected.framework ? `${detected.runtime} (${detected.framework})` : detected.runtime;
|
|
1798
1861
|
console.log();
|
|
1799
|
-
console.log(
|
|
1800
|
-
console.log(
|
|
1862
|
+
console.log(chalk5.bold(` Deploy ${chalk5.cyan(proposedName)}`));
|
|
1863
|
+
console.log(chalk5.dim(` ${detected.reason}`));
|
|
1801
1864
|
console.log(
|
|
1802
|
-
|
|
1865
|
+
chalk5.dim(
|
|
1803
1866
|
` Runtime: ${runtimeLabel} \xB7 First build: ${estimate} \xB7 Builds on Doable's servers (no local Docker)`
|
|
1804
1867
|
)
|
|
1805
1868
|
);
|
|
@@ -1818,7 +1881,7 @@ async function runDeploy(opts) {
|
|
|
1818
1881
|
// Lead with the permanent project path — user owns it, can
|
|
1819
1882
|
// update, add env vars / domains / addons, roll back. This
|
|
1820
1883
|
// is the right default for 95% of deploys.
|
|
1821
|
-
name:
|
|
1884
|
+
name: chalk5.green(`Create new: ${proposedName} (${runtimeLabel})`),
|
|
1822
1885
|
value: CREATE_NEW
|
|
1823
1886
|
}
|
|
1824
1887
|
];
|
|
@@ -1834,7 +1897,7 @@ async function runDeploy(opts) {
|
|
|
1834
1897
|
choices.push(
|
|
1835
1898
|
new inquirer3.Separator(" \u2500\u2500 or quick throwaway share \u2500\u2500"),
|
|
1836
1899
|
{
|
|
1837
|
-
name:
|
|
1900
|
+
name: chalk5.dim("8-hour preview ") + chalk5.dim.italic("(can't be modified unless claimed later)"),
|
|
1838
1901
|
value: QUICK_PREVIEW
|
|
1839
1902
|
}
|
|
1840
1903
|
);
|
|
@@ -1857,7 +1920,7 @@ async function runDeploy(opts) {
|
|
|
1857
1920
|
}
|
|
1858
1921
|
if (autoCreate && !projectId) {
|
|
1859
1922
|
const resolvedTarget = await promptTargetOption(opts.target, nonInteractive);
|
|
1860
|
-
const spinner =
|
|
1923
|
+
const spinner = ora10(`Creating project ${proposedName}...`).start();
|
|
1861
1924
|
try {
|
|
1862
1925
|
const created = await createProject({
|
|
1863
1926
|
name: proposedName,
|
|
@@ -1867,7 +1930,7 @@ async function runDeploy(opts) {
|
|
|
1867
1930
|
...resolvedTarget ? { target: resolvedTarget } : {}
|
|
1868
1931
|
});
|
|
1869
1932
|
projectId = created.project.id;
|
|
1870
|
-
spinner.succeed(`Created project ${
|
|
1933
|
+
spinner.succeed(`Created project ${chalk5.cyan(created.project.name)}.`);
|
|
1871
1934
|
} catch (err) {
|
|
1872
1935
|
spinner.fail(`Failed to create project.`);
|
|
1873
1936
|
throw err;
|
|
@@ -1883,8 +1946,8 @@ async function runDeploy(opts) {
|
|
|
1883
1946
|
writeDoableJsonFile(projectDir, project.id, project.slug);
|
|
1884
1947
|
}
|
|
1885
1948
|
console.log(
|
|
1886
|
-
|
|
1887
|
-
Deploying ${
|
|
1949
|
+
chalk5.bold(`
|
|
1950
|
+
Deploying ${chalk5.cyan(project.name)} (${project.slug})
|
|
1888
1951
|
`)
|
|
1889
1952
|
);
|
|
1890
1953
|
try {
|
|
@@ -1893,7 +1956,7 @@ async function runDeploy(opts) {
|
|
|
1893
1956
|
});
|
|
1894
1957
|
} catch (err) {
|
|
1895
1958
|
console.warn(
|
|
1896
|
-
|
|
1959
|
+
chalk5.yellow(
|
|
1897
1960
|
` (Addon setup skipped: ${err instanceof Error ? err.message : String(err)})`
|
|
1898
1961
|
)
|
|
1899
1962
|
);
|
|
@@ -1906,12 +1969,12 @@ async function runDeploy(opts) {
|
|
|
1906
1969
|
});
|
|
1907
1970
|
} catch (err) {
|
|
1908
1971
|
console.warn(
|
|
1909
|
-
|
|
1972
|
+
chalk5.yellow(
|
|
1910
1973
|
` (Env sync skipped: ${err instanceof Error ? err.message : String(err)})`
|
|
1911
1974
|
)
|
|
1912
1975
|
);
|
|
1913
1976
|
}
|
|
1914
|
-
const bundleSpinner =
|
|
1977
|
+
const bundleSpinner = ora10("Bundling project...").start();
|
|
1915
1978
|
const bundlePath = path7.join(projectDir, ".doable-bundle.tar.gz");
|
|
1916
1979
|
try {
|
|
1917
1980
|
const ig = ignore();
|
|
@@ -1988,7 +2051,7 @@ async function runDeploy(opts) {
|
|
|
1988
2051
|
const HARD_MAX_BUNDLE_MB = 500;
|
|
1989
2052
|
if (sizeMb > HARD_MAX_BUNDLE_MB) {
|
|
1990
2053
|
console.error(
|
|
1991
|
-
|
|
2054
|
+
chalk5.red(
|
|
1992
2055
|
`Bundle size (${sizeMb.toFixed(1)} MB) exceeds the maximum allowed (${HARD_MAX_BUNDLE_MB} MB). Add large files to .doableignore or .gitignore and try again.`
|
|
1993
2056
|
)
|
|
1994
2057
|
);
|
|
@@ -2001,7 +2064,7 @@ async function runDeploy(opts) {
|
|
|
2001
2064
|
const maxMb = limits?.maxBundleSizeMb;
|
|
2002
2065
|
if (maxMb && sizeMb > maxMb) {
|
|
2003
2066
|
console.error(
|
|
2004
|
-
|
|
2067
|
+
chalk5.red(
|
|
2005
2068
|
`Bundle size (${sizeMb.toFixed(1)} MB) exceeds your plan limit (${maxMb} MB). Upgrade your plan or reduce your bundle size.`
|
|
2006
2069
|
)
|
|
2007
2070
|
);
|
|
@@ -2010,7 +2073,7 @@ async function runDeploy(opts) {
|
|
|
2010
2073
|
}
|
|
2011
2074
|
} catch {
|
|
2012
2075
|
}
|
|
2013
|
-
const uploadSpinner =
|
|
2076
|
+
const uploadSpinner = ora10("Uploading bundle\u2026").start();
|
|
2014
2077
|
const initResp = await uploadInit({
|
|
2015
2078
|
projectId: project.id,
|
|
2016
2079
|
filename: "bundle.tar.gz",
|
|
@@ -2040,20 +2103,20 @@ async function runDeploy(opts) {
|
|
|
2040
2103
|
}
|
|
2041
2104
|
}
|
|
2042
2105
|
if (!key) {
|
|
2043
|
-
console.error(
|
|
2106
|
+
console.error(chalk5.red("Internal error: no upload key resolved."));
|
|
2044
2107
|
cleanup(bundlePath);
|
|
2045
2108
|
process.exit(1);
|
|
2046
2109
|
}
|
|
2047
|
-
const deploySpinner =
|
|
2110
|
+
const deploySpinner = ora10("Starting deployment...").start();
|
|
2048
2111
|
const deployment = await createDeployment(project.id, {
|
|
2049
2112
|
sourceUploadKey: key,
|
|
2050
2113
|
sourceSha256: sourceSha256 ?? void 0,
|
|
2051
2114
|
repoSizeMb: sizeMb,
|
|
2052
2115
|
referencedEnvKeys
|
|
2053
2116
|
});
|
|
2054
|
-
deploySpinner.succeed(`Deployment started: ${
|
|
2117
|
+
deploySpinner.succeed(`Deployment started: ${chalk5.dim(deployment.id)}`);
|
|
2055
2118
|
console.log();
|
|
2056
|
-
console.log(
|
|
2119
|
+
console.log(chalk5.dim(" --- Deployment logs ---"));
|
|
2057
2120
|
console.log();
|
|
2058
2121
|
const finalStatus = await streamDeploymentEvents(deployment.id);
|
|
2059
2122
|
console.log();
|
|
@@ -2090,15 +2153,15 @@ async function runDeploy(opts) {
|
|
|
2090
2153
|
}
|
|
2091
2154
|
const isFirstDeploy = finalDeploy.number === 1;
|
|
2092
2155
|
console.log(
|
|
2093
|
-
|
|
2156
|
+
chalk5.green.bold(
|
|
2094
2157
|
isFirstDeploy ? " Your app is live." : " Live."
|
|
2095
2158
|
)
|
|
2096
2159
|
);
|
|
2097
2160
|
if (liveUrl) {
|
|
2098
|
-
console.log(` ${osc8Link(liveUrl,
|
|
2161
|
+
console.log(` ${osc8Link(liveUrl, chalk5.underline.cyan(liveUrl))}`);
|
|
2099
2162
|
}
|
|
2100
2163
|
console.log(
|
|
2101
|
-
|
|
2164
|
+
chalk5.dim(
|
|
2102
2165
|
durationSec < 30 ? ` Took ${durationSec}s. Nice.` : ` Took ${durationSec}s.`
|
|
2103
2166
|
)
|
|
2104
2167
|
);
|
|
@@ -2107,7 +2170,7 @@ async function runDeploy(opts) {
|
|
|
2107
2170
|
printQr(liveUrl);
|
|
2108
2171
|
}
|
|
2109
2172
|
const shareUrl = `https://doable.do/s/${deployment.id}`;
|
|
2110
|
-
console.log(` ${
|
|
2173
|
+
console.log(` ${chalk5.dim("Share:")} ${chalk5.cyan(shareUrl)}`);
|
|
2111
2174
|
console.log();
|
|
2112
2175
|
let domainPromptShown = false;
|
|
2113
2176
|
if (isFirstDeploy && activeDomain?.hostname.endsWith(".doable.do") && process.stdout.isTTY) {
|
|
@@ -2116,10 +2179,10 @@ async function runDeploy(opts) {
|
|
|
2116
2179
|
}
|
|
2117
2180
|
const tips = [];
|
|
2118
2181
|
if (!domainPromptShown && !finalProject.domains?.some((d) => d.type === "custom" || d.type === "purchased")) {
|
|
2119
|
-
tips.push(`Add a custom domain: ${
|
|
2182
|
+
tips.push(`Add a custom domain: ${chalk5.cyan("doable domain search yourname.com")}`);
|
|
2120
2183
|
}
|
|
2121
2184
|
if (!finalProject.webhookUrl) {
|
|
2122
|
-
tips.push(`Get Slack notifications: ${
|
|
2185
|
+
tips.push(`Get Slack notifications: ${chalk5.cyan("doable.do/projects/" + project.id + " > Settings > Webhook")}`);
|
|
2123
2186
|
}
|
|
2124
2187
|
if (finalProject.referencedEnvKeys?.length > 0) {
|
|
2125
2188
|
try {
|
|
@@ -2127,15 +2190,15 @@ async function runDeploy(opts) {
|
|
|
2127
2190
|
const setKeys = new Set(envVars.map((v) => v.key));
|
|
2128
2191
|
const missing = finalProject.referencedEnvKeys.filter((k) => !setKeys.has(k));
|
|
2129
2192
|
if (missing.length > 0) {
|
|
2130
|
-
tips.push(`Your code references ${
|
|
2193
|
+
tips.push(`Your code references ${chalk5.yellow(missing.slice(0, 3).join(", "))} but ${missing.length === 1 ? "it's" : "they're"} not set`);
|
|
2131
2194
|
}
|
|
2132
2195
|
} catch {
|
|
2133
2196
|
}
|
|
2134
2197
|
}
|
|
2135
2198
|
if (tips.length > 0) {
|
|
2136
|
-
console.log(
|
|
2199
|
+
console.log(chalk5.dim(" Next steps:"));
|
|
2137
2200
|
for (const tip of tips.slice(0, 2)) {
|
|
2138
|
-
console.log(
|
|
2201
|
+
console.log(chalk5.dim(` ${tip}`));
|
|
2139
2202
|
}
|
|
2140
2203
|
console.log();
|
|
2141
2204
|
}
|
|
@@ -2153,12 +2216,12 @@ async function runDeploy(opts) {
|
|
|
2153
2216
|
const logText = tail.map((e) => e.message).join("\n");
|
|
2154
2217
|
const subcode = classifyBuildFailure(logText);
|
|
2155
2218
|
const subcodeLabel = subcode.replace(/_/g, " ");
|
|
2156
|
-
console.error(
|
|
2157
|
-
console.error(
|
|
2158
|
-
console.error(
|
|
2219
|
+
console.error(chalk5.red.bold(` Deploy failed. ${capitalize(subcodeLabel)}.`));
|
|
2220
|
+
console.error(chalk5.dim(` Status: ${finalStatus}`));
|
|
2221
|
+
console.error(chalk5.dim(" Your app is still running on the previous version."));
|
|
2159
2222
|
console.error();
|
|
2160
2223
|
if (tail.length > 0) {
|
|
2161
|
-
console.error(
|
|
2224
|
+
console.error(chalk5.dim(" Last log lines:"));
|
|
2162
2225
|
for (const ev of tail) {
|
|
2163
2226
|
printDeploymentEvent(ev.level, ev.message, ev.meta, true);
|
|
2164
2227
|
}
|
|
@@ -2166,13 +2229,13 @@ async function runDeploy(opts) {
|
|
|
2166
2229
|
}
|
|
2167
2230
|
const hint = buildFailureHint(subcode);
|
|
2168
2231
|
if (hint) {
|
|
2169
|
-
console.error(
|
|
2232
|
+
console.error(chalk5.cyan(` Fix hint: ${hint}`));
|
|
2170
2233
|
console.error();
|
|
2171
2234
|
}
|
|
2172
2235
|
console.error(
|
|
2173
|
-
|
|
2236
|
+
chalk5.dim(" Need a closer look? ") + chalk5.cyan(`doable doctor ${deployment.id}`)
|
|
2174
2237
|
);
|
|
2175
|
-
console.error(
|
|
2238
|
+
console.error(chalk5.dim(` Dashboard: ${chalk5.cyan(`doable.do/deployments/${deployment.id}`)}`));
|
|
2176
2239
|
console.log();
|
|
2177
2240
|
process.exit(1);
|
|
2178
2241
|
}
|
|
@@ -2222,8 +2285,8 @@ async function runDeployFromRepo(opts) {
|
|
|
2222
2285
|
);
|
|
2223
2286
|
}
|
|
2224
2287
|
console.log();
|
|
2225
|
-
console.log(
|
|
2226
|
-
console.log(
|
|
2288
|
+
console.log(chalk5.bold(` Deploying ${chalk5.cyan(project.name)} (${project.slug})`));
|
|
2289
|
+
console.log(chalk5.dim(` From: ${project.repoUrl} (branch: ${project.repoBranch || "main"})`));
|
|
2227
2290
|
console.log();
|
|
2228
2291
|
try {
|
|
2229
2292
|
await promptAddonSetup(projectDir, project.id, {
|
|
@@ -2231,16 +2294,16 @@ async function runDeployFromRepo(opts) {
|
|
|
2231
2294
|
});
|
|
2232
2295
|
} catch (err) {
|
|
2233
2296
|
console.warn(
|
|
2234
|
-
|
|
2297
|
+
chalk5.yellow(
|
|
2235
2298
|
` (Addon setup skipped: ${err instanceof Error ? err.message : String(err)})`
|
|
2236
2299
|
)
|
|
2237
2300
|
);
|
|
2238
2301
|
}
|
|
2239
|
-
const deploySpinner =
|
|
2302
|
+
const deploySpinner = ora10("Triggering deploy from GitHub...").start();
|
|
2240
2303
|
const deployment = await createDeployment(project.id, { fromRepo: true });
|
|
2241
|
-
deploySpinner.succeed(`Deployment started: ${
|
|
2304
|
+
deploySpinner.succeed(`Deployment started: ${chalk5.dim(deployment.id)}`);
|
|
2242
2305
|
console.log();
|
|
2243
|
-
console.log(
|
|
2306
|
+
console.log(chalk5.dim(" --- Deployment logs ---"));
|
|
2244
2307
|
console.log();
|
|
2245
2308
|
const finalStatus = await streamDeploymentEvents(deployment.id);
|
|
2246
2309
|
console.log();
|
|
@@ -2252,19 +2315,19 @@ async function runDeployFromRepo(opts) {
|
|
|
2252
2315
|
const startTime = new Date(finalDeploy.createdAt).getTime();
|
|
2253
2316
|
const endTime = finalDeploy.deployFinishedAt ? new Date(finalDeploy.deployFinishedAt).getTime() : Date.now();
|
|
2254
2317
|
const durationSec = Math.round((endTime - startTime) / 1e3);
|
|
2255
|
-
console.log(
|
|
2318
|
+
console.log(chalk5.green.bold(" Live."));
|
|
2256
2319
|
if (liveUrl) {
|
|
2257
|
-
console.log(` ${
|
|
2320
|
+
console.log(` ${chalk5.underline.cyan(liveUrl)}`);
|
|
2258
2321
|
}
|
|
2259
|
-
console.log(
|
|
2322
|
+
console.log(chalk5.dim(` Deployed in ${durationSec}s`));
|
|
2260
2323
|
console.log();
|
|
2261
2324
|
const isFirstDeploy = finalDeploy.number === 1;
|
|
2262
2325
|
if (isFirstDeploy && activeDomain?.hostname.endsWith(".doable.do") && process.stdout.isTTY) {
|
|
2263
2326
|
await offerCustomDomain(project, activeDomain.hostname);
|
|
2264
2327
|
}
|
|
2265
2328
|
} else {
|
|
2266
|
-
console.error(
|
|
2267
|
-
console.error(
|
|
2329
|
+
console.error(chalk5.red.bold(" Deploy didn't land."));
|
|
2330
|
+
console.error(chalk5.red(` Status: ${finalStatus}`));
|
|
2268
2331
|
console.log();
|
|
2269
2332
|
process.exit(1);
|
|
2270
2333
|
}
|
|
@@ -2359,31 +2422,31 @@ async function promptEnvSync(projectDir, projectId, opts) {
|
|
|
2359
2422
|
if (verbose) {
|
|
2360
2423
|
console.log();
|
|
2361
2424
|
console.log(
|
|
2362
|
-
|
|
2425
|
+
chalk5.bold(
|
|
2363
2426
|
` Found ${missingOnServer.length} key${plural} in ${foundFiles.join(", ")} not yet set on the server:`
|
|
2364
2427
|
)
|
|
2365
2428
|
);
|
|
2366
2429
|
for (const key of missingOnServer) {
|
|
2367
2430
|
const cls = classifySecret(localKeys[key]);
|
|
2368
|
-
const badge = cls ?
|
|
2369
|
-
console.log(` ${
|
|
2431
|
+
const badge = cls ? chalk5.yellow(` [${cls.label}]`) : "";
|
|
2432
|
+
console.log(` ${chalk5.cyan(key)}${badge}`);
|
|
2370
2433
|
}
|
|
2371
2434
|
console.log(
|
|
2372
|
-
|
|
2435
|
+
chalk5.dim(
|
|
2373
2436
|
" Values are encrypted at rest with DOABLE_KMS_MASTER_KEY and never returned by the API."
|
|
2374
2437
|
)
|
|
2375
2438
|
);
|
|
2376
2439
|
console.log();
|
|
2377
2440
|
} else {
|
|
2378
|
-
const preview = missingOnServer.slice(0, 3).map((k) =>
|
|
2441
|
+
const preview = missingOnServer.slice(0, 3).map((k) => chalk5.cyan(k)).join(", ");
|
|
2379
2442
|
const more = missingOnServer.length > 3 ? ` +${missingOnServer.length - 3} more` : "";
|
|
2380
2443
|
console.log(
|
|
2381
|
-
|
|
2444
|
+
chalk5.dim(` env: `) + `${missingOnServer.length} new key${plural} from ${foundFiles.join(", ")} \u2014 ${preview}${more}`
|
|
2382
2445
|
);
|
|
2383
2446
|
}
|
|
2384
2447
|
if (opts.nonInteractive) {
|
|
2385
2448
|
console.log(
|
|
2386
|
-
|
|
2449
|
+
chalk5.yellow(
|
|
2387
2450
|
" (Non-interactive run \u2014 skipping upload. Set these with `doable env set KEY=VALUE` or via the dashboard.)"
|
|
2388
2451
|
)
|
|
2389
2452
|
);
|
|
@@ -2400,7 +2463,7 @@ async function promptEnvSync(projectDir, projectId, opts) {
|
|
|
2400
2463
|
if (uploadEnv) {
|
|
2401
2464
|
const toUpload = {};
|
|
2402
2465
|
for (const k of missingOnServer) toUpload[k] = localKeys[k];
|
|
2403
|
-
const spin =
|
|
2466
|
+
const spin = ora10("Encrypting and uploading...").start();
|
|
2404
2467
|
try {
|
|
2405
2468
|
const resp = await putEnvVars(projectId, toUpload);
|
|
2406
2469
|
spin.succeed(
|
|
@@ -2427,41 +2490,41 @@ async function promptEnvSync(projectDir, projectId, opts) {
|
|
|
2427
2490
|
const more = report.runtime.length > 3 ? ` +${report.runtime.length - 3} more` : "";
|
|
2428
2491
|
if (verbose) {
|
|
2429
2492
|
console.log(
|
|
2430
|
-
|
|
2493
|
+
chalk5.yellow(
|
|
2431
2494
|
` \u26A0 ${report.runtime.length} env key${report.runtime.length === 1 ? " is" : "s are"} referenced in source but not set:`
|
|
2432
2495
|
)
|
|
2433
2496
|
);
|
|
2434
2497
|
for (const k of report.runtime) {
|
|
2435
|
-
console.log(` ${
|
|
2498
|
+
console.log(` ${chalk5.yellow(k)}`);
|
|
2436
2499
|
}
|
|
2437
2500
|
console.log(
|
|
2438
|
-
|
|
2501
|
+
chalk5.dim(
|
|
2439
2502
|
" The deploy will continue. Your app may crash on first request if any of these are required."
|
|
2440
2503
|
)
|
|
2441
2504
|
);
|
|
2442
2505
|
console.log();
|
|
2443
2506
|
} else {
|
|
2444
2507
|
console.log(
|
|
2445
|
-
|
|
2508
|
+
chalk5.yellow(` \u26A0 env: ${report.runtime.length} referenced but not set: ${firstThree}${more}`)
|
|
2446
2509
|
);
|
|
2447
2510
|
}
|
|
2448
2511
|
}
|
|
2449
2512
|
if (report.buildTime.length > 0) {
|
|
2450
2513
|
if (verbose) {
|
|
2451
2514
|
console.log(
|
|
2452
|
-
|
|
2515
|
+
chalk5.yellow(
|
|
2453
2516
|
` \u26A0 ${report.buildTime.length} NEXT_PUBLIC_* key${report.buildTime.length === 1 ? "" : "s"} referenced \u2014 these are inlined at build time, so setting them after deploy is a no-op. Set them BEFORE your next build.`
|
|
2454
2517
|
)
|
|
2455
2518
|
);
|
|
2456
2519
|
for (const k of report.buildTime) {
|
|
2457
|
-
console.log(` ${
|
|
2520
|
+
console.log(` ${chalk5.yellow(k)}`);
|
|
2458
2521
|
}
|
|
2459
2522
|
console.log();
|
|
2460
2523
|
} else {
|
|
2461
2524
|
const firstThree = report.buildTime.slice(0, 3).join(", ");
|
|
2462
2525
|
const more = report.buildTime.length > 3 ? ` +${report.buildTime.length - 3} more` : "";
|
|
2463
2526
|
console.log(
|
|
2464
|
-
|
|
2527
|
+
chalk5.yellow(` \u26A0 env (build-time): ${report.buildTime.length} NEXT_PUBLIC_* referenced but not set: ${firstThree}${more}`)
|
|
2465
2528
|
);
|
|
2466
2529
|
}
|
|
2467
2530
|
}
|
|
@@ -2502,7 +2565,7 @@ async function offerCustomDomain(project, subdomain) {
|
|
|
2502
2565
|
return;
|
|
2503
2566
|
}
|
|
2504
2567
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2505
|
-
console.log(
|
|
2568
|
+
console.log(chalk5.dim(` (domain prompt skipped: ${msg})`));
|
|
2506
2569
|
console.log();
|
|
2507
2570
|
}
|
|
2508
2571
|
}
|
|
@@ -2517,22 +2580,22 @@ async function runBuyFlow(project) {
|
|
|
2517
2580
|
}
|
|
2518
2581
|
]);
|
|
2519
2582
|
const hostname = domainName.trim().toLowerCase();
|
|
2520
|
-
const searchSpinner =
|
|
2583
|
+
const searchSpinner = ora10(`Checking ${chalk5.bold(hostname)}...`).start();
|
|
2521
2584
|
let result;
|
|
2522
2585
|
try {
|
|
2523
2586
|
result = await searchDomain(hostname);
|
|
2524
2587
|
} catch (err) {
|
|
2525
2588
|
searchSpinner.fail("Could not reach the domain registrar.");
|
|
2526
2589
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2527
|
-
console.log(
|
|
2590
|
+
console.log(chalk5.dim(` ${msg}`));
|
|
2528
2591
|
console.log();
|
|
2529
2592
|
return;
|
|
2530
2593
|
}
|
|
2531
2594
|
searchSpinner.stop();
|
|
2532
2595
|
if (!result.available) {
|
|
2533
|
-
console.log(` ${
|
|
2596
|
+
console.log(` ${chalk5.red("Not available.")} Try a different name or TLD.`);
|
|
2534
2597
|
console.log(
|
|
2535
|
-
|
|
2598
|
+
chalk5.dim(` Hint: doable domain search <name> lets you poke around.`)
|
|
2536
2599
|
);
|
|
2537
2600
|
console.log();
|
|
2538
2601
|
return;
|
|
@@ -2552,13 +2615,13 @@ async function runBuyFlow(project) {
|
|
|
2552
2615
|
return searchUrl;
|
|
2553
2616
|
})();
|
|
2554
2617
|
console.log();
|
|
2555
|
-
console.log(
|
|
2618
|
+
console.log(chalk5.bold(` Register ${hostname} via Namecheap${priceLine}`));
|
|
2556
2619
|
console.log();
|
|
2557
|
-
console.log(` ${
|
|
2620
|
+
console.log(` ${chalk5.cyan(namecheapUrl)}`);
|
|
2558
2621
|
console.log();
|
|
2559
|
-
console.log(
|
|
2560
|
-
console.log(
|
|
2561
|
-
console.log(` ${
|
|
2622
|
+
console.log(chalk5.dim(" Doable earns a small commission on this referral; price to you is unchanged."));
|
|
2623
|
+
console.log(chalk5.dim(" Once registered, attach it to this project with:"));
|
|
2624
|
+
console.log(` ${chalk5.cyan(`doable domain connect ${hostname} --project ${project.slug}`)}`);
|
|
2562
2625
|
console.log();
|
|
2563
2626
|
}
|
|
2564
2627
|
async function runConnectFlow(project) {
|
|
@@ -2571,8 +2634,8 @@ async function runConnectFlow(project) {
|
|
|
2571
2634
|
}
|
|
2572
2635
|
]);
|
|
2573
2636
|
const cleanHostname = hostname.trim().toLowerCase();
|
|
2574
|
-
const spinner =
|
|
2575
|
-
`Reserving ${
|
|
2637
|
+
const spinner = ora10(
|
|
2638
|
+
`Reserving ${chalk5.bold(cleanHostname)} and generating verification record...`
|
|
2576
2639
|
).start();
|
|
2577
2640
|
let resp;
|
|
2578
2641
|
try {
|
|
@@ -2583,27 +2646,27 @@ async function runConnectFlow(project) {
|
|
|
2583
2646
|
} catch (err) {
|
|
2584
2647
|
spinner.fail("Couldn't reserve the domain.");
|
|
2585
2648
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2586
|
-
console.log(
|
|
2649
|
+
console.log(chalk5.dim(` ${msg}`));
|
|
2587
2650
|
console.log();
|
|
2588
2651
|
return;
|
|
2589
2652
|
}
|
|
2590
|
-
spinner.succeed(`Reserved ${
|
|
2653
|
+
spinner.succeed(`Reserved ${chalk5.bold(resp.domain.hostname)}.`);
|
|
2591
2654
|
console.log();
|
|
2592
|
-
console.log(
|
|
2655
|
+
console.log(chalk5.bold(" Add this TXT record at your DNS provider:"));
|
|
2593
2656
|
console.log();
|
|
2594
|
-
console.log(` ${
|
|
2595
|
-
console.log(` ${
|
|
2596
|
-
console.log(` ${
|
|
2657
|
+
console.log(` ${chalk5.dim("Type: ")}${chalk5.cyan("TXT")}`);
|
|
2658
|
+
console.log(` ${chalk5.dim("Name: ")}${chalk5.cyan(resp.verification.name)}`);
|
|
2659
|
+
console.log(` ${chalk5.dim("Value: ")}${chalk5.cyan(resp.verification.value)}`);
|
|
2597
2660
|
console.log();
|
|
2598
2661
|
console.log(
|
|
2599
|
-
|
|
2600
|
-
` Once propagated (usually 1-10 min), run: ${
|
|
2662
|
+
chalk5.dim(
|
|
2663
|
+
` Once propagated (usually 1-10 min), run: ${chalk5.cyan(
|
|
2601
2664
|
`doable domain verify ${cleanHostname}`
|
|
2602
2665
|
)}`
|
|
2603
2666
|
)
|
|
2604
2667
|
);
|
|
2605
2668
|
console.log(
|
|
2606
|
-
|
|
2669
|
+
chalk5.dim(
|
|
2607
2670
|
" Doable auto-issues TLS the moment verification succeeds."
|
|
2608
2671
|
)
|
|
2609
2672
|
);
|
|
@@ -2650,19 +2713,19 @@ async function streamDeploymentEvents(deploymentId) {
|
|
|
2650
2713
|
});
|
|
2651
2714
|
} catch (fetchErr) {
|
|
2652
2715
|
const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
2653
|
-
console.warn(
|
|
2716
|
+
console.warn(chalk5.yellow(`Could not connect to log stream (${msg}). Polling deployment status instead.`));
|
|
2654
2717
|
const err = new Error("SSE unavailable");
|
|
2655
2718
|
err.name = "SSEUnavailable";
|
|
2656
2719
|
throw err;
|
|
2657
2720
|
}
|
|
2658
2721
|
if (!res.ok) {
|
|
2659
|
-
console.warn(
|
|
2722
|
+
console.warn(chalk5.yellow(`Could not connect to log stream (${res.statusText || `HTTP ${res.status}`}). Polling deployment status instead.`));
|
|
2660
2723
|
const err = new Error("SSE unavailable");
|
|
2661
2724
|
err.name = "SSEUnavailable";
|
|
2662
2725
|
throw err;
|
|
2663
2726
|
}
|
|
2664
2727
|
if (!res.body) {
|
|
2665
|
-
console.warn(
|
|
2728
|
+
console.warn(chalk5.yellow("Log stream had no response body. Polling deployment status instead."));
|
|
2666
2729
|
const err = new Error("SSE unavailable");
|
|
2667
2730
|
err.name = "SSEUnavailable";
|
|
2668
2731
|
throw err;
|
|
@@ -2707,7 +2770,7 @@ async function streamDeploymentEvents(deploymentId) {
|
|
|
2707
2770
|
}
|
|
2708
2771
|
} catch (err) {
|
|
2709
2772
|
if (err instanceof Error && (err.name === "AbortError" || err.name === "SSEUnavailable")) ; else {
|
|
2710
|
-
console.error(
|
|
2773
|
+
console.error(chalk5.red(`Log stream error: ${err instanceof Error ? err.message : String(err)}`));
|
|
2711
2774
|
}
|
|
2712
2775
|
}
|
|
2713
2776
|
const terminalStatuses = ["healthy", "failed", "canceled", "rolled_back", "suspended"];
|
|
@@ -2723,7 +2786,7 @@ async function streamDeploymentEvents(deploymentId) {
|
|
|
2723
2786
|
}
|
|
2724
2787
|
if (!droppedNoticed) {
|
|
2725
2788
|
console.log(
|
|
2726
|
-
|
|
2789
|
+
chalk5.dim(` (Stream disconnected, build still running. Polling\u2026)`)
|
|
2727
2790
|
);
|
|
2728
2791
|
droppedNoticed = true;
|
|
2729
2792
|
}
|
|
@@ -2740,13 +2803,13 @@ function processEvent(type, data) {
|
|
|
2740
2803
|
const message = parsed.message || data;
|
|
2741
2804
|
const level = parsed.level || type;
|
|
2742
2805
|
if (parsed.message === "live_url_ready" && parsed.meta && typeof parsed.meta.liveUrl === "string") {
|
|
2743
|
-
console.log(` ${
|
|
2806
|
+
console.log(` ${chalk5.green("\u2192")} ${chalk5.underline.cyan(parsed.meta.liveUrl)} ${chalk5.dim("(warming up\u2026)")}`);
|
|
2744
2807
|
return;
|
|
2745
2808
|
}
|
|
2746
2809
|
printDeploymentEvent(level, message, parsed.meta);
|
|
2747
2810
|
} catch {
|
|
2748
2811
|
if (data.trim()) {
|
|
2749
|
-
console.log(` ${
|
|
2812
|
+
console.log(` ${chalk5.dim("LOG")} ${data}`);
|
|
2750
2813
|
}
|
|
2751
2814
|
}
|
|
2752
2815
|
}
|
|
@@ -2755,33 +2818,33 @@ function printDeploymentEvent(level, message, meta, stderr = false) {
|
|
|
2755
2818
|
const isOriginalError = meta?.originalError === true;
|
|
2756
2819
|
const isPlatformFailure = meta?.failureCategory === "platform";
|
|
2757
2820
|
const isAutoFix = meta?.autoFix === true;
|
|
2758
|
-
const suffix = isPlatformFailure ?
|
|
2821
|
+
const suffix = isPlatformFailure ? chalk5.yellow(" (Doable-side)") : "";
|
|
2759
2822
|
let label;
|
|
2760
2823
|
switch (level) {
|
|
2761
2824
|
case "error":
|
|
2762
|
-
label =
|
|
2825
|
+
label = chalk5.red("ERROR");
|
|
2763
2826
|
break;
|
|
2764
2827
|
case "warning":
|
|
2765
2828
|
case "warn":
|
|
2766
|
-
label =
|
|
2829
|
+
label = chalk5.yellow("WARN ");
|
|
2767
2830
|
break;
|
|
2768
2831
|
case "success":
|
|
2769
2832
|
case "done":
|
|
2770
2833
|
case "complete":
|
|
2771
|
-
label =
|
|
2834
|
+
label = chalk5.green("OK ");
|
|
2772
2835
|
break;
|
|
2773
2836
|
case "step":
|
|
2774
|
-
label =
|
|
2837
|
+
label = chalk5.blue("STEP ");
|
|
2775
2838
|
break;
|
|
2776
2839
|
default:
|
|
2777
|
-
label = isAutoFix ?
|
|
2840
|
+
label = isAutoFix ? chalk5.green("FIX ") : chalk5.dim("LOG ");
|
|
2778
2841
|
break;
|
|
2779
2842
|
}
|
|
2780
2843
|
const lines = message.split(/\r?\n/);
|
|
2781
2844
|
if (isOriginalError) {
|
|
2782
|
-
write(` ${label} ${
|
|
2845
|
+
write(` ${label} ${chalk5.dim(lines[0] ?? "Original error:")}${suffix}`);
|
|
2783
2846
|
for (const line of lines.slice(1)) {
|
|
2784
|
-
write(
|
|
2847
|
+
write(chalk5.dim(` ${line}`));
|
|
2785
2848
|
}
|
|
2786
2849
|
return;
|
|
2787
2850
|
}
|
|
@@ -2842,7 +2905,7 @@ function printQr(url) {
|
|
|
2842
2905
|
console.log();
|
|
2843
2906
|
qrcode.generate(url, { small: true }, (output) => {
|
|
2844
2907
|
for (const line of output.split("\n")) {
|
|
2845
|
-
console.log(` ${
|
|
2908
|
+
console.log(` ${chalk5.dim(line)}`);
|
|
2846
2909
|
}
|
|
2847
2910
|
});
|
|
2848
2911
|
console.log();
|
|
@@ -2888,22 +2951,22 @@ function registerProjectsCommand(program2) {
|
|
|
2888
2951
|
);
|
|
2889
2952
|
}
|
|
2890
2953
|
async function runListProjects() {
|
|
2891
|
-
const spinner =
|
|
2954
|
+
const spinner = ora10("Fetching projects...").start();
|
|
2892
2955
|
const projects = await listProjects();
|
|
2893
2956
|
spinner.stop();
|
|
2894
2957
|
if (projects.length === 0) {
|
|
2895
|
-
console.log(
|
|
2958
|
+
console.log(chalk5.dim("\n No projects yet. Create one with `doable projects create`.\n"));
|
|
2896
2959
|
return;
|
|
2897
2960
|
}
|
|
2898
2961
|
console.log();
|
|
2899
|
-
console.log(
|
|
2962
|
+
console.log(chalk5.bold(" Your Projects"));
|
|
2900
2963
|
console.log();
|
|
2901
2964
|
const nameWidth = Math.max(6, ...projects.map((p) => p.name.length));
|
|
2902
2965
|
const slugWidth = Math.max(6, ...projects.map((p) => p.slug.length));
|
|
2903
2966
|
const runtimeWidth = Math.max(7, ...projects.map((p) => p.runtime.length));
|
|
2904
2967
|
const header = ` ${pad("NAME", nameWidth)} ${pad("SLUG", slugWidth)} ${pad("RUNTIME", runtimeWidth)} CREATED`;
|
|
2905
|
-
console.log(
|
|
2906
|
-
console.log(
|
|
2968
|
+
console.log(chalk5.dim(header));
|
|
2969
|
+
console.log(chalk5.dim(" " + "-".repeat(header.length - 2)));
|
|
2907
2970
|
for (const project of projects) {
|
|
2908
2971
|
const createdAt = formatDate(project.createdAt);
|
|
2909
2972
|
const row = ` ${pad(project.name, nameWidth)} ${pad(project.slug, slugWidth)} ${pad(project.runtime, runtimeWidth)} ${createdAt}`;
|
|
@@ -2961,7 +3024,7 @@ async function runCreateProject(opts = {}) {
|
|
|
2961
3024
|
}
|
|
2962
3025
|
} else {
|
|
2963
3026
|
console.log();
|
|
2964
|
-
console.log(
|
|
3027
|
+
console.log(chalk5.bold(" Create a new project"));
|
|
2965
3028
|
console.log();
|
|
2966
3029
|
const answers = await inquirer3.prompt(
|
|
2967
3030
|
[
|
|
@@ -3000,7 +3063,7 @@ async function runCreateProject(opts = {}) {
|
|
|
3000
3063
|
slug = slug ?? answers.slug;
|
|
3001
3064
|
runtime = runtime ?? answers.runtime;
|
|
3002
3065
|
}
|
|
3003
|
-
const spinner =
|
|
3066
|
+
const spinner = ora10("Creating project...").start();
|
|
3004
3067
|
let repoUrl;
|
|
3005
3068
|
let repoBranch;
|
|
3006
3069
|
let githubPat;
|
|
@@ -3037,20 +3100,20 @@ async function runCreateProject(opts = {}) {
|
|
|
3037
3100
|
}
|
|
3038
3101
|
throw err;
|
|
3039
3102
|
}
|
|
3040
|
-
spinner.succeed(`Project ${
|
|
3041
|
-
console.log(
|
|
3042
|
-
console.log(
|
|
3103
|
+
spinner.succeed(`Project ${chalk5.bold(resp.project.name)} created!`);
|
|
3104
|
+
console.log(chalk5.dim(` Slug: ${resp.project.slug}`));
|
|
3105
|
+
console.log(chalk5.dim(` ID: ${resp.project.id}`));
|
|
3043
3106
|
if (resp.subdomain) {
|
|
3044
|
-
console.log(
|
|
3107
|
+
console.log(chalk5.dim(` URL: https://${resp.subdomain}`));
|
|
3045
3108
|
}
|
|
3046
3109
|
if (repoUrl) {
|
|
3047
|
-
console.log(
|
|
3110
|
+
console.log(chalk5.dim(` Repo: ${repoUrl} (branch: ${repoBranch || "repo default"}${githubPat ? ", private" : ""})`));
|
|
3048
3111
|
}
|
|
3049
3112
|
console.log();
|
|
3050
3113
|
if (repoUrl) {
|
|
3051
|
-
console.log(` Deploy with: ${
|
|
3114
|
+
console.log(` Deploy with: ${chalk5.cyan(`doable deploy --from-repo --project ${resp.project.id}`)}`);
|
|
3052
3115
|
} else {
|
|
3053
|
-
console.log(` Deploy with: ${
|
|
3116
|
+
console.log(` Deploy with: ${chalk5.cyan(`doable deploy --project ${resp.project.id}`)}`);
|
|
3054
3117
|
}
|
|
3055
3118
|
console.log();
|
|
3056
3119
|
}
|
|
@@ -3072,33 +3135,33 @@ function formatDate(isoDate) {
|
|
|
3072
3135
|
function handleError(err) {
|
|
3073
3136
|
if (err instanceof ApiError) {
|
|
3074
3137
|
if (err.status === 401) {
|
|
3075
|
-
console.error(
|
|
3138
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
3076
3139
|
} else if (err.status === 0) {
|
|
3077
|
-
console.error(
|
|
3140
|
+
console.error(chalk5.red(`Connection error: ${err.message}`));
|
|
3078
3141
|
} else if (/project limit reached/i.test(err.message)) {
|
|
3079
3142
|
handlePlanLimitReached(err.message).finally(() => process.exit(1));
|
|
3080
3143
|
return;
|
|
3081
3144
|
} else {
|
|
3082
|
-
console.error(
|
|
3145
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3083
3146
|
}
|
|
3084
3147
|
} else if (err instanceof Error) {
|
|
3085
|
-
console.error(
|
|
3148
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3086
3149
|
} else {
|
|
3087
|
-
console.error(
|
|
3150
|
+
console.error(chalk5.red(`Unexpected error: ${String(err)}`));
|
|
3088
3151
|
}
|
|
3089
3152
|
process.exit(1);
|
|
3090
3153
|
}
|
|
3091
3154
|
async function handlePlanLimitReached(apiMessage) {
|
|
3092
3155
|
const upgradeUrl = "https://doable.do/billing";
|
|
3093
3156
|
console.log();
|
|
3094
|
-
console.log(
|
|
3157
|
+
console.log(chalk5.yellow.bold(" You've reached your plan's project limit."));
|
|
3095
3158
|
console.log();
|
|
3096
|
-
console.log(
|
|
3159
|
+
console.log(chalk5.dim(` ${apiMessage}`));
|
|
3097
3160
|
console.log();
|
|
3098
3161
|
console.log(" Upgrade your plan to create more projects:");
|
|
3099
|
-
console.log(` ${
|
|
3162
|
+
console.log(` ${chalk5.cyan.underline(upgradeUrl)}`);
|
|
3100
3163
|
console.log();
|
|
3101
|
-
console.log(
|
|
3164
|
+
console.log(chalk5.dim(" Free: 1 project Starter: 3 projects Pro: 10 projects"));
|
|
3102
3165
|
console.log();
|
|
3103
3166
|
if (!process.stdin.isTTY) return;
|
|
3104
3167
|
const { open: open4 } = await inquirer3.prompt([
|
|
@@ -3114,7 +3177,7 @@ async function handlePlanLimitReached(apiMessage) {
|
|
|
3114
3177
|
const { default: openBrowser2 } = await import('open');
|
|
3115
3178
|
await openBrowser2(upgradeUrl);
|
|
3116
3179
|
} catch {
|
|
3117
|
-
console.log(
|
|
3180
|
+
console.log(chalk5.dim(` Open manually: ${upgradeUrl}`));
|
|
3118
3181
|
}
|
|
3119
3182
|
}
|
|
3120
3183
|
}
|
|
@@ -3169,37 +3232,37 @@ function registerDomainCommand(program2) {
|
|
|
3169
3232
|
});
|
|
3170
3233
|
}
|
|
3171
3234
|
async function runSearch(name) {
|
|
3172
|
-
const spinner =
|
|
3235
|
+
const spinner = ora10(`Checking availability for ${chalk5.bold(name)}...`).start();
|
|
3173
3236
|
const result = await searchDomain(name);
|
|
3174
3237
|
spinner.stop();
|
|
3175
3238
|
console.log();
|
|
3176
|
-
console.log(
|
|
3239
|
+
console.log(chalk5.bold(` Domain: ${result.domain}`));
|
|
3177
3240
|
console.log();
|
|
3178
3241
|
if (result.available) {
|
|
3179
|
-
console.log(` Status: ${
|
|
3242
|
+
console.log(` Status: ${chalk5.green("Available")}`);
|
|
3180
3243
|
if (result.pricing) {
|
|
3181
3244
|
for (const [tld, prices] of Object.entries(result.pricing)) {
|
|
3182
|
-
console.log(` Price (${tld}): ${
|
|
3245
|
+
console.log(` Price (${tld}): ${chalk5.bold(`$${prices.registration}`)}/year (renewal: $${prices.renewal}/year)`);
|
|
3183
3246
|
}
|
|
3184
3247
|
}
|
|
3185
3248
|
console.log();
|
|
3186
3249
|
console.log(
|
|
3187
|
-
` Register at: ${
|
|
3250
|
+
` Register at: ${chalk5.cyan(buildNamecheapBuyUrl(result.domain))}`
|
|
3188
3251
|
);
|
|
3189
3252
|
console.log(
|
|
3190
|
-
|
|
3253
|
+
chalk5.dim(
|
|
3191
3254
|
" (Opens Namecheap. Once you own it, run `doable domain connect` to point it at your project.)"
|
|
3192
3255
|
)
|
|
3193
3256
|
);
|
|
3194
3257
|
} else {
|
|
3195
|
-
console.log(` Status: ${
|
|
3258
|
+
console.log(` Status: ${chalk5.red("Not available")}`);
|
|
3196
3259
|
console.log();
|
|
3197
|
-
console.log(
|
|
3260
|
+
console.log(chalk5.dim(" Try a different name or TLD."));
|
|
3198
3261
|
}
|
|
3199
3262
|
console.log();
|
|
3200
3263
|
}
|
|
3201
3264
|
async function runBuy(hostname) {
|
|
3202
|
-
const searchSpinner =
|
|
3265
|
+
const searchSpinner = ora10(`Checking availability for ${chalk5.bold(hostname)}...`).start();
|
|
3203
3266
|
let searchResult;
|
|
3204
3267
|
try {
|
|
3205
3268
|
searchResult = await searchDomain(hostname);
|
|
@@ -3208,75 +3271,75 @@ async function runBuy(hostname) {
|
|
|
3208
3271
|
searchSpinner.stop();
|
|
3209
3272
|
}
|
|
3210
3273
|
if (searchResult && !searchResult.available) {
|
|
3211
|
-
console.error(
|
|
3274
|
+
console.error(chalk5.red(`
|
|
3212
3275
|
Domain ${hostname} is not available.
|
|
3213
3276
|
`));
|
|
3214
3277
|
process.exit(1);
|
|
3215
3278
|
}
|
|
3216
3279
|
const url = buildNamecheapBuyUrl(hostname);
|
|
3217
3280
|
console.log();
|
|
3218
|
-
console.log(
|
|
3281
|
+
console.log(chalk5.bold(` Register ${hostname} via Namecheap`));
|
|
3219
3282
|
if (searchResult?.pricing) {
|
|
3220
3283
|
const tld = hostname.split(".").slice(1).join(".");
|
|
3221
3284
|
const pricing = searchResult.pricing[tld];
|
|
3222
3285
|
if (pricing) {
|
|
3223
3286
|
console.log(
|
|
3224
|
-
` Indicative price: ${
|
|
3287
|
+
` Indicative price: ${chalk5.bold(`$${pricing.registration}`)} / year (renewal $${pricing.renewal})`
|
|
3225
3288
|
);
|
|
3226
3289
|
}
|
|
3227
3290
|
}
|
|
3228
3291
|
console.log();
|
|
3229
|
-
console.log(` ${
|
|
3292
|
+
console.log(` ${chalk5.cyan(url)}`);
|
|
3230
3293
|
console.log();
|
|
3231
|
-
console.log(
|
|
3232
|
-
console.log(
|
|
3233
|
-
console.log(` ${
|
|
3294
|
+
console.log(chalk5.dim(" Doable earns a small commission on this referral; price to you is unchanged."));
|
|
3295
|
+
console.log(chalk5.dim(" Once registered, run:"));
|
|
3296
|
+
console.log(` ${chalk5.cyan(`doable domain connect ${hostname} --project <id>`)}`);
|
|
3234
3297
|
console.log();
|
|
3235
3298
|
tryOpenUrl(url);
|
|
3236
3299
|
}
|
|
3237
3300
|
async function runConnect(hostname, projectId) {
|
|
3238
|
-
const projectSpinner =
|
|
3301
|
+
const projectSpinner = ora10("Resolving project...").start();
|
|
3239
3302
|
const project = await getProject(projectId);
|
|
3240
3303
|
projectSpinner.stop();
|
|
3241
|
-
const connectSpinner =
|
|
3304
|
+
const connectSpinner = ora10(`Connecting ${chalk5.bold(hostname)}...`).start();
|
|
3242
3305
|
const resp = await connectDomain({
|
|
3243
3306
|
hostname,
|
|
3244
3307
|
projectId: project.id
|
|
3245
3308
|
});
|
|
3246
|
-
connectSpinner.succeed(`Domain ${
|
|
3309
|
+
connectSpinner.succeed(`Domain ${chalk5.bold(hostname)} connection started.`);
|
|
3247
3310
|
console.log();
|
|
3248
|
-
console.log(
|
|
3311
|
+
console.log(chalk5.bold(" Configure the following DNS record with your registrar:"));
|
|
3249
3312
|
console.log();
|
|
3250
3313
|
const v = resp.verification;
|
|
3251
3314
|
const typeWidth = v.type.length;
|
|
3252
3315
|
const nameWidth = v.name.length;
|
|
3253
3316
|
console.log(
|
|
3254
|
-
|
|
3317
|
+
chalk5.dim(` ${pad2("TYPE", typeWidth)} ${pad2("NAME", nameWidth)} VALUE`)
|
|
3255
3318
|
);
|
|
3256
|
-
console.log(
|
|
3319
|
+
console.log(chalk5.dim(" " + "-".repeat(typeWidth + nameWidth + 40)));
|
|
3257
3320
|
console.log(
|
|
3258
3321
|
` ${pad2(v.type, typeWidth)} ${pad2(v.name, nameWidth)} ${v.value}`
|
|
3259
3322
|
);
|
|
3260
3323
|
console.log();
|
|
3261
3324
|
console.log(
|
|
3262
|
-
` After configuring DNS, verify with: ${
|
|
3325
|
+
` After configuring DNS, verify with: ${chalk5.cyan(`doable domain verify ${resp.domain.id}`)}`
|
|
3263
3326
|
);
|
|
3264
3327
|
console.log();
|
|
3265
3328
|
}
|
|
3266
3329
|
async function runVerify(domainId) {
|
|
3267
|
-
const spinner =
|
|
3330
|
+
const spinner = ora10(`Verifying DNS for domain ${chalk5.bold(domainId)}...`).start();
|
|
3268
3331
|
const resp = await verifyDomain(domainId);
|
|
3269
3332
|
if (resp.verified) {
|
|
3270
|
-
spinner.succeed(`Domain is ${
|
|
3333
|
+
spinner.succeed(`Domain is ${chalk5.green("verified")} and active!`);
|
|
3271
3334
|
} else {
|
|
3272
|
-
spinner.fail(`Domain DNS verification ${
|
|
3335
|
+
spinner.fail(`Domain DNS verification ${chalk5.red("failed")}.`);
|
|
3273
3336
|
console.log();
|
|
3274
3337
|
console.log(
|
|
3275
|
-
|
|
3338
|
+
chalk5.yellow(
|
|
3276
3339
|
" DNS records have not propagated yet. This can take up to 48 hours."
|
|
3277
3340
|
)
|
|
3278
3341
|
);
|
|
3279
|
-
console.log(` Run ${
|
|
3342
|
+
console.log(` Run ${chalk5.cyan(`doable domain verify ${domainId}`)} again later.`);
|
|
3280
3343
|
}
|
|
3281
3344
|
console.log();
|
|
3282
3345
|
}
|
|
@@ -3286,18 +3349,18 @@ function pad2(str, width) {
|
|
|
3286
3349
|
function handleError2(err) {
|
|
3287
3350
|
if (err instanceof ApiError) {
|
|
3288
3351
|
if (err.status === 401) {
|
|
3289
|
-
console.error(
|
|
3352
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
3290
3353
|
} else if (err.status === 0) {
|
|
3291
|
-
console.error(
|
|
3354
|
+
console.error(chalk5.red(`Connection error: ${err.message}`));
|
|
3292
3355
|
} else if (err.status === 404) {
|
|
3293
|
-
console.error(
|
|
3356
|
+
console.error(chalk5.red(`Not found: ${err.message}`));
|
|
3294
3357
|
} else {
|
|
3295
|
-
console.error(
|
|
3358
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3296
3359
|
}
|
|
3297
3360
|
} else if (err instanceof Error) {
|
|
3298
|
-
console.error(
|
|
3361
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3299
3362
|
} else {
|
|
3300
|
-
console.error(
|
|
3363
|
+
console.error(chalk5.red(`Unexpected error: ${String(err)}`));
|
|
3301
3364
|
}
|
|
3302
3365
|
process.exit(1);
|
|
3303
3366
|
}
|
|
@@ -3334,24 +3397,24 @@ function registerServerCommand(program2) {
|
|
|
3334
3397
|
});
|
|
3335
3398
|
}
|
|
3336
3399
|
async function runSetAddonsEnabled(serverId, enabled) {
|
|
3337
|
-
const spinner =
|
|
3400
|
+
const spinner = ora10(
|
|
3338
3401
|
enabled ? "Enabling addons on this server..." : "Disabling addons on this server..."
|
|
3339
3402
|
).start();
|
|
3340
3403
|
try {
|
|
3341
3404
|
const updated = await updateServer(serverId, { addonsEnabled: enabled });
|
|
3342
3405
|
spinner.succeed(
|
|
3343
|
-
enabled ? `Addons enabled on ${
|
|
3406
|
+
enabled ? `Addons enabled on ${chalk5.bold(updated.name)}.` : `Addons disabled on ${chalk5.bold(updated.name)}.`
|
|
3344
3407
|
);
|
|
3345
3408
|
console.log();
|
|
3346
3409
|
if (enabled) {
|
|
3347
3410
|
console.log(
|
|
3348
|
-
|
|
3411
|
+
chalk5.dim(
|
|
3349
3412
|
" This BYO server will now receive new native addons (Postgres, Redis,\n MongoDB) for your account. Existing addons on cloud servers are not\n moved \u2014 only newly-provisioned ones will go here. Make sure the host\n has enough disk and memory for database workloads."
|
|
3350
3413
|
)
|
|
3351
3414
|
);
|
|
3352
3415
|
} else {
|
|
3353
3416
|
console.log(
|
|
3354
|
-
|
|
3417
|
+
chalk5.dim(
|
|
3355
3418
|
" New native addons will go to Doable Cloud instead. Any addons already\n running on this server are unaffected \u2014 delete them via the dashboard\n if you want to free up resources."
|
|
3356
3419
|
)
|
|
3357
3420
|
);
|
|
@@ -3363,52 +3426,52 @@ async function runSetAddonsEnabled(serverId, enabled) {
|
|
|
3363
3426
|
}
|
|
3364
3427
|
}
|
|
3365
3428
|
async function runAddServer(name) {
|
|
3366
|
-
const spinner =
|
|
3429
|
+
const spinner = ora10("Registering server...").start();
|
|
3367
3430
|
const resp = await addServer({ name });
|
|
3368
3431
|
const srv = resp.server;
|
|
3369
|
-
spinner.succeed(`Server ${
|
|
3432
|
+
spinner.succeed(`Server ${chalk5.bold(srv.name)} registered.`);
|
|
3370
3433
|
console.log();
|
|
3371
|
-
console.log(
|
|
3434
|
+
console.log(chalk5.bold(" Install the Doable agent on your server:"));
|
|
3372
3435
|
console.log();
|
|
3373
3436
|
console.log(
|
|
3374
|
-
` ${
|
|
3437
|
+
` ${chalk5.cyan(`curl -sSL https://doable.do/install-agent.sh | sh -s -- --enrollment-token ${resp.enrollmentToken}`)}`
|
|
3375
3438
|
);
|
|
3376
3439
|
console.log();
|
|
3377
3440
|
console.log(
|
|
3378
|
-
|
|
3441
|
+
chalk5.dim(" Run this command on the server you want to connect. The agent will")
|
|
3379
3442
|
);
|
|
3380
3443
|
console.log(
|
|
3381
|
-
|
|
3444
|
+
chalk5.dim(" register itself and appear as 'online' in `doable server list`.")
|
|
3382
3445
|
);
|
|
3383
3446
|
console.log();
|
|
3384
|
-
console.log(
|
|
3385
|
-
console.log(
|
|
3386
|
-
console.log(
|
|
3447
|
+
console.log(chalk5.dim(` Server ID: ${srv.id}`));
|
|
3448
|
+
console.log(chalk5.dim(` Status: ${srv.status}`));
|
|
3449
|
+
console.log(chalk5.dim(` Token expires: ${resp.expiresAt}`));
|
|
3387
3450
|
console.log();
|
|
3388
3451
|
}
|
|
3389
3452
|
async function runListServers() {
|
|
3390
|
-
const spinner =
|
|
3453
|
+
const spinner = ora10("Fetching servers...").start();
|
|
3391
3454
|
const servers = await listServers();
|
|
3392
3455
|
spinner.stop();
|
|
3393
3456
|
if (servers.length === 0) {
|
|
3394
|
-
console.log(
|
|
3457
|
+
console.log(chalk5.dim("\n No servers registered. Add one with `doable server add --name <name>`.\n"));
|
|
3395
3458
|
return;
|
|
3396
3459
|
}
|
|
3397
3460
|
console.log();
|
|
3398
|
-
console.log(
|
|
3461
|
+
console.log(chalk5.bold(" Your Servers"));
|
|
3399
3462
|
console.log();
|
|
3400
3463
|
const nameWidth = Math.max(4, ...servers.map((s) => s.name.length));
|
|
3401
3464
|
const statusWidth = Math.max(6, ...servers.map((s) => s.status.length));
|
|
3402
3465
|
const ipWidth = Math.max(10, ...servers.map((s) => (s.publicIp || "--").length));
|
|
3403
3466
|
const header = ` ${pad3("NAME", nameWidth)} ${pad3("STATUS", statusWidth)} ${pad3("IP ADDRESS", ipWidth)} ${pad3("ADDONS", 8)} LAST SEEN`;
|
|
3404
|
-
console.log(
|
|
3405
|
-
console.log(
|
|
3467
|
+
console.log(chalk5.dim(header));
|
|
3468
|
+
console.log(chalk5.dim(" " + "-".repeat(header.length - 2)));
|
|
3406
3469
|
for (const srv of servers) {
|
|
3407
3470
|
const statusColor = getStatusColor(srv.status);
|
|
3408
|
-
const lastSeen = srv.lastSeenAt ? formatDate2(srv.lastSeenAt) :
|
|
3409
|
-
const ip = srv.publicIp ||
|
|
3471
|
+
const lastSeen = srv.lastSeenAt ? formatDate2(srv.lastSeenAt) : chalk5.dim("never");
|
|
3472
|
+
const ip = srv.publicIp || chalk5.dim("--");
|
|
3410
3473
|
const addonsRaw = srv.type === "doable_cloud" ? "cloud" : srv.addonsEnabled ? "on" : "off";
|
|
3411
|
-
const addonsColor = srv.type === "doable_cloud" ?
|
|
3474
|
+
const addonsColor = srv.type === "doable_cloud" ? chalk5.green : srv.addonsEnabled ? chalk5.green : chalk5.dim;
|
|
3412
3475
|
const addonsCell = addonsColor(pad3(addonsRaw, 8));
|
|
3413
3476
|
console.log(
|
|
3414
3477
|
` ${pad3(srv.name, nameWidth)} ${statusColor(pad3(srv.status, statusWidth))} ${pad3(String(ip), ipWidth)} ${addonsCell} ${lastSeen}`
|
|
@@ -3436,30 +3499,30 @@ function getStatusColor(status) {
|
|
|
3436
3499
|
case "online":
|
|
3437
3500
|
case "connected":
|
|
3438
3501
|
case "active":
|
|
3439
|
-
return
|
|
3502
|
+
return chalk5.green;
|
|
3440
3503
|
case "pending":
|
|
3441
3504
|
case "connecting":
|
|
3442
|
-
return
|
|
3505
|
+
return chalk5.yellow;
|
|
3443
3506
|
case "offline":
|
|
3444
3507
|
case "disconnected":
|
|
3445
|
-
return
|
|
3508
|
+
return chalk5.red;
|
|
3446
3509
|
default:
|
|
3447
|
-
return
|
|
3510
|
+
return chalk5.dim;
|
|
3448
3511
|
}
|
|
3449
3512
|
}
|
|
3450
3513
|
function handleError3(err) {
|
|
3451
3514
|
if (err instanceof ApiError) {
|
|
3452
3515
|
if (err.status === 401) {
|
|
3453
|
-
console.error(
|
|
3516
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
3454
3517
|
} else if (err.status === 0) {
|
|
3455
|
-
console.error(
|
|
3518
|
+
console.error(chalk5.red(`Connection error: ${err.message}`));
|
|
3456
3519
|
} else {
|
|
3457
|
-
console.error(
|
|
3520
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3458
3521
|
}
|
|
3459
3522
|
} else if (err instanceof Error) {
|
|
3460
|
-
console.error(
|
|
3523
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3461
3524
|
} else {
|
|
3462
|
-
console.error(
|
|
3525
|
+
console.error(chalk5.red(`Unexpected error: ${String(err)}`));
|
|
3463
3526
|
}
|
|
3464
3527
|
process.exit(1);
|
|
3465
3528
|
}
|
|
@@ -3475,22 +3538,22 @@ function registerLogsCommand(program2) {
|
|
|
3475
3538
|
async function runLogs(deploymentId, follow) {
|
|
3476
3539
|
const token = getToken();
|
|
3477
3540
|
if (!token) {
|
|
3478
|
-
console.error(
|
|
3541
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
3479
3542
|
process.exit(1);
|
|
3480
3543
|
}
|
|
3481
|
-
const verifySpinner =
|
|
3544
|
+
const verifySpinner = ora10("Connecting to deployment...").start();
|
|
3482
3545
|
let deployment;
|
|
3483
3546
|
try {
|
|
3484
3547
|
deployment = await getDeployment(deploymentId);
|
|
3485
3548
|
} catch (err) {
|
|
3486
3549
|
verifySpinner.fail("Failed to find deployment.");
|
|
3487
3550
|
if (err instanceof ApiError) {
|
|
3488
|
-
console.error(
|
|
3551
|
+
console.error(chalk5.red(err.message));
|
|
3489
3552
|
}
|
|
3490
3553
|
process.exit(1);
|
|
3491
3554
|
}
|
|
3492
3555
|
verifySpinner.succeed(
|
|
3493
|
-
`Streaming logs for deployment ${
|
|
3556
|
+
`Streaming logs for deployment ${chalk5.dim(deployment.id)} (${deployment.status})`
|
|
3494
3557
|
);
|
|
3495
3558
|
console.log();
|
|
3496
3559
|
const url = getLogsStreamUrl(deploymentId);
|
|
@@ -3505,20 +3568,20 @@ async function runLogs(deploymentId, follow) {
|
|
|
3505
3568
|
});
|
|
3506
3569
|
} catch (fetchErr) {
|
|
3507
3570
|
const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
3508
|
-
console.error(
|
|
3571
|
+
console.error(chalk5.red(`Failed to connect to log stream: ${msg}`));
|
|
3509
3572
|
process.exit(1);
|
|
3510
3573
|
return;
|
|
3511
3574
|
}
|
|
3512
3575
|
if (!res.ok) {
|
|
3513
3576
|
if (res.status === 401) {
|
|
3514
|
-
console.error(
|
|
3577
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
3515
3578
|
} else {
|
|
3516
|
-
console.error(
|
|
3579
|
+
console.error(chalk5.red(`Failed to connect to log stream: ${res.statusText || `HTTP ${res.status}`}`));
|
|
3517
3580
|
}
|
|
3518
3581
|
process.exit(1);
|
|
3519
3582
|
}
|
|
3520
3583
|
if (!res.body) {
|
|
3521
|
-
console.error(
|
|
3584
|
+
console.error(chalk5.red("No response body from log stream."));
|
|
3522
3585
|
process.exit(1);
|
|
3523
3586
|
}
|
|
3524
3587
|
const reader = res.body.getReader();
|
|
@@ -3542,9 +3605,9 @@ async function runLogs(deploymentId, follow) {
|
|
|
3542
3605
|
if (eventType === "done" || eventType === "complete" || eventType === "error") {
|
|
3543
3606
|
console.log();
|
|
3544
3607
|
if (eventType === "error") {
|
|
3545
|
-
console.log(
|
|
3608
|
+
console.log(chalk5.red.bold(" Deployment failed."));
|
|
3546
3609
|
} else {
|
|
3547
|
-
console.log(
|
|
3610
|
+
console.log(chalk5.green.bold(" Deployment complete."));
|
|
3548
3611
|
}
|
|
3549
3612
|
console.log();
|
|
3550
3613
|
return;
|
|
@@ -3555,14 +3618,14 @@ async function runLogs(deploymentId, follow) {
|
|
|
3555
3618
|
}
|
|
3556
3619
|
}
|
|
3557
3620
|
console.log();
|
|
3558
|
-
console.log(
|
|
3621
|
+
console.log(chalk5.dim(" Log stream ended."));
|
|
3559
3622
|
console.log();
|
|
3560
3623
|
} catch (err) {
|
|
3561
3624
|
if (err instanceof Error && err.name === "AbortError") {
|
|
3562
3625
|
return;
|
|
3563
3626
|
}
|
|
3564
3627
|
console.error(
|
|
3565
|
-
|
|
3628
|
+
chalk5.red(`Log stream error: ${err instanceof Error ? err.message : String(err)}`)
|
|
3566
3629
|
);
|
|
3567
3630
|
process.exit(1);
|
|
3568
3631
|
}
|
|
@@ -3579,7 +3642,7 @@ function printLogEvent(type, data) {
|
|
|
3579
3642
|
if (ts) {
|
|
3580
3643
|
try {
|
|
3581
3644
|
const d = new Date(ts);
|
|
3582
|
-
timestamp =
|
|
3645
|
+
timestamp = chalk5.dim(
|
|
3583
3646
|
d.toLocaleTimeString("en-US", { hour12: false }) + " "
|
|
3584
3647
|
);
|
|
3585
3648
|
} catch {
|
|
@@ -3593,40 +3656,40 @@ function printLogEvent(type, data) {
|
|
|
3593
3656
|
function getLogPrefix(level) {
|
|
3594
3657
|
switch (level.toLowerCase()) {
|
|
3595
3658
|
case "error":
|
|
3596
|
-
return
|
|
3659
|
+
return chalk5.red("ERROR");
|
|
3597
3660
|
case "warning":
|
|
3598
3661
|
case "warn":
|
|
3599
|
-
return
|
|
3662
|
+
return chalk5.yellow("WARN ");
|
|
3600
3663
|
case "info":
|
|
3601
3664
|
case "message":
|
|
3602
|
-
return
|
|
3665
|
+
return chalk5.blue("INFO ");
|
|
3603
3666
|
case "debug":
|
|
3604
|
-
return
|
|
3667
|
+
return chalk5.dim("DEBUG");
|
|
3605
3668
|
case "step":
|
|
3606
|
-
return
|
|
3669
|
+
return chalk5.magenta("STEP ");
|
|
3607
3670
|
case "success":
|
|
3608
3671
|
case "done":
|
|
3609
3672
|
case "complete":
|
|
3610
|
-
return
|
|
3673
|
+
return chalk5.green("OK ");
|
|
3611
3674
|
default:
|
|
3612
|
-
return
|
|
3675
|
+
return chalk5.dim("LOG ");
|
|
3613
3676
|
}
|
|
3614
3677
|
}
|
|
3615
3678
|
function handleError4(err) {
|
|
3616
3679
|
if (err instanceof ApiError) {
|
|
3617
3680
|
if (err.status === 401) {
|
|
3618
|
-
console.error(
|
|
3681
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
3619
3682
|
} else if (err.status === 0) {
|
|
3620
|
-
console.error(
|
|
3683
|
+
console.error(chalk5.red(`Connection error: ${err.message}`));
|
|
3621
3684
|
} else if (err.status === 404) {
|
|
3622
|
-
console.error(
|
|
3685
|
+
console.error(chalk5.red(`Deployment not found. Check the deployment ID and try again.`));
|
|
3623
3686
|
} else {
|
|
3624
|
-
console.error(
|
|
3687
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3625
3688
|
}
|
|
3626
3689
|
} else if (err instanceof Error) {
|
|
3627
|
-
console.error(
|
|
3690
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
3628
3691
|
} else {
|
|
3629
|
-
console.error(
|
|
3692
|
+
console.error(chalk5.red(`Unexpected error: ${String(err)}`));
|
|
3630
3693
|
}
|
|
3631
3694
|
process.exit(1);
|
|
3632
3695
|
}
|
|
@@ -3651,14 +3714,14 @@ function writeMcpJson(filePath, label) {
|
|
|
3651
3714
|
}
|
|
3652
3715
|
const servers = existing.mcpServers ?? {};
|
|
3653
3716
|
if (servers.doable) {
|
|
3654
|
-
console.log(
|
|
3717
|
+
console.log(chalk5.dim(` ${label}: already configured`));
|
|
3655
3718
|
return "already_configured";
|
|
3656
3719
|
}
|
|
3657
3720
|
servers.doable = MCP_CONFIG.doable;
|
|
3658
3721
|
existing.mcpServers = servers;
|
|
3659
3722
|
ensureDir(path7.dirname(filePath));
|
|
3660
3723
|
fs6.writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n");
|
|
3661
|
-
console.log(
|
|
3724
|
+
console.log(chalk5.green(` ${label}: configured`));
|
|
3662
3725
|
return "created";
|
|
3663
3726
|
}
|
|
3664
3727
|
function detectEditor() {
|
|
@@ -3680,9 +3743,9 @@ async function runSetup(opts) {
|
|
|
3680
3743
|
const doAll = !opts.claude && !opts.cursor && !opts.codex && !opts.project;
|
|
3681
3744
|
const detected = detectEditor();
|
|
3682
3745
|
console.log();
|
|
3683
|
-
console.log(
|
|
3746
|
+
console.log(chalk5.bold(" Doable MCP Setup"));
|
|
3684
3747
|
if (detected !== "unknown") {
|
|
3685
|
-
console.log(
|
|
3748
|
+
console.log(chalk5.dim(` Detected editor: ${detected}`));
|
|
3686
3749
|
}
|
|
3687
3750
|
console.log();
|
|
3688
3751
|
let changedSomething = false;
|
|
@@ -3700,50 +3763,50 @@ async function runSetup(opts) {
|
|
|
3700
3763
|
const homeSettings = path7.join(os.homedir(), ".claude", "settings.json");
|
|
3701
3764
|
if (!fs6.existsSync(homeSettings)) {
|
|
3702
3765
|
console.log(
|
|
3703
|
-
|
|
3766
|
+
chalk5.dim(
|
|
3704
3767
|
" Tip: Claude Code uses per-project .mcp.json \u2014 no global config needed."
|
|
3705
3768
|
)
|
|
3706
3769
|
);
|
|
3707
3770
|
}
|
|
3708
3771
|
}
|
|
3709
3772
|
console.log();
|
|
3710
|
-
console.log(
|
|
3773
|
+
console.log(chalk5.bold(" Next:"));
|
|
3711
3774
|
const loggedIn = Boolean(getToken());
|
|
3712
3775
|
if (!loggedIn) {
|
|
3713
3776
|
console.log(
|
|
3714
|
-
` 1. ${
|
|
3777
|
+
` 1. ${chalk5.cyan("doable login")} \u2014 opens your browser for a one-time sign-in`
|
|
3715
3778
|
);
|
|
3716
3779
|
console.log(` 2. Restart your editor so the MCP loads`);
|
|
3717
3780
|
console.log(
|
|
3718
|
-
` 3. In your editor, say ${
|
|
3781
|
+
` 3. In your editor, say ${chalk5.cyan('"deploy this"')} or type ${chalk5.cyan("/deploy")}`
|
|
3719
3782
|
);
|
|
3720
3783
|
} else {
|
|
3721
3784
|
console.log(` 1. Restart your editor so the MCP loads`);
|
|
3722
3785
|
console.log(
|
|
3723
|
-
` 2. In your editor, say ${
|
|
3786
|
+
` 2. In your editor, say ${chalk5.cyan('"deploy this"')} or type ${chalk5.cyan("/deploy")}`
|
|
3724
3787
|
);
|
|
3725
3788
|
console.log(
|
|
3726
|
-
|
|
3789
|
+
chalk5.dim(` (you're already logged in \u2014 skip straight to deploying)`)
|
|
3727
3790
|
);
|
|
3728
3791
|
}
|
|
3729
3792
|
console.log();
|
|
3730
3793
|
if (detected === "claude-code") {
|
|
3731
3794
|
console.log(
|
|
3732
|
-
|
|
3733
|
-
` Verify it loaded: run ${
|
|
3795
|
+
chalk5.dim(
|
|
3796
|
+
` Verify it loaded: run ${chalk5.cyan("/mcp")} in Claude Code after restart.`
|
|
3734
3797
|
)
|
|
3735
3798
|
);
|
|
3736
3799
|
console.log();
|
|
3737
3800
|
} else if (detected === "cursor") {
|
|
3738
3801
|
console.log(
|
|
3739
|
-
|
|
3802
|
+
chalk5.dim(
|
|
3740
3803
|
` Verify it loaded: Cursor \u2192 Settings \u2192 MCP \u2192 Doable should show up.`
|
|
3741
3804
|
)
|
|
3742
3805
|
);
|
|
3743
3806
|
console.log();
|
|
3744
3807
|
}
|
|
3745
3808
|
if (!changedSomething) {
|
|
3746
|
-
console.log(
|
|
3809
|
+
console.log(chalk5.dim(` Config already in place \u2014 no changes needed.`));
|
|
3747
3810
|
console.log();
|
|
3748
3811
|
}
|
|
3749
3812
|
}
|
|
@@ -3752,7 +3815,7 @@ function registerSetupCommand(program2) {
|
|
|
3752
3815
|
try {
|
|
3753
3816
|
await runSetup(opts);
|
|
3754
3817
|
} catch (err) {
|
|
3755
|
-
console.error(
|
|
3818
|
+
console.error(chalk5.red(err instanceof Error ? err.message : "Setup failed"));
|
|
3756
3819
|
process.exit(1);
|
|
3757
3820
|
}
|
|
3758
3821
|
});
|
|
@@ -3998,13 +4061,13 @@ async function runAttach(opts) {
|
|
|
3998
4061
|
const providers = PROVIDERS[type];
|
|
3999
4062
|
if (providers.length > 0) {
|
|
4000
4063
|
console.log();
|
|
4001
|
-
console.log(
|
|
4064
|
+
console.log(chalk5.bold(` Recommended ${LABELS[type]} providers:`));
|
|
4002
4065
|
console.log();
|
|
4003
4066
|
for (const p of providers) {
|
|
4004
4067
|
console.log(
|
|
4005
|
-
` ${
|
|
4068
|
+
` ${chalk5.cyan(p.name.padEnd(10))} ${chalk5.dim(p.tagline)}`
|
|
4006
4069
|
);
|
|
4007
|
-
console.log(` ${" ".repeat(10)} ${
|
|
4070
|
+
console.log(` ${" ".repeat(10)} ${chalk5.underline.dim(p.signupUrl)}`);
|
|
4008
4071
|
console.log();
|
|
4009
4072
|
}
|
|
4010
4073
|
const { pick: pick2 } = await inquirer3.prompt([
|
|
@@ -4027,12 +4090,12 @@ async function runAttach(opts) {
|
|
|
4027
4090
|
try {
|
|
4028
4091
|
const { default: openBrowser2 } = await import('open');
|
|
4029
4092
|
await openBrowser2(chosen.signupUrl);
|
|
4030
|
-
console.log(
|
|
4093
|
+
console.log(chalk5.dim(` Opened ${chosen.signupUrl} in your browser.`));
|
|
4031
4094
|
} catch {
|
|
4032
|
-
console.log(
|
|
4095
|
+
console.log(chalk5.dim(` Open manually: ${chosen.signupUrl}`));
|
|
4033
4096
|
}
|
|
4034
4097
|
console.log(
|
|
4035
|
-
|
|
4098
|
+
chalk5.dim(" Create your database there, copy the connection string, then paste it below.")
|
|
4036
4099
|
);
|
|
4037
4100
|
console.log();
|
|
4038
4101
|
}
|
|
@@ -4057,7 +4120,7 @@ async function runAttach(opts) {
|
|
|
4057
4120
|
const expectedScheme = type === "postgres" ? "postgres" : type === "redis" ? "redis" : type === "mongodb" ? "mongodb" : null;
|
|
4058
4121
|
if (expectedScheme && !url.toLowerCase().startsWith(expectedScheme)) {
|
|
4059
4122
|
console.log(
|
|
4060
|
-
|
|
4123
|
+
chalk5.yellow(
|
|
4061
4124
|
` Warning: URL doesn't start with '${expectedScheme}://'. Saving anyway \u2014 double-check it's correct.`
|
|
4062
4125
|
)
|
|
4063
4126
|
);
|
|
@@ -4099,12 +4162,12 @@ async function runAttach(opts) {
|
|
|
4099
4162
|
}
|
|
4100
4163
|
]);
|
|
4101
4164
|
if (!confirm) {
|
|
4102
|
-
console.log(
|
|
4165
|
+
console.log(chalk5.dim(" Cancelled."));
|
|
4103
4166
|
return;
|
|
4104
4167
|
}
|
|
4105
4168
|
}
|
|
4106
4169
|
}
|
|
4107
|
-
const spinner =
|
|
4170
|
+
const spinner = ora10(`Attaching ${envKey}...`).start();
|
|
4108
4171
|
try {
|
|
4109
4172
|
await putEnvVars(projectId, { [envKey]: url });
|
|
4110
4173
|
} catch (err) {
|
|
@@ -4113,8 +4176,8 @@ async function runAttach(opts) {
|
|
|
4113
4176
|
}
|
|
4114
4177
|
spinner.succeed(`${envKey} attached to project.`);
|
|
4115
4178
|
console.log();
|
|
4116
|
-
console.log(
|
|
4117
|
-
console.log(` ${
|
|
4179
|
+
console.log(chalk5.dim(" Redeploy for the change to take effect:"));
|
|
4180
|
+
console.log(` ${chalk5.cyan("doable deploy")}`);
|
|
4118
4181
|
console.log();
|
|
4119
4182
|
}
|
|
4120
4183
|
async function runAttachNative(opts) {
|
|
@@ -4137,16 +4200,16 @@ async function runAttachNative(opts) {
|
|
|
4137
4200
|
const engineLabel = type === "postgres" ? "Postgres database" : type === "redis" ? "Redis cache" : "MongoDB database";
|
|
4138
4201
|
console.log();
|
|
4139
4202
|
console.log(
|
|
4140
|
-
|
|
4203
|
+
chalk5.bold(` Provisioning a Doable-managed ${engineLabel}`)
|
|
4141
4204
|
);
|
|
4142
4205
|
console.log(
|
|
4143
|
-
|
|
4206
|
+
chalk5.dim(
|
|
4144
4207
|
` Version ${version}. Doable will create a container on your project's network
|
|
4145
4208
|
and inject ${envKey} automatically \u2014 no connection string to paste.`
|
|
4146
4209
|
)
|
|
4147
4210
|
);
|
|
4148
4211
|
console.log();
|
|
4149
|
-
const creating =
|
|
4212
|
+
const creating = ora10("Requesting addon...").start();
|
|
4150
4213
|
let addon;
|
|
4151
4214
|
try {
|
|
4152
4215
|
addon = await createAddon(projectId, {
|
|
@@ -4163,7 +4226,7 @@ async function runAttachNative(opts) {
|
|
|
4163
4226
|
throw err;
|
|
4164
4227
|
}
|
|
4165
4228
|
creating.succeed(`Addon ${addon.id.slice(0, 8)} created.`);
|
|
4166
|
-
const spinner =
|
|
4229
|
+
const spinner = ora10("Waiting for database to start...").start();
|
|
4167
4230
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
4168
4231
|
const pollIntervalMs = 2e3;
|
|
4169
4232
|
let current = addon;
|
|
@@ -4185,11 +4248,11 @@ async function runAttachNative(opts) {
|
|
|
4185
4248
|
spinner.fail("Database provisioning failed.");
|
|
4186
4249
|
if (current.statusMessage) {
|
|
4187
4250
|
console.log();
|
|
4188
|
-
console.log(
|
|
4251
|
+
console.log(chalk5.red(` ${current.statusMessage}`));
|
|
4189
4252
|
}
|
|
4190
4253
|
console.log();
|
|
4191
4254
|
console.log(
|
|
4192
|
-
|
|
4255
|
+
chalk5.dim(
|
|
4193
4256
|
" You can retry with `doable addon attach --native`. If this keeps happening\n contact support with the addon ID above."
|
|
4194
4257
|
)
|
|
4195
4258
|
);
|
|
@@ -4202,7 +4265,7 @@ async function runAttachNative(opts) {
|
|
|
4202
4265
|
spinner.fail("Timed out waiting for database to start.");
|
|
4203
4266
|
console.log();
|
|
4204
4267
|
console.log(
|
|
4205
|
-
|
|
4268
|
+
chalk5.dim(
|
|
4206
4269
|
` The addon is still in status '${current.status}'. It may finish shortly \u2014
|
|
4207
4270
|
run \`doable addon list\` to check again.`
|
|
4208
4271
|
)
|
|
@@ -4212,23 +4275,23 @@ async function runAttachNative(opts) {
|
|
|
4212
4275
|
}
|
|
4213
4276
|
const readyLabel = type === "postgres" ? "Database ready." : type === "redis" ? "Cache ready." : "MongoDB ready.";
|
|
4214
4277
|
console.log();
|
|
4215
|
-
console.log(
|
|
4278
|
+
console.log(chalk5.green(` \u2714 ${readyLabel}`));
|
|
4216
4279
|
console.log();
|
|
4217
|
-
console.log(` ${
|
|
4280
|
+
console.log(` ${chalk5.dim("Engine:")} ${type} ${current.version}`);
|
|
4218
4281
|
console.log(
|
|
4219
|
-
` ${
|
|
4282
|
+
` ${chalk5.dim("Host:")} ${current.containerName}:${current.port}`
|
|
4220
4283
|
);
|
|
4221
4284
|
if (type === "postgres") {
|
|
4222
|
-
console.log(` ${
|
|
4285
|
+
console.log(` ${chalk5.dim("Database:")} ${current.database}`);
|
|
4223
4286
|
}
|
|
4224
|
-
console.log(` ${
|
|
4287
|
+
console.log(` ${chalk5.dim("Env var:")} ${envKey}`);
|
|
4225
4288
|
console.log();
|
|
4226
4289
|
console.log(
|
|
4227
|
-
|
|
4290
|
+
chalk5.dim(
|
|
4228
4291
|
` ${envKey} has been set on your project. Redeploy to inject it:`
|
|
4229
4292
|
)
|
|
4230
4293
|
);
|
|
4231
|
-
console.log(` ${
|
|
4294
|
+
console.log(` ${chalk5.cyan("doable deploy")}`);
|
|
4232
4295
|
console.log();
|
|
4233
4296
|
}
|
|
4234
4297
|
function formatBytes(bytes) {
|
|
@@ -4252,14 +4315,14 @@ function timeAgo(iso) {
|
|
|
4252
4315
|
}
|
|
4253
4316
|
async function runBackupCreate(addonId) {
|
|
4254
4317
|
console.log();
|
|
4255
|
-
console.log(
|
|
4318
|
+
console.log(chalk5.bold(` Backing up addon ${addonId.slice(0, 8)}`));
|
|
4256
4319
|
console.log(
|
|
4257
|
-
|
|
4320
|
+
chalk5.dim(
|
|
4258
4321
|
" pg_dump runs in a transient container on the addon's network,\n streams gzipped output to object storage, and reports size here."
|
|
4259
4322
|
)
|
|
4260
4323
|
);
|
|
4261
4324
|
console.log();
|
|
4262
|
-
const creating =
|
|
4325
|
+
const creating = ora10("Queuing backup...").start();
|
|
4263
4326
|
let backup;
|
|
4264
4327
|
try {
|
|
4265
4328
|
backup = await createAddonBackup(addonId);
|
|
@@ -4268,7 +4331,7 @@ async function runBackupCreate(addonId) {
|
|
|
4268
4331
|
throw err;
|
|
4269
4332
|
}
|
|
4270
4333
|
creating.succeed(`Backup ${backup.id.slice(0, 8)} queued.`);
|
|
4271
|
-
const spinner =
|
|
4334
|
+
const spinner = ora10("Waiting for agent to finish pg_dump...").start();
|
|
4272
4335
|
const deadline = Date.now() + 15 * 60 * 1e3;
|
|
4273
4336
|
const pollIntervalMs = 3e3;
|
|
4274
4337
|
let current = backup;
|
|
@@ -4289,11 +4352,11 @@ async function runBackupCreate(addonId) {
|
|
|
4289
4352
|
spinner.fail("Backup failed.");
|
|
4290
4353
|
if (current.statusMessage) {
|
|
4291
4354
|
console.log();
|
|
4292
|
-
console.log(
|
|
4355
|
+
console.log(chalk5.red(` ${current.statusMessage}`));
|
|
4293
4356
|
}
|
|
4294
4357
|
console.log();
|
|
4295
4358
|
console.log(
|
|
4296
|
-
|
|
4359
|
+
chalk5.dim(
|
|
4297
4360
|
" You can retry with `doable addon backup <addonId>`. If this keeps\n happening, check the agent logs on the addon's server."
|
|
4298
4361
|
)
|
|
4299
4362
|
);
|
|
@@ -4306,7 +4369,7 @@ async function runBackupCreate(addonId) {
|
|
|
4306
4369
|
spinner.fail("Timed out waiting for backup to complete.");
|
|
4307
4370
|
console.log();
|
|
4308
4371
|
console.log(
|
|
4309
|
-
|
|
4372
|
+
chalk5.dim(
|
|
4310
4373
|
` Backup is still in status '${current.status}'. It may still finish \u2014
|
|
4311
4374
|
run \`doable addon backups ${addonId}\` to check again.`
|
|
4312
4375
|
)
|
|
@@ -4315,20 +4378,20 @@ async function runBackupCreate(addonId) {
|
|
|
4315
4378
|
process.exit(1);
|
|
4316
4379
|
}
|
|
4317
4380
|
console.log();
|
|
4318
|
-
console.log(
|
|
4381
|
+
console.log(chalk5.green(" \u2714 Backup saved."));
|
|
4319
4382
|
console.log();
|
|
4320
|
-
console.log(` ${
|
|
4321
|
-
console.log(` ${
|
|
4322
|
-
console.log(` ${
|
|
4383
|
+
console.log(` ${chalk5.dim("ID:")} ${current.id}`);
|
|
4384
|
+
console.log(` ${chalk5.dim("Size:")} ${formatBytes(current.sizeBytes)}`);
|
|
4385
|
+
console.log(` ${chalk5.dim("Key:")} ${current.s3Key}`);
|
|
4323
4386
|
console.log();
|
|
4324
|
-
console.log(
|
|
4387
|
+
console.log(chalk5.dim(" Restore anytime with:"));
|
|
4325
4388
|
console.log(
|
|
4326
|
-
` ${
|
|
4389
|
+
` ${chalk5.cyan(`doable addon restore ${addonId} ${current.id}`)}`
|
|
4327
4390
|
);
|
|
4328
4391
|
console.log();
|
|
4329
4392
|
}
|
|
4330
4393
|
async function runBackupList(addonId) {
|
|
4331
|
-
const spinner =
|
|
4394
|
+
const spinner = ora10("Loading backups...").start();
|
|
4332
4395
|
let backups;
|
|
4333
4396
|
try {
|
|
4334
4397
|
backups = await listAddonBackups(addonId);
|
|
@@ -4338,12 +4401,12 @@ async function runBackupList(addonId) {
|
|
|
4338
4401
|
}
|
|
4339
4402
|
spinner.stop();
|
|
4340
4403
|
console.log();
|
|
4341
|
-
console.log(
|
|
4404
|
+
console.log(chalk5.bold(` Backups for addon ${addonId.slice(0, 8)}`));
|
|
4342
4405
|
console.log();
|
|
4343
4406
|
if (backups.length === 0) {
|
|
4344
|
-
console.log(
|
|
4407
|
+
console.log(chalk5.dim(" No backups yet."));
|
|
4345
4408
|
console.log(
|
|
4346
|
-
|
|
4409
|
+
chalk5.dim(" Take one with: ") + chalk5.cyan(`doable addon backup ${addonId}`)
|
|
4347
4410
|
);
|
|
4348
4411
|
console.log();
|
|
4349
4412
|
return;
|
|
@@ -4354,11 +4417,11 @@ async function runBackupList(addonId) {
|
|
|
4354
4417
|
const colAge = "AGE".padEnd(12);
|
|
4355
4418
|
const colType = "TYPE";
|
|
4356
4419
|
console.log(
|
|
4357
|
-
" " +
|
|
4420
|
+
" " + chalk5.dim(`${colId} ${colStatus} ${colSize} ${colAge} ${colType}`)
|
|
4358
4421
|
);
|
|
4359
4422
|
for (const b of backups) {
|
|
4360
4423
|
const idCell = b.id.slice(0, 8).padEnd(12);
|
|
4361
|
-
const statusColor = b.status === "completed" ?
|
|
4424
|
+
const statusColor = b.status === "completed" ? chalk5.green : b.status === "failed" ? chalk5.red : chalk5.yellow;
|
|
4362
4425
|
const statusCell = statusColor(b.status.padEnd(12));
|
|
4363
4426
|
const sizeCell = formatBytes(b.sizeBytes).padEnd(10);
|
|
4364
4427
|
const ageCell = timeAgo(b.createdAt).padEnd(12);
|
|
@@ -4383,11 +4446,11 @@ async function runRestore(addonId, backupId, opts) {
|
|
|
4383
4446
|
}
|
|
4384
4447
|
]);
|
|
4385
4448
|
if (!confirm) {
|
|
4386
|
-
console.log(
|
|
4449
|
+
console.log(chalk5.dim(" Cancelled."));
|
|
4387
4450
|
return;
|
|
4388
4451
|
}
|
|
4389
4452
|
}
|
|
4390
|
-
const kicking =
|
|
4453
|
+
const kicking = ora10("Starting restore...").start();
|
|
4391
4454
|
try {
|
|
4392
4455
|
await restoreAddonFromBackup(addonId, backupId);
|
|
4393
4456
|
} catch (err) {
|
|
@@ -4395,7 +4458,7 @@ async function runRestore(addonId, backupId, opts) {
|
|
|
4395
4458
|
throw err;
|
|
4396
4459
|
}
|
|
4397
4460
|
kicking.succeed("Restore started.");
|
|
4398
|
-
const spinner =
|
|
4461
|
+
const spinner = ora10("Waiting for psql restore to finish...").start();
|
|
4399
4462
|
const deadline = Date.now() + 15 * 60 * 1e3;
|
|
4400
4463
|
const pollIntervalMs = 3e3;
|
|
4401
4464
|
let current;
|
|
@@ -4412,7 +4475,7 @@ async function runRestore(addonId, backupId, opts) {
|
|
|
4412
4475
|
spinner.succeed("Restore completed \u2014 addon is running.");
|
|
4413
4476
|
console.log();
|
|
4414
4477
|
console.log(
|
|
4415
|
-
|
|
4478
|
+
chalk5.dim(
|
|
4416
4479
|
" The addon's DATABASE_URL is unchanged \u2014 only the data inside has\n been replaced. No redeploy needed."
|
|
4417
4480
|
)
|
|
4418
4481
|
);
|
|
@@ -4423,7 +4486,7 @@ async function runRestore(addonId, backupId, opts) {
|
|
|
4423
4486
|
spinner.fail("Restore failed.");
|
|
4424
4487
|
if (current.statusMessage) {
|
|
4425
4488
|
console.log();
|
|
4426
|
-
console.log(
|
|
4489
|
+
console.log(chalk5.red(` ${current.statusMessage}`));
|
|
4427
4490
|
}
|
|
4428
4491
|
console.log();
|
|
4429
4492
|
process.exit(1);
|
|
@@ -4445,11 +4508,11 @@ async function runBackupDelete(backupId, opts) {
|
|
|
4445
4508
|
}
|
|
4446
4509
|
]);
|
|
4447
4510
|
if (!confirm) {
|
|
4448
|
-
console.log(
|
|
4511
|
+
console.log(chalk5.dim(" Cancelled."));
|
|
4449
4512
|
return;
|
|
4450
4513
|
}
|
|
4451
4514
|
}
|
|
4452
|
-
const spinner =
|
|
4515
|
+
const spinner = ora10("Deleting backup...").start();
|
|
4453
4516
|
try {
|
|
4454
4517
|
await deleteAddonBackup(backupId);
|
|
4455
4518
|
} catch (err) {
|
|
@@ -4472,7 +4535,7 @@ async function runMigrate(opts) {
|
|
|
4472
4535
|
const loadTool = engine === "mongodb" ? "mongorestore" : "psql";
|
|
4473
4536
|
let envKey = opts.fromEnv?.trim().toUpperCase();
|
|
4474
4537
|
if (!envKey) {
|
|
4475
|
-
const spinner2 =
|
|
4538
|
+
const spinner2 = ora10("Loading project env vars...").start();
|
|
4476
4539
|
let existing = [];
|
|
4477
4540
|
try {
|
|
4478
4541
|
existing = await listEnvVars(projectId);
|
|
@@ -4496,7 +4559,7 @@ async function runMigrate(opts) {
|
|
|
4496
4559
|
if (candidates.length === 1) {
|
|
4497
4560
|
envKey = candidates[0].key.toUpperCase();
|
|
4498
4561
|
console.log(
|
|
4499
|
-
|
|
4562
|
+
chalk5.dim(` Using ${chalk5.cyan(envKey)} (the only ${engine}-ish env var on the project).`)
|
|
4500
4563
|
);
|
|
4501
4564
|
} else if (nonInteractive) {
|
|
4502
4565
|
throw new Error(
|
|
@@ -4523,10 +4586,10 @@ async function runMigrate(opts) {
|
|
|
4523
4586
|
const version = opts.version?.trim() || defaultVersion;
|
|
4524
4587
|
console.log();
|
|
4525
4588
|
console.log(
|
|
4526
|
-
|
|
4589
|
+
chalk5.bold(` Migrate external ${engineLabel} \u2192 Doable-managed ${engineLabel}`)
|
|
4527
4590
|
);
|
|
4528
4591
|
console.log(
|
|
4529
|
-
|
|
4592
|
+
chalk5.dim(
|
|
4530
4593
|
` 1. Provision a new ${engineLabel} container on your project's network
|
|
4531
4594
|
2. ${dumpTool} from the source URL in ${envKey}
|
|
4532
4595
|
3. Load the dump into the new container via ${loadTool}
|
|
@@ -4549,11 +4612,11 @@ async function runMigrate(opts) {
|
|
|
4549
4612
|
}
|
|
4550
4613
|
]);
|
|
4551
4614
|
if (!confirm) {
|
|
4552
|
-
console.log(
|
|
4615
|
+
console.log(chalk5.dim(" Cancelled."));
|
|
4553
4616
|
return;
|
|
4554
4617
|
}
|
|
4555
4618
|
}
|
|
4556
|
-
const kicking =
|
|
4619
|
+
const kicking = ora10("Requesting migrate...").start();
|
|
4557
4620
|
let addon;
|
|
4558
4621
|
try {
|
|
4559
4622
|
addon = await migrateAddonFromEnv(projectId, { envKey, engine, version });
|
|
@@ -4562,7 +4625,7 @@ async function runMigrate(opts) {
|
|
|
4562
4625
|
throw err;
|
|
4563
4626
|
}
|
|
4564
4627
|
kicking.succeed(`Migrate queued (addon ${addon.id.slice(0, 8)}).`);
|
|
4565
|
-
const spinner =
|
|
4628
|
+
const spinner = ora10(
|
|
4566
4629
|
`Provisioning + copying data... (pull image \u2192 run container \u2192 ${dumpTool} \u2192 ${loadTool})`
|
|
4567
4630
|
).start();
|
|
4568
4631
|
const deadline = Date.now() + 15 * 60 * 1e3;
|
|
@@ -4580,15 +4643,15 @@ async function runMigrate(opts) {
|
|
|
4580
4643
|
if (current.status === "running") {
|
|
4581
4644
|
spinner.succeed("Migrate complete.");
|
|
4582
4645
|
console.log();
|
|
4583
|
-
console.log(
|
|
4646
|
+
console.log(chalk5.green(` \u2714 ${envKey} now points at the Doable-managed ${engineLabel}.`));
|
|
4584
4647
|
console.log();
|
|
4585
|
-
console.log(` ${
|
|
4586
|
-
console.log(` ${
|
|
4587
|
-
console.log(` ${
|
|
4588
|
-
console.log(` ${
|
|
4648
|
+
console.log(` ${chalk5.dim("Engine:")} ${current.engine} ${current.version}`);
|
|
4649
|
+
console.log(` ${chalk5.dim("Host:")} ${current.containerName}:${current.port}`);
|
|
4650
|
+
console.log(` ${chalk5.dim("Database:")} ${current.database}`);
|
|
4651
|
+
console.log(` ${chalk5.dim("Env var:")} ${envKey}`);
|
|
4589
4652
|
console.log();
|
|
4590
|
-
console.log(
|
|
4591
|
-
console.log(` ${
|
|
4653
|
+
console.log(chalk5.dim(" Redeploy to pick up the new env var:"));
|
|
4654
|
+
console.log(` ${chalk5.cyan("doable deploy")}`);
|
|
4592
4655
|
console.log();
|
|
4593
4656
|
return;
|
|
4594
4657
|
}
|
|
@@ -4596,11 +4659,11 @@ async function runMigrate(opts) {
|
|
|
4596
4659
|
spinner.fail("Migrate failed.");
|
|
4597
4660
|
if (current.statusMessage) {
|
|
4598
4661
|
console.log();
|
|
4599
|
-
console.log(
|
|
4662
|
+
console.log(chalk5.red(` ${current.statusMessage}`));
|
|
4600
4663
|
}
|
|
4601
4664
|
console.log();
|
|
4602
4665
|
console.log(
|
|
4603
|
-
|
|
4666
|
+
chalk5.dim(
|
|
4604
4667
|
" The source database is unchanged. If the native addon was partially\n created, delete it from the dashboard + retry. If the source URL is\n wrong, fix it and run `doable addon migrate` again."
|
|
4605
4668
|
)
|
|
4606
4669
|
);
|
|
@@ -4612,7 +4675,7 @@ async function runMigrate(opts) {
|
|
|
4612
4675
|
spinner.fail("Timed out waiting for migrate to complete.");
|
|
4613
4676
|
console.log();
|
|
4614
4677
|
console.log(
|
|
4615
|
-
|
|
4678
|
+
chalk5.dim(
|
|
4616
4679
|
` Addon is still in status '${current.status}'. It may still finish \u2014
|
|
4617
4680
|
run \`doable addon list\` to check again.`
|
|
4618
4681
|
)
|
|
@@ -4629,7 +4692,7 @@ function classifyKey(key) {
|
|
|
4629
4692
|
}
|
|
4630
4693
|
async function runList(opts) {
|
|
4631
4694
|
const projectId = await resolveProject(opts.project, !process.stdin.isTTY);
|
|
4632
|
-
const spinner =
|
|
4695
|
+
const spinner = ora10("Fetching addons...").start();
|
|
4633
4696
|
const vars = await listEnvVars(projectId);
|
|
4634
4697
|
spinner.stop();
|
|
4635
4698
|
const addons = [];
|
|
@@ -4638,22 +4701,22 @@ async function runList(opts) {
|
|
|
4638
4701
|
if (t) addons.push({ key: v.key, type: t, updatedAt: v.updatedAt });
|
|
4639
4702
|
}
|
|
4640
4703
|
console.log();
|
|
4641
|
-
console.log(
|
|
4704
|
+
console.log(chalk5.bold(" Attached addons"));
|
|
4642
4705
|
console.log();
|
|
4643
4706
|
if (addons.length === 0) {
|
|
4644
|
-
console.log(
|
|
4645
|
-
console.log(
|
|
4707
|
+
console.log(chalk5.dim(" No addons attached yet."));
|
|
4708
|
+
console.log(chalk5.dim(" Attach one with: ") + chalk5.cyan("doable addon attach"));
|
|
4646
4709
|
console.log();
|
|
4647
4710
|
return;
|
|
4648
4711
|
}
|
|
4649
4712
|
for (const a of addons) {
|
|
4650
4713
|
console.log(
|
|
4651
|
-
` ${
|
|
4714
|
+
` ${chalk5.cyan(a.key.padEnd(20))} ${chalk5.dim(LABELS[a.type])}`
|
|
4652
4715
|
);
|
|
4653
4716
|
}
|
|
4654
4717
|
console.log();
|
|
4655
4718
|
console.log(
|
|
4656
|
-
|
|
4719
|
+
chalk5.dim(
|
|
4657
4720
|
" Values are encrypted and not shown. Update by running `doable addon attach` again."
|
|
4658
4721
|
)
|
|
4659
4722
|
);
|
|
@@ -4675,11 +4738,11 @@ async function runDetach(key, opts) {
|
|
|
4675
4738
|
}
|
|
4676
4739
|
]);
|
|
4677
4740
|
if (!confirm) {
|
|
4678
|
-
console.log(
|
|
4741
|
+
console.log(chalk5.dim(" Cancelled."));
|
|
4679
4742
|
return;
|
|
4680
4743
|
}
|
|
4681
4744
|
}
|
|
4682
|
-
const spinner =
|
|
4745
|
+
const spinner = ora10(`Removing ${normalized}...`).start();
|
|
4683
4746
|
try {
|
|
4684
4747
|
await deleteEnvVar(projectId, normalized);
|
|
4685
4748
|
} catch (err) {
|
|
@@ -4688,7 +4751,7 @@ async function runDetach(key, opts) {
|
|
|
4688
4751
|
}
|
|
4689
4752
|
spinner.succeed(`${normalized} removed.`);
|
|
4690
4753
|
console.log();
|
|
4691
|
-
console.log(
|
|
4754
|
+
console.log(chalk5.dim(" Redeploy for the change to take effect."));
|
|
4692
4755
|
console.log();
|
|
4693
4756
|
}
|
|
4694
4757
|
async function resolveProject(explicit, nonInteractive) {
|
|
@@ -4757,18 +4820,18 @@ async function runConnect2(addonId, opts) {
|
|
|
4757
4820
|
}
|
|
4758
4821
|
})();
|
|
4759
4822
|
console.log();
|
|
4760
|
-
console.log(
|
|
4823
|
+
console.log(chalk5.bold(` ${conn.engine} connection string`));
|
|
4761
4824
|
console.log();
|
|
4762
|
-
console.log(` ${
|
|
4763
|
-
console.log(` ${
|
|
4764
|
-
console.log(` ${
|
|
4765
|
-
console.log(` ${
|
|
4825
|
+
console.log(` ${chalk5.dim("URL:")} ${conn.url}`);
|
|
4826
|
+
console.log(` ${chalk5.dim("Host:")} ${conn.host}:${conn.port}`);
|
|
4827
|
+
console.log(` ${chalk5.dim("Database:")} ${conn.database}`);
|
|
4828
|
+
console.log(` ${chalk5.dim("Username:")} ${conn.username}`);
|
|
4766
4829
|
console.log();
|
|
4767
|
-
console.log(
|
|
4768
|
-
console.log(
|
|
4830
|
+
console.log(chalk5.dim(" This URL is only reachable from sibling containers on the same"));
|
|
4831
|
+
console.log(chalk5.dim(" Doable account network. External clients can't connect by name."));
|
|
4769
4832
|
console.log();
|
|
4770
|
-
console.log(
|
|
4771
|
-
console.log(` ${
|
|
4833
|
+
console.log(chalk5.dim(" To set it as an env var on your project:"));
|
|
4834
|
+
console.log(` ${chalk5.cyan(`doable env set ${envVarHint}="<paste URL>"`)}`);
|
|
4772
4835
|
console.log();
|
|
4773
4836
|
}
|
|
4774
4837
|
async function runTables(addonId) {
|
|
@@ -4785,8 +4848,8 @@ async function runTables(addonId) {
|
|
|
4785
4848
|
}
|
|
4786
4849
|
if (resp.tables.length === 0) {
|
|
4787
4850
|
console.log();
|
|
4788
|
-
console.log(
|
|
4789
|
-
console.log(
|
|
4851
|
+
console.log(chalk5.dim(" No user tables yet."));
|
|
4852
|
+
console.log(chalk5.dim(" Create some by running your app's migrations against this addon."));
|
|
4790
4853
|
console.log();
|
|
4791
4854
|
return;
|
|
4792
4855
|
}
|
|
@@ -4800,10 +4863,10 @@ async function runTables(addonId) {
|
|
|
4800
4863
|
bySchema.set(schema, list);
|
|
4801
4864
|
}
|
|
4802
4865
|
console.log();
|
|
4803
|
-
console.log(
|
|
4866
|
+
console.log(chalk5.bold(` Tables in addon (${resp.tables.length} total)`));
|
|
4804
4867
|
for (const [schema, tables] of Array.from(bySchema.entries()).sort()) {
|
|
4805
4868
|
console.log();
|
|
4806
|
-
console.log(
|
|
4869
|
+
console.log(chalk5.dim(` ${schema}`));
|
|
4807
4870
|
for (const t of tables) {
|
|
4808
4871
|
console.log(` ${t}`);
|
|
4809
4872
|
}
|
|
@@ -4818,7 +4881,7 @@ async function runLogs2(addonId, opts) {
|
|
|
4818
4881
|
process.stdout.write(resp.logs);
|
|
4819
4882
|
if (!resp.logs.endsWith("\n")) process.stdout.write("\n");
|
|
4820
4883
|
if (resp.truncated) {
|
|
4821
|
-
console.log(
|
|
4884
|
+
console.log(chalk5.dim(` (truncated to ${tail} lines)`));
|
|
4822
4885
|
}
|
|
4823
4886
|
previous = resp.logs;
|
|
4824
4887
|
} catch (err) {
|
|
@@ -4830,7 +4893,7 @@ async function runLogs2(addonId, opts) {
|
|
|
4830
4893
|
throw err;
|
|
4831
4894
|
}
|
|
4832
4895
|
if (!opts.follow) return;
|
|
4833
|
-
console.log(
|
|
4896
|
+
console.log(chalk5.dim(" (following. Ctrl-C to stop.)"));
|
|
4834
4897
|
const interval = 3e3;
|
|
4835
4898
|
while (true) {
|
|
4836
4899
|
await new Promise((r) => setTimeout(r, interval));
|
|
@@ -4851,9 +4914,9 @@ async function runLogs2(addonId, opts) {
|
|
|
4851
4914
|
previous = resp.logs;
|
|
4852
4915
|
} catch (err) {
|
|
4853
4916
|
if (err instanceof ApiError) {
|
|
4854
|
-
console.error(
|
|
4917
|
+
console.error(chalk5.dim(` (fetch error: ${err.message}, retrying...)`));
|
|
4855
4918
|
} else if (err instanceof Error) {
|
|
4856
|
-
console.error(
|
|
4919
|
+
console.error(chalk5.dim(` (${err.message}, retrying...)`));
|
|
4857
4920
|
}
|
|
4858
4921
|
}
|
|
4859
4922
|
}
|
|
@@ -4880,12 +4943,12 @@ async function runClone(srcAddonId, destAddonId, opts) {
|
|
|
4880
4943
|
throw new Error("Source and destination must be different addons.");
|
|
4881
4944
|
}
|
|
4882
4945
|
console.log();
|
|
4883
|
-
console.log(
|
|
4946
|
+
console.log(chalk5.bold(` Clone ${src.engine} addon`));
|
|
4884
4947
|
console.log(
|
|
4885
|
-
|
|
4948
|
+
chalk5.dim(` from: ${src.containerName} (${srcAddonId.slice(0, 8)})`)
|
|
4886
4949
|
);
|
|
4887
4950
|
console.log(
|
|
4888
|
-
|
|
4951
|
+
chalk5.dim(` to: ${dest.containerName} (${destAddonId.slice(0, 8)})`)
|
|
4889
4952
|
);
|
|
4890
4953
|
console.log();
|
|
4891
4954
|
if (!opts.yes) {
|
|
@@ -4903,11 +4966,11 @@ async function runClone(srcAddonId, destAddonId, opts) {
|
|
|
4903
4966
|
}
|
|
4904
4967
|
]);
|
|
4905
4968
|
if (!confirm) {
|
|
4906
|
-
console.log(
|
|
4969
|
+
console.log(chalk5.dim(" Cancelled."));
|
|
4907
4970
|
return;
|
|
4908
4971
|
}
|
|
4909
4972
|
}
|
|
4910
|
-
const kicking =
|
|
4973
|
+
const kicking = ora10("Taking source snapshot...").start();
|
|
4911
4974
|
let backup;
|
|
4912
4975
|
try {
|
|
4913
4976
|
backup = await createAddonBackup(srcAddonId);
|
|
@@ -4917,7 +4980,7 @@ async function runClone(srcAddonId, destAddonId, opts) {
|
|
|
4917
4980
|
}
|
|
4918
4981
|
kicking.succeed(`Backup ${backup.id.slice(0, 8)} queued.`);
|
|
4919
4982
|
const backupDeadline = Date.now() + 15 * 60 * 1e3;
|
|
4920
|
-
const backupSpinner =
|
|
4983
|
+
const backupSpinner = ora10("Waiting for source backup to finish...").start();
|
|
4921
4984
|
let currentBackup = backup;
|
|
4922
4985
|
while (Date.now() < backupDeadline) {
|
|
4923
4986
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -4944,7 +5007,7 @@ async function runClone(srcAddonId, destAddonId, opts) {
|
|
|
4944
5007
|
backupSpinner.fail("Timed out waiting for backup.");
|
|
4945
5008
|
throw new Error("Backup did not complete within 15 minutes.");
|
|
4946
5009
|
}
|
|
4947
|
-
const restoreKicking =
|
|
5010
|
+
const restoreKicking = ora10("Starting restore into destination...").start();
|
|
4948
5011
|
try {
|
|
4949
5012
|
await restoreAddonFromBackup(destAddonId, currentBackup.id);
|
|
4950
5013
|
} catch (err) {
|
|
@@ -4953,7 +5016,7 @@ async function runClone(srcAddonId, destAddonId, opts) {
|
|
|
4953
5016
|
}
|
|
4954
5017
|
restoreKicking.succeed("Restore queued.");
|
|
4955
5018
|
const restoreDeadline = Date.now() + 15 * 60 * 1e3;
|
|
4956
|
-
const restoreSpinner =
|
|
5019
|
+
const restoreSpinner = ora10("Waiting for restore to finish...").start();
|
|
4957
5020
|
let currentDest = dest;
|
|
4958
5021
|
while (Date.now() < restoreDeadline) {
|
|
4959
5022
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -4965,7 +5028,7 @@ async function runClone(srcAddonId, destAddonId, opts) {
|
|
|
4965
5028
|
if (currentDest.status === "running") {
|
|
4966
5029
|
restoreSpinner.succeed("Clone completed.");
|
|
4967
5030
|
console.log();
|
|
4968
|
-
console.log(
|
|
5031
|
+
console.log(chalk5.green(` \u2714 Destination addon now mirrors the source.`));
|
|
4969
5032
|
console.log();
|
|
4970
5033
|
return;
|
|
4971
5034
|
}
|
|
@@ -4983,16 +5046,16 @@ async function runClone(srcAddonId, destAddonId, opts) {
|
|
|
4983
5046
|
function handleError5(err) {
|
|
4984
5047
|
if (err instanceof ApiError) {
|
|
4985
5048
|
if (err.status === 401) {
|
|
4986
|
-
console.error(
|
|
5049
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
4987
5050
|
} else if (err.status === 0) {
|
|
4988
|
-
console.error(
|
|
5051
|
+
console.error(chalk5.red(`Connection error: ${err.message}`));
|
|
4989
5052
|
} else {
|
|
4990
|
-
console.error(
|
|
5053
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
4991
5054
|
}
|
|
4992
5055
|
} else if (err instanceof Error) {
|
|
4993
|
-
console.error(
|
|
5056
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
4994
5057
|
} else {
|
|
4995
|
-
console.error(
|
|
5058
|
+
console.error(chalk5.red(`Unexpected error: ${String(err)}`));
|
|
4996
5059
|
}
|
|
4997
5060
|
process.exit(1);
|
|
4998
5061
|
}
|
|
@@ -5035,30 +5098,30 @@ async function runList2(opts) {
|
|
|
5035
5098
|
if (isNaN(limit) || limit < 1) {
|
|
5036
5099
|
throw new Error("--limit must be a positive integer");
|
|
5037
5100
|
}
|
|
5038
|
-
const spinner =
|
|
5101
|
+
const spinner = ora10("Fetching deployments...").start();
|
|
5039
5102
|
const deployments = await listDeployments(projectId, { limit });
|
|
5040
5103
|
spinner.stop();
|
|
5041
5104
|
if (deployments.length === 0) {
|
|
5042
|
-
console.log(
|
|
5105
|
+
console.log(chalk5.dim("\n No deployments yet.\n"));
|
|
5043
5106
|
return;
|
|
5044
5107
|
}
|
|
5045
5108
|
console.log();
|
|
5046
|
-
console.log(
|
|
5109
|
+
console.log(chalk5.bold(" Deployment History"));
|
|
5047
5110
|
console.log();
|
|
5048
5111
|
const header = ` ${pad4("#", 5)} ${pad4("STATUS", 10)} ${pad4("GIT SHA", 9)} ${pad4("ARTIFACT", 10)} CREATED`;
|
|
5049
|
-
console.log(
|
|
5050
|
-
console.log(
|
|
5112
|
+
console.log(chalk5.dim(header));
|
|
5113
|
+
console.log(chalk5.dim(" " + "-".repeat(header.length - 2)));
|
|
5051
5114
|
for (let i = 0; i < deployments.length; i++) {
|
|
5052
5115
|
const d = deployments[i];
|
|
5053
5116
|
const isCurrent = i === 0;
|
|
5054
5117
|
const num = `#${d.number}`;
|
|
5055
5118
|
const statusLabel2 = colorStatus(d.status);
|
|
5056
5119
|
const gitShort = d.gitSha ? d.gitSha.slice(0, 7) : "-";
|
|
5057
|
-
const hasArtifact = d.artifactImageRef ? "yes" :
|
|
5120
|
+
const hasArtifact = d.artifactImageRef ? "yes" : chalk5.dim("no");
|
|
5058
5121
|
const created = new Date(d.createdAt).toLocaleString();
|
|
5059
|
-
const currentMarker = isCurrent ?
|
|
5122
|
+
const currentMarker = isCurrent ? chalk5.green(" \u2190 live") : "";
|
|
5060
5123
|
console.log(
|
|
5061
|
-
` ${pad4(num, 5)} ${padRaw(statusLabel2, 10)} ${pad4(gitShort, 9)} ${padRaw(hasArtifact, 10)} ${
|
|
5124
|
+
` ${pad4(num, 5)} ${padRaw(statusLabel2, 10)} ${pad4(gitShort, 9)} ${padRaw(hasArtifact, 10)} ${chalk5.dim(created)}${currentMarker}`
|
|
5062
5125
|
);
|
|
5063
5126
|
}
|
|
5064
5127
|
const cancelable = deployments.filter((d) => CANCELABLE_STATUSES.includes(d.status));
|
|
@@ -5067,12 +5130,12 @@ async function runList2(opts) {
|
|
|
5067
5130
|
console.log();
|
|
5068
5131
|
if (cancelable.length > 0) {
|
|
5069
5132
|
console.log(
|
|
5070
|
-
|
|
5133
|
+
chalk5.dim(` Cancel with: ${chalk5.cyan(`doable deployments cancel`)}`)
|
|
5071
5134
|
);
|
|
5072
5135
|
}
|
|
5073
5136
|
if (rollbackable.length > 0) {
|
|
5074
5137
|
console.log(
|
|
5075
|
-
|
|
5138
|
+
chalk5.dim(` Roll back with: ${chalk5.cyan(`doable deployments rollback <number>`)}`)
|
|
5076
5139
|
);
|
|
5077
5140
|
}
|
|
5078
5141
|
}
|
|
@@ -5084,7 +5147,7 @@ async function runRollback(numberArg, opts) {
|
|
|
5084
5147
|
throw new Error(`Invalid deployment number: ${numberArg}`);
|
|
5085
5148
|
}
|
|
5086
5149
|
const projectId = await resolveProject2(opts.project, !process.stdin.isTTY);
|
|
5087
|
-
const spinner =
|
|
5150
|
+
const spinner = ora10("Looking up deployment...").start();
|
|
5088
5151
|
const deployments = await listDeployments(projectId, { limit: 100 });
|
|
5089
5152
|
spinner.stop();
|
|
5090
5153
|
const target = deployments.find((d) => d.number === number);
|
|
@@ -5111,16 +5174,16 @@ async function runRollback(numberArg, opts) {
|
|
|
5111
5174
|
if (!opts.yes && process.stdin.isTTY) {
|
|
5112
5175
|
console.log();
|
|
5113
5176
|
console.log(
|
|
5114
|
-
` Target: ${
|
|
5177
|
+
` Target: ${chalk5.cyan(`#${target.number}`)} ${chalk5.dim(`(${new Date(target.createdAt).toLocaleString()})`)}`
|
|
5115
5178
|
);
|
|
5116
5179
|
if (target.gitSha) {
|
|
5117
|
-
console.log(` Git SHA: ${
|
|
5180
|
+
console.log(` Git SHA: ${chalk5.dim(target.gitSha.slice(0, 7))}`);
|
|
5118
5181
|
}
|
|
5119
5182
|
console.log();
|
|
5120
5183
|
console.log(
|
|
5121
|
-
|
|
5184
|
+
chalk5.dim(" This will create a new deployment that reuses the artifact from")
|
|
5122
5185
|
);
|
|
5123
|
-
console.log(
|
|
5186
|
+
console.log(chalk5.dim(` #${target.number}. Your current environment variables will be used.`));
|
|
5124
5187
|
console.log();
|
|
5125
5188
|
const { confirm } = await inquirer3.prompt([
|
|
5126
5189
|
{
|
|
@@ -5131,15 +5194,15 @@ async function runRollback(numberArg, opts) {
|
|
|
5131
5194
|
}
|
|
5132
5195
|
]);
|
|
5133
5196
|
if (!confirm) {
|
|
5134
|
-
console.log(
|
|
5197
|
+
console.log(chalk5.dim(" Cancelled."));
|
|
5135
5198
|
return;
|
|
5136
5199
|
}
|
|
5137
5200
|
}
|
|
5138
|
-
const rollbackSpinner =
|
|
5201
|
+
const rollbackSpinner = ora10("Starting rollback...").start();
|
|
5139
5202
|
try {
|
|
5140
5203
|
const newDeployment = await rollbackDeployment(target.id);
|
|
5141
5204
|
rollbackSpinner.succeed(
|
|
5142
|
-
`Rollback started: new deployment ${
|
|
5205
|
+
`Rollback started: new deployment ${chalk5.bold(`#${newDeployment.number}`)} (${chalk5.dim(newDeployment.id)})`
|
|
5143
5206
|
);
|
|
5144
5207
|
} catch (err) {
|
|
5145
5208
|
rollbackSpinner.fail("Rollback failed to start.");
|
|
@@ -5147,15 +5210,15 @@ async function runRollback(numberArg, opts) {
|
|
|
5147
5210
|
}
|
|
5148
5211
|
console.log();
|
|
5149
5212
|
console.log(
|
|
5150
|
-
|
|
5151
|
-
` Track progress with: ${
|
|
5213
|
+
chalk5.dim(
|
|
5214
|
+
` Track progress with: ${chalk5.cyan(`doable logs --deployment <id>`)}`
|
|
5152
5215
|
)
|
|
5153
5216
|
);
|
|
5154
5217
|
console.log();
|
|
5155
5218
|
}
|
|
5156
5219
|
async function runCancel(numberArg, opts) {
|
|
5157
5220
|
const projectId = await resolveProject2(opts.project, !process.stdin.isTTY);
|
|
5158
|
-
const spinner =
|
|
5221
|
+
const spinner = ora10("Fetching deployments...").start();
|
|
5159
5222
|
const deployments = await listDeployments(projectId, { limit: 20 });
|
|
5160
5223
|
spinner.stop();
|
|
5161
5224
|
let target;
|
|
@@ -5173,7 +5236,7 @@ async function runCancel(numberArg, opts) {
|
|
|
5173
5236
|
} else {
|
|
5174
5237
|
target = deployments.find((d) => CANCELABLE_STATUSES.includes(d.status));
|
|
5175
5238
|
if (!target) {
|
|
5176
|
-
console.log(
|
|
5239
|
+
console.log(chalk5.dim("\n No in-progress deployment to cancel.\n"));
|
|
5177
5240
|
return;
|
|
5178
5241
|
}
|
|
5179
5242
|
}
|
|
@@ -5185,7 +5248,7 @@ async function runCancel(numberArg, opts) {
|
|
|
5185
5248
|
if (!opts.yes && process.stdin.isTTY) {
|
|
5186
5249
|
console.log();
|
|
5187
5250
|
console.log(
|
|
5188
|
-
` Target: ${
|
|
5251
|
+
` Target: ${chalk5.cyan(`#${target.number}`)} ${chalk5.yellow(target.status)} ${chalk5.dim(`(${new Date(target.createdAt).toLocaleString()})`)}`
|
|
5189
5252
|
);
|
|
5190
5253
|
console.log();
|
|
5191
5254
|
const { confirm } = await inquirer3.prompt([
|
|
@@ -5197,22 +5260,22 @@ async function runCancel(numberArg, opts) {
|
|
|
5197
5260
|
}
|
|
5198
5261
|
]);
|
|
5199
5262
|
if (!confirm) {
|
|
5200
|
-
console.log(
|
|
5263
|
+
console.log(chalk5.dim(" Aborted."));
|
|
5201
5264
|
return;
|
|
5202
5265
|
}
|
|
5203
5266
|
}
|
|
5204
|
-
const cancelSpinner =
|
|
5267
|
+
const cancelSpinner = ora10("Canceling deployment...").start();
|
|
5205
5268
|
try {
|
|
5206
5269
|
const updated = await cancelDeployment(target.id);
|
|
5207
5270
|
cancelSpinner.succeed(
|
|
5208
|
-
`Deployment ${
|
|
5271
|
+
`Deployment ${chalk5.bold(`#${updated.number}`)} canceled.`
|
|
5209
5272
|
);
|
|
5210
5273
|
} catch (err) {
|
|
5211
5274
|
cancelSpinner.fail("Cancel failed.");
|
|
5212
5275
|
throw err;
|
|
5213
5276
|
}
|
|
5214
5277
|
console.log(
|
|
5215
|
-
|
|
5278
|
+
chalk5.dim(
|
|
5216
5279
|
"\n Your app continues running on the previous healthy version.\n"
|
|
5217
5280
|
)
|
|
5218
5281
|
);
|
|
@@ -5255,19 +5318,19 @@ async function resolveProject2(explicit, nonInteractive) {
|
|
|
5255
5318
|
function colorStatus(status) {
|
|
5256
5319
|
switch (status) {
|
|
5257
5320
|
case "healthy":
|
|
5258
|
-
return
|
|
5321
|
+
return chalk5.green("healthy");
|
|
5259
5322
|
case "failed":
|
|
5260
|
-
return
|
|
5323
|
+
return chalk5.red("failed");
|
|
5261
5324
|
case "canceled":
|
|
5262
|
-
return
|
|
5325
|
+
return chalk5.gray("canceled");
|
|
5263
5326
|
case "building":
|
|
5264
5327
|
case "deploying":
|
|
5265
5328
|
case "uploading":
|
|
5266
|
-
return
|
|
5329
|
+
return chalk5.yellow(status);
|
|
5267
5330
|
case "queued":
|
|
5268
|
-
return
|
|
5331
|
+
return chalk5.blue("queued");
|
|
5269
5332
|
default:
|
|
5270
|
-
return
|
|
5333
|
+
return chalk5.dim(status);
|
|
5271
5334
|
}
|
|
5272
5335
|
}
|
|
5273
5336
|
function pad4(str, width) {
|
|
@@ -5282,16 +5345,16 @@ function padRaw(str, width) {
|
|
|
5282
5345
|
function handleError6(err) {
|
|
5283
5346
|
if (err instanceof ApiError) {
|
|
5284
5347
|
if (err.status === 401) {
|
|
5285
|
-
console.error(
|
|
5348
|
+
console.error(chalk5.red("Not authenticated. Run `doable login` first."));
|
|
5286
5349
|
} else if (err.status === 0) {
|
|
5287
|
-
console.error(
|
|
5350
|
+
console.error(chalk5.red(`Connection error: ${err.message}`));
|
|
5288
5351
|
} else {
|
|
5289
|
-
console.error(
|
|
5352
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
5290
5353
|
}
|
|
5291
5354
|
} else if (err instanceof Error) {
|
|
5292
|
-
console.error(
|
|
5355
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
5293
5356
|
} else {
|
|
5294
|
-
console.error(
|
|
5357
|
+
console.error(chalk5.red(`Unexpected error: ${String(err)}`));
|
|
5295
5358
|
}
|
|
5296
5359
|
process.exit(1);
|
|
5297
5360
|
}
|
|
@@ -5375,9 +5438,9 @@ async function runLocalPreflight(projectDir) {
|
|
|
5375
5438
|
findings.push({ level: "fail", message: `.doable.json port is invalid: ${config2.port}` });
|
|
5376
5439
|
}
|
|
5377
5440
|
console.log();
|
|
5378
|
-
console.log(
|
|
5441
|
+
console.log(chalk5.bold(" Doable local preflight"));
|
|
5379
5442
|
for (const finding of findings) {
|
|
5380
|
-
const icon = finding.level === "ok" ?
|
|
5443
|
+
const icon = finding.level === "ok" ? chalk5.green("OK ") : finding.level === "warn" ? chalk5.yellow("WARN") : chalk5.red("FAIL");
|
|
5381
5444
|
console.log(` ${icon} ${finding.message}`);
|
|
5382
5445
|
}
|
|
5383
5446
|
console.log();
|
|
@@ -5387,7 +5450,7 @@ async function runLocalPreflight(projectDir) {
|
|
|
5387
5450
|
}
|
|
5388
5451
|
async function runDoctorCommand(deploymentId, opts) {
|
|
5389
5452
|
if (opts.run) {
|
|
5390
|
-
const spin =
|
|
5453
|
+
const spin = ora10("Requesting diagnosis...").start();
|
|
5391
5454
|
try {
|
|
5392
5455
|
await runDoctor(deploymentId);
|
|
5393
5456
|
spin.text = "Diagnosing (this can take 10\u201320s)...";
|
|
@@ -5406,15 +5469,15 @@ async function runDoctorCommand(deploymentId, opts) {
|
|
|
5406
5469
|
const report = await getDoctorReport(deploymentId);
|
|
5407
5470
|
if (!report) {
|
|
5408
5471
|
console.log();
|
|
5409
|
-
console.log(
|
|
5472
|
+
console.log(chalk5.dim(" No Doctor report for this deployment."));
|
|
5410
5473
|
console.log(
|
|
5411
|
-
|
|
5474
|
+
chalk5.dim(" Run with --run to generate one, or wait for an auto-diagnosis after a failure.")
|
|
5412
5475
|
);
|
|
5413
5476
|
console.log();
|
|
5414
5477
|
return;
|
|
5415
5478
|
}
|
|
5416
5479
|
if (opts.dismiss) {
|
|
5417
|
-
const spin =
|
|
5480
|
+
const spin = ora10("Dismissing...").start();
|
|
5418
5481
|
try {
|
|
5419
5482
|
await dismissDoctorReport(report.id);
|
|
5420
5483
|
spin.succeed("Report dismissed.");
|
|
@@ -5428,7 +5491,7 @@ async function runDoctorCommand(deploymentId, opts) {
|
|
|
5428
5491
|
const idx = parseInt(opts.apply, 10);
|
|
5429
5492
|
if (!Number.isFinite(idx) || idx < 0 || idx >= report.actions.length) {
|
|
5430
5493
|
console.error(
|
|
5431
|
-
|
|
5494
|
+
chalk5.red(`Invalid action index ${opts.apply}. Report has ${report.actions.length} action(s).`)
|
|
5432
5495
|
);
|
|
5433
5496
|
process.exit(1);
|
|
5434
5497
|
}
|
|
@@ -5455,11 +5518,11 @@ async function runDoctorCommand(deploymentId, opts) {
|
|
|
5455
5518
|
}
|
|
5456
5519
|
]);
|
|
5457
5520
|
if (!answer.ok) {
|
|
5458
|
-
console.log(
|
|
5521
|
+
console.log(chalk5.dim(" Aborted."));
|
|
5459
5522
|
return;
|
|
5460
5523
|
}
|
|
5461
5524
|
}
|
|
5462
|
-
const spin =
|
|
5525
|
+
const spin = ora10(`Applying ${actionLabel(action)}...`).start();
|
|
5463
5526
|
try {
|
|
5464
5527
|
const result = await applyDoctorAction(report.id, idx, value);
|
|
5465
5528
|
spin.succeed(result.message);
|
|
@@ -5473,34 +5536,34 @@ async function runDoctorCommand(deploymentId, opts) {
|
|
|
5473
5536
|
}
|
|
5474
5537
|
function renderReport(report) {
|
|
5475
5538
|
console.log();
|
|
5476
|
-
console.log(
|
|
5539
|
+
console.log(chalk5.bold(" \u{1F9E0} Doctor report"));
|
|
5477
5540
|
console.log(
|
|
5478
|
-
|
|
5541
|
+
chalk5.dim(
|
|
5479
5542
|
` ${report.aiModel} \xB7 confidence ${report.confidence} \xB7 status ${report.status}`
|
|
5480
5543
|
)
|
|
5481
5544
|
);
|
|
5482
5545
|
console.log();
|
|
5483
|
-
console.log(
|
|
5546
|
+
console.log(chalk5.bold(` ${report.summary}`));
|
|
5484
5547
|
console.log();
|
|
5485
5548
|
console.log(wrapLines(report.diagnosis, 76, " "));
|
|
5486
5549
|
console.log();
|
|
5487
5550
|
if (report.applyError) {
|
|
5488
|
-
console.log(
|
|
5551
|
+
console.log(chalk5.red(` Last apply error: ${report.applyError}`));
|
|
5489
5552
|
console.log();
|
|
5490
5553
|
}
|
|
5491
5554
|
if (report.actions.length === 0) {
|
|
5492
|
-
console.log(
|
|
5555
|
+
console.log(chalk5.dim(" No suggested actions."));
|
|
5493
5556
|
console.log();
|
|
5494
5557
|
return;
|
|
5495
5558
|
}
|
|
5496
|
-
console.log(
|
|
5559
|
+
console.log(chalk5.bold(" Suggested actions:"));
|
|
5497
5560
|
report.actions.forEach((action, idx) => {
|
|
5498
|
-
console.log(` ${
|
|
5499
|
-
console.log(` ${
|
|
5561
|
+
console.log(` ${chalk5.cyan(`[${idx}]`)} ${actionLabel(action)}`);
|
|
5562
|
+
console.log(` ${chalk5.dim(action.rationale)}`);
|
|
5500
5563
|
});
|
|
5501
5564
|
console.log();
|
|
5502
5565
|
console.log(
|
|
5503
|
-
|
|
5566
|
+
chalk5.dim(
|
|
5504
5567
|
` Apply with: doable doctor <deploymentId> --apply <idx>`
|
|
5505
5568
|
)
|
|
5506
5569
|
);
|
|
@@ -5544,11 +5607,11 @@ function sleep2(ms) {
|
|
|
5544
5607
|
}
|
|
5545
5608
|
function handleError7(err) {
|
|
5546
5609
|
if (err instanceof ApiError) {
|
|
5547
|
-
console.error(
|
|
5610
|
+
console.error(chalk5.red(` API error: ${err.message}`));
|
|
5548
5611
|
} else if (err instanceof Error) {
|
|
5549
|
-
console.error(
|
|
5612
|
+
console.error(chalk5.red(` ${err.message}`));
|
|
5550
5613
|
} else {
|
|
5551
|
-
console.error(
|
|
5614
|
+
console.error(chalk5.red(` Unknown error: ${String(err)}`));
|
|
5552
5615
|
}
|
|
5553
5616
|
process.exit(1);
|
|
5554
5617
|
}
|
|
@@ -5558,7 +5621,7 @@ function registerOpenCommand(program2) {
|
|
|
5558
5621
|
const projectId = resolveProjectId(opts.project);
|
|
5559
5622
|
if (!projectId) {
|
|
5560
5623
|
console.error(
|
|
5561
|
-
|
|
5624
|
+
chalk5.red(
|
|
5562
5625
|
"No project found. Pass --project <id> or run from a directory with .doable.json."
|
|
5563
5626
|
)
|
|
5564
5627
|
);
|
|
@@ -5566,13 +5629,13 @@ function registerOpenCommand(program2) {
|
|
|
5566
5629
|
}
|
|
5567
5630
|
const project = await getProject(projectId);
|
|
5568
5631
|
const url = pickUrl(project);
|
|
5569
|
-
console.log(
|
|
5632
|
+
console.log(chalk5.dim(` Opening ${chalk5.cyan(url)}...`));
|
|
5570
5633
|
openBrowser(url);
|
|
5571
5634
|
} catch (err) {
|
|
5572
5635
|
if (err instanceof ApiError) {
|
|
5573
|
-
console.error(
|
|
5636
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
5574
5637
|
} else if (err instanceof Error) {
|
|
5575
|
-
console.error(
|
|
5638
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
5576
5639
|
}
|
|
5577
5640
|
process.exit(1);
|
|
5578
5641
|
}
|
|
@@ -5614,14 +5677,14 @@ function openBrowser(url) {
|
|
|
5614
5677
|
function registerStatusCommand(program2) {
|
|
5615
5678
|
program2.command("status").description("Show all your projects at a glance.").action(async () => {
|
|
5616
5679
|
try {
|
|
5617
|
-
const spinner =
|
|
5680
|
+
const spinner = ora10("Loading\u2026").start();
|
|
5618
5681
|
const projects = await listProjects();
|
|
5619
5682
|
spinner.stop();
|
|
5620
5683
|
if (projects.length === 0) {
|
|
5621
5684
|
console.log();
|
|
5622
|
-
console.log(
|
|
5685
|
+
console.log(chalk5.bold(" Nothing shipped yet."));
|
|
5623
5686
|
console.log(
|
|
5624
|
-
|
|
5687
|
+
chalk5.dim(" Run ") + chalk5.cyan("doable preview") + chalk5.dim(" to put something live with no signup, or ") + chalk5.cyan("doable new next") + chalk5.dim(" to scaffold a starter.")
|
|
5625
5688
|
);
|
|
5626
5689
|
console.log();
|
|
5627
5690
|
return;
|
|
@@ -5634,7 +5697,7 @@ function registerStatusCommand(program2) {
|
|
|
5634
5697
|
const counts = countStatuses(projects);
|
|
5635
5698
|
console.log();
|
|
5636
5699
|
console.log(
|
|
5637
|
-
|
|
5700
|
+
chalk5.bold(` Your projects (${projects.length})`) + " " + chalk5.dim(formatCounts(counts))
|
|
5638
5701
|
);
|
|
5639
5702
|
console.log();
|
|
5640
5703
|
for (const p of projects) {
|
|
@@ -5644,13 +5707,13 @@ function registerStatusCommand(program2) {
|
|
|
5644
5707
|
if (err instanceof ApiError) {
|
|
5645
5708
|
if (err.status === 401) {
|
|
5646
5709
|
console.error(
|
|
5647
|
-
|
|
5710
|
+
chalk5.red("Not authenticated. Run `doable login` first.")
|
|
5648
5711
|
);
|
|
5649
5712
|
} else {
|
|
5650
|
-
console.error(
|
|
5713
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
5651
5714
|
}
|
|
5652
5715
|
} else if (err instanceof Error) {
|
|
5653
|
-
console.error(
|
|
5716
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
5654
5717
|
}
|
|
5655
5718
|
process.exit(1);
|
|
5656
5719
|
}
|
|
@@ -5660,31 +5723,31 @@ function renderTile(p) {
|
|
|
5660
5723
|
const dot = statusDot(p.latestDeployment?.status);
|
|
5661
5724
|
const status = formatStatus(p.latestDeployment);
|
|
5662
5725
|
const liveUrl = `https://${p.slug}.doable.do`;
|
|
5663
|
-
console.log(` ${dot} ${
|
|
5664
|
-
console.log(` ${
|
|
5726
|
+
console.log(` ${dot} ${chalk5.bold(p.name)} ${chalk5.dim(status)}`);
|
|
5727
|
+
console.log(` ${chalk5.cyan(liveUrl)}`);
|
|
5665
5728
|
if (p.latestDeployment) {
|
|
5666
5729
|
const shareUrl = `https://doable.do/s/${p.latestDeployment.id}`;
|
|
5667
|
-
console.log(` ${
|
|
5730
|
+
console.log(` ${chalk5.dim("Share: ")}${chalk5.cyan(shareUrl)}`);
|
|
5668
5731
|
}
|
|
5669
5732
|
console.log();
|
|
5670
5733
|
}
|
|
5671
5734
|
function statusDot(status) {
|
|
5672
5735
|
switch (status) {
|
|
5673
5736
|
case "healthy":
|
|
5674
|
-
return
|
|
5737
|
+
return chalk5.green("\u25CF");
|
|
5675
5738
|
case "failed":
|
|
5676
|
-
return
|
|
5739
|
+
return chalk5.red("\u25CF");
|
|
5677
5740
|
case "building":
|
|
5678
5741
|
case "deploying":
|
|
5679
5742
|
case "uploading":
|
|
5680
5743
|
case "queued":
|
|
5681
|
-
return
|
|
5744
|
+
return chalk5.yellow("\u25CF");
|
|
5682
5745
|
case "canceled":
|
|
5683
5746
|
case "rolled_back":
|
|
5684
5747
|
case "suspended":
|
|
5685
|
-
return
|
|
5748
|
+
return chalk5.gray("\u25CF");
|
|
5686
5749
|
default:
|
|
5687
|
-
return
|
|
5750
|
+
return chalk5.gray("\u25CB");
|
|
5688
5751
|
}
|
|
5689
5752
|
}
|
|
5690
5753
|
function formatStatus(d) {
|
|
@@ -5749,7 +5812,7 @@ function registerInitCommand(program2) {
|
|
|
5749
5812
|
await runInit(opts.path);
|
|
5750
5813
|
} catch (err) {
|
|
5751
5814
|
if (err instanceof Error) {
|
|
5752
|
-
console.error(
|
|
5815
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
5753
5816
|
}
|
|
5754
5817
|
process.exit(1);
|
|
5755
5818
|
}
|
|
@@ -5765,7 +5828,7 @@ async function runInit(dirPath) {
|
|
|
5765
5828
|
throw new Error(`Directory not found: ${projectDir}`);
|
|
5766
5829
|
}
|
|
5767
5830
|
console.log();
|
|
5768
|
-
console.log(
|
|
5831
|
+
console.log(chalk5.bold(" Scanning your project..."));
|
|
5769
5832
|
console.log();
|
|
5770
5833
|
const detected = await detectRuntime(projectDir, nodeDetectFS4);
|
|
5771
5834
|
const runtimeLabel = detected.framework ? `${detected.runtime} (${detected.framework})` : detected.runtime;
|
|
@@ -5850,12 +5913,12 @@ async function runInit(dirPath) {
|
|
|
5850
5913
|
});
|
|
5851
5914
|
}
|
|
5852
5915
|
for (const check of checks) {
|
|
5853
|
-
const icon = check.status === "ok" ?
|
|
5854
|
-
const detail = check.status === "warn" ?
|
|
5855
|
-
console.log(`${icon} ${
|
|
5916
|
+
const icon = check.status === "ok" ? chalk5.green(" +") : check.status === "fix" ? chalk5.yellow(" ~") : chalk5.dim(" !");
|
|
5917
|
+
const detail = check.status === "warn" ? chalk5.yellow(check.detail) : chalk5.dim(check.detail);
|
|
5918
|
+
console.log(`${icon} ${chalk5.white(check.label)} ${detail}`);
|
|
5856
5919
|
}
|
|
5857
5920
|
console.log();
|
|
5858
|
-
console.log(
|
|
5921
|
+
console.log(chalk5.dim(` Build estimate: ${estimate}`));
|
|
5859
5922
|
console.log();
|
|
5860
5923
|
const fixes = checks.filter((c) => c.status === "fix");
|
|
5861
5924
|
if (fixes.length > 0 || !hasDoableJson) {
|
|
@@ -5868,13 +5931,13 @@ async function runInit(dirPath) {
|
|
|
5868
5931
|
}
|
|
5869
5932
|
]);
|
|
5870
5933
|
if (!proceed) {
|
|
5871
|
-
console.log(
|
|
5934
|
+
console.log(chalk5.dim(" Skipped."));
|
|
5872
5935
|
return;
|
|
5873
5936
|
}
|
|
5874
5937
|
if (!hasGitignore) {
|
|
5875
5938
|
const gitignoreContent = detected.runtime === "node" ? "node_modules/\n.env\n.env.local\ndist/\n.next/\n" : detected.runtime === "python" ? "__pycache__/\n*.pyc\n.env\n.venv/\nvenv/\n" : ".env\n";
|
|
5876
5939
|
fs6.writeFileSync(path7.join(projectDir, ".gitignore"), gitignoreContent);
|
|
5877
|
-
console.log(
|
|
5940
|
+
console.log(chalk5.green(" Created .gitignore"));
|
|
5878
5941
|
}
|
|
5879
5942
|
if (detected.runtime === "node") {
|
|
5880
5943
|
const pkgPath = path7.join(projectDir, "package.json");
|
|
@@ -5886,7 +5949,7 @@ async function runInit(dirPath) {
|
|
|
5886
5949
|
pkg.scripts = pkg.scripts || {};
|
|
5887
5950
|
pkg.scripts.start = `node ${mainFile}`;
|
|
5888
5951
|
fs6.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
5889
|
-
console.log(
|
|
5952
|
+
console.log(chalk5.green(` Added start script: node ${mainFile}`));
|
|
5890
5953
|
}
|
|
5891
5954
|
} catch {
|
|
5892
5955
|
}
|
|
@@ -5900,17 +5963,17 @@ async function runInit(dirPath) {
|
|
|
5900
5963
|
target: "cloud"
|
|
5901
5964
|
};
|
|
5902
5965
|
fs6.writeFileSync(doableJsonPath, JSON.stringify(doableJson, null, 2) + "\n");
|
|
5903
|
-
console.log(
|
|
5966
|
+
console.log(chalk5.green(" Created .doable.json"));
|
|
5904
5967
|
}
|
|
5905
5968
|
const claudeMdPath = path7.join(projectDir, "CLAUDE.md");
|
|
5906
5969
|
if (!fs6.existsSync(claudeMdPath)) {
|
|
5907
5970
|
fs6.writeFileSync(claudeMdPath, generateClaudeMd(detected.runtime, detected.framework ?? null));
|
|
5908
|
-
console.log(
|
|
5971
|
+
console.log(chalk5.green(" Created CLAUDE.md (teaches AI assistants how to deploy)"));
|
|
5909
5972
|
}
|
|
5910
5973
|
console.log();
|
|
5911
5974
|
}
|
|
5912
|
-
console.log(
|
|
5913
|
-
console.log(
|
|
5975
|
+
console.log(chalk5.bold(" Ready to deploy."));
|
|
5976
|
+
console.log(chalk5.dim(` Run ${chalk5.cyan("doable deploy")} to go live.`));
|
|
5914
5977
|
console.log();
|
|
5915
5978
|
}
|
|
5916
5979
|
function generateClaudeMd(runtime, framework) {
|
|
@@ -5970,14 +6033,14 @@ function registerClaimCommand(program2) {
|
|
|
5970
6033
|
async (tokenArg, opts) => {
|
|
5971
6034
|
const token = tokenArg || readTokenFromPreviewJson();
|
|
5972
6035
|
if (!token) {
|
|
5973
|
-
console.error(
|
|
6036
|
+
console.error(chalk5.red("No preview token provided."));
|
|
5974
6037
|
console.error(
|
|
5975
|
-
|
|
5976
|
-
" Pass it as an argument: " +
|
|
6038
|
+
chalk5.dim(
|
|
6039
|
+
" Pass it as an argument: " + chalk5.cyan("doable claim prv_abc123")
|
|
5977
6040
|
)
|
|
5978
6041
|
);
|
|
5979
6042
|
console.error(
|
|
5980
|
-
|
|
6043
|
+
chalk5.dim(
|
|
5981
6044
|
" Or run this from the same directory where you ran `doable preview` so the CLI can pick it up automatically."
|
|
5982
6045
|
)
|
|
5983
6046
|
);
|
|
@@ -5985,13 +6048,13 @@ function registerClaimCommand(program2) {
|
|
|
5985
6048
|
}
|
|
5986
6049
|
const apiToken = getToken();
|
|
5987
6050
|
if (!apiToken) {
|
|
5988
|
-
console.error(
|
|
6051
|
+
console.error(chalk5.red("You're not logged in."));
|
|
5989
6052
|
console.error(
|
|
5990
|
-
|
|
6053
|
+
chalk5.dim(" Run " + chalk5.cyan("doable login") + " first, then retry.")
|
|
5991
6054
|
);
|
|
5992
6055
|
process.exit(1);
|
|
5993
6056
|
}
|
|
5994
|
-
const spinner =
|
|
6057
|
+
const spinner = ora10("Claiming preview\u2026").start();
|
|
5995
6058
|
let result;
|
|
5996
6059
|
try {
|
|
5997
6060
|
const res = await fetch(`${getApiUrl()}/v1/preview/claim`, {
|
|
@@ -6013,31 +6076,31 @@ function registerClaimCommand(program2) {
|
|
|
6013
6076
|
spinner.fail(
|
|
6014
6077
|
"Couldn't reach the Doable API. Check your connection and try again."
|
|
6015
6078
|
);
|
|
6016
|
-
console.error(
|
|
6079
|
+
console.error(chalk5.dim(err instanceof Error ? err.message : String(err)));
|
|
6017
6080
|
process.exit(1);
|
|
6018
6081
|
}
|
|
6019
6082
|
spinner.succeed("Preview claimed.");
|
|
6020
6083
|
const liveUrl = `https://${result.projectSlug}.doable.do`;
|
|
6021
6084
|
console.log();
|
|
6022
|
-
console.log(
|
|
6023
|
-
console.log(` Live at ${
|
|
6085
|
+
console.log(chalk5.green.bold(" It's yours."));
|
|
6086
|
+
console.log(` Live at ${chalk5.underline.cyan(liveUrl)}`);
|
|
6024
6087
|
console.log(
|
|
6025
|
-
` Share ${
|
|
6088
|
+
` Share ${chalk5.cyan(`https://doable.do/s/${result.projectId}`)}`
|
|
6026
6089
|
);
|
|
6027
|
-
console.log(
|
|
6090
|
+
console.log(chalk5.dim(` Project ${result.projectSlug}`));
|
|
6028
6091
|
console.log();
|
|
6029
|
-
console.log(
|
|
6092
|
+
console.log(chalk5.dim(" Next:"));
|
|
6030
6093
|
console.log(
|
|
6031
|
-
|
|
6032
|
-
" Add a custom domain: " +
|
|
6094
|
+
chalk5.dim(
|
|
6095
|
+
" Add a custom domain: " + chalk5.cyan(`doable domain search yourname.com`)
|
|
6033
6096
|
)
|
|
6034
6097
|
);
|
|
6035
6098
|
console.log(
|
|
6036
|
-
|
|
6099
|
+
chalk5.dim(" Manage env vars: " + chalk5.cyan("doable env list"))
|
|
6037
6100
|
);
|
|
6038
6101
|
console.log(
|
|
6039
|
-
|
|
6040
|
-
" Watch logs: " +
|
|
6102
|
+
chalk5.dim(
|
|
6103
|
+
" Watch logs: " + chalk5.cyan(`doable logs --deployment <id>`)
|
|
6041
6104
|
)
|
|
6042
6105
|
);
|
|
6043
6106
|
console.log();
|
|
@@ -6305,13 +6368,13 @@ function registerNewCommand(program2) {
|
|
|
6305
6368
|
return;
|
|
6306
6369
|
}
|
|
6307
6370
|
if (!templateArg) {
|
|
6308
|
-
console.error(
|
|
6371
|
+
console.error(chalk5.red("Pick a template."));
|
|
6309
6372
|
printTemplateList();
|
|
6310
6373
|
process.exit(1);
|
|
6311
6374
|
}
|
|
6312
6375
|
const template = TEMPLATES[templateArg];
|
|
6313
6376
|
if (!template) {
|
|
6314
|
-
console.error(
|
|
6377
|
+
console.error(chalk5.red(`Unknown template: ${templateArg}`));
|
|
6315
6378
|
printTemplateList();
|
|
6316
6379
|
process.exit(1);
|
|
6317
6380
|
}
|
|
@@ -6319,7 +6382,7 @@ function registerNewCommand(program2) {
|
|
|
6319
6382
|
const safeName = sanitizeName(projectName);
|
|
6320
6383
|
if (!safeName) {
|
|
6321
6384
|
console.error(
|
|
6322
|
-
|
|
6385
|
+
chalk5.red(
|
|
6323
6386
|
`"${projectName}" isn't a valid project name. Use letters, numbers, dashes.`
|
|
6324
6387
|
)
|
|
6325
6388
|
);
|
|
@@ -6330,12 +6393,12 @@ function registerNewCommand(program2) {
|
|
|
6330
6393
|
const entries = fs6.readdirSync(targetDir);
|
|
6331
6394
|
if (entries.length > 0) {
|
|
6332
6395
|
console.error(
|
|
6333
|
-
|
|
6396
|
+
chalk5.red(
|
|
6334
6397
|
`Directory ${path7.relative(process.cwd(), targetDir) || "."} already exists and isn't empty.`
|
|
6335
6398
|
)
|
|
6336
6399
|
);
|
|
6337
6400
|
console.error(
|
|
6338
|
-
|
|
6401
|
+
chalk5.dim(
|
|
6339
6402
|
" Pick a different name, or pass --into <dir> to be explicit."
|
|
6340
6403
|
)
|
|
6341
6404
|
);
|
|
@@ -6347,19 +6410,19 @@ function registerNewCommand(program2) {
|
|
|
6347
6410
|
scaffold(template, safeName, targetDir);
|
|
6348
6411
|
const relDir = path7.relative(process.cwd(), targetDir) || ".";
|
|
6349
6412
|
console.log();
|
|
6350
|
-
console.log(
|
|
6413
|
+
console.log(chalk5.green.bold(` Done. Created ${safeName} from the ${template.name} template.`));
|
|
6351
6414
|
console.log();
|
|
6352
|
-
console.log(
|
|
6415
|
+
console.log(chalk5.dim(" Next:"));
|
|
6353
6416
|
if (relDir !== ".") {
|
|
6354
6417
|
console.log(
|
|
6355
|
-
" " +
|
|
6418
|
+
" " + chalk5.cyan(`cd ${relDir}`) + chalk5.dim(" switch to the new directory")
|
|
6356
6419
|
);
|
|
6357
6420
|
}
|
|
6358
6421
|
console.log(
|
|
6359
|
-
" " +
|
|
6422
|
+
" " + chalk5.cyan("doable preview") + chalk5.dim(" deploy with no signup, live in seconds")
|
|
6360
6423
|
);
|
|
6361
6424
|
console.log(
|
|
6362
|
-
" " +
|
|
6425
|
+
" " + chalk5.cyan("doable deploy") + chalk5.dim(" deploy to your account")
|
|
6363
6426
|
);
|
|
6364
6427
|
console.log();
|
|
6365
6428
|
}
|
|
@@ -6382,19 +6445,19 @@ function scaffold(template, name, targetDir) {
|
|
|
6382
6445
|
written.push(".doable.json");
|
|
6383
6446
|
}
|
|
6384
6447
|
for (const file of written) {
|
|
6385
|
-
console.log(
|
|
6448
|
+
console.log(chalk5.dim(` + ${file}`));
|
|
6386
6449
|
}
|
|
6387
6450
|
}
|
|
6388
6451
|
function printTemplateList() {
|
|
6389
6452
|
console.log();
|
|
6390
|
-
console.log(
|
|
6453
|
+
console.log(chalk5.bold(" Templates"));
|
|
6391
6454
|
console.log();
|
|
6392
6455
|
for (const t of listTemplates()) {
|
|
6393
|
-
console.log(` ${
|
|
6456
|
+
console.log(` ${chalk5.cyan(t.name.padEnd(12))}${chalk5.dim(t.description)}`);
|
|
6394
6457
|
}
|
|
6395
6458
|
console.log();
|
|
6396
|
-
console.log(
|
|
6397
|
-
console.log(" " +
|
|
6459
|
+
console.log(chalk5.dim(" Usage:"));
|
|
6460
|
+
console.log(" " + chalk5.cyan("doable new next my-app"));
|
|
6398
6461
|
console.log();
|
|
6399
6462
|
}
|
|
6400
6463
|
function sanitizeName(input) {
|
|
@@ -6410,7 +6473,7 @@ function registerUpgradeCommand(program2) {
|
|
|
6410
6473
|
"Exit code only: 0 if up to date, 1 if a newer version is available."
|
|
6411
6474
|
).action(async (opts) => {
|
|
6412
6475
|
const current = readCliVersion();
|
|
6413
|
-
const spinner =
|
|
6476
|
+
const spinner = ora10("Checking the npm registry\u2026").start();
|
|
6414
6477
|
let latest;
|
|
6415
6478
|
try {
|
|
6416
6479
|
latest = await fetchLatestVersion();
|
|
@@ -6419,42 +6482,42 @@ function registerUpgradeCommand(program2) {
|
|
|
6419
6482
|
"Couldn't reach the npm registry. Try again, or upgrade manually."
|
|
6420
6483
|
);
|
|
6421
6484
|
console.error(
|
|
6422
|
-
|
|
6485
|
+
chalk5.dim(err instanceof Error ? err.message : String(err))
|
|
6423
6486
|
);
|
|
6424
6487
|
process.exit(1);
|
|
6425
6488
|
}
|
|
6426
6489
|
spinner.stop();
|
|
6427
6490
|
const cmp = compareSemver(current, latest);
|
|
6428
6491
|
if (cmp >= 0) {
|
|
6429
|
-
console.log(
|
|
6492
|
+
console.log(chalk5.green(` You're on ${current}. That's the latest.`));
|
|
6430
6493
|
process.exit(0);
|
|
6431
6494
|
}
|
|
6432
6495
|
if (opts.check) {
|
|
6433
6496
|
console.log(
|
|
6434
|
-
|
|
6497
|
+
chalk5.yellow(` ${current} \u2192 ${latest}. A newer version is available.`)
|
|
6435
6498
|
);
|
|
6436
6499
|
process.exit(1);
|
|
6437
6500
|
}
|
|
6438
6501
|
console.log();
|
|
6439
|
-
console.log(
|
|
6502
|
+
console.log(chalk5.bold(` Update available: ${current} \u2192 ${latest}`));
|
|
6440
6503
|
console.log();
|
|
6441
6504
|
if (!opts.install) {
|
|
6442
|
-
console.log(
|
|
6443
|
-
console.log(" " +
|
|
6444
|
-
console.log(" " +
|
|
6445
|
-
console.log(" " +
|
|
6505
|
+
console.log(chalk5.dim(" Install it with one of:"));
|
|
6506
|
+
console.log(" " + chalk5.cyan("npm install -g doable-cli@latest"));
|
|
6507
|
+
console.log(" " + chalk5.cyan("brew upgrade doublewltd/doable/doable") + chalk5.dim(" (Homebrew)"));
|
|
6508
|
+
console.log(" " + chalk5.cyan("curl -fsSL https://doable.do/install.sh | bash"));
|
|
6446
6509
|
console.log();
|
|
6447
|
-
console.log(
|
|
6510
|
+
console.log(chalk5.dim(" Or rerun this with --install to do it now."));
|
|
6448
6511
|
return;
|
|
6449
6512
|
}
|
|
6450
|
-
const installSpinner =
|
|
6513
|
+
const installSpinner = ora10("Running npm install -g doable-cli@latest\u2026").start();
|
|
6451
6514
|
const code = await runNpmGlobal();
|
|
6452
6515
|
if (code === 0) {
|
|
6453
6516
|
installSpinner.succeed(`Upgraded to ${latest}.`);
|
|
6454
6517
|
} else {
|
|
6455
6518
|
installSpinner.fail(`npm install exited with code ${code}.`);
|
|
6456
6519
|
console.error(
|
|
6457
|
-
|
|
6520
|
+
chalk5.dim(
|
|
6458
6521
|
" If your CLI was installed via Homebrew or the install script, use that instead."
|
|
6459
6522
|
)
|
|
6460
6523
|
);
|
|
@@ -6511,10 +6574,11 @@ function runNpmGlobal() {
|
|
|
6511
6574
|
}
|
|
6512
6575
|
|
|
6513
6576
|
// src/index.ts
|
|
6514
|
-
var CLI_VERSION = "0.1.
|
|
6577
|
+
var CLI_VERSION = "0.1.4" ;
|
|
6515
6578
|
var program = new Command();
|
|
6516
6579
|
program.name("doable").description("Doable -- AI-native deployment platform").version(CLI_VERSION, "-v, --version");
|
|
6517
6580
|
registerLoginCommand(program);
|
|
6581
|
+
registerLogoutCommand(program);
|
|
6518
6582
|
registerDeployCommand(program);
|
|
6519
6583
|
registerProjectsCommand(program);
|
|
6520
6584
|
registerDomainCommand(program);
|