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