superslate 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2367 -1842
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1243,7 +1243,7 @@ ${i}
|
|
|
1243
1243
|
|
|
1244
1244
|
// src/cli.ts
|
|
1245
1245
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
1246
|
-
import { mkdir as mkdir5, mkdtemp as mkdtemp2, readFile as readFile6, readdir as readdir2, rename as
|
|
1246
|
+
import { mkdir as mkdir5, mkdtemp as mkdtemp2, readFile as readFile6, readdir as readdir2, rename as rename3, rm as rm5, rmdir, stat as stat5 } from "fs/promises";
|
|
1247
1247
|
import { createConnection, createServer } from "net";
|
|
1248
1248
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1249
1249
|
import { dirname as dirname3, join as join8, relative as relative2, resolve as resolve4, sep } from "path";
|
|
@@ -1300,1484 +1300,1670 @@ function validateBrandName(value) {
|
|
|
1300
1300
|
}
|
|
1301
1301
|
var SCOPE_EXTENSIONS = new Set([".json", ".jsonc", ".md", ".ts", ".tsx", ".yaml", ".yml"]);
|
|
1302
1302
|
|
|
1303
|
-
// src/
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1303
|
+
// src/core.ts
|
|
1304
|
+
import { randomBytes } from "node:crypto";
|
|
1305
|
+
import { extname } from "node:path";
|
|
1306
|
+
var BRAND_MANIFEST = [
|
|
1307
|
+
{ path: "apps/web/src/configs/config.ts", occurrences: 2 },
|
|
1308
|
+
{ path: "apps/web/index.html", occurrences: 4 },
|
|
1309
|
+
{ path: "apps/web/public/favicon/site.webmanifest", occurrences: 2 },
|
|
1310
|
+
{ path: "apps/web/public/offline.html", occurrences: 6 },
|
|
1311
|
+
{ path: "apps/docs/lib/shared.ts", occurrences: 1 },
|
|
1312
|
+
{ path: "apps/docs/README.md", occurrences: 1 },
|
|
1313
|
+
{ path: "README.md", occurrences: 1 },
|
|
1314
|
+
{ path: "docs/meta.json", occurrences: 1 },
|
|
1315
|
+
{ path: "apps/server/.env.example", occurrences: 1 },
|
|
1316
|
+
{ path: "apps/server/src/config/env.ts", occurrences: 2 },
|
|
1317
|
+
{ path: "apps/server/src/config/env.test.ts", occurrences: 1 },
|
|
1318
|
+
{ path: "packages/email/src/billing-payment-failed.tsx", occurrences: 2 },
|
|
1319
|
+
{ path: "packages/email/src/login-email.tsx", occurrences: 3 },
|
|
1320
|
+
{ path: "packages/email/src/verify-email.tsx", occurrences: 1 },
|
|
1321
|
+
{ path: "packages/email/src/reset-password.tsx", occurrences: 1 },
|
|
1322
|
+
{ path: "packages/email/src/security-alert.tsx", occurrences: 1 },
|
|
1323
|
+
{ path: "packages/email/src/components/EmailSignature.tsx", occurrences: 1 },
|
|
1324
|
+
{ path: "packages/email/src/components/EmailLayout.tsx", occurrences: 1 },
|
|
1325
|
+
{ path: "packages/email/src/components/EmailCopyright.tsx", occurrences: 1 },
|
|
1326
|
+
{ path: "packages/email/src/welcome-email.tsx", occurrences: 4 },
|
|
1327
|
+
{ path: "packages/email/src/billing-payment-successful.tsx", occurrences: 1 },
|
|
1328
|
+
{ path: "packages/email/src/billing-subscription-created.tsx", occurrences: 2 },
|
|
1329
|
+
{ path: "packages/email/src/billing-subscription-canceled.tsx", occurrences: 2 },
|
|
1330
|
+
{ path: "apps/server/email-templates/billing-payment-failed.html", occurrences: 6 },
|
|
1331
|
+
{ path: "apps/server/email-templates/billing-payment-successful.html", occurrences: 5 },
|
|
1332
|
+
{ path: "apps/server/email-templates/billing-subscription-canceled.html", occurrences: 6 },
|
|
1333
|
+
{ path: "apps/server/email-templates/billing-subscription-created.html", occurrences: 6 },
|
|
1334
|
+
{ path: "apps/server/email-templates/login-email.html", occurrences: 7 },
|
|
1335
|
+
{ path: "apps/server/email-templates/security-alert.html", occurrences: 4 },
|
|
1336
|
+
{ path: "apps/server/email-templates/verify-email.html", occurrences: 5 },
|
|
1337
|
+
{ path: "apps/server/email-templates/reset-password.html", occurrences: 5 },
|
|
1338
|
+
{ path: "apps/server/email-templates/organization-invitation.html", occurrences: 3 },
|
|
1339
|
+
{ path: "apps/server/email-templates/welcome-email.html", occurrences: 8 }
|
|
1340
|
+
];
|
|
1341
|
+
var START_BRAND_MANIFEST = [
|
|
1342
|
+
{ path: "apps/start/AGENTS.md", occurrences: 1 },
|
|
1343
|
+
{ path: "apps/start/README.md", occurrences: 1 },
|
|
1344
|
+
{ path: "apps/start/src/server/config.ts", occurrences: 2 },
|
|
1345
|
+
{ path: "apps/start/src/shared/site.ts", occurrences: 1 },
|
|
1346
|
+
{ path: "apps/start/wrangler.jsonc", occurrences: 2 },
|
|
1347
|
+
{ path: "apps/start/worker-configuration.d.ts", occurrences: 2 }
|
|
1348
|
+
];
|
|
1349
|
+
var MOBILE_EDITION_PATHS = ["apps/mobile", "docs/mobile.md"];
|
|
1350
|
+
var UNIVERSAL_LINK_PATH_PREFIXES = ["/auth", "/invitations"];
|
|
1351
|
+
function validateAppName2(value) {
|
|
1352
|
+
if (!value)
|
|
1353
|
+
return "App name is required.";
|
|
1354
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
1355
|
+
return "App name must be kebab-case (lowercase letters, numbers, and single hyphens).";
|
|
1313
1356
|
}
|
|
1314
1357
|
return;
|
|
1315
1358
|
}
|
|
1316
|
-
function
|
|
1317
|
-
|
|
1359
|
+
function normalizeScope2(value) {
|
|
1360
|
+
const scope = value.startsWith("@") ? value : `@${value}`;
|
|
1361
|
+
if (/\s/.test(scope)) {
|
|
1362
|
+
throw new Error("Use hyphens instead of spaces, for example test-package.");
|
|
1363
|
+
}
|
|
1364
|
+
if (!/^@[a-z0-9][a-z0-9._-]*$/.test(scope)) {
|
|
1365
|
+
throw new Error("Use lowercase letters and numbers, with optional periods, underscores, or hyphens.");
|
|
1366
|
+
}
|
|
1367
|
+
return scope;
|
|
1318
1368
|
}
|
|
1319
|
-
function
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
1369
|
+
function validateBrandName2(value) {
|
|
1370
|
+
if (!value.trim())
|
|
1371
|
+
return "Brand name cannot be empty.";
|
|
1372
|
+
if (/\r|\n/.test(value))
|
|
1373
|
+
return "Brand name must fit on one line.";
|
|
1374
|
+
if (value !== value.trim())
|
|
1375
|
+
return "Brand name cannot start or end with whitespace.";
|
|
1376
|
+
if (value.length > 48)
|
|
1377
|
+
return "Brand name must be 48 characters or less.";
|
|
1378
|
+
if (!/^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u.test(value)) {
|
|
1379
|
+
return "Brand name may contain letters, numbers, spaces, periods, underscores, and hyphens.";
|
|
1324
1380
|
}
|
|
1325
|
-
return
|
|
1381
|
+
return;
|
|
1326
1382
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1383
|
+
function replaceLiteral(content, search, replacement) {
|
|
1384
|
+
if (search === replacement || !content.includes(search))
|
|
1385
|
+
return { content, replacements: 0 };
|
|
1386
|
+
const parts = content.split(search);
|
|
1387
|
+
return { content: parts.join(replacement), replacements: parts.length - 1 };
|
|
1388
|
+
}
|
|
1389
|
+
function rewriteScopeContent(content, scope, originalScope = DEFAULT_SCOPE) {
|
|
1390
|
+
return replaceLiteral(content, `${originalScope}/`, `${normalizeScope2(scope)}/`);
|
|
1391
|
+
}
|
|
1392
|
+
function rewriteBrandContent(content, brandName) {
|
|
1393
|
+
const error = validateBrandName2(brandName);
|
|
1394
|
+
if (error)
|
|
1395
|
+
throw new Error(error);
|
|
1396
|
+
const escapedSourceBrand = SOURCE_BRAND_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1397
|
+
const sourceBrandPattern = new RegExp(`\\b${escapedSourceBrand}\\b`, "gi");
|
|
1398
|
+
const matches = content.match(sourceBrandPattern);
|
|
1399
|
+
if (brandName === SOURCE_BRAND_NAME || !matches)
|
|
1400
|
+
return { content, replacements: 0 };
|
|
1401
|
+
return {
|
|
1402
|
+
content: content.replace(sourceBrandPattern, () => brandName),
|
|
1403
|
+
replacements: matches.length
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function mobileSchemeFor(appName) {
|
|
1407
|
+
const error = validateAppName2(appName);
|
|
1408
|
+
if (error)
|
|
1409
|
+
throw new Error(error);
|
|
1410
|
+
return appName;
|
|
1411
|
+
}
|
|
1412
|
+
function androidPackageSegment(appName) {
|
|
1413
|
+
const segment = appName.replace(/[^a-z0-9]/g, "");
|
|
1414
|
+
return /^[0-9]/.test(segment) || !segment ? `app${segment}` : segment;
|
|
1415
|
+
}
|
|
1416
|
+
function validateAppDomain(value) {
|
|
1417
|
+
if (!value.trim())
|
|
1418
|
+
return "Application domain cannot be empty.";
|
|
1419
|
+
if (/^https?:\/\//.test(value))
|
|
1420
|
+
return "Use a bare host such as app.example.com, not a URL.";
|
|
1421
|
+
if (value.includes("/"))
|
|
1422
|
+
return "Use a bare host such as app.example.com, with no path.";
|
|
1423
|
+
if (!/^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(value)) {
|
|
1424
|
+
return "Use a lowercase hostname with at least two labels, for example app.example.com.";
|
|
1335
1425
|
}
|
|
1336
|
-
return
|
|
1426
|
+
return;
|
|
1337
1427
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
var S_STEP_ERROR2 = unicodeOr2("▲", "x");
|
|
1344
|
-
var S_STEP_SUBMIT2 = unicodeOr2("◇", "o");
|
|
1345
|
-
var S_BAR_START2 = unicodeOr2("┌", "T");
|
|
1346
|
-
var S_BAR2 = unicodeOr2("│", "|");
|
|
1347
|
-
var S_BAR_END2 = unicodeOr2("└", "—");
|
|
1348
|
-
var S_BAR_START_RIGHT2 = unicodeOr2("┐", "T");
|
|
1349
|
-
var S_BAR_END_RIGHT2 = unicodeOr2("┘", "—");
|
|
1350
|
-
var S_RADIO_ACTIVE2 = unicodeOr2("●", ">");
|
|
1351
|
-
var S_RADIO_INACTIVE2 = unicodeOr2("○", " ");
|
|
1352
|
-
var S_CHECKBOX_ACTIVE2 = unicodeOr2("◻", "[•]");
|
|
1353
|
-
var S_CHECKBOX_SELECTED2 = unicodeOr2("◼", "[+]");
|
|
1354
|
-
var S_CHECKBOX_INACTIVE2 = unicodeOr2("◻", "[ ]");
|
|
1355
|
-
var S_PASSWORD_MASK2 = unicodeOr2("▪", "•");
|
|
1356
|
-
var S_BAR_H2 = unicodeOr2("─", "-");
|
|
1357
|
-
var S_CORNER_TOP_RIGHT2 = unicodeOr2("╮", "+");
|
|
1358
|
-
var S_CONNECT_LEFT2 = unicodeOr2("├", "+");
|
|
1359
|
-
var S_CORNER_BOTTOM_RIGHT2 = unicodeOr2("╯", "+");
|
|
1360
|
-
var S_CORNER_BOTTOM_LEFT2 = unicodeOr2("╰", "+");
|
|
1361
|
-
var S_CORNER_TOP_LEFT2 = unicodeOr2("╭", "+");
|
|
1362
|
-
var S_INFO2 = unicodeOr2("●", "•");
|
|
1363
|
-
var S_SUCCESS2 = unicodeOr2("◆", "*");
|
|
1364
|
-
var S_WARN2 = unicodeOr2("▲", "!");
|
|
1365
|
-
var S_ERROR2 = unicodeOr2("■", "x");
|
|
1366
|
-
var symbol2 = (o) => {
|
|
1367
|
-
switch (o) {
|
|
1368
|
-
case "initial":
|
|
1369
|
-
case "active":
|
|
1370
|
-
return styleText3("cyan", S_STEP_ACTIVE2);
|
|
1371
|
-
case "cancel":
|
|
1372
|
-
return styleText3("red", S_STEP_CANCEL2);
|
|
1373
|
-
case "error":
|
|
1374
|
-
return styleText3("yellow", S_STEP_ERROR2);
|
|
1375
|
-
case "submit":
|
|
1376
|
-
return styleText3("green", S_STEP_SUBMIT2);
|
|
1377
|
-
case "validating":
|
|
1378
|
-
return styleText3("dim", S_STEP_ACTIVE2);
|
|
1428
|
+
function validateBundleIdentifier(value) {
|
|
1429
|
+
if (!value.trim())
|
|
1430
|
+
return "Bundle identifier cannot be empty.";
|
|
1431
|
+
if (!/^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){2,}$/.test(value)) {
|
|
1432
|
+
return "Use at least three lowercase reverse-DNS segments, for example com.example.app.";
|
|
1379
1433
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
function deriveMobileIdentity(appName, brandName, bundleIdentifier, appDomain) {
|
|
1437
|
+
const brandError = validateBrandName2(brandName);
|
|
1438
|
+
if (brandError)
|
|
1439
|
+
throw new Error(brandError);
|
|
1440
|
+
const resolvedBundleId = bundleIdentifier ?? `com.${androidPackageSegment(appName)}.app`;
|
|
1441
|
+
const bundleError = validateBundleIdentifier(resolvedBundleId);
|
|
1442
|
+
if (bundleError)
|
|
1443
|
+
throw new Error(bundleError);
|
|
1444
|
+
const domain = appDomain?.trim().toLowerCase();
|
|
1445
|
+
if (domain) {
|
|
1446
|
+
const domainError = validateAppDomain(domain);
|
|
1447
|
+
if (domainError)
|
|
1448
|
+
throw new Error(domainError);
|
|
1392
1449
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1450
|
+
return {
|
|
1451
|
+
displayName: brandName,
|
|
1452
|
+
slug: appName,
|
|
1453
|
+
scheme: mobileSchemeFor(appName),
|
|
1454
|
+
bundleIdentifier: resolvedBundleId,
|
|
1455
|
+
...domain ? { appDomain: domain } : {}
|
|
1456
|
+
};
|
|
1397
1457
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1458
|
+
function universalLinkConfig(domain) {
|
|
1459
|
+
return {
|
|
1460
|
+
associatedDomains: [`applinks:${domain}`],
|
|
1461
|
+
intentFilters: [
|
|
1462
|
+
{
|
|
1463
|
+
action: "VIEW",
|
|
1464
|
+
autoVerify: true,
|
|
1465
|
+
category: ["BROWSABLE", "DEFAULT"],
|
|
1466
|
+
data: UNIVERSAL_LINK_PATH_PREFIXES.map((pathPrefix) => ({
|
|
1467
|
+
scheme: "https",
|
|
1468
|
+
host: domain,
|
|
1469
|
+
pathPrefix
|
|
1470
|
+
}))
|
|
1471
|
+
}
|
|
1472
|
+
]
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function rewriteMobileAppConfig(content, identity) {
|
|
1476
|
+
const parsed = JSON.parse(content);
|
|
1477
|
+
const { expo } = parsed;
|
|
1478
|
+
if (!expo)
|
|
1479
|
+
throw new Error('Expo configuration is missing its "expo" block.');
|
|
1480
|
+
expo.name = identity.displayName;
|
|
1481
|
+
expo.slug = identity.slug;
|
|
1482
|
+
expo.scheme = identity.scheme;
|
|
1483
|
+
if (expo.ios)
|
|
1484
|
+
expo.ios.bundleIdentifier = identity.bundleIdentifier;
|
|
1485
|
+
if (expo.android)
|
|
1486
|
+
expo.android.package = identity.bundleIdentifier;
|
|
1487
|
+
const links = identity.appDomain ? universalLinkConfig(identity.appDomain) : undefined;
|
|
1488
|
+
if (expo.ios) {
|
|
1489
|
+
if (links)
|
|
1490
|
+
expo.ios.associatedDomains = links.associatedDomains;
|
|
1491
|
+
else
|
|
1492
|
+
delete expo.ios.associatedDomains;
|
|
1493
|
+
}
|
|
1494
|
+
if (expo.android) {
|
|
1495
|
+
if (links)
|
|
1496
|
+
expo.android.intentFilters = links.intentFilters;
|
|
1497
|
+
else
|
|
1498
|
+
delete expo.android.intentFilters;
|
|
1499
|
+
}
|
|
1500
|
+
delete expo.updates;
|
|
1501
|
+
delete expo.owner;
|
|
1502
|
+
if (expo.extra && typeof expo.extra === "object")
|
|
1503
|
+
delete expo.extra.eas;
|
|
1504
|
+
return `${stringifyRepositoryJson(parsed)}
|
|
1505
|
+
`;
|
|
1506
|
+
}
|
|
1507
|
+
function rewriteMobileAppScheme(content, scheme) {
|
|
1508
|
+
if (!/^MOBILE_APP_SCHEME=.*$/m.test(content)) {
|
|
1509
|
+
throw new Error("Server .env.example must contain a MOBILE_APP_SCHEME entry.");
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
content: content.replace(/^MOBILE_APP_SCHEME=.*$/m, `MOBILE_APP_SCHEME=${scheme}`),
|
|
1513
|
+
replacements: 1
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
var JSON_PRINT_WIDTH = 100;
|
|
1517
|
+
function isJsonPrimitive(value) {
|
|
1518
|
+
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
1519
|
+
}
|
|
1520
|
+
function stringifyRepositoryJson(value, indent = 0, column = 0) {
|
|
1521
|
+
const pad = " ".repeat(indent);
|
|
1522
|
+
const innerPad = " ".repeat(indent + 2);
|
|
1523
|
+
if (Array.isArray(value)) {
|
|
1524
|
+
if (value.length === 0)
|
|
1525
|
+
return "[]";
|
|
1526
|
+
if (value.every(isJsonPrimitive)) {
|
|
1527
|
+
const inline = `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
|
|
1528
|
+
if (column + inline.length <= JSON_PRINT_WIDTH)
|
|
1529
|
+
return inline;
|
|
1411
1530
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const T = f + (d ? 1 : 0), y = W - (c ? 1 : 0);
|
|
1431
|
-
for (let t = T;t < y; t++) {
|
|
1432
|
-
const n = e[t], o = n ? w(n, t === l) : "", h = wrapAnsi(o, i, {
|
|
1433
|
-
hard: true,
|
|
1434
|
-
trim: false
|
|
1435
|
-
}).split(`
|
|
1436
|
-
`);
|
|
1437
|
-
s.push(h), g += h.length;
|
|
1531
|
+
const items = value.map((item) => `${innerPad}${stringifyRepositoryJson(item, indent + 2, indent + 2)}`);
|
|
1532
|
+
return `[
|
|
1533
|
+
${items.join(`,
|
|
1534
|
+
`)}
|
|
1535
|
+
${pad}]`;
|
|
1536
|
+
}
|
|
1537
|
+
if (value !== null && typeof value === "object") {
|
|
1538
|
+
const entries = Object.entries(value);
|
|
1539
|
+
if (entries.length === 0)
|
|
1540
|
+
return "{}";
|
|
1541
|
+
const items = entries.map(([key, item]) => {
|
|
1542
|
+
const label = `${JSON.stringify(key)}: `;
|
|
1543
|
+
return `${innerPad}${label}${stringifyRepositoryJson(item, indent + 2, indent + 2 + label.length)}`;
|
|
1544
|
+
});
|
|
1545
|
+
return `{
|
|
1546
|
+
${items.join(`,
|
|
1547
|
+
`)}
|
|
1548
|
+
${pad}}`;
|
|
1438
1549
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1550
|
+
return JSON.stringify(value);
|
|
1551
|
+
}
|
|
1552
|
+
function stripMobileRootScripts(content) {
|
|
1553
|
+
const parsed = JSON.parse(content);
|
|
1554
|
+
if (parsed.scripts) {
|
|
1555
|
+
delete parsed.scripts["dev:mobile"];
|
|
1556
|
+
delete parsed.scripts["build:mobile"];
|
|
1445
1557
|
}
|
|
1446
|
-
|
|
1447
|
-
d && x.push(M);
|
|
1448
|
-
for (const t of s)
|
|
1449
|
-
for (const n of t)
|
|
1450
|
-
x.push(n);
|
|
1451
|
-
return c && x.push(M), x;
|
|
1452
|
-
};
|
|
1453
|
-
var confirm2 = (e) => {
|
|
1454
|
-
const a = e.active ?? "Yes", o = e.inactive ?? "No";
|
|
1455
|
-
return new r({
|
|
1456
|
-
active: a,
|
|
1457
|
-
inactive: o,
|
|
1458
|
-
signal: e.signal,
|
|
1459
|
-
input: e.input,
|
|
1460
|
-
output: e.output,
|
|
1461
|
-
initialValue: e.initialValue ?? true,
|
|
1462
|
-
render() {
|
|
1463
|
-
const i = e.withGuide ?? settings.withGuide, u = `${symbol2(this.state)} `, l = i ? `${styleText3("gray", S_BAR2)} ` : "", f = wrapTextWithPrefix(e.output, e.message, l, u), s = `${i ? `${styleText3("gray", S_BAR2)}
|
|
1464
|
-
` : ""}${f}
|
|
1465
|
-
`, c = this.value ? a : o;
|
|
1466
|
-
switch (this.state) {
|
|
1467
|
-
case "submit": {
|
|
1468
|
-
const r = i ? `${styleText3("gray", S_BAR2)} ` : "";
|
|
1469
|
-
return `${s}${r}${styleText3("dim", c)}`;
|
|
1470
|
-
}
|
|
1471
|
-
case "cancel": {
|
|
1472
|
-
const r = i ? `${styleText3("gray", S_BAR2)} ` : "";
|
|
1473
|
-
return `${s}${r}${styleText3(["strikethrough", "dim"], c)}${i ? `
|
|
1474
|
-
${styleText3("gray", S_BAR2)}` : ""}`;
|
|
1475
|
-
}
|
|
1476
|
-
default: {
|
|
1477
|
-
const r = i ? `${styleText3("cyan", S_BAR2)} ` : "", g = i ? styleText3("cyan", S_BAR_END2) : "";
|
|
1478
|
-
return `${s}${r}${this.value ? `${styleText3("green", S_RADIO_ACTIVE2)} ${a}` : `${styleText3("dim", S_RADIO_INACTIVE2)} ${styleText3("dim", a)}`}${e.vertical ? i ? `
|
|
1479
|
-
${styleText3("cyan", S_BAR2)} ` : `
|
|
1480
|
-
` : ` ${styleText3("dim", "/")} `}${this.value ? `${styleText3("dim", S_RADIO_INACTIVE2)} ${styleText3("dim", o)}` : `${styleText3("green", S_RADIO_ACTIVE2)} ${o}`}
|
|
1481
|
-
${g}
|
|
1558
|
+
return `${JSON.stringify(parsed, null, 2)}
|
|
1482
1559
|
`;
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
`${styleText3("dim", "↑/↓")} to navigate`,
|
|
1490
|
-
`${styleText3("dim", "Space:")} select`,
|
|
1491
|
-
`${styleText3("dim", "Enter:")} confirm`
|
|
1492
|
-
];
|
|
1493
|
-
var m = (i, u) => i.split(`
|
|
1494
|
-
`).map((d) => u(d)).join(`
|
|
1495
|
-
`);
|
|
1496
|
-
var multiselect = (i) => {
|
|
1497
|
-
const u = (t, a) => {
|
|
1498
|
-
const r = t.label ?? String(t.value);
|
|
1499
|
-
return a === "disabled" ? `${styleText3("gray", S_CHECKBOX_INACTIVE2)} ${m(r, (o) => styleText3(["strikethrough", "gray"], o))}${t.hint ? ` ${styleText3("dim", `(${t.hint ?? "disabled"})`)}` : ""}` : a === "active" ? `${styleText3("cyan", S_CHECKBOX_ACTIVE2)} ${r}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "selected" ? `${styleText3("green", S_CHECKBOX_SELECTED2)} ${m(r, (o) => styleText3("dim", o))}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "cancelled" ? `${m(r, (o) => styleText3(["strikethrough", "dim"], o))}` : a === "active-selected" ? `${styleText3("green", S_CHECKBOX_SELECTED2)} ${r}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "submitted" ? `${m(r, (o) => styleText3("dim", o))}` : `${styleText3("dim", S_CHECKBOX_INACTIVE2)} ${m(r, (o) => styleText3("dim", o))}`;
|
|
1500
|
-
}, d = i.required ?? true, x = i.showInstructions ?? true;
|
|
1501
|
-
return new a({
|
|
1502
|
-
options: i.options,
|
|
1503
|
-
signal: i.signal,
|
|
1504
|
-
input: i.input,
|
|
1505
|
-
output: i.output,
|
|
1506
|
-
initialValues: i.initialValues,
|
|
1507
|
-
required: d,
|
|
1508
|
-
cursorAt: i.cursorAt,
|
|
1509
|
-
validate(t) {
|
|
1510
|
-
if (d && (t === undefined || t.length === 0))
|
|
1511
|
-
return `Please select at least one option.
|
|
1512
|
-
${styleText3("reset", styleText3("dim", `Press ${styleText3(["gray", "bgWhite", "inverse"], " space ")} to select, ${styleText3("gray", styleText3("bgWhite", styleText3("inverse", " enter ")))} to submit`))}`;
|
|
1513
|
-
},
|
|
1514
|
-
render() {
|
|
1515
|
-
const t = i.withGuide ?? settings.withGuide, a = wrapTextWithPrefix(i.output, i.message, t ? `${symbolBar(this.state)} ` : "", `${symbol2(this.state)} `), r = `${t ? `${styleText3("gray", S_BAR2)}
|
|
1516
|
-
` : ""}${a}
|
|
1517
|
-
`, o = this.value ?? [], g = (n, l) => {
|
|
1518
|
-
if (n.disabled)
|
|
1519
|
-
return u(n, "disabled");
|
|
1520
|
-
const s = o.includes(n.value);
|
|
1521
|
-
return l && s ? u(n, "active-selected") : s ? u(n, "selected") : u(n, l ? "active" : "inactive");
|
|
1522
|
-
};
|
|
1523
|
-
switch (this.state) {
|
|
1524
|
-
case "submit": {
|
|
1525
|
-
const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "submitted")).join(styleText3("dim", ", ")) || styleText3("dim", "none"), l = wrapTextWithPrefix(i.output, n, t ? `${styleText3("gray", S_BAR2)} ` : "");
|
|
1526
|
-
return `${r}${l}`;
|
|
1527
|
-
}
|
|
1528
|
-
case "cancel": {
|
|
1529
|
-
const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "cancelled")).join(styleText3("dim", ", "));
|
|
1530
|
-
if (n.trim() === "")
|
|
1531
|
-
return `${r}${styleText3("gray", S_BAR2)}`;
|
|
1532
|
-
const l = wrapTextWithPrefix(i.output, n, t ? `${styleText3("gray", S_BAR2)} ` : "");
|
|
1533
|
-
return `${r}${l}${t ? `
|
|
1534
|
-
${styleText3("gray", S_BAR2)}` : ""}`;
|
|
1535
|
-
}
|
|
1536
|
-
case "error": {
|
|
1537
|
-
const n = t ? `${styleText3("yellow", S_BAR2)} ` : "", l = this.error.split(`
|
|
1538
|
-
`).map(($, v) => v === 0 ? `${t ? `${styleText3("yellow", S_BAR_END2)} ` : ""}${styleText3("yellow", $)}` : ` ${$}`).join(`
|
|
1539
|
-
`), s = r.split(`
|
|
1540
|
-
`).length, h = l.split(`
|
|
1541
|
-
`).length + 1;
|
|
1542
|
-
return `${r}${n}${limitOptions({
|
|
1543
|
-
output: i.output,
|
|
1544
|
-
options: this.options,
|
|
1545
|
-
cursor: this.cursor,
|
|
1546
|
-
maxItems: i.maxItems,
|
|
1547
|
-
columnPadding: n.length,
|
|
1548
|
-
rowPadding: s + h,
|
|
1549
|
-
style: g
|
|
1550
|
-
}).join(`
|
|
1551
|
-
${n}`)}
|
|
1552
|
-
${l}
|
|
1560
|
+
}
|
|
1561
|
+
function stripMobileKnipWorkspace(content) {
|
|
1562
|
+
const parsed = JSON.parse(content);
|
|
1563
|
+
if (parsed.workspaces)
|
|
1564
|
+
delete parsed.workspaces["apps/mobile"];
|
|
1565
|
+
return `${stringifyRepositoryJson(parsed)}
|
|
1553
1566
|
`;
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
options: this.options,
|
|
1562
|
-
cursor: this.cursor,
|
|
1563
|
-
maxItems: i.maxItems,
|
|
1564
|
-
columnPadding: n.length,
|
|
1565
|
-
rowPadding: l + $,
|
|
1566
|
-
style: g
|
|
1567
|
-
}).join(`
|
|
1568
|
-
${n}`)}
|
|
1569
|
-
${h}
|
|
1567
|
+
}
|
|
1568
|
+
function stripMobileDocsPage(content) {
|
|
1569
|
+
const parsed = JSON.parse(content);
|
|
1570
|
+
if (Array.isArray(parsed.pages)) {
|
|
1571
|
+
parsed.pages = parsed.pages.filter((page) => page !== "mobile");
|
|
1572
|
+
}
|
|
1573
|
+
return `${stringifyRepositoryJson(parsed)}
|
|
1570
1574
|
`;
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
if (g.length > 0) {
|
|
1590
|
-
const [i, ...y] = g;
|
|
1591
|
-
i.length > 0 ? t.push(`${O}${i}`) : t.push(o ? e : "");
|
|
1592
|
-
for (const p of y)
|
|
1593
|
-
p.length > 0 ? t.push(`${u}${p}`) : t.push(o ? r : "");
|
|
1575
|
+
}
|
|
1576
|
+
function stripMobileDocsSource(content) {
|
|
1577
|
+
return content.replace(/^\s*'mobile\.md',\n/m, "");
|
|
1578
|
+
}
|
|
1579
|
+
function stripMobileDocsReferences(content) {
|
|
1580
|
+
return content.replace(/^## Mobile edition\n[\s\S]*?(?=^## |(?![\s\S]))/m, "").replace(/^\| `apps\/mobile` .*\n/gm, "").replace(/^`apps\/mobile`[\s\S]*?(?:\n\n|\n?(?![\s\S]))/gm, "").replace(/\[([^\]]+)\]\(\.\/mobile\.md(?:#[^)]*)?\)/g, "$1");
|
|
1581
|
+
}
|
|
1582
|
+
function formatRewrittenPackageJson(content) {
|
|
1583
|
+
const parsed = JSON.parse(content);
|
|
1584
|
+
for (const key of [
|
|
1585
|
+
"dependencies",
|
|
1586
|
+
"devDependencies",
|
|
1587
|
+
"optionalDependencies",
|
|
1588
|
+
"peerDependencies"
|
|
1589
|
+
]) {
|
|
1590
|
+
const values = parsed[key];
|
|
1591
|
+
if (values && typeof values === "object" && !Array.isArray(values)) {
|
|
1592
|
+
parsed[key] = Object.fromEntries(Object.entries(values).sort(([left], [right]) => left.localeCompare(right)));
|
|
1594
1593
|
}
|
|
1595
|
-
m.write(`${t.join(`
|
|
1596
|
-
`)}
|
|
1597
|
-
`);
|
|
1598
|
-
},
|
|
1599
|
-
info: (s, e) => {
|
|
1600
|
-
log2.message(s, { ...e, symbol: styleText3("blue", S_INFO2) });
|
|
1601
|
-
},
|
|
1602
|
-
success: (s, e) => {
|
|
1603
|
-
log2.message(s, { ...e, symbol: styleText3("green", S_SUCCESS2) });
|
|
1604
|
-
},
|
|
1605
|
-
step: (s, e) => {
|
|
1606
|
-
log2.message(s, { ...e, symbol: styleText3("green", S_STEP_SUBMIT2) });
|
|
1607
|
-
},
|
|
1608
|
-
warn: (s, e) => {
|
|
1609
|
-
log2.message(s, { ...e, symbol: styleText3("yellow", S_WARN2) });
|
|
1610
|
-
},
|
|
1611
|
-
warning: (s, e) => {
|
|
1612
|
-
log2.warn(s, e);
|
|
1613
|
-
},
|
|
1614
|
-
error: (s, e) => {
|
|
1615
|
-
log2.message(s, { ...e, symbol: styleText3("red", S_ERROR2) });
|
|
1616
1594
|
}
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
`)
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
${
|
|
1632
|
-
|
|
1633
|
-
|
|
1595
|
+
return `${JSON.stringify(parsed, null, 2)}
|
|
1596
|
+
`;
|
|
1597
|
+
}
|
|
1598
|
+
function wrapLongNamedImports(content, width = 100) {
|
|
1599
|
+
return content.split(`
|
|
1600
|
+
`).map((line) => {
|
|
1601
|
+
if (line.length <= width)
|
|
1602
|
+
return line;
|
|
1603
|
+
const match = line.match(/^(\s*)(import(?:\s+type)?\s+)\{([^}]+)\}(\s+from\s+.+)$/);
|
|
1604
|
+
if (!match)
|
|
1605
|
+
return line;
|
|
1606
|
+
const [, indentation = "", prefix = "", names = "", suffix = ""] = match;
|
|
1607
|
+
const members = names.split(",").map((name) => name.trim());
|
|
1608
|
+
return [
|
|
1609
|
+
`${indentation}${prefix}{`,
|
|
1610
|
+
...members.map((name) => `${indentation} ${name},`),
|
|
1611
|
+
`${indentation}}${suffix}`
|
|
1612
|
+
].join(`
|
|
1634
1613
|
`);
|
|
1635
|
-
}
|
|
1636
|
-
var W2 = (l) => styleText3("magenta", l);
|
|
1637
|
-
var spinner2 = ({
|
|
1638
|
-
indicator: l = "dots",
|
|
1639
|
-
onCancel: h,
|
|
1640
|
-
output: n = process.stdout,
|
|
1641
|
-
cancelMessage: G,
|
|
1642
|
-
errorMessage: O,
|
|
1643
|
-
frames: E = unicode2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"],
|
|
1644
|
-
delay: F = unicode2 ? 80 : 120,
|
|
1645
|
-
signal: m,
|
|
1646
|
-
...I
|
|
1647
|
-
} = {}) => {
|
|
1648
|
-
const u = isCI2();
|
|
1649
|
-
let M, T, d = false, S = false, s = "", p, w = performance.now();
|
|
1650
|
-
const x = getColumns(n), k = I?.styleFrame ?? W2, g = (e) => {
|
|
1651
|
-
const r = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel;
|
|
1652
|
-
S = e === 1, d && (a(r, e), S && typeof h == "function" && h());
|
|
1653
|
-
}, f = () => g(2), i = () => g(1), A = () => {
|
|
1654
|
-
process.on("uncaughtExceptionMonitor", f), process.on("unhandledRejection", f), process.on("SIGINT", i), process.on("SIGTERM", i), process.on("exit", g), m && m.addEventListener("abort", i);
|
|
1655
|
-
}, H = () => {
|
|
1656
|
-
process.removeListener("uncaughtExceptionMonitor", f), process.removeListener("unhandledRejection", f), process.removeListener("SIGINT", i), process.removeListener("SIGTERM", i), process.removeListener("exit", g), m && m.removeEventListener("abort", i);
|
|
1657
|
-
}, y = () => {
|
|
1658
|
-
if (p === undefined)
|
|
1659
|
-
return;
|
|
1660
|
-
u && n.write(`
|
|
1614
|
+
}).join(`
|
|
1661
1615
|
`);
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}).split(`
|
|
1616
|
+
}
|
|
1617
|
+
function wrapBrandedJsxText(content, brandName, width = 100) {
|
|
1618
|
+
const lines = content.split(`
|
|
1666
1619
|
`);
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
A(), T = setInterval(() => {
|
|
1676
|
-
if (u && s === p)
|
|
1677
|
-
return;
|
|
1678
|
-
y(), p = s;
|
|
1679
|
-
const o = k(E[r]);
|
|
1680
|
-
let v;
|
|
1681
|
-
if (u)
|
|
1682
|
-
v = `${o} ${s}...`;
|
|
1683
|
-
else if (l === "timer")
|
|
1684
|
-
v = `${o} ${s} ${_(w)}`;
|
|
1685
|
-
else {
|
|
1686
|
-
const B = ".".repeat(Math.floor(t)).slice(0, 3);
|
|
1687
|
-
v = `${o} ${s}${B}`;
|
|
1688
|
-
}
|
|
1689
|
-
const j = wrapAnsi(v, x, {
|
|
1690
|
-
hard: true,
|
|
1691
|
-
trim: false
|
|
1692
|
-
});
|
|
1693
|
-
n.write(j), r = r + 1 < E.length ? r + 1 : 0, t = t < 4 ? t + 0.125 : 0;
|
|
1694
|
-
}, F);
|
|
1695
|
-
}, a = (e = "", r = 0, t = false) => {
|
|
1696
|
-
if (!d)
|
|
1697
|
-
return;
|
|
1698
|
-
d = false, clearInterval(T), y();
|
|
1699
|
-
const o = r === 0 ? styleText3("green", S_STEP_SUBMIT2) : r === 1 ? styleText3("red", S_STEP_CANCEL2) : styleText3("red", S_STEP_ERROR2);
|
|
1700
|
-
s = e ?? s, t || (l === "timer" ? n.write(`${o} ${s} ${_(w)}
|
|
1701
|
-
`) : n.write(`${o} ${s}
|
|
1702
|
-
`)), H(), M();
|
|
1703
|
-
};
|
|
1704
|
-
return {
|
|
1705
|
-
start: P,
|
|
1706
|
-
stop: (e = "") => a(e, 0),
|
|
1707
|
-
message: (e = "") => {
|
|
1708
|
-
s = C(e ?? s);
|
|
1709
|
-
},
|
|
1710
|
-
cancel: (e = "") => a(e, 1),
|
|
1711
|
-
error: (e = "") => a(e, 2),
|
|
1712
|
-
clear: () => a("", 0, true),
|
|
1713
|
-
get isCancelled() {
|
|
1714
|
-
return S;
|
|
1620
|
+
const output = [];
|
|
1621
|
+
for (let index = 0;index < lines.length; ) {
|
|
1622
|
+
const line = lines[index] ?? "";
|
|
1623
|
+
const text = line.trim();
|
|
1624
|
+
if (!text || text.startsWith("<") || text.startsWith("{") || text.startsWith("//") || text.includes("<") || text.includes(">")) {
|
|
1625
|
+
output.push(line);
|
|
1626
|
+
index += 1;
|
|
1627
|
+
continue;
|
|
1715
1628
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
var c = (t, o) => t.includes(`
|
|
1728
|
-
`) ? t.split(`
|
|
1729
|
-
`).map((d) => o(d)).join(`
|
|
1730
|
-
`) : o(t);
|
|
1731
|
-
var select = (t) => {
|
|
1732
|
-
const o = (n, m) => {
|
|
1733
|
-
if (n === undefined)
|
|
1734
|
-
return "";
|
|
1735
|
-
const s = n.label ?? String(n.value);
|
|
1736
|
-
switch (m) {
|
|
1737
|
-
case "disabled":
|
|
1738
|
-
return `${styleText3("gray", S_RADIO_INACTIVE2)} ${c(s, (i) => styleText3("gray", i))}${n.hint ? ` ${styleText3("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
|
|
1739
|
-
case "selected":
|
|
1740
|
-
return `${c(s, (i) => styleText3("dim", i))}`;
|
|
1741
|
-
case "active":
|
|
1742
|
-
return `${styleText3("green", S_RADIO_ACTIVE2)} ${s}${n.hint ? ` ${styleText3("dim", `(${n.hint})`)}` : ""}`;
|
|
1743
|
-
case "cancelled":
|
|
1744
|
-
return `${c(s, (i) => styleText3(["strikethrough", "dim"], i))}`;
|
|
1745
|
-
default:
|
|
1746
|
-
return `${styleText3("dim", S_RADIO_INACTIVE2)} ${c(s, (i) => styleText3("dim", i))}`;
|
|
1629
|
+
const paragraph = [];
|
|
1630
|
+
const originalParagraph = [];
|
|
1631
|
+
const indentation = line.slice(0, line.length - line.trimStart().length);
|
|
1632
|
+
while (index < lines.length) {
|
|
1633
|
+
const candidate = lines[index] ?? "";
|
|
1634
|
+
const candidateText = candidate.trim();
|
|
1635
|
+
if (!candidateText || candidateText.startsWith("<") || candidateText.startsWith("{"))
|
|
1636
|
+
break;
|
|
1637
|
+
paragraph.push(candidateText);
|
|
1638
|
+
originalParagraph.push(candidate);
|
|
1639
|
+
index += 1;
|
|
1747
1640
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
input: t.input,
|
|
1753
|
-
output: t.output,
|
|
1754
|
-
initialValue: t.initialValue,
|
|
1755
|
-
render() {
|
|
1756
|
-
const n = t.withGuide ?? settings.withGuide, m = `${symbol2(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText3("gray", S_BAR2)}
|
|
1757
|
-
` : ""}${i}
|
|
1758
|
-
`;
|
|
1759
|
-
switch (this.state) {
|
|
1760
|
-
case "submit": {
|
|
1761
|
-
const r = n ? `${styleText3("gray", S_BAR2)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r);
|
|
1762
|
-
return `${u}${a}`;
|
|
1763
|
-
}
|
|
1764
|
-
case "cancel": {
|
|
1765
|
-
const r = n ? `${styleText3("gray", S_BAR2)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r);
|
|
1766
|
-
return `${u}${a}${n ? `
|
|
1767
|
-
${styleText3("gray", S_BAR2)}` : ""}`;
|
|
1768
|
-
}
|
|
1769
|
-
default: {
|
|
1770
|
-
const r = n ? `${styleText3("cyan", S_BAR2)} ` : "", a = u.split(`
|
|
1771
|
-
`).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS2, n) : n ? [styleText3("cyan", S_BAR_END2)] : [], f = p.join(`
|
|
1772
|
-
`), b = p.length + 1;
|
|
1773
|
-
return `${u}${r}${limitOptions({
|
|
1774
|
-
output: t.output,
|
|
1775
|
-
cursor: this.cursor,
|
|
1776
|
-
options: this.options,
|
|
1777
|
-
maxItems: t.maxItems,
|
|
1778
|
-
columnPadding: r.length,
|
|
1779
|
-
rowPadding: a + b,
|
|
1780
|
-
style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
|
|
1781
|
-
}).join(`
|
|
1782
|
-
${r}`)}
|
|
1783
|
-
${f}
|
|
1784
|
-
`;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1641
|
+
const joined = paragraph.join(" ");
|
|
1642
|
+
if (!joined.includes(brandName)) {
|
|
1643
|
+
output.push(...originalParagraph);
|
|
1644
|
+
continue;
|
|
1787
1645
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
output: t.output,
|
|
1797
|
-
signal: t.signal,
|
|
1798
|
-
input: t.input,
|
|
1799
|
-
render() {
|
|
1800
|
-
const r = t?.withGuide ?? settings.withGuide, l = `${`${r ? `${styleText3("gray", S_BAR2)}
|
|
1801
|
-
` : ""}${symbol2(this.state)} `}${t.message}
|
|
1802
|
-
`, d = t.placeholder && t.placeholder.length > 0 ? styleText3("inverse", t.placeholder[0]) + styleText3("dim", t.placeholder.slice(1)) : styleText3(["inverse", "hidden"], "_"), o = this.userInput ? this.userInputWithCursor : d, s = this.value ?? "";
|
|
1803
|
-
switch (this.state) {
|
|
1804
|
-
case "validating": {
|
|
1805
|
-
const n = r ? `${styleText3("cyan", S_BAR2)} ` : "", i = r ? styleText3("cyan", S_BAR_END2) : "", c = styleText3("dim", o), $ = styleText3("dim", "Validating...");
|
|
1806
|
-
return `${l}${n}${c}
|
|
1807
|
-
${i} ${$}
|
|
1808
|
-
`;
|
|
1809
|
-
}
|
|
1810
|
-
case "error": {
|
|
1811
|
-
const n = this.error ? ` ${styleText3("yellow", this.error)}` : "", i = r ? `${styleText3("yellow", S_BAR2)} ` : "", c = r ? styleText3("yellow", S_BAR_END2) : "";
|
|
1812
|
-
return `${l.trim()}
|
|
1813
|
-
${i}${o}
|
|
1814
|
-
${c}${n}
|
|
1815
|
-
`;
|
|
1816
|
-
}
|
|
1817
|
-
case "submit": {
|
|
1818
|
-
const n = s ? `${r ? " " : ""}${styleText3("dim", s)}` : "", i = r ? styleText3("gray", S_BAR2) : "";
|
|
1819
|
-
return `${l}${i}${n}`;
|
|
1820
|
-
}
|
|
1821
|
-
case "cancel": {
|
|
1822
|
-
const n = s ? ` ${styleText3(["strikethrough", "dim"], s)}` : "", i = r ? styleText3("gray", S_BAR2) : "";
|
|
1823
|
-
return `${l}${i}${n}${s.trim() ? `
|
|
1824
|
-
${i}` : ""}`;
|
|
1825
|
-
}
|
|
1826
|
-
default: {
|
|
1827
|
-
const n = r ? `${styleText3("cyan", S_BAR2)} ` : "", i = r ? styleText3("cyan", S_BAR_END2) : "";
|
|
1828
|
-
return `${l}${n}${o}
|
|
1829
|
-
${i}
|
|
1830
|
-
`;
|
|
1831
|
-
}
|
|
1646
|
+
let current = indentation;
|
|
1647
|
+
for (const word of joined.split(/\s+/)) {
|
|
1648
|
+
const candidate = current === indentation ? `${indentation}${word}` : `${current} ${word}`;
|
|
1649
|
+
if (candidate.length > width && current !== indentation) {
|
|
1650
|
+
output.push(current);
|
|
1651
|
+
current = `${indentation}${word}`;
|
|
1652
|
+
} else
|
|
1653
|
+
current = candidate;
|
|
1832
1654
|
}
|
|
1655
|
+
output.push(current);
|
|
1833
1656
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
// src/agent-setup.ts
|
|
1837
|
-
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
1838
|
-
import { dirname, join as join2, resolve } from "node:path";
|
|
1839
|
-
|
|
1840
|
-
// src/runtime.ts
|
|
1841
|
-
import { spawn } from "node:child_process";
|
|
1842
|
-
import { accessSync, constants, realpathSync } from "node:fs";
|
|
1843
|
-
import { delimiter, join, resolve as resolvePath } from "node:path";
|
|
1844
|
-
function runCaptured(command, options) {
|
|
1845
|
-
const [file, ...args] = command;
|
|
1846
|
-
if (!file)
|
|
1847
|
-
throw new Error("runCaptured requires a command");
|
|
1848
|
-
return new Promise((resolve, reject) => {
|
|
1849
|
-
const child = spawn(file, args, {
|
|
1850
|
-
cwd: options.cwd,
|
|
1851
|
-
env: options.env ?? process.env,
|
|
1852
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1853
|
-
});
|
|
1854
|
-
let stdout = "";
|
|
1855
|
-
let stderr = "";
|
|
1856
|
-
child.stdout.setEncoding("utf8");
|
|
1857
|
-
child.stderr.setEncoding("utf8");
|
|
1858
|
-
child.stdout.on("data", (chunk) => {
|
|
1859
|
-
stdout += chunk;
|
|
1860
|
-
});
|
|
1861
|
-
child.stderr.on("data", (chunk) => {
|
|
1862
|
-
stderr += chunk;
|
|
1863
|
-
});
|
|
1864
|
-
child.on("error", reject);
|
|
1865
|
-
child.on("close", (code, signal) => {
|
|
1866
|
-
resolve({ exitCode: code ?? (signal ? 1 : 0), stdout, stderr });
|
|
1867
|
-
});
|
|
1868
|
-
});
|
|
1657
|
+
return output.join(`
|
|
1658
|
+
`);
|
|
1869
1659
|
}
|
|
1870
|
-
function
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
return candidate;
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
return null;
|
|
1660
|
+
function rewriteRootPackageName(content, appName) {
|
|
1661
|
+
const error = validateAppName2(appName);
|
|
1662
|
+
if (error)
|
|
1663
|
+
throw new Error(error);
|
|
1664
|
+
const parsed = JSON.parse(content);
|
|
1665
|
+
if (parsed.name === appName)
|
|
1666
|
+
return { content, replacements: 0 };
|
|
1667
|
+
parsed.name = appName;
|
|
1668
|
+
return { content: `${JSON.stringify(parsed, null, 2)}
|
|
1669
|
+
`, replacements: 1 };
|
|
1884
1670
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1671
|
+
var WORKER_NAME_DECLARATION = /^name = "[^"]*"$/m;
|
|
1672
|
+
function rewriteWorkerName(content, appName) {
|
|
1673
|
+
const error = validateAppName2(appName);
|
|
1674
|
+
if (error)
|
|
1675
|
+
throw new Error(error);
|
|
1676
|
+
const declaration = content.match(WORKER_NAME_DECLARATION)?.[0];
|
|
1677
|
+
if (!declaration) {
|
|
1678
|
+
throw new Error("Cloudflare Worker configuration is missing a name declaration.");
|
|
1891
1679
|
}
|
|
1680
|
+
const replacement = `name = "${appName}"`;
|
|
1681
|
+
if (declaration === replacement)
|
|
1682
|
+
return { content, replacements: 0 };
|
|
1683
|
+
return { content: content.replace(WORKER_NAME_DECLARATION, replacement), replacements: 1 };
|
|
1892
1684
|
}
|
|
1685
|
+
function replaceMarkdownSection(content, heading, replacement) {
|
|
1686
|
+
const marker = `## ${heading}
|
|
1687
|
+
`;
|
|
1688
|
+
const start = content.indexOf(marker);
|
|
1689
|
+
if (start === -1)
|
|
1690
|
+
throw new Error(`Agent instructions are missing the "${heading}" section.`);
|
|
1691
|
+
const end = content.indexOf(`
|
|
1692
|
+
## `, start + marker.length);
|
|
1693
|
+
if (end === -1)
|
|
1694
|
+
throw new Error(`Agent instructions cannot delimit the "${heading}" section.`);
|
|
1695
|
+
const sections = [content.slice(0, start).trim(), replacement.trim(), content.slice(end).trim()];
|
|
1696
|
+
return `${sections.filter(Boolean).join(`
|
|
1893
1697
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
var SERVER_NAME = "agent-docs";
|
|
1897
|
-
var CODEX_START = "# agent-context:start";
|
|
1898
|
-
var CODEX_END = "# agent-context:end";
|
|
1899
|
-
var AGENT_CONTEXT_TOOLS = [
|
|
1900
|
-
"search_docs",
|
|
1901
|
-
"search_implementation",
|
|
1902
|
-
"get_repository_map",
|
|
1903
|
-
"plan_change",
|
|
1904
|
-
"get_invariants",
|
|
1905
|
-
"get_verification"
|
|
1906
|
-
];
|
|
1907
|
-
function takeValue(args, index, flag) {
|
|
1908
|
-
const value = args[index + 1];
|
|
1909
|
-
if (!value || value.startsWith("-"))
|
|
1910
|
-
throw new Error(`${flag} requires a value.`);
|
|
1911
|
-
return value;
|
|
1912
|
-
}
|
|
1913
|
-
function parseAgentSetupArguments(argv) {
|
|
1914
|
-
const parsed = { yes: false, dryRun: false, help: false };
|
|
1915
|
-
for (let index = 0;index < argv.length; index += 1) {
|
|
1916
|
-
const argument = argv[index];
|
|
1917
|
-
if (argument === "--clients") {
|
|
1918
|
-
const values = takeValue(argv, index++, argument).split(",").map((value) => value.trim()).filter(Boolean);
|
|
1919
|
-
const invalid = values.filter((value) => !CLIENTS.includes(value));
|
|
1920
|
-
if (invalid.length > 0)
|
|
1921
|
-
throw new Error(`Unknown agent client: ${invalid.join(", ")}`);
|
|
1922
|
-
parsed.clients = [...new Set(values)];
|
|
1923
|
-
} else if (argument === "--yes" || argument === "-y")
|
|
1924
|
-
parsed.yes = true;
|
|
1925
|
-
else if (argument === "--dry-run")
|
|
1926
|
-
parsed.dryRun = true;
|
|
1927
|
-
else if (argument === "--help" || argument === "-h")
|
|
1928
|
-
parsed.help = true;
|
|
1929
|
-
else
|
|
1930
|
-
throw new Error(`Unknown agent setup option: ${argument}`);
|
|
1931
|
-
}
|
|
1932
|
-
return parsed;
|
|
1698
|
+
`)}
|
|
1699
|
+
`;
|
|
1933
1700
|
}
|
|
1934
|
-
function
|
|
1935
|
-
|
|
1701
|
+
function removeParagraphContaining(content, fragment) {
|
|
1702
|
+
const paragraphs = content.split(`
|
|
1936
1703
|
|
|
1937
|
-
|
|
1704
|
+
`);
|
|
1705
|
+
const index = paragraphs.findIndex((paragraph) => paragraph.includes(fragment));
|
|
1706
|
+
if (index === -1)
|
|
1707
|
+
throw new Error(`Agent instructions are missing expected text: ${fragment}`);
|
|
1708
|
+
paragraphs.splice(index, 1);
|
|
1709
|
+
return paragraphs.join(`
|
|
1938
1710
|
|
|
1939
|
-
|
|
1940
|
-
--clients <list> Comma-separated clients: codex,claude,cursor
|
|
1941
|
-
--yes Configure every detected client without prompting
|
|
1942
|
-
--dry-run Show the selected configuration without changing client settings
|
|
1943
|
-
-h, --help Show this help`;
|
|
1944
|
-
}
|
|
1945
|
-
async function pathExists(path) {
|
|
1946
|
-
return Boolean(await stat(path).catch(() => {
|
|
1947
|
-
return;
|
|
1948
|
-
}));
|
|
1711
|
+
`);
|
|
1949
1712
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
if (await pathExists(join2(current, "AGENTS.md")) && await pathExists(join2(current, "packages/agent-context/package.json")) && await pathExists(join2(current, "agent-tooling/manifest.json"))) {
|
|
1954
|
-
return current;
|
|
1955
|
-
}
|
|
1956
|
-
const parent = dirname(current);
|
|
1957
|
-
if (parent === current)
|
|
1958
|
-
throw new Error("Run agent setup inside a project repository.");
|
|
1959
|
-
current = parent;
|
|
1713
|
+
function rewriteBuyerAgentInstructions(content) {
|
|
1714
|
+
if (content.includes("Audit and remediate the complete buyer-owned SaaS application") && !/internal\/|landing\/|seller-surface|studio-owned/i.test(content)) {
|
|
1715
|
+
return content;
|
|
1960
1716
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
}
|
|
1965
|
-
function codexManagedBlock(root) {
|
|
1966
|
-
const server = serverCommand(root);
|
|
1967
|
-
const args = server.args.map((argument) => JSON.stringify(argument)).join(", ");
|
|
1968
|
-
return `${CODEX_START}
|
|
1969
|
-
[mcp_servers.${SERVER_NAME}]
|
|
1970
|
-
command = ${JSON.stringify(server.command)}
|
|
1971
|
-
args = [${args}]
|
|
1972
|
-
required = false
|
|
1973
|
-
enabled_tools = ${JSON.stringify(AGENT_CONTEXT_TOOLS)}
|
|
1974
|
-
${CODEX_END}`;
|
|
1975
|
-
}
|
|
1976
|
-
function mergeCodexConfig(existing, root) {
|
|
1977
|
-
const managed = codexManagedBlock(root);
|
|
1978
|
-
const start = existing.indexOf(CODEX_START);
|
|
1979
|
-
const end = existing.indexOf(CODEX_END);
|
|
1980
|
-
if (start >= 0 || end >= 0) {
|
|
1981
|
-
if (start < 0 || end < start)
|
|
1982
|
-
throw new Error("Codex config has an incomplete agent-context block.");
|
|
1983
|
-
const after = end + CODEX_END.length;
|
|
1984
|
-
return `${existing.slice(0, start).trimEnd()}
|
|
1717
|
+
let rewritten = removeParagraphContaining(content, "`internal/` holds every seller-only surface");
|
|
1718
|
+
rewritten = removeParagraphContaining(rewritten, "The root pnpm workspace includes only `apps/*` and `packages/*`.");
|
|
1719
|
+
rewritten = replaceMarkdownSection(rewritten, "Product security audit scope", `## Product security audit scope
|
|
1985
1720
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1721
|
+
Audit and remediate the complete buyer-owned SaaS application: \`apps/\`, \`packages/\`, buyer
|
|
1722
|
+
documentation, and the configuration and deployment files that ship with it. \`agent-tooling/\` and
|
|
1723
|
+
\`packages/agent-context\` are optional, buyer-distributed development tooling; review them when agent
|
|
1724
|
+
behavior or distribution changes, but do not treat them as application runtime code. Do not vendor
|
|
1725
|
+
machine-local installed copies, compatibility symlinks, client configuration, or skill lock records
|
|
1726
|
+
into this repository.
|
|
1727
|
+
`);
|
|
1728
|
+
rewritten = replaceMarkdownSection(rewritten, "Landing boundary", "");
|
|
1729
|
+
rewritten = rewritten.split(`
|
|
1730
|
+
`).filter((line) => !line.includes("| `internal`")).join(`
|
|
1731
|
+
`).replace("real env files, `internal/`, or\n `bun.lock`", "real env files, or `bun.lock`");
|
|
1732
|
+
if (/internal\/|landing\/|seller-surface|studio-owned/i.test(rewritten)) {
|
|
1733
|
+
throw new Error("Buyer agent instructions still contain seller-only guidance.");
|
|
1990
1734
|
}
|
|
1991
|
-
return
|
|
1992
|
-
|
|
1993
|
-
` : ""}${managed}
|
|
1994
|
-
`;
|
|
1735
|
+
return rewritten;
|
|
1995
1736
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
if (
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
1737
|
+
var BUYER_README_HEADING = "## Create your product";
|
|
1738
|
+
function stripDistributionReadmeBootstrap(content) {
|
|
1739
|
+
if (!content.includes(BUYER_README_HEADING))
|
|
1740
|
+
return content;
|
|
1741
|
+
return replaceMarkdownSection(content, "Create your product", "");
|
|
1742
|
+
}
|
|
1743
|
+
var REQUIRED_BUYER_CLI_CONSTANTS = ["CLI_NAME", "CLI_PACKAGE_NAME", "DEFAULT_SCOPE"];
|
|
1744
|
+
function rewriteBuyerCliConstants(content, scope, brandName) {
|
|
1745
|
+
const buyerVariablePrefix = BUYER_CLI_NAME.toUpperCase().replaceAll("-", "_");
|
|
1746
|
+
const buyerConstants = [
|
|
1747
|
+
["CLI_NAME", BUYER_CLI_NAME],
|
|
1748
|
+
["CLI_PACKAGE_NAME", BUYER_CLI_PACKAGE_NAME],
|
|
1749
|
+
["DEFAULT_SCOPE", normalizeScope2(scope)],
|
|
1750
|
+
["SOURCE_BRAND_NAME", brandName],
|
|
1751
|
+
["TEMPLATE_REPOSITORY", "OWNER/REPOSITORY"],
|
|
1752
|
+
["DELIVERY_BASE_URL", "https://releases.example.com"],
|
|
1753
|
+
["DOCS_BASE_URL", "https://docs.example.com"],
|
|
1754
|
+
["DELIVERY_URL_VARIABLE", `${buyerVariablePrefix}_DELIVERY_URL`],
|
|
1755
|
+
["LICENSE_KEY_VARIABLE", `${buyerVariablePrefix}_LICENSE_KEY`],
|
|
1756
|
+
["LICENSE_KEY_PREFIX", "LICENSE-"]
|
|
1757
|
+
];
|
|
1758
|
+
let rewritten = content;
|
|
1759
|
+
for (const [name, value] of buyerConstants) {
|
|
1760
|
+
const declaration = new RegExp(`^export const ${name} = '[^']*'$`, "m");
|
|
1761
|
+
if (!declaration.test(rewritten)) {
|
|
1762
|
+
if (REQUIRED_BUYER_CLI_CONSTANTS.includes(name)) {
|
|
1763
|
+
throw new Error(`Source CLI constants are missing ${name}.`);
|
|
2003
1764
|
}
|
|
2004
|
-
|
|
2005
|
-
} catch (error) {
|
|
2006
|
-
if (error instanceof SyntaxError)
|
|
2007
|
-
throw new Error("Cursor MCP configuration is invalid JSON.");
|
|
2008
|
-
throw error;
|
|
1765
|
+
continue;
|
|
2009
1766
|
}
|
|
1767
|
+
rewritten = rewritten.replace(declaration, `export const ${name} = '${value}'`);
|
|
2010
1768
|
}
|
|
2011
|
-
const
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
1769
|
+
const leaked = [
|
|
1770
|
+
CLI_PACKAGE_NAME,
|
|
1771
|
+
SOURCE_BRAND_NAME,
|
|
1772
|
+
DELIVERY_BASE_URL,
|
|
1773
|
+
DOCS_BASE_URL,
|
|
1774
|
+
DELIVERY_URL_VARIABLE,
|
|
1775
|
+
LICENSE_KEY_VARIABLE2,
|
|
1776
|
+
LICENSE_KEY_PREFIX
|
|
1777
|
+
].filter((identity) => rewritten.includes(identity));
|
|
1778
|
+
if (leaked.length > 0) {
|
|
1779
|
+
throw new Error(`Buyer CLI constants still name the source product: ${leaked.join(", ")}`);
|
|
2016
1780
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
1781
|
+
return rewritten;
|
|
1782
|
+
}
|
|
1783
|
+
var SELLER_ONLY_ROOT_SCRIPTS = [
|
|
1784
|
+
"release:distribute",
|
|
1785
|
+
"release:publish-cli",
|
|
1786
|
+
"release:upload"
|
|
1787
|
+
];
|
|
1788
|
+
function stripSellerOnlyRootScripts(content) {
|
|
1789
|
+
const manifest = JSON.parse(content);
|
|
1790
|
+
for (const script of SELLER_ONLY_ROOT_SCRIPTS)
|
|
1791
|
+
delete manifest.scripts?.[script];
|
|
1792
|
+
return `${JSON.stringify(manifest, null, 2)}
|
|
2019
1793
|
`;
|
|
2020
1794
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
1795
|
+
function rewriteBuyerClaudeInstructions(content) {
|
|
1796
|
+
if (!/internal\/|landing\/|studio-owned/i.test(content))
|
|
1797
|
+
return content;
|
|
1798
|
+
const rewritten = removeParagraphContaining(content, "`internal/landing/AGENTS.md`");
|
|
1799
|
+
if (/internal\/|landing\/|studio-owned/i.test(rewritten)) {
|
|
1800
|
+
throw new Error("Buyer Claude instructions still contain seller-only guidance.");
|
|
1801
|
+
}
|
|
1802
|
+
return rewritten;
|
|
2026
1803
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
1804
|
+
function rewriteBuyerSetupInstructions(content) {
|
|
1805
|
+
if (!content.includes("## Create a product repository"))
|
|
1806
|
+
return content;
|
|
1807
|
+
const rewritten = replaceMarkdownSection(content, "Create a product repository", "");
|
|
1808
|
+
const escapedCliName = CLI_NAME2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1809
|
+
if (new RegExp(`pnpm dlx ${escapedCliName}|bunx ${escapedCliName}|${escapedCliName} repository`, "i").test(rewritten)) {
|
|
1810
|
+
throw new Error("Buyer setup instructions still contain source-delivery guidance.");
|
|
1811
|
+
}
|
|
1812
|
+
return rewritten;
|
|
2029
1813
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
const
|
|
2035
|
-
if (
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
const missing = paths.filter((path) => !existing.split(/\r?\n/).includes(path));
|
|
2040
|
-
if (missing.length === 0)
|
|
2041
|
-
return true;
|
|
2042
|
-
const prefix = existing && !existing.endsWith(`
|
|
2043
|
-
`) ? `
|
|
2044
|
-
` : "";
|
|
2045
|
-
const marker = "# Local agent configuration";
|
|
2046
|
-
const heading = existing.split(/\r?\n/).includes(marker) ? "" : `${marker}
|
|
2047
|
-
`;
|
|
2048
|
-
await atomicWrite(excludePath, `${existing}${prefix}${heading}${missing.join(`
|
|
2049
|
-
`)}
|
|
2050
|
-
`);
|
|
2051
|
-
return true;
|
|
1814
|
+
function rewriteBuyerReleaseInstructions(content) {
|
|
1815
|
+
if (!content.includes("## npm bootstrapper"))
|
|
1816
|
+
return content;
|
|
1817
|
+
const rewritten = replaceMarkdownSection(content, "npm bootstrapper", "");
|
|
1818
|
+
const escapedCliName = CLI_NAME2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1819
|
+
if (new RegExp(`${escapedCliName}@\\d|public \`${escapedCliName}\` npm package`, "i").test(rewritten)) {
|
|
1820
|
+
throw new Error("Buyer release instructions still contain vendor bootstrapper guidance.");
|
|
1821
|
+
}
|
|
1822
|
+
return rewritten;
|
|
2052
1823
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
return excluded ? "Configured project-local .codex/config.toml (excluded from Git)." : "Configured project-local .codex/config.toml; review Git status before committing it.";
|
|
1824
|
+
function generateStartDevVars(example, secret = randomBytes(32).toString("base64")) {
|
|
1825
|
+
if (!/^BETTER_AUTH_SECRET=.*$/m.test(example)) {
|
|
1826
|
+
throw new Error("apps/start/.dev.vars.example must contain a BETTER_AUTH_SECRET entry.");
|
|
1827
|
+
}
|
|
1828
|
+
return example.replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${secret}`);
|
|
2059
1829
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
1830
|
+
function generateServerEnv(example, secret = randomBytes(32).toString("hex"), databasePort = 5432) {
|
|
1831
|
+
if (!/^[a-f0-9]{64}$/.test(secret)) {
|
|
1832
|
+
throw new Error("Better Auth secret must be 32 bytes encoded as hex.");
|
|
1833
|
+
}
|
|
1834
|
+
if (!/^DATABASE_URL=.*$/m.test(example) || !/^BETTER_AUTH_SECRET=.*$/m.test(example)) {
|
|
1835
|
+
throw new Error("Server .env.example must contain DATABASE_URL and BETTER_AUTH_SECRET entries.");
|
|
1836
|
+
}
|
|
1837
|
+
if (!Number.isInteger(databasePort) || databasePort < 1 || databasePort > 65535) {
|
|
1838
|
+
throw new Error("Database port must be an integer between 1 and 65535.");
|
|
1839
|
+
}
|
|
1840
|
+
const databaseUrl = `postgresql://app:app@localhost:${databasePort}/app?sslmode=disable`;
|
|
1841
|
+
return example.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}`).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${secret}`);
|
|
1842
|
+
}
|
|
1843
|
+
var SCOPE_EXTENSIONS2 = new Set([".json", ".jsonc", ".md", ".ts", ".tsx", ".yaml", ".yml"]);
|
|
1844
|
+
function isScopeRewriteFile(path) {
|
|
1845
|
+
return SCOPE_EXTENSIONS2.has(extname(path)) || path.replaceAll("\\", "/").endsWith("/.vite-hooks/pre-commit");
|
|
1846
|
+
}
|
|
1847
|
+
function isTemplatePathExcluded(relativePath) {
|
|
1848
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
1849
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
1850
|
+
const basename = parts.at(-1) ?? "";
|
|
1851
|
+
if (parts.includes(".git") || parts.includes(".worktrees") || parts.includes("node_modules"))
|
|
1852
|
+
return true;
|
|
1853
|
+
if (parts.includes(".terraform"))
|
|
1854
|
+
return true;
|
|
1855
|
+
if (parts.includes(".impeccable"))
|
|
1856
|
+
return true;
|
|
1857
|
+
if (parts.includes(".agent-evals") || parts.includes(".agents") || parts.includes(".claude"))
|
|
1858
|
+
return true;
|
|
1859
|
+
if (normalized === "skills-lock.json")
|
|
1860
|
+
return true;
|
|
1861
|
+
if (normalized === "internal" || normalized.startsWith("internal/"))
|
|
1862
|
+
return true;
|
|
1863
|
+
if (normalized === "marketing" || normalized.startsWith("marketing/"))
|
|
1864
|
+
return true;
|
|
1865
|
+
if (normalized === ".github/workflows/internal-commercial.yml")
|
|
1866
|
+
return true;
|
|
1867
|
+
if (normalized === "apps/worker" || normalized.startsWith("apps/worker/"))
|
|
1868
|
+
return true;
|
|
1869
|
+
if (parts[0] === "apps" && parts[1] === "mobile") {
|
|
1870
|
+
const mobileEntry = parts[2] ?? "";
|
|
1871
|
+
if ([".expo", "android", "ios", "credentials", "dist-verify"].includes(mobileEntry))
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
if (parts.includes(".wrangler"))
|
|
1875
|
+
return true;
|
|
1876
|
+
if (parts.includes("dist") || parts.includes("out"))
|
|
1877
|
+
return true;
|
|
1878
|
+
if (parts.includes(".next") || parts.includes(".source") || parts.includes(".turbo"))
|
|
1879
|
+
return true;
|
|
1880
|
+
if (normalized === "bun.lock")
|
|
1881
|
+
return true;
|
|
1882
|
+
if (normalized === ".vercelignore")
|
|
1883
|
+
return true;
|
|
1884
|
+
if (basename.endsWith(".tfvars") || basename === "backend.hcl")
|
|
1885
|
+
return true;
|
|
1886
|
+
if (basename.includes(".tfstate") || /^gha-creds-.*\.json$/.test(basename))
|
|
1887
|
+
return true;
|
|
1888
|
+
if (normalized === "PLAN.md" || normalized === "comments.md")
|
|
1889
|
+
return true;
|
|
1890
|
+
if (normalized === "packages/create-app/src/cli-brand-contract.test.ts")
|
|
1891
|
+
return true;
|
|
1892
|
+
if (normalized === "packages/create-app/src/commercial-contract.test.ts")
|
|
1893
|
+
return true;
|
|
1894
|
+
if (normalized === "packages/create-app/LICENSE")
|
|
1895
|
+
return true;
|
|
1896
|
+
if (normalized === "packages/create-app/README.md")
|
|
1897
|
+
return true;
|
|
1898
|
+
if (normalized === "apps/server/server")
|
|
1899
|
+
return true;
|
|
1900
|
+
if (basename.endsWith(".tsbuildinfo"))
|
|
1901
|
+
return true;
|
|
1902
|
+
if (basename === ".dev.vars" || basename.startsWith(".dev.vars.") && basename !== ".dev.vars.example")
|
|
1903
|
+
return true;
|
|
1904
|
+
if (basename === ".env" || basename.startsWith(".env.") && basename !== ".env.example")
|
|
1905
|
+
return true;
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/editions.ts
|
|
1910
|
+
var EDITION_NAMES = ["web", "mobile", "start"];
|
|
1911
|
+
var START_EDITION_PATHS = ["apps/start", ".github/workflows/deploy-start.yml"];
|
|
1912
|
+
var START_EDITION_OVERLAY = "apps/start/edition";
|
|
1913
|
+
var START_ROOT_CONTRACTS = {
|
|
1914
|
+
"ROOT-AGENTS.md": "AGENTS.md",
|
|
1915
|
+
"ROOT-CLAUDE.md": "CLAUDE.md"
|
|
1916
|
+
};
|
|
1917
|
+
var START_ONLY_KEPT_PATHS = [
|
|
1918
|
+
"apps/start",
|
|
1919
|
+
"packages/billing",
|
|
1920
|
+
"packages/tsconfig",
|
|
1921
|
+
".github/workflows/deploy-start.yml",
|
|
1922
|
+
".gitattributes",
|
|
1923
|
+
".gitignore",
|
|
1924
|
+
".node-version",
|
|
1925
|
+
"LICENSE",
|
|
1926
|
+
"package.json",
|
|
1927
|
+
"pnpm-lock.yaml",
|
|
1928
|
+
"pnpm-workspace.yaml",
|
|
1929
|
+
"vite.config.ts",
|
|
1930
|
+
"knip.json"
|
|
1931
|
+
];
|
|
1932
|
+
function normalizeEditions(selection) {
|
|
1933
|
+
return { ...selection, web: selection.web || selection.mobile };
|
|
1934
|
+
}
|
|
1935
|
+
function selectedEditions(selection) {
|
|
1936
|
+
const normalized = normalizeEditions(selection);
|
|
1937
|
+
return EDITION_NAMES.filter((name) => normalized[name]);
|
|
1938
|
+
}
|
|
1939
|
+
function selectionFromEditions(editions) {
|
|
1940
|
+
return normalizeEditions({
|
|
1941
|
+
web: editions.includes("web"),
|
|
1942
|
+
mobile: editions.includes("mobile"),
|
|
1943
|
+
start: editions.includes("start")
|
|
2064
1944
|
});
|
|
2065
|
-
await atomicWrite(path, mergeCursorConfig(existing, root));
|
|
2066
|
-
const excluded = await addGitInfoExcludes(root, [".cursor/mcp.json"]);
|
|
2067
|
-
return excluded ? "Configured project-local .cursor/mcp.json (excluded from Git)." : "Configured project-local .cursor/mcp.json; review Git status before committing it.";
|
|
2068
1945
|
}
|
|
2069
|
-
|
|
2070
|
-
if (
|
|
2071
|
-
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
1946
|
+
function pruneLockfileImporters(content, removedImporters) {
|
|
1947
|
+
if (removedImporters.length === 0)
|
|
1948
|
+
return content;
|
|
1949
|
+
const removed = new Set(removedImporters);
|
|
1950
|
+
const lines = content.split(`
|
|
1951
|
+
`);
|
|
1952
|
+
const output = [];
|
|
1953
|
+
let inImporters = false;
|
|
1954
|
+
let skipping = false;
|
|
1955
|
+
for (const line of lines) {
|
|
1956
|
+
if (/^\S/.test(line)) {
|
|
1957
|
+
inImporters = line === "importers:";
|
|
1958
|
+
skipping = false;
|
|
1959
|
+
output.push(line);
|
|
1960
|
+
continue;
|
|
2078
1961
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
1962
|
+
if (inImporters) {
|
|
1963
|
+
const importer = line.match(/^ {2}(\S[^:]*):\s*$/)?.[1];
|
|
1964
|
+
if (importer !== undefined)
|
|
1965
|
+
skipping = removed.has(importer);
|
|
1966
|
+
if (skipping)
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
output.push(line);
|
|
2086
1970
|
}
|
|
2087
|
-
return
|
|
1971
|
+
return output.join(`
|
|
1972
|
+
`).replace(/\n{3,}/g, `
|
|
1973
|
+
|
|
1974
|
+
`);
|
|
2088
1975
|
}
|
|
2089
|
-
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
1976
|
+
function stripKnipWorkspaces(content, removedWorkspaces) {
|
|
1977
|
+
const parsed = JSON.parse(content);
|
|
1978
|
+
for (const workspace of removedWorkspaces)
|
|
1979
|
+
delete parsed.workspaces?.[workspace];
|
|
1980
|
+
if (parsed.ignoreWorkspaces) {
|
|
1981
|
+
parsed.ignoreWorkspaces = parsed.ignoreWorkspaces.filter((workspace) => !removedWorkspaces.includes(workspace));
|
|
2094
1982
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
1983
|
+
return `${stringifyRepositoryJson(parsed)}
|
|
1984
|
+
`;
|
|
1985
|
+
}
|
|
1986
|
+
function stripStartReferences(content) {
|
|
1987
|
+
return content.replace(/^\| `apps\/start` .*\n/gm, "").replace(/^`apps\/start`[\s\S]*?(?:\n\n|\n?(?![\s\S]))/gm, "");
|
|
1988
|
+
}
|
|
1989
|
+
function rewriteStartWorkerIdentifiers(content, appName) {
|
|
1990
|
+
return content.replace(/("name":\s*")[^"]+(")/, `$1${appName}$2`).replace(/("database_name":\s*")[^"]+(")/, `$1${appName}$2`);
|
|
1991
|
+
}
|
|
1992
|
+
function startOnlyRootPackage(rootPackage, overlayPackage) {
|
|
1993
|
+
const root = JSON.parse(rootPackage);
|
|
1994
|
+
const overlay = JSON.parse(overlayPackage);
|
|
1995
|
+
return `${JSON.stringify({ ...root, scripts: overlay.scripts }, null, 2)}
|
|
1996
|
+
`;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/delivery.ts
|
|
2000
|
+
var RELEASE_TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
|
|
2001
|
+
function validateLicenseKey(value) {
|
|
2002
|
+
const key = value.trim();
|
|
2003
|
+
if (!key)
|
|
2004
|
+
return "Enter the licence key from your purchase receipt.";
|
|
2005
|
+
if (/\s/.test(key))
|
|
2006
|
+
return "A licence key does not contain spaces.";
|
|
2007
|
+
if (!key.startsWith(LICENSE_KEY_PREFIX) || key.length === LICENSE_KEY_PREFIX.length) {
|
|
2008
|
+
return `A ${CLI_NAME2} licence key starts with ${LICENSE_KEY_PREFIX}. Copy it from your purchase receipt or the customer portal.`;
|
|
2098
2009
|
}
|
|
2010
|
+
return;
|
|
2099
2011
|
}
|
|
2100
|
-
function
|
|
2101
|
-
return
|
|
2102
|
-
if (client === "cursor")
|
|
2103
|
-
return Boolean(which("cursor"));
|
|
2104
|
-
return Boolean(which(client));
|
|
2105
|
-
});
|
|
2012
|
+
function readLicenseKeyFromEnvironment(environment = process.env) {
|
|
2013
|
+
return environment[LICENSE_KEY_VARIABLE2]?.trim() || undefined;
|
|
2106
2014
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2015
|
+
function normalizeSourceVersion(value) {
|
|
2016
|
+
const trimmed = value.trim();
|
|
2017
|
+
const releaseTag = trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
|
|
2018
|
+
if (!RELEASE_TAG_PATTERN.test(releaseTag)) {
|
|
2019
|
+
throw new Error(`Source version must be an immutable release such as 1.2.3: ${value}`);
|
|
2112
2020
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2021
|
+
return releaseTag;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// node_modules/@clack/prompts/dist/index.mjs
|
|
2025
|
+
import { styleText as styleText3, stripVTControlCharacters as stripVTControlCharacters2 } from "node:util";
|
|
2026
|
+
import process$12 from "node:process";
|
|
2027
|
+
var import_sisteransi3 = __toESM(require_src(), 1);
|
|
2028
|
+
function isUnicodeSupported2() {
|
|
2029
|
+
if (process$12.platform !== "win32") {
|
|
2030
|
+
return process$12.env.TERM !== "linux";
|
|
2119
2031
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2032
|
+
return Boolean(process$12.env.CI) || Boolean(process$12.env.WT_SESSION) || Boolean(process$12.env.TERMINUS_SUBLIME) || process$12.env.ConEmuTask === "{cmd::Cmder}" || process$12.env.TERM_PROGRAM === "Terminus-Sublime" || process$12.env.TERM_PROGRAM === "vscode" || process$12.env.TERM === "xterm-256color" || process$12.env.TERM === "alacritty" || process$12.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
2033
|
+
}
|
|
2034
|
+
var unicode2 = isUnicodeSupported2();
|
|
2035
|
+
var isCI2 = () => process.env.CI === "true";
|
|
2036
|
+
var unicodeOr2 = (o, e) => unicode2 ? o : e;
|
|
2037
|
+
var S_STEP_ACTIVE2 = unicodeOr2("◆", "*");
|
|
2038
|
+
var S_STEP_CANCEL2 = unicodeOr2("■", "x");
|
|
2039
|
+
var S_STEP_ERROR2 = unicodeOr2("▲", "x");
|
|
2040
|
+
var S_STEP_SUBMIT2 = unicodeOr2("◇", "o");
|
|
2041
|
+
var S_BAR_START2 = unicodeOr2("┌", "T");
|
|
2042
|
+
var S_BAR2 = unicodeOr2("│", "|");
|
|
2043
|
+
var S_BAR_END2 = unicodeOr2("└", "—");
|
|
2044
|
+
var S_BAR_START_RIGHT2 = unicodeOr2("┐", "T");
|
|
2045
|
+
var S_BAR_END_RIGHT2 = unicodeOr2("┘", "—");
|
|
2046
|
+
var S_RADIO_ACTIVE2 = unicodeOr2("●", ">");
|
|
2047
|
+
var S_RADIO_INACTIVE2 = unicodeOr2("○", " ");
|
|
2048
|
+
var S_CHECKBOX_ACTIVE2 = unicodeOr2("◻", "[•]");
|
|
2049
|
+
var S_CHECKBOX_SELECTED2 = unicodeOr2("◼", "[+]");
|
|
2050
|
+
var S_CHECKBOX_INACTIVE2 = unicodeOr2("◻", "[ ]");
|
|
2051
|
+
var S_PASSWORD_MASK2 = unicodeOr2("▪", "•");
|
|
2052
|
+
var S_BAR_H2 = unicodeOr2("─", "-");
|
|
2053
|
+
var S_CORNER_TOP_RIGHT2 = unicodeOr2("╮", "+");
|
|
2054
|
+
var S_CONNECT_LEFT2 = unicodeOr2("├", "+");
|
|
2055
|
+
var S_CORNER_BOTTOM_RIGHT2 = unicodeOr2("╯", "+");
|
|
2056
|
+
var S_CORNER_BOTTOM_LEFT2 = unicodeOr2("╰", "+");
|
|
2057
|
+
var S_CORNER_TOP_LEFT2 = unicodeOr2("╭", "+");
|
|
2058
|
+
var S_INFO2 = unicodeOr2("●", "•");
|
|
2059
|
+
var S_SUCCESS2 = unicodeOr2("◆", "*");
|
|
2060
|
+
var S_WARN2 = unicodeOr2("▲", "!");
|
|
2061
|
+
var S_ERROR2 = unicodeOr2("■", "x");
|
|
2062
|
+
var symbol2 = (o) => {
|
|
2063
|
+
switch (o) {
|
|
2064
|
+
case "initial":
|
|
2065
|
+
case "active":
|
|
2066
|
+
return styleText3("cyan", S_STEP_ACTIVE2);
|
|
2067
|
+
case "cancel":
|
|
2068
|
+
return styleText3("red", S_STEP_CANCEL2);
|
|
2069
|
+
case "error":
|
|
2070
|
+
return styleText3("yellow", S_STEP_ERROR2);
|
|
2071
|
+
case "submit":
|
|
2072
|
+
return styleText3("green", S_STEP_SUBMIT2);
|
|
2073
|
+
case "validating":
|
|
2074
|
+
return styleText3("dim", S_STEP_ACTIVE2);
|
|
2122
2075
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
return;
|
|
2076
|
+
};
|
|
2077
|
+
var symbolBar = (o) => {
|
|
2078
|
+
switch (o) {
|
|
2079
|
+
case "initial":
|
|
2080
|
+
case "active":
|
|
2081
|
+
return styleText3("cyan", S_BAR2);
|
|
2082
|
+
case "cancel":
|
|
2083
|
+
return styleText3("red", S_BAR2);
|
|
2084
|
+
case "error":
|
|
2085
|
+
return styleText3("yellow", S_BAR2);
|
|
2086
|
+
case "submit":
|
|
2087
|
+
return styleText3("green", S_BAR2);
|
|
2136
2088
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
return mergeCodexConfig("", root).trim();
|
|
2142
|
-
if (client === "cursor")
|
|
2143
|
-
return mergeCursorConfig(undefined, root).trim();
|
|
2144
|
-
const server = serverCommand(root);
|
|
2145
|
-
return [
|
|
2146
|
-
"claude",
|
|
2147
|
-
"mcp",
|
|
2148
|
-
"add",
|
|
2149
|
-
SERVER_NAME,
|
|
2150
|
-
"--scope",
|
|
2151
|
-
"local",
|
|
2152
|
-
"--",
|
|
2153
|
-
server.command,
|
|
2154
|
-
...server.args
|
|
2155
|
-
].map((value) => value.includes(" ") ? JSON.stringify(value) : value).join(" ");
|
|
2089
|
+
};
|
|
2090
|
+
function formatInstructionFooter(o, e) {
|
|
2091
|
+
const r = [`${e ? `${styleText3("cyan", S_BAR2)} ` : ""}${o.join(" • ")}`];
|
|
2092
|
+
return e && r.push(styleText3("cyan", S_BAR_END2)), r;
|
|
2156
2093
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
if (
|
|
2160
|
-
|
|
2094
|
+
var I = (l, e, w, p, b, C = false) => {
|
|
2095
|
+
let r = e, O = 0;
|
|
2096
|
+
if (C)
|
|
2097
|
+
for (let i = p - 1;i >= w; i--) {
|
|
2098
|
+
const m = l[i];
|
|
2099
|
+
if (m && (r -= m.length), O++, r <= b)
|
|
2100
|
+
break;
|
|
2101
|
+
}
|
|
2102
|
+
else
|
|
2103
|
+
for (let i = w;i < p; i++) {
|
|
2104
|
+
const m = l[i];
|
|
2105
|
+
if (m && (r -= m.length), O++, r <= b)
|
|
2106
|
+
break;
|
|
2107
|
+
}
|
|
2108
|
+
return { lineCount: r, removals: O };
|
|
2109
|
+
};
|
|
2110
|
+
var limitOptions = ({
|
|
2111
|
+
cursor: l,
|
|
2112
|
+
options: e,
|
|
2113
|
+
style: w,
|
|
2114
|
+
output: p = process.stdout,
|
|
2115
|
+
maxItems: b = Number.POSITIVE_INFINITY,
|
|
2116
|
+
columnPadding: C = 0,
|
|
2117
|
+
rowPadding: r = 4
|
|
2118
|
+
}) => {
|
|
2119
|
+
const i = getColumns(p) - C, m = getRows(p), M = styleText3("dim", "..."), v = Math.max(m - r, 0), a = Math.max(Math.min(b, v), 5);
|
|
2120
|
+
let f = 0;
|
|
2121
|
+
l >= a - 3 && (f = Math.max(Math.min(l - a + 3, e.length - a), 0));
|
|
2122
|
+
let d = a < e.length && f > 0, c = a < e.length && f + a < e.length;
|
|
2123
|
+
const W = Math.min(f + a, e.length), s = [];
|
|
2124
|
+
let g = 0;
|
|
2125
|
+
d && g++, c && g++;
|
|
2126
|
+
const T = f + (d ? 1 : 0), y = W - (c ? 1 : 0);
|
|
2127
|
+
for (let t = T;t < y; t++) {
|
|
2128
|
+
const n = e[t], o = n ? w(n, t === l) : "", h = wrapAnsi(o, i, {
|
|
2129
|
+
hard: true,
|
|
2130
|
+
trim: false
|
|
2131
|
+
}).split(`
|
|
2132
|
+
`);
|
|
2133
|
+
s.push(h), g += h.length;
|
|
2134
|
+
}
|
|
2135
|
+
if (g > v) {
|
|
2136
|
+
let t = 0, n = 0, o = g;
|
|
2137
|
+
const h = l - T;
|
|
2138
|
+
let u = v;
|
|
2139
|
+
const L = () => I(s, o, 0, h, u), E = () => I(s, o, h + 1, s.length, u, true);
|
|
2140
|
+
d ? ({ lineCount: o, removals: t } = L(), o > u && (c || (u -= 1), { lineCount: o, removals: n } = E())) : (c || (u -= 1), { lineCount: o, removals: n } = E(), o > u && (u -= 1, { lineCount: o, removals: t } = L())), t > 0 && (d = true, s.splice(0, t)), n > 0 && (c = true, s.splice(s.length - n, n));
|
|
2141
|
+
}
|
|
2142
|
+
const x = [];
|
|
2143
|
+
d && x.push(M);
|
|
2144
|
+
for (const t of s)
|
|
2145
|
+
for (const n of t)
|
|
2146
|
+
x.push(n);
|
|
2147
|
+
return c && x.push(M), x;
|
|
2148
|
+
};
|
|
2149
|
+
var confirm2 = (e) => {
|
|
2150
|
+
const a = e.active ?? "Yes", o = e.inactive ?? "No";
|
|
2151
|
+
return new r({
|
|
2152
|
+
active: a,
|
|
2153
|
+
inactive: o,
|
|
2154
|
+
signal: e.signal,
|
|
2155
|
+
input: e.input,
|
|
2156
|
+
output: e.output,
|
|
2157
|
+
initialValue: e.initialValue ?? true,
|
|
2158
|
+
render() {
|
|
2159
|
+
const i = e.withGuide ?? settings.withGuide, u = `${symbol2(this.state)} `, l = i ? `${styleText3("gray", S_BAR2)} ` : "", f = wrapTextWithPrefix(e.output, e.message, l, u), s = `${i ? `${styleText3("gray", S_BAR2)}
|
|
2160
|
+
` : ""}${f}
|
|
2161
|
+
`, c = this.value ? a : o;
|
|
2162
|
+
switch (this.state) {
|
|
2163
|
+
case "submit": {
|
|
2164
|
+
const r = i ? `${styleText3("gray", S_BAR2)} ` : "";
|
|
2165
|
+
return `${s}${r}${styleText3("dim", c)}`;
|
|
2166
|
+
}
|
|
2167
|
+
case "cancel": {
|
|
2168
|
+
const r = i ? `${styleText3("gray", S_BAR2)} ` : "";
|
|
2169
|
+
return `${s}${r}${styleText3(["strikethrough", "dim"], c)}${i ? `
|
|
2170
|
+
${styleText3("gray", S_BAR2)}` : ""}`;
|
|
2171
|
+
}
|
|
2172
|
+
default: {
|
|
2173
|
+
const r = i ? `${styleText3("cyan", S_BAR2)} ` : "", g = i ? styleText3("cyan", S_BAR_END2) : "";
|
|
2174
|
+
return `${s}${r}${this.value ? `${styleText3("green", S_RADIO_ACTIVE2)} ${a}` : `${styleText3("dim", S_RADIO_INACTIVE2)} ${styleText3("dim", a)}`}${e.vertical ? i ? `
|
|
2175
|
+
${styleText3("cyan", S_BAR2)} ` : `
|
|
2176
|
+
` : ` ${styleText3("dim", "/")} `}${this.value ? `${styleText3("dim", S_RADIO_INACTIVE2)} ${styleText3("dim", o)}` : `${styleText3("green", S_RADIO_ACTIVE2)} ${o}`}
|
|
2177
|
+
${g}
|
|
2178
|
+
`;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}).prompt();
|
|
2183
|
+
};
|
|
2184
|
+
var MULTISELECT_INSTRUCTIONS2 = [
|
|
2185
|
+
`${styleText3("dim", "↑/↓")} to navigate`,
|
|
2186
|
+
`${styleText3("dim", "Space:")} select`,
|
|
2187
|
+
`${styleText3("dim", "Enter:")} confirm`
|
|
2188
|
+
];
|
|
2189
|
+
var m = (i, u) => i.split(`
|
|
2190
|
+
`).map((d) => u(d)).join(`
|
|
2191
|
+
`);
|
|
2192
|
+
var multiselect = (i) => {
|
|
2193
|
+
const u = (t, a) => {
|
|
2194
|
+
const r = t.label ?? String(t.value);
|
|
2195
|
+
return a === "disabled" ? `${styleText3("gray", S_CHECKBOX_INACTIVE2)} ${m(r, (o) => styleText3(["strikethrough", "gray"], o))}${t.hint ? ` ${styleText3("dim", `(${t.hint ?? "disabled"})`)}` : ""}` : a === "active" ? `${styleText3("cyan", S_CHECKBOX_ACTIVE2)} ${r}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "selected" ? `${styleText3("green", S_CHECKBOX_SELECTED2)} ${m(r, (o) => styleText3("dim", o))}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "cancelled" ? `${m(r, (o) => styleText3(["strikethrough", "dim"], o))}` : a === "active-selected" ? `${styleText3("green", S_CHECKBOX_SELECTED2)} ${r}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "submitted" ? `${m(r, (o) => styleText3("dim", o))}` : `${styleText3("dim", S_CHECKBOX_INACTIVE2)} ${m(r, (o) => styleText3("dim", o))}`;
|
|
2196
|
+
}, d = i.required ?? true, x = i.showInstructions ?? true;
|
|
2197
|
+
return new a({
|
|
2198
|
+
options: i.options,
|
|
2199
|
+
signal: i.signal,
|
|
2200
|
+
input: i.input,
|
|
2201
|
+
output: i.output,
|
|
2202
|
+
initialValues: i.initialValues,
|
|
2203
|
+
required: d,
|
|
2204
|
+
cursorAt: i.cursorAt,
|
|
2205
|
+
validate(t) {
|
|
2206
|
+
if (d && (t === undefined || t.length === 0))
|
|
2207
|
+
return `Please select at least one option.
|
|
2208
|
+
${styleText3("reset", styleText3("dim", `Press ${styleText3(["gray", "bgWhite", "inverse"], " space ")} to select, ${styleText3("gray", styleText3("bgWhite", styleText3("inverse", " enter ")))} to submit`))}`;
|
|
2209
|
+
},
|
|
2210
|
+
render() {
|
|
2211
|
+
const t = i.withGuide ?? settings.withGuide, a = wrapTextWithPrefix(i.output, i.message, t ? `${symbolBar(this.state)} ` : "", `${symbol2(this.state)} `), r = `${t ? `${styleText3("gray", S_BAR2)}
|
|
2212
|
+
` : ""}${a}
|
|
2213
|
+
`, o = this.value ?? [], g = (n, l) => {
|
|
2214
|
+
if (n.disabled)
|
|
2215
|
+
return u(n, "disabled");
|
|
2216
|
+
const s = o.includes(n.value);
|
|
2217
|
+
return l && s ? u(n, "active-selected") : s ? u(n, "selected") : u(n, l ? "active" : "inactive");
|
|
2218
|
+
};
|
|
2219
|
+
switch (this.state) {
|
|
2220
|
+
case "submit": {
|
|
2221
|
+
const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "submitted")).join(styleText3("dim", ", ")) || styleText3("dim", "none"), l = wrapTextWithPrefix(i.output, n, t ? `${styleText3("gray", S_BAR2)} ` : "");
|
|
2222
|
+
return `${r}${l}`;
|
|
2223
|
+
}
|
|
2224
|
+
case "cancel": {
|
|
2225
|
+
const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "cancelled")).join(styleText3("dim", ", "));
|
|
2226
|
+
if (n.trim() === "")
|
|
2227
|
+
return `${r}${styleText3("gray", S_BAR2)}`;
|
|
2228
|
+
const l = wrapTextWithPrefix(i.output, n, t ? `${styleText3("gray", S_BAR2)} ` : "");
|
|
2229
|
+
return `${r}${l}${t ? `
|
|
2230
|
+
${styleText3("gray", S_BAR2)}` : ""}`;
|
|
2231
|
+
}
|
|
2232
|
+
case "error": {
|
|
2233
|
+
const n = t ? `${styleText3("yellow", S_BAR2)} ` : "", l = this.error.split(`
|
|
2234
|
+
`).map(($, v) => v === 0 ? `${t ? `${styleText3("yellow", S_BAR_END2)} ` : ""}${styleText3("yellow", $)}` : ` ${$}`).join(`
|
|
2235
|
+
`), s = r.split(`
|
|
2236
|
+
`).length, h = l.split(`
|
|
2237
|
+
`).length + 1;
|
|
2238
|
+
return `${r}${n}${limitOptions({
|
|
2239
|
+
output: i.output,
|
|
2240
|
+
options: this.options,
|
|
2241
|
+
cursor: this.cursor,
|
|
2242
|
+
maxItems: i.maxItems,
|
|
2243
|
+
columnPadding: n.length,
|
|
2244
|
+
rowPadding: s + h,
|
|
2245
|
+
style: g
|
|
2246
|
+
}).join(`
|
|
2247
|
+
${n}`)}
|
|
2248
|
+
${l}
|
|
2249
|
+
`;
|
|
2250
|
+
}
|
|
2251
|
+
default: {
|
|
2252
|
+
const n = t ? `${styleText3("cyan", S_BAR2)} ` : "", l = r.split(`
|
|
2253
|
+
`).length, s = x ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS2, t) : t ? [styleText3("cyan", S_BAR_END2)] : [], h = s.join(`
|
|
2254
|
+
`), $ = s.length + 1;
|
|
2255
|
+
return `${r}${n}${limitOptions({
|
|
2256
|
+
output: i.output,
|
|
2257
|
+
options: this.options,
|
|
2258
|
+
cursor: this.cursor,
|
|
2259
|
+
maxItems: i.maxItems,
|
|
2260
|
+
columnPadding: n.length,
|
|
2261
|
+
rowPadding: l + $,
|
|
2262
|
+
style: g
|
|
2263
|
+
}).join(`
|
|
2264
|
+
${n}`)}
|
|
2265
|
+
${h}
|
|
2266
|
+
`;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}).prompt();
|
|
2271
|
+
};
|
|
2272
|
+
var log2 = {
|
|
2273
|
+
message: (s = [], {
|
|
2274
|
+
symbol: e = styleText3("gray", S_BAR2),
|
|
2275
|
+
secondarySymbol: r = styleText3("gray", S_BAR2),
|
|
2276
|
+
output: m = process.stdout,
|
|
2277
|
+
spacing: l = 1,
|
|
2278
|
+
withGuide: c
|
|
2279
|
+
} = {}) => {
|
|
2280
|
+
const t = [], o = c ?? settings.withGuide, f = o ? r : "", O = o ? `${e} ` : "", u = o ? `${r} ` : "";
|
|
2281
|
+
for (let i = 0;i < l; i++)
|
|
2282
|
+
t.push(f);
|
|
2283
|
+
const g = Array.isArray(s) ? s : s.split(`
|
|
2161
2284
|
`);
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
return;
|
|
2168
|
-
intro2("Local agent context");
|
|
2169
|
-
if (parsed.dryRun) {
|
|
2170
|
-
for (const client of clients) {
|
|
2171
|
-
log2.info(`${client}
|
|
2172
|
-
${dryRunDescription(client, root)}`);
|
|
2285
|
+
if (g.length > 0) {
|
|
2286
|
+
const [i, ...y] = g;
|
|
2287
|
+
i.length > 0 ? t.push(`${O}${i}`) : t.push(o ? e : "");
|
|
2288
|
+
for (const p of y)
|
|
2289
|
+
p.length > 0 ? t.push(`${u}${p}`) : t.push(o ? r : "");
|
|
2173
2290
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2291
|
+
m.write(`${t.join(`
|
|
2292
|
+
`)}
|
|
2293
|
+
`);
|
|
2294
|
+
},
|
|
2295
|
+
info: (s, e) => {
|
|
2296
|
+
log2.message(s, { ...e, symbol: styleText3("blue", S_INFO2) });
|
|
2297
|
+
},
|
|
2298
|
+
success: (s, e) => {
|
|
2299
|
+
log2.message(s, { ...e, symbol: styleText3("green", S_SUCCESS2) });
|
|
2300
|
+
},
|
|
2301
|
+
step: (s, e) => {
|
|
2302
|
+
log2.message(s, { ...e, symbol: styleText3("green", S_STEP_SUBMIT2) });
|
|
2303
|
+
},
|
|
2304
|
+
warn: (s, e) => {
|
|
2305
|
+
log2.message(s, { ...e, symbol: styleText3("yellow", S_WARN2) });
|
|
2306
|
+
},
|
|
2307
|
+
warning: (s, e) => {
|
|
2308
|
+
log2.warn(s, e);
|
|
2309
|
+
},
|
|
2310
|
+
error: (s, e) => {
|
|
2311
|
+
log2.message(s, { ...e, symbol: styleText3("red", S_ERROR2) });
|
|
2184
2312
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
// src/deployment-cli.ts
|
|
2190
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
2191
|
-
import { resolve as resolve2 } from "node:path";
|
|
2313
|
+
};
|
|
2314
|
+
var cancel2 = (o = "", t) => {
|
|
2315
|
+
const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText3("gray", S_BAR_END2)} ` : "";
|
|
2316
|
+
i.write(`${e}${styleText3("red", o)}
|
|
2192
2317
|
|
|
2193
|
-
|
|
2194
|
-
import { mkdir as mkdir2, readFile as readFile2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2195
|
-
import { join as join3 } from "node:path";
|
|
2196
|
-
var deploymentClouds = ["aws", "gcp", "azure"];
|
|
2197
|
-
var frontendTargets = ["native", "vercel", "cloudflare"];
|
|
2198
|
-
var storageProviders = ["r2", "s3", "gcs"];
|
|
2199
|
-
var defaultRegions = {
|
|
2200
|
-
aws: "us-east-1",
|
|
2201
|
-
gcp: "us-central1",
|
|
2202
|
-
azure: "eastus2"
|
|
2318
|
+
`);
|
|
2203
2319
|
};
|
|
2204
|
-
var
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2320
|
+
var intro2 = (o = "", t) => {
|
|
2321
|
+
const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText3("gray", S_BAR_START2)} ` : "";
|
|
2322
|
+
i.write(`${e}${o}
|
|
2323
|
+
`);
|
|
2208
2324
|
};
|
|
2209
|
-
var
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2325
|
+
var outro2 = (o = "", t) => {
|
|
2326
|
+
const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText3("gray", S_BAR2)}
|
|
2327
|
+
${styleText3("gray", S_BAR_END2)} ` : "";
|
|
2328
|
+
i.write(`${e}${o}
|
|
2329
|
+
|
|
2330
|
+
`);
|
|
2213
2331
|
};
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
}
|
|
2282
|
-
function createDeploymentConfig(projectName, selection) {
|
|
2283
|
-
validateDeploymentSelection(selection);
|
|
2332
|
+
var W2 = (l) => styleText3("magenta", l);
|
|
2333
|
+
var spinner2 = ({
|
|
2334
|
+
indicator: l = "dots",
|
|
2335
|
+
onCancel: h,
|
|
2336
|
+
output: n = process.stdout,
|
|
2337
|
+
cancelMessage: G,
|
|
2338
|
+
errorMessage: O,
|
|
2339
|
+
frames: E = unicode2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"],
|
|
2340
|
+
delay: F = unicode2 ? 80 : 120,
|
|
2341
|
+
signal: m,
|
|
2342
|
+
...I
|
|
2343
|
+
} = {}) => {
|
|
2344
|
+
const u = isCI2();
|
|
2345
|
+
let M, T, d = false, S = false, s = "", p, w = performance.now();
|
|
2346
|
+
const x = getColumns(n), k = I?.styleFrame ?? W2, g = (e) => {
|
|
2347
|
+
const r = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel;
|
|
2348
|
+
S = e === 1, d && (a(r, e), S && typeof h == "function" && h());
|
|
2349
|
+
}, f = () => g(2), i = () => g(1), A = () => {
|
|
2350
|
+
process.on("uncaughtExceptionMonitor", f), process.on("unhandledRejection", f), process.on("SIGINT", i), process.on("SIGTERM", i), process.on("exit", g), m && m.addEventListener("abort", i);
|
|
2351
|
+
}, H = () => {
|
|
2352
|
+
process.removeListener("uncaughtExceptionMonitor", f), process.removeListener("unhandledRejection", f), process.removeListener("SIGINT", i), process.removeListener("SIGTERM", i), process.removeListener("exit", g), m && m.removeEventListener("abort", i);
|
|
2353
|
+
}, y = () => {
|
|
2354
|
+
if (p === undefined)
|
|
2355
|
+
return;
|
|
2356
|
+
u && n.write(`
|
|
2357
|
+
`);
|
|
2358
|
+
const r = wrapAnsi(p, x, {
|
|
2359
|
+
hard: true,
|
|
2360
|
+
trim: false
|
|
2361
|
+
}).split(`
|
|
2362
|
+
`);
|
|
2363
|
+
r.length > 1 && n.write(import_sisteransi3.cursor.up(r.length - 1)), n.write(import_sisteransi3.cursor.to(0)), n.write(import_sisteransi3.erase.down());
|
|
2364
|
+
}, C = (e) => e.replace(/\.+$/, ""), _ = (e) => {
|
|
2365
|
+
const r = (performance.now() - e) / 1000, t = Math.floor(r / 60), o = Math.floor(r % 60);
|
|
2366
|
+
return t > 0 ? `[${t}m ${o}s]` : `[${o}s]`;
|
|
2367
|
+
}, N = I.withGuide ?? settings.withGuide, P = (e = "") => {
|
|
2368
|
+
d = true, M = block({ output: n }), s = C(e), w = performance.now(), N && n.write(`${styleText3("gray", S_BAR2)}
|
|
2369
|
+
`);
|
|
2370
|
+
let r = 0, t = 0;
|
|
2371
|
+
A(), T = setInterval(() => {
|
|
2372
|
+
if (u && s === p)
|
|
2373
|
+
return;
|
|
2374
|
+
y(), p = s;
|
|
2375
|
+
const o = k(E[r]);
|
|
2376
|
+
let v;
|
|
2377
|
+
if (u)
|
|
2378
|
+
v = `${o} ${s}...`;
|
|
2379
|
+
else if (l === "timer")
|
|
2380
|
+
v = `${o} ${s} ${_(w)}`;
|
|
2381
|
+
else {
|
|
2382
|
+
const B = ".".repeat(Math.floor(t)).slice(0, 3);
|
|
2383
|
+
v = `${o} ${s}${B}`;
|
|
2384
|
+
}
|
|
2385
|
+
const j = wrapAnsi(v, x, {
|
|
2386
|
+
hard: true,
|
|
2387
|
+
trim: false
|
|
2388
|
+
});
|
|
2389
|
+
n.write(j), r = r + 1 < E.length ? r + 1 : 0, t = t < 4 ? t + 0.125 : 0;
|
|
2390
|
+
}, F);
|
|
2391
|
+
}, a = (e = "", r = 0, t = false) => {
|
|
2392
|
+
if (!d)
|
|
2393
|
+
return;
|
|
2394
|
+
d = false, clearInterval(T), y();
|
|
2395
|
+
const o = r === 0 ? styleText3("green", S_STEP_SUBMIT2) : r === 1 ? styleText3("red", S_STEP_CANCEL2) : styleText3("red", S_STEP_ERROR2);
|
|
2396
|
+
s = e ?? s, t || (l === "timer" ? n.write(`${o} ${s} ${_(w)}
|
|
2397
|
+
`) : n.write(`${o} ${s}
|
|
2398
|
+
`)), H(), M();
|
|
2399
|
+
};
|
|
2284
2400
|
return {
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2401
|
+
start: P,
|
|
2402
|
+
stop: (e = "") => a(e, 0),
|
|
2403
|
+
message: (e = "") => {
|
|
2404
|
+
s = C(e ?? s);
|
|
2405
|
+
},
|
|
2406
|
+
cancel: (e = "") => a(e, 1),
|
|
2407
|
+
error: (e = "") => a(e, 2),
|
|
2408
|
+
clear: () => a("", 0, true),
|
|
2409
|
+
get isCancelled() {
|
|
2410
|
+
return S;
|
|
2411
|
+
}
|
|
2296
2412
|
};
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
The workflow creates the registry first, pushes an immutable image, provisions the API with public
|
|
2340
|
-
traffic disabled, runs migrations, and admits traffic only after migration succeeds.
|
|
2341
|
-
|
|
2342
|
-
The provider lock is deliberate. Restore the deployment directories from a fresh licensed release
|
|
2343
|
-
before changing cloud providers.
|
|
2413
|
+
};
|
|
2414
|
+
var u3 = {
|
|
2415
|
+
light: unicodeOr2("─", "-"),
|
|
2416
|
+
heavy: unicodeOr2("━", "="),
|
|
2417
|
+
block: unicodeOr2("█", "#")
|
|
2418
|
+
};
|
|
2419
|
+
var SELECT_INSTRUCTIONS2 = [
|
|
2420
|
+
`${styleText3("dim", "↑/↓")} to navigate`,
|
|
2421
|
+
`${styleText3("dim", "Enter:")} confirm`
|
|
2422
|
+
];
|
|
2423
|
+
var c = (t, o) => t.includes(`
|
|
2424
|
+
`) ? t.split(`
|
|
2425
|
+
`).map((d) => o(d)).join(`
|
|
2426
|
+
`) : o(t);
|
|
2427
|
+
var select = (t) => {
|
|
2428
|
+
const o = (n, m) => {
|
|
2429
|
+
if (n === undefined)
|
|
2430
|
+
return "";
|
|
2431
|
+
const s = n.label ?? String(n.value);
|
|
2432
|
+
switch (m) {
|
|
2433
|
+
case "disabled":
|
|
2434
|
+
return `${styleText3("gray", S_RADIO_INACTIVE2)} ${c(s, (i) => styleText3("gray", i))}${n.hint ? ` ${styleText3("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
|
|
2435
|
+
case "selected":
|
|
2436
|
+
return `${c(s, (i) => styleText3("dim", i))}`;
|
|
2437
|
+
case "active":
|
|
2438
|
+
return `${styleText3("green", S_RADIO_ACTIVE2)} ${s}${n.hint ? ` ${styleText3("dim", `(${n.hint})`)}` : ""}`;
|
|
2439
|
+
case "cancelled":
|
|
2440
|
+
return `${c(s, (i) => styleText3(["strikethrough", "dim"], i))}`;
|
|
2441
|
+
default:
|
|
2442
|
+
return `${styleText3("dim", S_RADIO_INACTIVE2)} ${c(s, (i) => styleText3("dim", i))}`;
|
|
2443
|
+
}
|
|
2444
|
+
}, d = t.showInstructions ?? true;
|
|
2445
|
+
return new n$1({
|
|
2446
|
+
options: t.options,
|
|
2447
|
+
signal: t.signal,
|
|
2448
|
+
input: t.input,
|
|
2449
|
+
output: t.output,
|
|
2450
|
+
initialValue: t.initialValue,
|
|
2451
|
+
render() {
|
|
2452
|
+
const n = t.withGuide ?? settings.withGuide, m = `${symbol2(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText3("gray", S_BAR2)}
|
|
2453
|
+
` : ""}${i}
|
|
2344
2454
|
`;
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
`infra/roots/${cloud}`,
|
|
2374
|
-
`scripts/deploy/release-${cloud}.sh`,
|
|
2375
|
-
`scripts/deploy/frontend-${cloud}.sh`
|
|
2376
|
-
]) {
|
|
2377
|
-
if (await removeIfPresent(join3(repositoryRoot, relativePath)))
|
|
2378
|
-
removed.push(relativePath);
|
|
2455
|
+
switch (this.state) {
|
|
2456
|
+
case "submit": {
|
|
2457
|
+
const r = n ? `${styleText3("gray", S_BAR2)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r);
|
|
2458
|
+
return `${u}${a}`;
|
|
2459
|
+
}
|
|
2460
|
+
case "cancel": {
|
|
2461
|
+
const r = n ? `${styleText3("gray", S_BAR2)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r);
|
|
2462
|
+
return `${u}${a}${n ? `
|
|
2463
|
+
${styleText3("gray", S_BAR2)}` : ""}`;
|
|
2464
|
+
}
|
|
2465
|
+
default: {
|
|
2466
|
+
const r = n ? `${styleText3("cyan", S_BAR2)} ` : "", a = u.split(`
|
|
2467
|
+
`).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS2, n) : n ? [styleText3("cyan", S_BAR_END2)] : [], f = p.join(`
|
|
2468
|
+
`), b = p.length + 1;
|
|
2469
|
+
return `${u}${r}${limitOptions({
|
|
2470
|
+
output: t.output,
|
|
2471
|
+
cursor: this.cursor,
|
|
2472
|
+
options: this.options,
|
|
2473
|
+
maxItems: t.maxItems,
|
|
2474
|
+
columnPadding: r.length,
|
|
2475
|
+
rowPadding: a + b,
|
|
2476
|
+
style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
|
|
2477
|
+
}).join(`
|
|
2478
|
+
${r}`)}
|
|
2479
|
+
${f}
|
|
2480
|
+
`;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2379
2483
|
}
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2484
|
+
}).prompt();
|
|
2485
|
+
};
|
|
2486
|
+
var i2 = `${styleText3("gray", S_BAR2)} `;
|
|
2487
|
+
var text2 = (t) => new n({
|
|
2488
|
+
validate: t.validate,
|
|
2489
|
+
placeholder: t.placeholder,
|
|
2490
|
+
defaultValue: t.defaultValue,
|
|
2491
|
+
initialValue: t.initialValue,
|
|
2492
|
+
output: t.output,
|
|
2493
|
+
signal: t.signal,
|
|
2494
|
+
input: t.input,
|
|
2495
|
+
render() {
|
|
2496
|
+
const r = t?.withGuide ?? settings.withGuide, l = `${`${r ? `${styleText3("gray", S_BAR2)}
|
|
2497
|
+
` : ""}${symbol2(this.state)} `}${t.message}
|
|
2498
|
+
`, d = t.placeholder && t.placeholder.length > 0 ? styleText3("inverse", t.placeholder[0]) + styleText3("dim", t.placeholder.slice(1)) : styleText3(["inverse", "hidden"], "_"), o = this.userInput ? this.userInputWithCursor : d, s = this.value ?? "";
|
|
2499
|
+
switch (this.state) {
|
|
2500
|
+
case "validating": {
|
|
2501
|
+
const n = r ? `${styleText3("cyan", S_BAR2)} ` : "", i = r ? styleText3("cyan", S_BAR_END2) : "", c = styleText3("dim", o), $ = styleText3("dim", "Validating...");
|
|
2502
|
+
return `${l}${n}${c}
|
|
2503
|
+
${i} ${$}
|
|
2504
|
+
`;
|
|
2505
|
+
}
|
|
2506
|
+
case "error": {
|
|
2507
|
+
const n = this.error ? ` ${styleText3("yellow", this.error)}` : "", i = r ? `${styleText3("yellow", S_BAR2)} ` : "", c = r ? styleText3("yellow", S_BAR_END2) : "";
|
|
2508
|
+
return `${l.trim()}
|
|
2509
|
+
${i}${o}
|
|
2510
|
+
${c}${n}
|
|
2511
|
+
`;
|
|
2512
|
+
}
|
|
2513
|
+
case "submit": {
|
|
2514
|
+
const n = s ? `${r ? " " : ""}${styleText3("dim", s)}` : "", i = r ? styleText3("gray", S_BAR2) : "";
|
|
2515
|
+
return `${l}${i}${n}`;
|
|
2516
|
+
}
|
|
2517
|
+
case "cancel": {
|
|
2518
|
+
const n = s ? ` ${styleText3(["strikethrough", "dim"], s)}` : "", i = r ? styleText3("gray", S_BAR2) : "";
|
|
2519
|
+
return `${l}${i}${n}${s.trim() ? `
|
|
2520
|
+
${i}` : ""}`;
|
|
2521
|
+
}
|
|
2522
|
+
default: {
|
|
2523
|
+
const n = r ? `${styleText3("cyan", S_BAR2)} ` : "", i = r ? styleText3("cyan", S_BAR_END2) : "";
|
|
2524
|
+
return `${l}${n}${o}
|
|
2525
|
+
${i}
|
|
2526
|
+
`;
|
|
2527
|
+
}
|
|
2387
2528
|
}
|
|
2388
|
-
];
|
|
2389
|
-
const selectedAdapter = selection.frontendTarget === "native" ? selection.cloud === "azure" ? "azure-native" : undefined : selection.frontendTarget;
|
|
2390
|
-
for (const adapter of adapters) {
|
|
2391
|
-
if (adapter.target === selectedAdapter)
|
|
2392
|
-
continue;
|
|
2393
|
-
if (await removeIfPresent(join3(repositoryRoot, adapter.path)))
|
|
2394
|
-
removed.push(adapter.path);
|
|
2395
2529
|
}
|
|
2396
|
-
|
|
2397
|
-
const nativeScript = `scripts/deploy/frontend-${selection.cloud}.sh`;
|
|
2398
|
-
if (await removeIfPresent(join3(repositoryRoot, nativeScript)))
|
|
2399
|
-
removed.push(nativeScript);
|
|
2400
|
-
}
|
|
2401
|
-
return removed;
|
|
2402
|
-
}
|
|
2530
|
+
}).prompt();
|
|
2403
2531
|
|
|
2404
|
-
// src/
|
|
2405
|
-
|
|
2532
|
+
// src/agent-setup.ts
|
|
2533
|
+
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2534
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
2535
|
+
|
|
2536
|
+
// src/runtime.ts
|
|
2537
|
+
import { spawn } from "node:child_process";
|
|
2538
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
2539
|
+
import { delimiter, join, resolve as resolvePath } from "node:path";
|
|
2540
|
+
function runCaptured(command, options) {
|
|
2541
|
+
const [file, ...args] = command;
|
|
2542
|
+
if (!file)
|
|
2543
|
+
throw new Error("runCaptured requires a command");
|
|
2544
|
+
return new Promise((resolve, reject) => {
|
|
2545
|
+
const child = spawn(file, args, {
|
|
2546
|
+
cwd: options.cwd,
|
|
2547
|
+
env: options.env ?? process.env,
|
|
2548
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2549
|
+
});
|
|
2550
|
+
let stdout = "";
|
|
2551
|
+
let stderr = "";
|
|
2552
|
+
child.stdout.setEncoding("utf8");
|
|
2553
|
+
child.stderr.setEncoding("utf8");
|
|
2554
|
+
child.stdout.on("data", (chunk) => {
|
|
2555
|
+
stdout += chunk;
|
|
2556
|
+
});
|
|
2557
|
+
child.stderr.on("data", (chunk) => {
|
|
2558
|
+
stderr += chunk;
|
|
2559
|
+
});
|
|
2560
|
+
child.on("error", reject);
|
|
2561
|
+
child.on("close", (code, signal) => {
|
|
2562
|
+
resolve({ exitCode: code ?? (signal ? 1 : 0), stdout, stderr });
|
|
2563
|
+
});
|
|
2564
|
+
});
|
|
2406
2565
|
}
|
|
2407
|
-
|
|
2408
|
-
if (
|
|
2409
|
-
|
|
2410
|
-
|
|
2566
|
+
function which(command) {
|
|
2567
|
+
if (command.includes("/"))
|
|
2568
|
+
return isExecutable(command) ? command : null;
|
|
2569
|
+
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? "").split(";") : [""];
|
|
2570
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
2571
|
+
if (!directory)
|
|
2572
|
+
continue;
|
|
2573
|
+
for (const extension of extensions) {
|
|
2574
|
+
const candidate = join(directory, `${command}${extension}`);
|
|
2575
|
+
if (isExecutable(candidate))
|
|
2576
|
+
return candidate;
|
|
2577
|
+
}
|
|
2411
2578
|
}
|
|
2412
|
-
return
|
|
2579
|
+
return null;
|
|
2413
2580
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
return
|
|
2420
|
-
cloud: defaults.cloud,
|
|
2421
|
-
storageProvider: defaults.storageProvider ?? defaultStorageProviders[defaults.cloud],
|
|
2422
|
-
frontendTarget: defaults.frontendTarget ?? "native",
|
|
2423
|
-
environment: defaults.environment ?? "production",
|
|
2424
|
-
region: defaults.region ?? defaultRegions[defaults.cloud],
|
|
2425
|
-
apiDomain: defaults.apiDomain,
|
|
2426
|
-
frontendDomain: defaults.frontendDomain,
|
|
2427
|
-
haEnabled: defaults.haEnabled ?? false
|
|
2428
|
-
};
|
|
2581
|
+
function isExecutable(path) {
|
|
2582
|
+
try {
|
|
2583
|
+
accessSync(path, constants.X_OK);
|
|
2584
|
+
return true;
|
|
2585
|
+
} catch {
|
|
2586
|
+
return false;
|
|
2429
2587
|
}
|
|
2430
|
-
const cloud = defaults.cloud ?? parseDeploymentCloud(await unwrap(await select({
|
|
2431
|
-
message: "Backend cloud",
|
|
2432
|
-
options: [
|
|
2433
|
-
{ value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL" },
|
|
2434
|
-
{ value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL" },
|
|
2435
|
-
{ value: "azure", label: "Azure", hint: "Container Apps + Flexible Server" }
|
|
2436
|
-
]
|
|
2437
|
-
})));
|
|
2438
|
-
const frontendTarget = defaults.frontendTarget ?? parseFrontendTarget(await unwrap(await select({
|
|
2439
|
-
message: "Frontend host",
|
|
2440
|
-
options: [
|
|
2441
|
-
{ value: "native", label: "Cloud native", hint: "Managed with the backend" },
|
|
2442
|
-
{ value: "vercel", label: "Vercel SPA", hint: "Static frontend only" },
|
|
2443
|
-
{
|
|
2444
|
-
value: "cloudflare",
|
|
2445
|
-
label: "Cloudflare static assets",
|
|
2446
|
-
hint: "No API Worker"
|
|
2447
|
-
}
|
|
2448
|
-
]
|
|
2449
|
-
})));
|
|
2450
|
-
const storageProvider = defaults.storageProvider ?? (storageProvidersByCloud[cloud].length === 1 ? storageProvidersByCloud[cloud][0] ?? defaultStorageProviders[cloud] : parseStorageProvider(await unwrap(await select({
|
|
2451
|
-
message: "Object storage",
|
|
2452
|
-
options: storageProvidersByCloud[cloud].map((provider) => ({
|
|
2453
|
-
value: provider,
|
|
2454
|
-
label: provider === "s3" ? "Amazon S3" : provider === "gcs" ? "Google Cloud Storage" : "Cloudflare R2",
|
|
2455
|
-
hint: provider === defaultStorageProviders[cloud] ? "Cloud-native default" : ""
|
|
2456
|
-
}))
|
|
2457
|
-
}))));
|
|
2458
|
-
const environment = defaults.environment ?? await unwrap(await text2({
|
|
2459
|
-
message: "Deployment environment",
|
|
2460
|
-
placeholder: "production",
|
|
2461
|
-
defaultValue: "production",
|
|
2462
|
-
validate: (value) => validateDeploymentEnvironment(value || "production")
|
|
2463
|
-
}));
|
|
2464
|
-
const region = defaults.region ?? await unwrap(await text2({
|
|
2465
|
-
message: `${cloud.toUpperCase()} region`,
|
|
2466
|
-
placeholder: defaultRegions[cloud],
|
|
2467
|
-
defaultValue: defaultRegions[cloud],
|
|
2468
|
-
validate: (value) => validateRegion(value || defaultRegions[cloud])
|
|
2469
|
-
}));
|
|
2470
|
-
const apiDomain = defaults.apiDomain ?? await unwrap(await text2({
|
|
2471
|
-
message: "Production API domain",
|
|
2472
|
-
placeholder: "api.example.com",
|
|
2473
|
-
validate: (value) => validateDomain(value ?? "")
|
|
2474
|
-
}));
|
|
2475
|
-
const frontendDomain = defaults.frontendDomain ?? await unwrap(await text2({
|
|
2476
|
-
message: "Production frontend domain",
|
|
2477
|
-
placeholder: "app.example.com",
|
|
2478
|
-
validate: (value) => validateDomain(value ?? "")
|
|
2479
|
-
}));
|
|
2480
|
-
const haEnabled = defaults.haEnabled ?? await unwrap(await confirm2({
|
|
2481
|
-
message: "Enable the higher-availability profile?",
|
|
2482
|
-
initialValue: false
|
|
2483
|
-
}));
|
|
2484
|
-
return {
|
|
2485
|
-
cloud,
|
|
2486
|
-
storageProvider,
|
|
2487
|
-
frontendTarget,
|
|
2488
|
-
environment,
|
|
2489
|
-
region,
|
|
2490
|
-
apiDomain,
|
|
2491
|
-
frontendDomain,
|
|
2492
|
-
haEnabled
|
|
2493
|
-
};
|
|
2494
2588
|
}
|
|
2495
2589
|
|
|
2496
|
-
// src/
|
|
2497
|
-
|
|
2590
|
+
// src/agent-setup.ts
|
|
2591
|
+
var CLIENTS = ["codex", "claude", "cursor"];
|
|
2592
|
+
var SERVER_NAME = "agent-docs";
|
|
2593
|
+
var CODEX_START = "# agent-context:start";
|
|
2594
|
+
var CODEX_END = "# agent-context:end";
|
|
2595
|
+
var AGENT_CONTEXT_TOOLS = [
|
|
2596
|
+
"search_docs",
|
|
2597
|
+
"search_implementation",
|
|
2598
|
+
"get_repository_map",
|
|
2599
|
+
"plan_change",
|
|
2600
|
+
"get_invariants",
|
|
2601
|
+
"get_verification"
|
|
2602
|
+
];
|
|
2603
|
+
function takeValue(args, index, flag) {
|
|
2498
2604
|
const value = args[index + 1];
|
|
2499
|
-
if (!value || value.startsWith("
|
|
2605
|
+
if (!value || value.startsWith("-"))
|
|
2500
2606
|
throw new Error(`${flag} requires a value.`);
|
|
2501
2607
|
return value;
|
|
2502
2608
|
}
|
|
2503
|
-
function
|
|
2504
|
-
const parsed = { yes: false };
|
|
2609
|
+
function parseAgentSetupArguments(argv) {
|
|
2610
|
+
const parsed = { yes: false, dryRun: false, help: false };
|
|
2505
2611
|
for (let index = 0;index < argv.length; index += 1) {
|
|
2506
2612
|
const argument = argv[index];
|
|
2507
|
-
if (argument === "--
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
parsed.
|
|
2513
|
-
} else if (argument === "--
|
|
2514
|
-
parsed.environment = takeValue2(argv, index++, argument);
|
|
2515
|
-
} else if (argument === "--region") {
|
|
2516
|
-
parsed.region = takeValue2(argv, index++, argument);
|
|
2517
|
-
} else if (argument === "--api-domain") {
|
|
2518
|
-
parsed.apiDomain = takeValue2(argv, index++, argument);
|
|
2519
|
-
} else if (argument === "--frontend-domain") {
|
|
2520
|
-
parsed.frontendDomain = takeValue2(argv, index++, argument);
|
|
2521
|
-
} else if (argument === "--ha")
|
|
2522
|
-
parsed.haEnabled = true;
|
|
2523
|
-
else if (argument === "--no-ha")
|
|
2524
|
-
parsed.haEnabled = false;
|
|
2525
|
-
else if (argument === "--yes" || argument === "-y")
|
|
2613
|
+
if (argument === "--clients") {
|
|
2614
|
+
const values = takeValue(argv, index++, argument).split(",").map((value) => value.trim()).filter(Boolean);
|
|
2615
|
+
const invalid = values.filter((value) => !CLIENTS.includes(value));
|
|
2616
|
+
if (invalid.length > 0)
|
|
2617
|
+
throw new Error(`Unknown agent client: ${invalid.join(", ")}`);
|
|
2618
|
+
parsed.clients = [...new Set(values)];
|
|
2619
|
+
} else if (argument === "--yes" || argument === "-y")
|
|
2526
2620
|
parsed.yes = true;
|
|
2621
|
+
else if (argument === "--dry-run")
|
|
2622
|
+
parsed.dryRun = true;
|
|
2623
|
+
else if (argument === "--help" || argument === "-h")
|
|
2624
|
+
parsed.help = true;
|
|
2527
2625
|
else
|
|
2528
|
-
throw new Error(`Unknown
|
|
2626
|
+
throw new Error(`Unknown agent setup option: ${argument}`);
|
|
2627
|
+
}
|
|
2628
|
+
return parsed;
|
|
2629
|
+
}
|
|
2630
|
+
function agentSetupUsage(cliName) {
|
|
2631
|
+
return `Usage: ${cliName} agent setup [options]
|
|
2632
|
+
|
|
2633
|
+
Connect local coding clients to this checkout's read-only project context server.
|
|
2634
|
+
|
|
2635
|
+
Options:
|
|
2636
|
+
--clients <list> Comma-separated clients: codex,claude,cursor
|
|
2637
|
+
--yes Configure every detected client without prompting
|
|
2638
|
+
--dry-run Show the selected configuration without changing client settings
|
|
2639
|
+
-h, --help Show this help`;
|
|
2640
|
+
}
|
|
2641
|
+
async function pathExists(path) {
|
|
2642
|
+
return Boolean(await stat(path).catch(() => {
|
|
2643
|
+
return;
|
|
2644
|
+
}));
|
|
2645
|
+
}
|
|
2646
|
+
async function findAgentRepositoryRoot(start = process.cwd()) {
|
|
2647
|
+
let current = resolve(start);
|
|
2648
|
+
while (true) {
|
|
2649
|
+
if (await pathExists(join2(current, "AGENTS.md")) && await pathExists(join2(current, "packages/agent-context/package.json")) && await pathExists(join2(current, "agent-tooling/manifest.json"))) {
|
|
2650
|
+
return current;
|
|
2651
|
+
}
|
|
2652
|
+
const parent = dirname(current);
|
|
2653
|
+
if (parent === current)
|
|
2654
|
+
throw new Error("Run agent setup inside a project repository.");
|
|
2655
|
+
current = parent;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
function serverCommand(root) {
|
|
2659
|
+
return { command: "pnpm", args: ["--dir", resolve(root), "agent:mcp"] };
|
|
2660
|
+
}
|
|
2661
|
+
function codexManagedBlock(root) {
|
|
2662
|
+
const server = serverCommand(root);
|
|
2663
|
+
const args = server.args.map((argument) => JSON.stringify(argument)).join(", ");
|
|
2664
|
+
return `${CODEX_START}
|
|
2665
|
+
[mcp_servers.${SERVER_NAME}]
|
|
2666
|
+
command = ${JSON.stringify(server.command)}
|
|
2667
|
+
args = [${args}]
|
|
2668
|
+
required = false
|
|
2669
|
+
enabled_tools = ${JSON.stringify(AGENT_CONTEXT_TOOLS)}
|
|
2670
|
+
${CODEX_END}`;
|
|
2671
|
+
}
|
|
2672
|
+
function mergeCodexConfig(existing, root) {
|
|
2673
|
+
const managed = codexManagedBlock(root);
|
|
2674
|
+
const start = existing.indexOf(CODEX_START);
|
|
2675
|
+
const end = existing.indexOf(CODEX_END);
|
|
2676
|
+
if (start >= 0 || end >= 0) {
|
|
2677
|
+
if (start < 0 || end < start)
|
|
2678
|
+
throw new Error("Codex config has an incomplete agent-context block.");
|
|
2679
|
+
const after = end + CODEX_END.length;
|
|
2680
|
+
return `${existing.slice(0, start).trimEnd()}
|
|
2681
|
+
|
|
2682
|
+
${managed}${existing.slice(after)}`.trimStart();
|
|
2683
|
+
}
|
|
2684
|
+
if (/^\[mcp_servers\.(?:agent-docs|agent_docs|"agent-docs")\]$/m.test(existing)) {
|
|
2685
|
+
throw new Error("Codex already has an unmanaged agent-docs server; remove or rename it first.");
|
|
2686
|
+
}
|
|
2687
|
+
return `${existing.trimEnd()}${existing.trim() ? `
|
|
2688
|
+
|
|
2689
|
+
` : ""}${managed}
|
|
2690
|
+
`;
|
|
2691
|
+
}
|
|
2692
|
+
function mergeCursorConfig(existing, root) {
|
|
2693
|
+
let config = {};
|
|
2694
|
+
if (existing?.trim()) {
|
|
2695
|
+
try {
|
|
2696
|
+
const parsed = JSON.parse(existing);
|
|
2697
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2698
|
+
throw new Error("Cursor MCP configuration must be a JSON object.");
|
|
2699
|
+
}
|
|
2700
|
+
config = parsed;
|
|
2701
|
+
} catch (error) {
|
|
2702
|
+
if (error instanceof SyntaxError)
|
|
2703
|
+
throw new Error("Cursor MCP configuration is invalid JSON.");
|
|
2704
|
+
throw error;
|
|
2705
|
+
}
|
|
2529
2706
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
const rootPackage = JSON.parse(await readFile3(`${root}/package.json`, "utf8"));
|
|
2536
|
-
if (!rootPackage.name)
|
|
2537
|
-
throw new Error("Root package name is required for deployment setup.");
|
|
2538
|
-
intro2(`${productName} deployment setup`);
|
|
2539
|
-
try {
|
|
2540
|
-
const selection = await collectDeploymentSelection(parsed, parsed.yes);
|
|
2541
|
-
const removed = await applyDeploymentSelection(root, rootPackage.name, selection);
|
|
2542
|
-
log2.success(`${selection.cloud.toUpperCase()} + ${selection.storageProvider} + ${selection.frontendTarget} is locked for ${selection.environment}.`);
|
|
2543
|
-
log2.info(`${removed.length} unused deployment paths removed.`);
|
|
2544
|
-
log2.step("Next");
|
|
2545
|
-
log2.message(`Review deployment/config.json
|
|
2546
|
-
Configure the protected GitHub environment
|
|
2547
|
-
Dispatch .github/workflows/deploy-cloud.yml`);
|
|
2548
|
-
outro2("Deployment configuration is ready to commit.");
|
|
2549
|
-
} catch (error) {
|
|
2550
|
-
if (error instanceof DeploymentPromptCancelled)
|
|
2551
|
-
return;
|
|
2552
|
-
throw error;
|
|
2707
|
+
const servers = config.mcpServers ?? {};
|
|
2708
|
+
const current = servers[SERVER_NAME];
|
|
2709
|
+
const desired = serverCommand(root);
|
|
2710
|
+
if (current !== undefined && JSON.stringify(current) !== JSON.stringify(desired)) {
|
|
2711
|
+
throw new Error("Cursor already has a different agent-docs server; remove or rename it first.");
|
|
2553
2712
|
}
|
|
2713
|
+
config.mcpServers = { ...servers, [SERVER_NAME]: desired };
|
|
2714
|
+
return `${JSON.stringify(config, null, 2)}
|
|
2715
|
+
`;
|
|
2554
2716
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
import { fileURLToPath } from "node:url";
|
|
2561
|
-
function runCaptured2(command, options) {
|
|
2562
|
-
const [file, ...args] = command;
|
|
2563
|
-
if (!file)
|
|
2564
|
-
throw new Error("runCaptured requires a command");
|
|
2565
|
-
return new Promise((resolve, reject) => {
|
|
2566
|
-
const child = spawn2(file, args, {
|
|
2567
|
-
cwd: options.cwd,
|
|
2568
|
-
env: options.env ?? process.env,
|
|
2569
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2570
|
-
});
|
|
2571
|
-
let stdout = "";
|
|
2572
|
-
let stderr = "";
|
|
2573
|
-
child.stdout.setEncoding("utf8");
|
|
2574
|
-
child.stderr.setEncoding("utf8");
|
|
2575
|
-
child.stdout.on("data", (chunk) => {
|
|
2576
|
-
stdout += chunk;
|
|
2577
|
-
});
|
|
2578
|
-
child.stderr.on("data", (chunk) => {
|
|
2579
|
-
stderr += chunk;
|
|
2580
|
-
});
|
|
2581
|
-
child.on("error", reject);
|
|
2582
|
-
child.on("close", (code, signal) => {
|
|
2583
|
-
resolve({ exitCode: code ?? (signal ? 1 : 0), stdout, stderr });
|
|
2584
|
-
});
|
|
2585
|
-
});
|
|
2717
|
+
async function atomicWrite(path, content) {
|
|
2718
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2719
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
2720
|
+
await writeFile(temporary, content, { mode: 384 });
|
|
2721
|
+
await rename(temporary, path);
|
|
2586
2722
|
}
|
|
2587
|
-
function
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2723
|
+
async function capture(command, cwd) {
|
|
2724
|
+
return runCaptured(command, { cwd });
|
|
2725
|
+
}
|
|
2726
|
+
async function addGitInfoExcludes(root, paths) {
|
|
2727
|
+
const result = await capture(["git", "rev-parse", "--git-path", "info/exclude"], root);
|
|
2728
|
+
if (result.exitCode !== 0)
|
|
2729
|
+
return false;
|
|
2730
|
+
const rawPath = result.stdout.trim();
|
|
2731
|
+
if (!rawPath)
|
|
2732
|
+
return false;
|
|
2733
|
+
const excludePath = resolve(root, rawPath);
|
|
2734
|
+
const existing = await readFile(excludePath, "utf8").catch(() => "");
|
|
2735
|
+
const missing = paths.filter((path) => !existing.split(/\r?\n/).includes(path));
|
|
2736
|
+
if (missing.length === 0)
|
|
2737
|
+
return true;
|
|
2738
|
+
const prefix = existing && !existing.endsWith(`
|
|
2739
|
+
`) ? `
|
|
2740
|
+
` : "";
|
|
2741
|
+
const marker = "# Local agent configuration";
|
|
2742
|
+
const heading = existing.split(/\r?\n/).includes(marker) ? "" : `${marker}
|
|
2743
|
+
`;
|
|
2744
|
+
await atomicWrite(excludePath, `${existing}${prefix}${heading}${missing.join(`
|
|
2745
|
+
`)}
|
|
2746
|
+
`);
|
|
2747
|
+
return true;
|
|
2748
|
+
}
|
|
2749
|
+
async function configureCodex(root) {
|
|
2750
|
+
const path = join2(root, ".codex/config.toml");
|
|
2751
|
+
const existing = await readFile(path, "utf8").catch(() => "");
|
|
2752
|
+
await atomicWrite(path, mergeCodexConfig(existing, root));
|
|
2753
|
+
const excluded = await addGitInfoExcludes(root, [".codex/config.toml"]);
|
|
2754
|
+
return excluded ? "Configured project-local .codex/config.toml (excluded from Git)." : "Configured project-local .codex/config.toml; review Git status before committing it.";
|
|
2755
|
+
}
|
|
2756
|
+
async function configureCursor(root) {
|
|
2757
|
+
const path = join2(root, ".cursor/mcp.json");
|
|
2758
|
+
const existing = await readFile(path, "utf8").catch(() => {
|
|
2759
|
+
return;
|
|
2601
2760
|
});
|
|
2761
|
+
await atomicWrite(path, mergeCursorConfig(existing, root));
|
|
2762
|
+
const excluded = await addGitInfoExcludes(root, [".cursor/mcp.json"]);
|
|
2763
|
+
return excluded ? "Configured project-local .cursor/mcp.json (excluded from Git)." : "Configured project-local .cursor/mcp.json; review Git status before committing it.";
|
|
2602
2764
|
}
|
|
2603
|
-
function
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2765
|
+
async function configureClaude(root) {
|
|
2766
|
+
if (!which("claude"))
|
|
2767
|
+
throw new Error("Claude Code CLI is not installed or not on PATH.");
|
|
2768
|
+
const existing = await capture(["claude", "mcp", "get", SERVER_NAME], root);
|
|
2769
|
+
if (existing.exitCode === 0) {
|
|
2770
|
+
const combined = `${existing.stdout}
|
|
2771
|
+
${existing.stderr}`;
|
|
2772
|
+
if (combined.includes("agent:mcp") && combined.includes(root)) {
|
|
2773
|
+
return "Claude Code already has the matching local server.";
|
|
2774
|
+
}
|
|
2775
|
+
throw new Error("Claude Code already has a different agent-docs server; remove it first.");
|
|
2608
2776
|
}
|
|
2777
|
+
const server = serverCommand(root);
|
|
2778
|
+
const result = await capture(["claude", "mcp", "add", SERVER_NAME, "--scope", "local", "--", server.command, ...server.args], root);
|
|
2779
|
+
if (result.exitCode !== 0) {
|
|
2780
|
+
const output = result.stderr.trim() || result.stdout.trim();
|
|
2781
|
+
throw new Error(`Claude Code MCP setup failed${output ? `: ${output}` : "."}`);
|
|
2782
|
+
}
|
|
2783
|
+
return "Configured Claude Code local project scope.";
|
|
2609
2784
|
}
|
|
2610
|
-
function
|
|
2611
|
-
const
|
|
2612
|
-
if (
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2785
|
+
async function smokeServer(root) {
|
|
2786
|
+
const result = await capture(["pnpm", "--dir", root, "agent:mcp", "--check", "--root", root], root);
|
|
2787
|
+
if (result.exitCode !== 0) {
|
|
2788
|
+
const output = result.stderr.trim() || result.stdout.trim();
|
|
2789
|
+
throw new Error(`Agent context MCP self-check failed${output ? `: ${output}` : "."}`);
|
|
2790
|
+
}
|
|
2791
|
+
const report = JSON.parse(result.stdout);
|
|
2792
|
+
if (!report.ok || report.tools !== AGENT_CONTEXT_TOOLS.length) {
|
|
2793
|
+
throw new Error("Agent context MCP self-check was incomplete.");
|
|
2618
2794
|
}
|
|
2619
2795
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2796
|
+
function detectedClients() {
|
|
2797
|
+
return CLIENTS.filter((client) => {
|
|
2798
|
+
if (client === "cursor")
|
|
2799
|
+
return Boolean(which("cursor"));
|
|
2800
|
+
return Boolean(which(client));
|
|
2801
|
+
});
|
|
2623
2802
|
}
|
|
2624
|
-
async function
|
|
2625
|
-
if (
|
|
2626
|
-
|
|
2627
|
-
|
|
2803
|
+
async function chooseClients(parsed) {
|
|
2804
|
+
if (parsed.clients) {
|
|
2805
|
+
if (parsed.clients.length === 0)
|
|
2806
|
+
throw new Error("--clients must select at least one client.");
|
|
2807
|
+
return parsed.clients;
|
|
2628
2808
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (!defaults.cloud || !defaults.apiDomain || !defaults.frontendDomain) {
|
|
2634
|
-
throw new Error("Non-interactive deployment setup requires --cloud, --api-domain, and --frontend-domain.");
|
|
2809
|
+
const detected = detectedClients();
|
|
2810
|
+
if (parsed.yes) {
|
|
2811
|
+
if (detected.length === 0) {
|
|
2812
|
+
throw new Error("No supported client was detected. Pass --clients codex,claude,cursor.");
|
|
2635
2813
|
}
|
|
2636
|
-
return
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2814
|
+
return detected;
|
|
2815
|
+
}
|
|
2816
|
+
if (!process.stdin.isTTY) {
|
|
2817
|
+
throw new Error("Interactive setup needs a terminal. Pass --clients and --yes instead.");
|
|
2818
|
+
}
|
|
2819
|
+
const selected = await multiselect({
|
|
2820
|
+
message: "Connect which local coding clients?",
|
|
2821
|
+
required: true,
|
|
2822
|
+
options: CLIENTS.map((client) => ({
|
|
2823
|
+
value: client,
|
|
2824
|
+
label: client === "claude" ? "Claude Code" : `${client[0].toUpperCase()}${client.slice(1)}`,
|
|
2825
|
+
hint: detected.includes(client) ? "detected" : "not detected on PATH"
|
|
2826
|
+
})),
|
|
2827
|
+
initialValues: detected
|
|
2828
|
+
});
|
|
2829
|
+
if (isCancel(selected)) {
|
|
2830
|
+
cancel2("No agent clients were configured.");
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
return selected;
|
|
2834
|
+
}
|
|
2835
|
+
function dryRunDescription(client, root) {
|
|
2836
|
+
if (client === "codex")
|
|
2837
|
+
return mergeCodexConfig("", root).trim();
|
|
2838
|
+
if (client === "cursor")
|
|
2839
|
+
return mergeCursorConfig(undefined, root).trim();
|
|
2840
|
+
const server = serverCommand(root);
|
|
2841
|
+
return [
|
|
2842
|
+
"claude",
|
|
2843
|
+
"mcp",
|
|
2844
|
+
"add",
|
|
2845
|
+
SERVER_NAME,
|
|
2846
|
+
"--scope",
|
|
2847
|
+
"local",
|
|
2848
|
+
"--",
|
|
2849
|
+
server.command,
|
|
2850
|
+
...server.args
|
|
2851
|
+
].map((value) => value.includes(" ") ? JSON.stringify(value) : value).join(" ");
|
|
2852
|
+
}
|
|
2853
|
+
async function runAgentSetup(argv, cliName) {
|
|
2854
|
+
const parsed = parseAgentSetupArguments(argv);
|
|
2855
|
+
if (parsed.help) {
|
|
2856
|
+
process.stdout.write(`${agentSetupUsage(cliName)}
|
|
2857
|
+
`);
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
const root = await findAgentRepositoryRoot();
|
|
2861
|
+
const clients = await chooseClients(parsed);
|
|
2862
|
+
if (!clients)
|
|
2863
|
+
return;
|
|
2864
|
+
intro2("Local agent context");
|
|
2865
|
+
if (parsed.dryRun) {
|
|
2866
|
+
for (const client of clients) {
|
|
2867
|
+
log2.info(`${client}
|
|
2868
|
+
${dryRunDescription(client, root)}`);
|
|
2869
|
+
}
|
|
2870
|
+
outro2("Dry run complete. No client configuration was changed.");
|
|
2871
|
+
return;
|
|
2646
2872
|
}
|
|
2647
|
-
const
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
options: [
|
|
2658
|
-
{ value: "native", label: "Cloud native", hint: "Managed with the backend" },
|
|
2659
|
-
{ value: "vercel", label: "Vercel SPA", hint: "Static frontend only" },
|
|
2660
|
-
{
|
|
2661
|
-
value: "cloudflare",
|
|
2662
|
-
label: "Cloudflare static assets",
|
|
2663
|
-
hint: "No API Worker"
|
|
2664
|
-
}
|
|
2665
|
-
]
|
|
2666
|
-
})));
|
|
2667
|
-
const storageProvider = defaults.storageProvider ?? (storageProvidersByCloud[cloud].length === 1 ? storageProvidersByCloud[cloud][0] ?? defaultStorageProviders[cloud] : parseStorageProvider(await unwrap2(await select({
|
|
2668
|
-
message: "Object storage",
|
|
2669
|
-
options: storageProvidersByCloud[cloud].map((provider) => ({
|
|
2670
|
-
value: provider,
|
|
2671
|
-
label: provider === "s3" ? "Amazon S3" : provider === "gcs" ? "Google Cloud Storage" : "Cloudflare R2",
|
|
2672
|
-
hint: provider === defaultStorageProviders[cloud] ? "Cloud-native default" : ""
|
|
2673
|
-
}))
|
|
2674
|
-
}))));
|
|
2675
|
-
const environment = defaults.environment ?? await unwrap2(await text2({
|
|
2676
|
-
message: "Deployment environment",
|
|
2677
|
-
placeholder: "production",
|
|
2678
|
-
defaultValue: "production",
|
|
2679
|
-
validate: (value) => validateDeploymentEnvironment(value || "production")
|
|
2680
|
-
}));
|
|
2681
|
-
const region = defaults.region ?? await unwrap2(await text2({
|
|
2682
|
-
message: `${cloud.toUpperCase()} region`,
|
|
2683
|
-
placeholder: defaultRegions[cloud],
|
|
2684
|
-
defaultValue: defaultRegions[cloud],
|
|
2685
|
-
validate: (value) => validateRegion(value || defaultRegions[cloud])
|
|
2686
|
-
}));
|
|
2687
|
-
const apiDomain = defaults.apiDomain ?? await unwrap2(await text2({
|
|
2688
|
-
message: "Production API domain",
|
|
2689
|
-
placeholder: "api.example.com",
|
|
2690
|
-
validate: (value) => validateDomain(value ?? "")
|
|
2691
|
-
}));
|
|
2692
|
-
const frontendDomain = defaults.frontendDomain ?? await unwrap2(await text2({
|
|
2693
|
-
message: "Production frontend domain",
|
|
2694
|
-
placeholder: "app.example.com",
|
|
2695
|
-
validate: (value) => validateDomain(value ?? "")
|
|
2696
|
-
}));
|
|
2697
|
-
const haEnabled = defaults.haEnabled ?? await unwrap2(await confirm2({
|
|
2698
|
-
message: "Enable the higher-availability profile?",
|
|
2699
|
-
initialValue: false
|
|
2700
|
-
}));
|
|
2701
|
-
return {
|
|
2702
|
-
cloud,
|
|
2703
|
-
storageProvider,
|
|
2704
|
-
frontendTarget,
|
|
2705
|
-
environment,
|
|
2706
|
-
region,
|
|
2707
|
-
apiDomain,
|
|
2708
|
-
frontendDomain,
|
|
2709
|
-
haEnabled
|
|
2710
|
-
};
|
|
2873
|
+
const progress = spinner2();
|
|
2874
|
+
progress.start("Testing the local MCP server");
|
|
2875
|
+
await smokeServer(root);
|
|
2876
|
+
progress.stop("Local MCP server passed its protocol self-check");
|
|
2877
|
+
for (const client of clients) {
|
|
2878
|
+
const result = client === "codex" ? await configureCodex(root) : client === "cursor" ? await configureCursor(root) : await configureClaude(root);
|
|
2879
|
+
log2.success(`${client}: ${result}`);
|
|
2880
|
+
}
|
|
2881
|
+
log2.info("Restart configured clients, then ask: “Use plan_change for my next feature.”");
|
|
2882
|
+
outro2("Agent context is ready.");
|
|
2711
2883
|
}
|
|
2712
2884
|
|
|
2885
|
+
// src/deployment-cli.ts
|
|
2886
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2887
|
+
import { resolve as resolve2 } from "node:path";
|
|
2888
|
+
|
|
2713
2889
|
// src/deployment.ts
|
|
2714
|
-
import { mkdir as
|
|
2715
|
-
import { join as
|
|
2716
|
-
var
|
|
2717
|
-
var
|
|
2718
|
-
var
|
|
2719
|
-
var
|
|
2890
|
+
import { mkdir as mkdir2, readFile as readFile2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2891
|
+
import { join as join3 } from "node:path";
|
|
2892
|
+
var deploymentClouds = ["aws", "gcp", "azure"];
|
|
2893
|
+
var frontendTargets = ["native", "vercel", "cloudflare"];
|
|
2894
|
+
var storageProviders = ["r2", "s3", "gcs"];
|
|
2895
|
+
var defaultRegions = {
|
|
2896
|
+
aws: "us-east-1",
|
|
2897
|
+
gcp: "us-central1",
|
|
2898
|
+
azure: "eastus2"
|
|
2899
|
+
};
|
|
2900
|
+
var defaultStorageProviders = {
|
|
2901
|
+
aws: "s3",
|
|
2902
|
+
gcp: "gcs",
|
|
2903
|
+
azure: "r2"
|
|
2904
|
+
};
|
|
2905
|
+
var storageProvidersByCloud = {
|
|
2720
2906
|
aws: ["s3", "r2"],
|
|
2721
2907
|
gcp: ["gcs", "r2"],
|
|
2722
2908
|
azure: ["r2"]
|
|
2723
2909
|
};
|
|
2724
|
-
function
|
|
2725
|
-
return
|
|
2910
|
+
function isDeploymentCloud(value) {
|
|
2911
|
+
return deploymentClouds.some((cloud) => cloud === value);
|
|
2726
2912
|
}
|
|
2727
|
-
function
|
|
2728
|
-
return
|
|
2913
|
+
function isFrontendTarget(value) {
|
|
2914
|
+
return frontendTargets.some((target) => target === value);
|
|
2729
2915
|
}
|
|
2730
|
-
function
|
|
2731
|
-
return
|
|
2916
|
+
function isStorageProvider(value) {
|
|
2917
|
+
return storageProviders.some((provider) => provider === value);
|
|
2732
2918
|
}
|
|
2733
|
-
function
|
|
2734
|
-
if (!
|
|
2919
|
+
function parseDeploymentCloud(value) {
|
|
2920
|
+
if (!isDeploymentCloud(value))
|
|
2735
2921
|
throw new Error("Cloud must be aws, gcp, or azure.");
|
|
2736
2922
|
return value;
|
|
2737
2923
|
}
|
|
2738
|
-
function
|
|
2739
|
-
if (!
|
|
2924
|
+
function parseFrontendTarget(value) {
|
|
2925
|
+
if (!isFrontendTarget(value)) {
|
|
2740
2926
|
throw new Error("Frontend target must be native, vercel, or cloudflare.");
|
|
2741
2927
|
}
|
|
2742
2928
|
return value;
|
|
2743
2929
|
}
|
|
2744
|
-
function
|
|
2745
|
-
if (!
|
|
2930
|
+
function parseStorageProvider(value) {
|
|
2931
|
+
if (!isStorageProvider(value))
|
|
2746
2932
|
throw new Error("Storage provider must be r2, s3, or gcs.");
|
|
2747
2933
|
return value;
|
|
2748
2934
|
}
|
|
2749
|
-
function
|
|
2935
|
+
function validateDeploymentEnvironment(value) {
|
|
2750
2936
|
if (!/^[a-z][a-z0-9-]{1,30}$/.test(value)) {
|
|
2751
2937
|
return "Environment must use 2-31 lowercase letters, numbers, or hyphens.";
|
|
2752
2938
|
}
|
|
2753
2939
|
return;
|
|
2754
2940
|
}
|
|
2755
|
-
function
|
|
2941
|
+
function validateRegion(value) {
|
|
2756
2942
|
if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(value)) {
|
|
2757
2943
|
return "Region must use 2-32 lowercase letters, numbers, or hyphens.";
|
|
2758
2944
|
}
|
|
2759
2945
|
return;
|
|
2760
2946
|
}
|
|
2761
|
-
function
|
|
2947
|
+
function validateDomain(value) {
|
|
2762
2948
|
if (value.length > 80 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value)) {
|
|
2763
2949
|
return "Enter a lowercase hostname such as api.example.com, without a protocol or path.";
|
|
2764
2950
|
}
|
|
2765
2951
|
return;
|
|
2766
2952
|
}
|
|
2767
|
-
function
|
|
2768
|
-
if (!
|
|
2953
|
+
function validateDeploymentSelection(selection) {
|
|
2954
|
+
if (!storageProvidersByCloud[selection.cloud].includes(selection.storageProvider)) {
|
|
2769
2955
|
throw new Error(`${selection.storageProvider} storage is not supported for ${selection.cloud} deployments.`);
|
|
2770
2956
|
}
|
|
2771
|
-
const environmentError =
|
|
2957
|
+
const environmentError = validateDeploymentEnvironment(selection.environment);
|
|
2772
2958
|
if (environmentError)
|
|
2773
2959
|
throw new Error(environmentError);
|
|
2774
|
-
const regionError =
|
|
2960
|
+
const regionError = validateRegion(selection.region);
|
|
2775
2961
|
if (regionError)
|
|
2776
2962
|
throw new Error(regionError);
|
|
2777
|
-
const apiError =
|
|
2963
|
+
const apiError = validateDomain(selection.apiDomain);
|
|
2778
2964
|
if (apiError)
|
|
2779
2965
|
throw new Error(`API domain: ${apiError}`);
|
|
2780
|
-
const frontendError =
|
|
2966
|
+
const frontendError = validateDomain(selection.frontendDomain);
|
|
2781
2967
|
if (frontendError)
|
|
2782
2968
|
throw new Error(`Frontend domain: ${frontendError}`);
|
|
2783
2969
|
if (selection.apiDomain === selection.frontendDomain) {
|
|
@@ -2789,8 +2975,8 @@ function validateDeploymentSelection2(selection) {
|
|
|
2789
2975
|
throw new Error("API and frontend domains must be sibling hosts under the same site.");
|
|
2790
2976
|
}
|
|
2791
2977
|
}
|
|
2792
|
-
function
|
|
2793
|
-
|
|
2978
|
+
function createDeploymentConfig(projectName, selection) {
|
|
2979
|
+
validateDeploymentSelection(selection);
|
|
2794
2980
|
return {
|
|
2795
2981
|
schema_version: 2,
|
|
2796
2982
|
cloud: selection.cloud,
|
|
@@ -2805,7 +2991,7 @@ function createDeploymentConfig2(projectName, selection) {
|
|
|
2805
2991
|
ha_enabled: selection.haEnabled
|
|
2806
2992
|
};
|
|
2807
2993
|
}
|
|
2808
|
-
function
|
|
2994
|
+
function deploymentReadme(config) {
|
|
2809
2995
|
const identity = config.cloud === "aws" ? "`AWS_ROLE_ARN` secret" : config.cloud === "gcp" ? "`GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT` secrets" : "`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID` secrets";
|
|
2810
2996
|
const providerInputs = config.cloud === "aws" ? "Route 53 zone or certificate identifiers and runtime Secrets Manager ARNs" : config.cloud === "gcp" ? "GCP project, Cloud DNS zone, and runtime Secret Manager names" : "Azure DNS zone ID and runtime Key Vault secret identifiers";
|
|
2811
2997
|
const storageSetup = config.storage_provider === "r2" ? `Create private staging and public Cloudflare R2 buckets, then store the required storage
|
|
@@ -2853,28 +3039,28 @@ The provider lock is deliberate. Restore the deployment directories from a fresh
|
|
|
2853
3039
|
before changing cloud providers.
|
|
2854
3040
|
`;
|
|
2855
3041
|
}
|
|
2856
|
-
async function
|
|
2857
|
-
if (!await
|
|
3042
|
+
async function removeIfPresent(target) {
|
|
3043
|
+
if (!await stat2(target).catch(() => {
|
|
2858
3044
|
return;
|
|
2859
3045
|
}))
|
|
2860
3046
|
return false;
|
|
2861
|
-
await
|
|
3047
|
+
await rm(target, { recursive: true, force: true });
|
|
2862
3048
|
return true;
|
|
2863
3049
|
}
|
|
2864
|
-
async function
|
|
2865
|
-
const config =
|
|
2866
|
-
const configPath =
|
|
2867
|
-
if (await
|
|
3050
|
+
async function applyDeploymentSelection(repositoryRoot, projectName, selection) {
|
|
3051
|
+
const config = createDeploymentConfig(projectName, selection);
|
|
3052
|
+
const configPath = join3(repositoryRoot, "deployment/config.json");
|
|
3053
|
+
if (await stat2(configPath).catch(() => {
|
|
2868
3054
|
return;
|
|
2869
3055
|
})) {
|
|
2870
3056
|
throw new Error("Deployment is already locked. Restore a fresh release to change providers.");
|
|
2871
3057
|
}
|
|
2872
|
-
await
|
|
2873
|
-
await
|
|
3058
|
+
await mkdir2(join3(repositoryRoot, "deployment"), { recursive: true });
|
|
3059
|
+
await writeFile2(configPath, `${JSON.stringify(config, null, 2)}
|
|
2874
3060
|
`);
|
|
2875
|
-
await
|
|
3061
|
+
await writeFile2(join3(repositoryRoot, "deployment/README.md"), deploymentReadme(config));
|
|
2876
3062
|
const removed = [];
|
|
2877
|
-
for (const cloud of
|
|
3063
|
+
for (const cloud of deploymentClouds) {
|
|
2878
3064
|
if (cloud === selection.cloud)
|
|
2879
3065
|
continue;
|
|
2880
3066
|
for (const relativePath of [
|
|
@@ -2884,7 +3070,7 @@ async function applyDeploymentSelection2(repositoryRoot, projectName, selection)
|
|
|
2884
3070
|
`scripts/deploy/release-${cloud}.sh`,
|
|
2885
3071
|
`scripts/deploy/frontend-${cloud}.sh`
|
|
2886
3072
|
]) {
|
|
2887
|
-
if (await
|
|
3073
|
+
if (await removeIfPresent(join3(repositoryRoot, relativePath)))
|
|
2888
3074
|
removed.push(relativePath);
|
|
2889
3075
|
}
|
|
2890
3076
|
}
|
|
@@ -2900,531 +3086,673 @@ async function applyDeploymentSelection2(repositoryRoot, projectName, selection)
|
|
|
2900
3086
|
for (const adapter of adapters) {
|
|
2901
3087
|
if (adapter.target === selectedAdapter)
|
|
2902
3088
|
continue;
|
|
2903
|
-
if (await
|
|
3089
|
+
if (await removeIfPresent(join3(repositoryRoot, adapter.path)))
|
|
2904
3090
|
removed.push(adapter.path);
|
|
2905
3091
|
}
|
|
2906
|
-
if (selection.frontendTarget !== "native") {
|
|
2907
|
-
const nativeScript = `scripts/deploy/frontend-${selection.cloud}.sh`;
|
|
2908
|
-
if (await
|
|
2909
|
-
removed.push(nativeScript);
|
|
2910
|
-
}
|
|
2911
|
-
return removed;
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
// src/scaffold.ts
|
|
2915
|
-
import { execFile } from "node:child_process";
|
|
2916
|
-
import { createHash } from "node:crypto";
|
|
2917
|
-
import { cp, lstat, mkdir as mkdir4, mkdtemp, readFile as readFile5, readdir, rm as rm4, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
|
|
2918
|
-
import { tmpdir } from "node:os";
|
|
2919
|
-
import { basename, dirname as dirname2, join as join7, relative, resolve as resolve3 } from "node:path";
|
|
2920
|
-
import { promisify } from "node:util";
|
|
2921
|
-
|
|
2922
|
-
// src/delivery.ts
|
|
2923
|
-
import { createWriteStream } from "node:fs";
|
|
2924
|
-
import { rm as rm3 } from "node:fs/promises";
|
|
2925
|
-
import { join as join6 } from "node:path";
|
|
2926
|
-
import { Readable, Transform } from "node:stream";
|
|
2927
|
-
import { pipeline } from "node:stream/promises";
|
|
2928
|
-
var RELEASE_TAG_PATTERN2 = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
|
|
2929
|
-
var INDEX_TIMEOUT_MS = 15000;
|
|
2930
|
-
var DOWNLOAD_TIMEOUT_MS = 300000;
|
|
2931
|
-
function deliveryBaseUrl(environment = process.env) {
|
|
2932
|
-
const configured = environment[DELIVERY_URL_VARIABLE]?.trim();
|
|
2933
|
-
return (configured || DELIVERY_BASE_URL).replace(/\/+$/, "");
|
|
2934
|
-
}
|
|
2935
|
-
function releaseAssetNames(releaseTag) {
|
|
2936
|
-
const artifact = `foundation-source-${releaseTag}.tar.gz`;
|
|
2937
|
-
return {
|
|
2938
|
-
artifact,
|
|
2939
|
-
checksum: `${artifact}.sha256`,
|
|
2940
|
-
manifest: `foundation-source-${releaseTag}.manifest.json`
|
|
2941
|
-
};
|
|
2942
|
-
}
|
|
2943
|
-
function describeFailure(status, subject) {
|
|
2944
|
-
if (status === 401) {
|
|
2945
|
-
return "That licence key was not accepted. Copy it exactly from your purchase receipt or the customer portal (polar.sh).";
|
|
2946
|
-
}
|
|
2947
|
-
if (status === 429) {
|
|
2948
|
-
return "Too many download requests for this licence key. Wait a minute and try again.";
|
|
2949
|
-
}
|
|
2950
|
-
if (status === 404)
|
|
2951
|
-
return `${subject} is not available for download.`;
|
|
2952
|
-
if (status >= 500) {
|
|
2953
|
-
return "Source delivery is temporarily unavailable. Wait a few minutes and try again.";
|
|
2954
|
-
}
|
|
2955
|
-
return `Source delivery refused the request (HTTP ${status}).`;
|
|
2956
|
-
}
|
|
2957
|
-
async function requestDelivery(url, init, subject) {
|
|
2958
|
-
let response;
|
|
2959
|
-
try {
|
|
2960
|
-
response = await fetch(url, init);
|
|
2961
|
-
} catch {
|
|
2962
|
-
throw new Error("Could not reach source delivery. Check your network connection and try again.");
|
|
2963
|
-
}
|
|
2964
|
-
if (!response.ok)
|
|
2965
|
-
throw new Error(describeFailure(response.status, subject));
|
|
2966
|
-
return response;
|
|
2967
|
-
}
|
|
2968
|
-
async function resolveRelease(licenseKey, baseUrl = deliveryBaseUrl()) {
|
|
2969
|
-
const response = await requestDelivery(`${baseUrl}/v1/releases`, {
|
|
2970
|
-
method: "POST",
|
|
2971
|
-
headers: { "content-type": "application/json" },
|
|
2972
|
-
body: JSON.stringify({ key: licenseKey }),
|
|
2973
|
-
signal: AbortSignal.timeout(INDEX_TIMEOUT_MS)
|
|
2974
|
-
}, "The release index");
|
|
2975
|
-
let index;
|
|
2976
|
-
try {
|
|
2977
|
-
index = await response.json();
|
|
2978
|
-
} catch {
|
|
2979
|
-
index = undefined;
|
|
2980
|
-
}
|
|
2981
|
-
if (!index || !RELEASE_TAG_PATTERN2.test(index.latest) || !Array.isArray(index.versions)) {
|
|
2982
|
-
throw new Error(`Source delivery returned an unreadable release index.`);
|
|
3092
|
+
if (selection.frontendTarget !== "native") {
|
|
3093
|
+
const nativeScript = `scripts/deploy/frontend-${selection.cloud}.sh`;
|
|
3094
|
+
if (await removeIfPresent(join3(repositoryRoot, nativeScript)))
|
|
3095
|
+
removed.push(nativeScript);
|
|
2983
3096
|
}
|
|
2984
|
-
return
|
|
3097
|
+
return removed;
|
|
2985
3098
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
return;
|
|
2990
|
-
const total = Number(header);
|
|
2991
|
-
return Number.isSafeInteger(total) && total > 0 ? total : undefined;
|
|
3099
|
+
|
|
3100
|
+
// src/deployment-prompts.ts
|
|
3101
|
+
class DeploymentPromptCancelled extends Error {
|
|
2992
3102
|
}
|
|
2993
|
-
function
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
done(null, chunk);
|
|
3000
|
-
}
|
|
3001
|
-
});
|
|
3103
|
+
async function unwrap(value) {
|
|
3104
|
+
if (isCancel(value)) {
|
|
3105
|
+
cancel2("Deployment configuration was not changed.");
|
|
3106
|
+
throw new DeploymentPromptCancelled;
|
|
3107
|
+
}
|
|
3108
|
+
return value;
|
|
3002
3109
|
}
|
|
3003
|
-
async function
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
}, releaseTag);
|
|
3008
|
-
if (!response.body)
|
|
3009
|
-
throw new Error(`Source delivery returned an empty ${assetName}.`);
|
|
3010
|
-
const path = join6(directory, assetName);
|
|
3011
|
-
const source = Readable.fromWeb(response.body);
|
|
3012
|
-
try {
|
|
3013
|
-
if (onBytes) {
|
|
3014
|
-
await pipeline(source, countBytes(readContentLength(response), onBytes), createWriteStream(path));
|
|
3015
|
-
} else {
|
|
3016
|
-
await pipeline(source, createWriteStream(path));
|
|
3110
|
+
async function collectDeploymentSelection(defaults = {}, nonInteractive = false) {
|
|
3111
|
+
if (nonInteractive) {
|
|
3112
|
+
if (!defaults.cloud || !defaults.apiDomain || !defaults.frontendDomain) {
|
|
3113
|
+
throw new Error("Non-interactive deployment setup requires --cloud, --api-domain, and --frontend-domain.");
|
|
3017
3114
|
}
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3115
|
+
return {
|
|
3116
|
+
cloud: defaults.cloud,
|
|
3117
|
+
storageProvider: defaults.storageProvider ?? defaultStorageProviders[defaults.cloud],
|
|
3118
|
+
frontendTarget: defaults.frontendTarget ?? "native",
|
|
3119
|
+
environment: defaults.environment ?? "production",
|
|
3120
|
+
region: defaults.region ?? defaultRegions[defaults.cloud],
|
|
3121
|
+
apiDomain: defaults.apiDomain,
|
|
3122
|
+
frontendDomain: defaults.frontendDomain,
|
|
3123
|
+
haEnabled: defaults.haEnabled ?? false
|
|
3124
|
+
};
|
|
3021
3125
|
}
|
|
3022
|
-
|
|
3126
|
+
const cloud = defaults.cloud ?? parseDeploymentCloud(await unwrap(await select({
|
|
3127
|
+
message: "Backend cloud",
|
|
3128
|
+
options: [
|
|
3129
|
+
{ value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL" },
|
|
3130
|
+
{ value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL" },
|
|
3131
|
+
{ value: "azure", label: "Azure", hint: "Container Apps + Flexible Server" }
|
|
3132
|
+
]
|
|
3133
|
+
})));
|
|
3134
|
+
const frontendTarget = defaults.frontendTarget ?? parseFrontendTarget(await unwrap(await select({
|
|
3135
|
+
message: "Frontend host",
|
|
3136
|
+
options: [
|
|
3137
|
+
{ value: "native", label: "Cloud native", hint: "Managed with the backend" },
|
|
3138
|
+
{ value: "vercel", label: "Vercel SPA", hint: "Static frontend only" },
|
|
3139
|
+
{
|
|
3140
|
+
value: "cloudflare",
|
|
3141
|
+
label: "Cloudflare static assets",
|
|
3142
|
+
hint: "No API Worker"
|
|
3143
|
+
}
|
|
3144
|
+
]
|
|
3145
|
+
})));
|
|
3146
|
+
const storageProvider = defaults.storageProvider ?? (storageProvidersByCloud[cloud].length === 1 ? storageProvidersByCloud[cloud][0] ?? defaultStorageProviders[cloud] : parseStorageProvider(await unwrap(await select({
|
|
3147
|
+
message: "Object storage",
|
|
3148
|
+
options: storageProvidersByCloud[cloud].map((provider) => ({
|
|
3149
|
+
value: provider,
|
|
3150
|
+
label: provider === "s3" ? "Amazon S3" : provider === "gcs" ? "Google Cloud Storage" : "Cloudflare R2",
|
|
3151
|
+
hint: provider === defaultStorageProviders[cloud] ? "Cloud-native default" : ""
|
|
3152
|
+
}))
|
|
3153
|
+
}))));
|
|
3154
|
+
const environment = defaults.environment ?? await unwrap(await text2({
|
|
3155
|
+
message: "Deployment environment",
|
|
3156
|
+
placeholder: "production",
|
|
3157
|
+
defaultValue: "production",
|
|
3158
|
+
validate: (value) => validateDeploymentEnvironment(value || "production")
|
|
3159
|
+
}));
|
|
3160
|
+
const region = defaults.region ?? await unwrap(await text2({
|
|
3161
|
+
message: `${cloud.toUpperCase()} region`,
|
|
3162
|
+
placeholder: defaultRegions[cloud],
|
|
3163
|
+
defaultValue: defaultRegions[cloud],
|
|
3164
|
+
validate: (value) => validateRegion(value || defaultRegions[cloud])
|
|
3165
|
+
}));
|
|
3166
|
+
const apiDomain = defaults.apiDomain ?? await unwrap(await text2({
|
|
3167
|
+
message: "Production API domain",
|
|
3168
|
+
placeholder: "api.example.com",
|
|
3169
|
+
validate: (value) => validateDomain(value ?? "")
|
|
3170
|
+
}));
|
|
3171
|
+
const frontendDomain = defaults.frontendDomain ?? await unwrap(await text2({
|
|
3172
|
+
message: "Production frontend domain",
|
|
3173
|
+
placeholder: "app.example.com",
|
|
3174
|
+
validate: (value) => validateDomain(value ?? "")
|
|
3175
|
+
}));
|
|
3176
|
+
const haEnabled = defaults.haEnabled ?? await unwrap(await confirm2({
|
|
3177
|
+
message: "Enable the higher-availability profile?",
|
|
3178
|
+
initialValue: false
|
|
3179
|
+
}));
|
|
3180
|
+
return {
|
|
3181
|
+
cloud,
|
|
3182
|
+
storageProvider,
|
|
3183
|
+
frontendTarget,
|
|
3184
|
+
environment,
|
|
3185
|
+
region,
|
|
3186
|
+
apiDomain,
|
|
3187
|
+
frontendDomain,
|
|
3188
|
+
haEnabled
|
|
3189
|
+
};
|
|
3023
3190
|
}
|
|
3024
3191
|
|
|
3025
|
-
// src/
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
{ path: "apps/server/email-templates/verify-email.html", occurrences: 5 },
|
|
3059
|
-
{ path: "apps/server/email-templates/reset-password.html", occurrences: 5 },
|
|
3060
|
-
{ path: "apps/server/email-templates/organization-invitation.html", occurrences: 3 },
|
|
3061
|
-
{ path: "apps/server/email-templates/welcome-email.html", occurrences: 8 }
|
|
3062
|
-
];
|
|
3063
|
-
function validateAppName2(value) {
|
|
3064
|
-
if (!value)
|
|
3065
|
-
return "App name is required.";
|
|
3066
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
3067
|
-
return "App name must be kebab-case (lowercase letters, numbers, and single hyphens).";
|
|
3192
|
+
// src/deployment-cli.ts
|
|
3193
|
+
function takeValue2(args, index, flag) {
|
|
3194
|
+
const value = args[index + 1];
|
|
3195
|
+
if (!value || value.startsWith("--"))
|
|
3196
|
+
throw new Error(`${flag} requires a value.`);
|
|
3197
|
+
return value;
|
|
3198
|
+
}
|
|
3199
|
+
function parseDeploymentArguments(argv) {
|
|
3200
|
+
const parsed = { yes: false };
|
|
3201
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
3202
|
+
const argument = argv[index];
|
|
3203
|
+
if (argument === "--cloud") {
|
|
3204
|
+
parsed.cloud = parseDeploymentCloud(takeValue2(argv, index++, argument));
|
|
3205
|
+
} else if (argument === "--frontend") {
|
|
3206
|
+
parsed.frontendTarget = parseFrontendTarget(takeValue2(argv, index++, argument));
|
|
3207
|
+
} else if (argument === "--storage") {
|
|
3208
|
+
parsed.storageProvider = parseStorageProvider(takeValue2(argv, index++, argument));
|
|
3209
|
+
} else if (argument === "--environment") {
|
|
3210
|
+
parsed.environment = takeValue2(argv, index++, argument);
|
|
3211
|
+
} else if (argument === "--region") {
|
|
3212
|
+
parsed.region = takeValue2(argv, index++, argument);
|
|
3213
|
+
} else if (argument === "--api-domain") {
|
|
3214
|
+
parsed.apiDomain = takeValue2(argv, index++, argument);
|
|
3215
|
+
} else if (argument === "--frontend-domain") {
|
|
3216
|
+
parsed.frontendDomain = takeValue2(argv, index++, argument);
|
|
3217
|
+
} else if (argument === "--ha")
|
|
3218
|
+
parsed.haEnabled = true;
|
|
3219
|
+
else if (argument === "--no-ha")
|
|
3220
|
+
parsed.haEnabled = false;
|
|
3221
|
+
else if (argument === "--yes" || argument === "-y")
|
|
3222
|
+
parsed.yes = true;
|
|
3223
|
+
else
|
|
3224
|
+
throw new Error(`Unknown deployment option: ${argument}`);
|
|
3068
3225
|
}
|
|
3069
|
-
return;
|
|
3226
|
+
return parsed;
|
|
3227
|
+
}
|
|
3228
|
+
async function runDeploymentConfigure(argv, productName, repositoryRoot = process.cwd()) {
|
|
3229
|
+
const parsed = parseDeploymentArguments(argv);
|
|
3230
|
+
const root = resolve2(repositoryRoot);
|
|
3231
|
+
const rootPackage = JSON.parse(await readFile3(`${root}/package.json`, "utf8"));
|
|
3232
|
+
if (!rootPackage.name)
|
|
3233
|
+
throw new Error("Root package name is required for deployment setup.");
|
|
3234
|
+
intro2(`${productName} deployment setup`);
|
|
3235
|
+
try {
|
|
3236
|
+
const selection = await collectDeploymentSelection(parsed, parsed.yes);
|
|
3237
|
+
const removed = await applyDeploymentSelection(root, rootPackage.name, selection);
|
|
3238
|
+
log2.success(`${selection.cloud.toUpperCase()} + ${selection.storageProvider} + ${selection.frontendTarget} is locked for ${selection.environment}.`);
|
|
3239
|
+
log2.info(`${removed.length} unused deployment paths removed.`);
|
|
3240
|
+
log2.step("Next");
|
|
3241
|
+
log2.message(`Review deployment/config.json
|
|
3242
|
+
Configure the protected GitHub environment
|
|
3243
|
+
Dispatch .github/workflows/deploy-cloud.yml`);
|
|
3244
|
+
outro2("Deployment configuration is ready to commit.");
|
|
3245
|
+
} catch (error) {
|
|
3246
|
+
if (error instanceof DeploymentPromptCancelled)
|
|
3247
|
+
return;
|
|
3248
|
+
throw error;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
// src/runtime.ts
|
|
3253
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3254
|
+
import { accessSync as accessSync2, constants as constants2, realpathSync as realpathSync2 } from "node:fs";
|
|
3255
|
+
import { delimiter as delimiter2, join as join4, resolve as resolvePath2 } from "node:path";
|
|
3256
|
+
import { fileURLToPath } from "node:url";
|
|
3257
|
+
function runCaptured2(command, options) {
|
|
3258
|
+
const [file, ...args] = command;
|
|
3259
|
+
if (!file)
|
|
3260
|
+
throw new Error("runCaptured requires a command");
|
|
3261
|
+
return new Promise((resolve, reject) => {
|
|
3262
|
+
const child = spawn2(file, args, {
|
|
3263
|
+
cwd: options.cwd,
|
|
3264
|
+
env: options.env ?? process.env,
|
|
3265
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3266
|
+
});
|
|
3267
|
+
let stdout = "";
|
|
3268
|
+
let stderr = "";
|
|
3269
|
+
child.stdout.setEncoding("utf8");
|
|
3270
|
+
child.stderr.setEncoding("utf8");
|
|
3271
|
+
child.stdout.on("data", (chunk) => {
|
|
3272
|
+
stdout += chunk;
|
|
3273
|
+
});
|
|
3274
|
+
child.stderr.on("data", (chunk) => {
|
|
3275
|
+
stderr += chunk;
|
|
3276
|
+
});
|
|
3277
|
+
child.on("error", reject);
|
|
3278
|
+
child.on("close", (code, signal) => {
|
|
3279
|
+
resolve({ exitCode: code ?? (signal ? 1 : 0), stdout, stderr });
|
|
3280
|
+
});
|
|
3281
|
+
});
|
|
3070
3282
|
}
|
|
3071
|
-
function
|
|
3072
|
-
const
|
|
3073
|
-
if (
|
|
3074
|
-
throw new Error("
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3283
|
+
function runInherited(command, options) {
|
|
3284
|
+
const [file, ...args] = command;
|
|
3285
|
+
if (!file)
|
|
3286
|
+
throw new Error("runInherited requires a command");
|
|
3287
|
+
return new Promise((resolve, reject) => {
|
|
3288
|
+
const child = spawn2(file, args, {
|
|
3289
|
+
cwd: options.cwd,
|
|
3290
|
+
env: options.env ?? process.env,
|
|
3291
|
+
stdio: "inherit"
|
|
3292
|
+
});
|
|
3293
|
+
child.on("error", reject);
|
|
3294
|
+
child.on("close", (code, signal) => {
|
|
3295
|
+
resolve(code ?? (signal === "SIGINT" ? 130 : 1));
|
|
3296
|
+
});
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
function canonicalPath(path) {
|
|
3300
|
+
try {
|
|
3301
|
+
return realpathSync2(path);
|
|
3302
|
+
} catch {
|
|
3303
|
+
return resolvePath2(path);
|
|
3078
3304
|
}
|
|
3079
|
-
return scope;
|
|
3080
3305
|
}
|
|
3081
|
-
function
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
return "Brand name must be 48 characters or less.";
|
|
3090
|
-
if (!/^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u.test(value)) {
|
|
3091
|
-
return "Brand name may contain letters, numbers, spaces, periods, underscores, and hyphens.";
|
|
3306
|
+
function isEntrypoint(moduleUrl) {
|
|
3307
|
+
const entry = process.argv[1];
|
|
3308
|
+
if (!entry)
|
|
3309
|
+
return false;
|
|
3310
|
+
try {
|
|
3311
|
+
return canonicalPath(fileURLToPath(moduleUrl)) === canonicalPath(entry);
|
|
3312
|
+
} catch {
|
|
3313
|
+
return false;
|
|
3092
3314
|
}
|
|
3093
|
-
return;
|
|
3094
3315
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
const parts = content.split(search);
|
|
3099
|
-
return { content: parts.join(replacement), replacements: parts.length - 1 };
|
|
3316
|
+
|
|
3317
|
+
// src/deployment-prompts.ts
|
|
3318
|
+
class DeploymentPromptCancelled2 extends Error {
|
|
3100
3319
|
}
|
|
3101
|
-
function
|
|
3102
|
-
|
|
3320
|
+
async function unwrap2(value) {
|
|
3321
|
+
if (isCancel(value)) {
|
|
3322
|
+
cancel2("Deployment configuration was not changed.");
|
|
3323
|
+
throw new DeploymentPromptCancelled2;
|
|
3324
|
+
}
|
|
3325
|
+
return value;
|
|
3103
3326
|
}
|
|
3104
|
-
function
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3327
|
+
async function collectDeploymentSelection2(defaults = {}, nonInteractive = false) {
|
|
3328
|
+
if (nonInteractive) {
|
|
3329
|
+
if (!defaults.cloud || !defaults.apiDomain || !defaults.frontendDomain) {
|
|
3330
|
+
throw new Error("Non-interactive deployment setup requires --cloud, --api-domain, and --frontend-domain.");
|
|
3331
|
+
}
|
|
3332
|
+
return {
|
|
3333
|
+
cloud: defaults.cloud,
|
|
3334
|
+
storageProvider: defaults.storageProvider ?? defaultStorageProviders[defaults.cloud],
|
|
3335
|
+
frontendTarget: defaults.frontendTarget ?? "native",
|
|
3336
|
+
environment: defaults.environment ?? "production",
|
|
3337
|
+
region: defaults.region ?? defaultRegions[defaults.cloud],
|
|
3338
|
+
apiDomain: defaults.apiDomain,
|
|
3339
|
+
frontendDomain: defaults.frontendDomain,
|
|
3340
|
+
haEnabled: defaults.haEnabled ?? false
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
const cloud = defaults.cloud ?? parseDeploymentCloud(await unwrap2(await select({
|
|
3344
|
+
message: "Backend cloud",
|
|
3345
|
+
options: [
|
|
3346
|
+
{ value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL" },
|
|
3347
|
+
{ value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL" },
|
|
3348
|
+
{ value: "azure", label: "Azure", hint: "Container Apps + Flexible Server" }
|
|
3349
|
+
]
|
|
3350
|
+
})));
|
|
3351
|
+
const frontendTarget = defaults.frontendTarget ?? parseFrontendTarget(await unwrap2(await select({
|
|
3352
|
+
message: "Frontend host",
|
|
3353
|
+
options: [
|
|
3354
|
+
{ value: "native", label: "Cloud native", hint: "Managed with the backend" },
|
|
3355
|
+
{ value: "vercel", label: "Vercel SPA", hint: "Static frontend only" },
|
|
3356
|
+
{
|
|
3357
|
+
value: "cloudflare",
|
|
3358
|
+
label: "Cloudflare static assets",
|
|
3359
|
+
hint: "No API Worker"
|
|
3360
|
+
}
|
|
3361
|
+
]
|
|
3362
|
+
})));
|
|
3363
|
+
const storageProvider = defaults.storageProvider ?? (storageProvidersByCloud[cloud].length === 1 ? storageProvidersByCloud[cloud][0] ?? defaultStorageProviders[cloud] : parseStorageProvider(await unwrap2(await select({
|
|
3364
|
+
message: "Object storage",
|
|
3365
|
+
options: storageProvidersByCloud[cloud].map((provider) => ({
|
|
3366
|
+
value: provider,
|
|
3367
|
+
label: provider === "s3" ? "Amazon S3" : provider === "gcs" ? "Google Cloud Storage" : "Cloudflare R2",
|
|
3368
|
+
hint: provider === defaultStorageProviders[cloud] ? "Cloud-native default" : ""
|
|
3369
|
+
}))
|
|
3370
|
+
}))));
|
|
3371
|
+
const environment = defaults.environment ?? await unwrap2(await text2({
|
|
3372
|
+
message: "Deployment environment",
|
|
3373
|
+
placeholder: "production",
|
|
3374
|
+
defaultValue: "production",
|
|
3375
|
+
validate: (value) => validateDeploymentEnvironment(value || "production")
|
|
3376
|
+
}));
|
|
3377
|
+
const region = defaults.region ?? await unwrap2(await text2({
|
|
3378
|
+
message: `${cloud.toUpperCase()} region`,
|
|
3379
|
+
placeholder: defaultRegions[cloud],
|
|
3380
|
+
defaultValue: defaultRegions[cloud],
|
|
3381
|
+
validate: (value) => validateRegion(value || defaultRegions[cloud])
|
|
3382
|
+
}));
|
|
3383
|
+
const apiDomain = defaults.apiDomain ?? await unwrap2(await text2({
|
|
3384
|
+
message: "Production API domain",
|
|
3385
|
+
placeholder: "api.example.com",
|
|
3386
|
+
validate: (value) => validateDomain(value ?? "")
|
|
3387
|
+
}));
|
|
3388
|
+
const frontendDomain = defaults.frontendDomain ?? await unwrap2(await text2({
|
|
3389
|
+
message: "Production frontend domain",
|
|
3390
|
+
placeholder: "app.example.com",
|
|
3391
|
+
validate: (value) => validateDomain(value ?? "")
|
|
3392
|
+
}));
|
|
3393
|
+
const haEnabled = defaults.haEnabled ?? await unwrap2(await confirm2({
|
|
3394
|
+
message: "Enable the higher-availability profile?",
|
|
3395
|
+
initialValue: false
|
|
3396
|
+
}));
|
|
3113
3397
|
return {
|
|
3114
|
-
|
|
3115
|
-
|
|
3398
|
+
cloud,
|
|
3399
|
+
storageProvider,
|
|
3400
|
+
frontendTarget,
|
|
3401
|
+
environment,
|
|
3402
|
+
region,
|
|
3403
|
+
apiDomain,
|
|
3404
|
+
frontendDomain,
|
|
3405
|
+
haEnabled
|
|
3116
3406
|
};
|
|
3117
3407
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
return
|
|
3132
|
-
`;
|
|
3408
|
+
|
|
3409
|
+
// src/deployment.ts
|
|
3410
|
+
import { mkdir as mkdir3, readFile as readFile4, rm as rm2, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
|
|
3411
|
+
import { join as join5 } from "node:path";
|
|
3412
|
+
var deploymentClouds2 = ["aws", "gcp", "azure"];
|
|
3413
|
+
var frontendTargets2 = ["native", "vercel", "cloudflare"];
|
|
3414
|
+
var storageProviders2 = ["r2", "s3", "gcs"];
|
|
3415
|
+
var storageProvidersByCloud2 = {
|
|
3416
|
+
aws: ["s3", "r2"],
|
|
3417
|
+
gcp: ["gcs", "r2"],
|
|
3418
|
+
azure: ["r2"]
|
|
3419
|
+
};
|
|
3420
|
+
function isDeploymentCloud2(value) {
|
|
3421
|
+
return deploymentClouds2.some((cloud) => cloud === value);
|
|
3133
3422
|
}
|
|
3134
|
-
function
|
|
3135
|
-
return
|
|
3136
|
-
`).map((line) => {
|
|
3137
|
-
if (line.length <= width)
|
|
3138
|
-
return line;
|
|
3139
|
-
const match = line.match(/^(\s*)(import(?:\s+type)?\s+)\{([^}]+)\}(\s+from\s+.+)$/);
|
|
3140
|
-
if (!match)
|
|
3141
|
-
return line;
|
|
3142
|
-
const [, indentation = "", prefix = "", names = "", suffix = ""] = match;
|
|
3143
|
-
const members = names.split(",").map((name) => name.trim());
|
|
3144
|
-
return [
|
|
3145
|
-
`${indentation}${prefix}{`,
|
|
3146
|
-
...members.map((name) => `${indentation} ${name},`),
|
|
3147
|
-
`${indentation}}${suffix}`
|
|
3148
|
-
].join(`
|
|
3149
|
-
`);
|
|
3150
|
-
}).join(`
|
|
3151
|
-
`);
|
|
3423
|
+
function isFrontendTarget2(value) {
|
|
3424
|
+
return frontendTargets2.some((target) => target === value);
|
|
3152
3425
|
}
|
|
3153
|
-
function
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
}
|
|
3165
|
-
const paragraph = [];
|
|
3166
|
-
const originalParagraph = [];
|
|
3167
|
-
const indentation = line.slice(0, line.length - line.trimStart().length);
|
|
3168
|
-
while (index < lines.length) {
|
|
3169
|
-
const candidate = lines[index] ?? "";
|
|
3170
|
-
const candidateText = candidate.trim();
|
|
3171
|
-
if (!candidateText || candidateText.startsWith("<") || candidateText.startsWith("{"))
|
|
3172
|
-
break;
|
|
3173
|
-
paragraph.push(candidateText);
|
|
3174
|
-
originalParagraph.push(candidate);
|
|
3175
|
-
index += 1;
|
|
3176
|
-
}
|
|
3177
|
-
const joined = paragraph.join(" ");
|
|
3178
|
-
if (!joined.includes(brandName)) {
|
|
3179
|
-
output.push(...originalParagraph);
|
|
3180
|
-
continue;
|
|
3181
|
-
}
|
|
3182
|
-
let current = indentation;
|
|
3183
|
-
for (const word of joined.split(/\s+/)) {
|
|
3184
|
-
const candidate = current === indentation ? `${indentation}${word}` : `${current} ${word}`;
|
|
3185
|
-
if (candidate.length > width && current !== indentation) {
|
|
3186
|
-
output.push(current);
|
|
3187
|
-
current = `${indentation}${word}`;
|
|
3188
|
-
} else
|
|
3189
|
-
current = candidate;
|
|
3190
|
-
}
|
|
3191
|
-
output.push(current);
|
|
3426
|
+
function isStorageProvider2(value) {
|
|
3427
|
+
return storageProviders2.some((provider) => provider === value);
|
|
3428
|
+
}
|
|
3429
|
+
function parseDeploymentCloud2(value) {
|
|
3430
|
+
if (!isDeploymentCloud2(value))
|
|
3431
|
+
throw new Error("Cloud must be aws, gcp, or azure.");
|
|
3432
|
+
return value;
|
|
3433
|
+
}
|
|
3434
|
+
function parseFrontendTarget2(value) {
|
|
3435
|
+
if (!isFrontendTarget2(value)) {
|
|
3436
|
+
throw new Error("Frontend target must be native, vercel, or cloudflare.");
|
|
3192
3437
|
}
|
|
3193
|
-
return
|
|
3194
|
-
`);
|
|
3438
|
+
return value;
|
|
3195
3439
|
}
|
|
3196
|
-
function
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
const parsed = JSON.parse(content);
|
|
3201
|
-
if (parsed.name === appName)
|
|
3202
|
-
return { content, replacements: 0 };
|
|
3203
|
-
parsed.name = appName;
|
|
3204
|
-
return { content: `${JSON.stringify(parsed, null, 2)}
|
|
3205
|
-
`, replacements: 1 };
|
|
3440
|
+
function parseStorageProvider2(value) {
|
|
3441
|
+
if (!isStorageProvider2(value))
|
|
3442
|
+
throw new Error("Storage provider must be r2, s3, or gcs.");
|
|
3443
|
+
return value;
|
|
3206
3444
|
}
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
if (error)
|
|
3211
|
-
throw new Error(error);
|
|
3212
|
-
const declaration = content.match(WORKER_NAME_DECLARATION)?.[0];
|
|
3213
|
-
if (!declaration) {
|
|
3214
|
-
throw new Error("Cloudflare Worker configuration is missing a name declaration.");
|
|
3445
|
+
function validateDeploymentEnvironment2(value) {
|
|
3446
|
+
if (!/^[a-z][a-z0-9-]{1,30}$/.test(value)) {
|
|
3447
|
+
return "Environment must use 2-31 lowercase letters, numbers, or hyphens.";
|
|
3215
3448
|
}
|
|
3216
|
-
|
|
3217
|
-
if (declaration === replacement)
|
|
3218
|
-
return { content, replacements: 0 };
|
|
3219
|
-
return { content: content.replace(WORKER_NAME_DECLARATION, replacement), replacements: 1 };
|
|
3449
|
+
return;
|
|
3220
3450
|
}
|
|
3221
|
-
function
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
throw new Error(`Agent instructions are missing the "${heading}" section.`);
|
|
3227
|
-
const end = content.indexOf(`
|
|
3228
|
-
## `, start + marker.length);
|
|
3229
|
-
if (end === -1)
|
|
3230
|
-
throw new Error(`Agent instructions cannot delimit the "${heading}" section.`);
|
|
3231
|
-
const sections = [content.slice(0, start).trim(), replacement.trim(), content.slice(end).trim()];
|
|
3232
|
-
return `${sections.filter(Boolean).join(`
|
|
3233
|
-
|
|
3234
|
-
`)}
|
|
3235
|
-
`;
|
|
3451
|
+
function validateRegion2(value) {
|
|
3452
|
+
if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(value)) {
|
|
3453
|
+
return "Region must use 2-32 lowercase letters, numbers, or hyphens.";
|
|
3454
|
+
}
|
|
3455
|
+
return;
|
|
3236
3456
|
}
|
|
3237
|
-
function
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
if (index === -1)
|
|
3243
|
-
throw new Error(`Agent instructions are missing expected text: ${fragment}`);
|
|
3244
|
-
paragraphs.splice(index, 1);
|
|
3245
|
-
return paragraphs.join(`
|
|
3246
|
-
|
|
3247
|
-
`);
|
|
3457
|
+
function validateDomain2(value) {
|
|
3458
|
+
if (value.length > 80 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value)) {
|
|
3459
|
+
return "Enter a lowercase hostname such as api.example.com, without a protocol or path.";
|
|
3460
|
+
}
|
|
3461
|
+
return;
|
|
3248
3462
|
}
|
|
3249
|
-
function
|
|
3250
|
-
if (
|
|
3251
|
-
|
|
3463
|
+
function validateDeploymentSelection2(selection) {
|
|
3464
|
+
if (!storageProvidersByCloud2[selection.cloud].includes(selection.storageProvider)) {
|
|
3465
|
+
throw new Error(`${selection.storageProvider} storage is not supported for ${selection.cloud} deployments.`);
|
|
3252
3466
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3467
|
+
const environmentError = validateDeploymentEnvironment2(selection.environment);
|
|
3468
|
+
if (environmentError)
|
|
3469
|
+
throw new Error(environmentError);
|
|
3470
|
+
const regionError = validateRegion2(selection.region);
|
|
3471
|
+
if (regionError)
|
|
3472
|
+
throw new Error(regionError);
|
|
3473
|
+
const apiError = validateDomain2(selection.apiDomain);
|
|
3474
|
+
if (apiError)
|
|
3475
|
+
throw new Error(`API domain: ${apiError}`);
|
|
3476
|
+
const frontendError = validateDomain2(selection.frontendDomain);
|
|
3477
|
+
if (frontendError)
|
|
3478
|
+
throw new Error(`Frontend domain: ${frontendError}`);
|
|
3479
|
+
if (selection.apiDomain === selection.frontendDomain) {
|
|
3480
|
+
throw new Error("API and frontend domains must be different hostnames.");
|
|
3481
|
+
}
|
|
3482
|
+
const apiSite = selection.apiDomain.split(".").slice(1).join(".");
|
|
3483
|
+
const frontendSite = selection.frontendDomain.split(".").slice(1).join(".");
|
|
3484
|
+
if (apiSite !== frontendSite) {
|
|
3485
|
+
throw new Error("API and frontend domains must be sibling hosts under the same site.");
|
|
3270
3486
|
}
|
|
3271
|
-
return rewritten;
|
|
3272
3487
|
}
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3488
|
+
function createDeploymentConfig2(projectName, selection) {
|
|
3489
|
+
validateDeploymentSelection2(selection);
|
|
3490
|
+
return {
|
|
3491
|
+
schema_version: 2,
|
|
3492
|
+
cloud: selection.cloud,
|
|
3493
|
+
storage_provider: selection.storageProvider,
|
|
3494
|
+
project_name: projectName,
|
|
3495
|
+
environment: selection.environment,
|
|
3496
|
+
region: selection.region,
|
|
3497
|
+
frontend_target: selection.frontendTarget,
|
|
3498
|
+
external_frontend_origin: selection.frontendTarget === "native" ? null : `https://${selection.frontendDomain}`,
|
|
3499
|
+
api_domain: selection.apiDomain,
|
|
3500
|
+
frontend_domain: selection.frontendDomain,
|
|
3501
|
+
ha_enabled: selection.haEnabled
|
|
3502
|
+
};
|
|
3278
3503
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
const
|
|
3282
|
-
const
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3504
|
+
function deploymentReadme2(config) {
|
|
3505
|
+
const identity = config.cloud === "aws" ? "`AWS_ROLE_ARN` secret" : config.cloud === "gcp" ? "`GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT` secrets" : "`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID` secrets";
|
|
3506
|
+
const providerInputs = config.cloud === "aws" ? "Route 53 zone or certificate identifiers and runtime Secrets Manager ARNs" : config.cloud === "gcp" ? "GCP project, Cloud DNS zone, and runtime Secret Manager names" : "Azure DNS zone ID and runtime Key Vault secret identifiers";
|
|
3507
|
+
const storageSetup = config.storage_provider === "r2" ? `Create private staging and public Cloudflare R2 buckets, then store the required storage
|
|
3508
|
+
variables in the runtime secret container.` : `${config.storage_provider.toUpperCase()} application buckets, lifecycle policy, CORS, public access, and workload permissions are
|
|
3509
|
+
managed by the selected OpenTofu root.`;
|
|
3510
|
+
return `# Deployment setup
|
|
3511
|
+
|
|
3512
|
+
The deployment target is locked:
|
|
3513
|
+
|
|
3514
|
+
- backend: **${config.cloud.toUpperCase()}**;
|
|
3515
|
+
- storage: **${config.storage_provider}**;
|
|
3516
|
+
- frontend: **${config.frontend_target}**;
|
|
3517
|
+
- environment: **${config.environment}**.
|
|
3518
|
+
|
|
3519
|
+
## Protected GitHub environment
|
|
3520
|
+
|
|
3521
|
+
Create a GitHub environment named \`${config.environment}\` and configure:
|
|
3522
|
+
|
|
3523
|
+
- ${identity};
|
|
3524
|
+
- a \`BACKEND_HCL\` secret containing the selected remote-state backend configuration;
|
|
3525
|
+
- a \`TFVARS_JSON\` secret containing only:
|
|
3526
|
+
- ${providerInputs};
|
|
3527
|
+
- \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a
|
|
3528
|
+
public marketing site.
|
|
3529
|
+
|
|
3530
|
+
## Cloud preparation
|
|
3531
|
+
|
|
3532
|
+
1. Run \`infra/bootstrap/${config.cloud}\` once with an administrator identity.
|
|
3533
|
+
2. Configure OIDC trust for this repository and the \`${config.environment}\` GitHub environment.
|
|
3534
|
+
3. Create the DNS zone or supply the existing zone identifier required by the selected root.
|
|
3535
|
+
4. ${storageSetup}
|
|
3536
|
+
5. Create application secret entries for Better Auth and enabled email, billing, monitoring, OAuth,
|
|
3537
|
+
and credential-based providers.
|
|
3538
|
+
6. Put only secret identifiers in \`TFVARS_JSON\`; write secret values directly through the cloud
|
|
3539
|
+
provider.
|
|
3540
|
+
7. Dispatch \`.github/workflows/deploy-cloud.yml\` with:
|
|
3541
|
+
- cloud: \`${config.cloud}\`;
|
|
3542
|
+
- environment: \`${config.environment}\`;
|
|
3543
|
+
- frontend: \`${config.frontend_target}\`.
|
|
3544
|
+
|
|
3545
|
+
The workflow creates the registry first, pushes an immutable image, provisions the API with public
|
|
3546
|
+
traffic disabled, runs migrations, and admits traffic only after migration succeeds.
|
|
3547
|
+
|
|
3548
|
+
The provider lock is deliberate. Restore the deployment directories from a fresh licensed release
|
|
3549
|
+
before changing cloud providers.
|
|
3550
|
+
`;
|
|
3551
|
+
}
|
|
3552
|
+
async function removeIfPresent2(target) {
|
|
3553
|
+
if (!await stat3(target).catch(() => {
|
|
3554
|
+
return;
|
|
3555
|
+
}))
|
|
3556
|
+
return false;
|
|
3557
|
+
await rm2(target, { recursive: true, force: true });
|
|
3558
|
+
return true;
|
|
3559
|
+
}
|
|
3560
|
+
async function applyDeploymentSelection2(repositoryRoot, projectName, selection) {
|
|
3561
|
+
const config = createDeploymentConfig2(projectName, selection);
|
|
3562
|
+
const configPath = join5(repositoryRoot, "deployment/config.json");
|
|
3563
|
+
if (await stat3(configPath).catch(() => {
|
|
3564
|
+
return;
|
|
3565
|
+
})) {
|
|
3566
|
+
throw new Error("Deployment is already locked. Restore a fresh release to change providers.");
|
|
3567
|
+
}
|
|
3568
|
+
await mkdir3(join5(repositoryRoot, "deployment"), { recursive: true });
|
|
3569
|
+
await writeFile3(configPath, `${JSON.stringify(config, null, 2)}
|
|
3570
|
+
`);
|
|
3571
|
+
await writeFile3(join5(repositoryRoot, "deployment/README.md"), deploymentReadme2(config));
|
|
3572
|
+
const removed = [];
|
|
3573
|
+
for (const cloud of deploymentClouds2) {
|
|
3574
|
+
if (cloud === selection.cloud)
|
|
3301
3575
|
continue;
|
|
3576
|
+
for (const relativePath of [
|
|
3577
|
+
`infra/bootstrap/${cloud}`,
|
|
3578
|
+
`infra/modules/${cloud}`,
|
|
3579
|
+
`infra/roots/${cloud}`,
|
|
3580
|
+
`scripts/deploy/release-${cloud}.sh`,
|
|
3581
|
+
`scripts/deploy/frontend-${cloud}.sh`
|
|
3582
|
+
]) {
|
|
3583
|
+
if (await removeIfPresent2(join5(repositoryRoot, relativePath)))
|
|
3584
|
+
removed.push(relativePath);
|
|
3302
3585
|
}
|
|
3303
|
-
rewritten = rewritten.replace(declaration, `export const ${name} = '${value}'`);
|
|
3304
3586
|
}
|
|
3305
|
-
const
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3587
|
+
const adapters = [
|
|
3588
|
+
{ target: "vercel", path: "apps/web/vercel.json" },
|
|
3589
|
+
{ target: "cloudflare", path: "apps/web/wrangler.toml" },
|
|
3590
|
+
{
|
|
3591
|
+
target: "azure-native",
|
|
3592
|
+
path: "apps/web/public/staticwebapp.config.json"
|
|
3593
|
+
}
|
|
3594
|
+
];
|
|
3595
|
+
const selectedAdapter = selection.frontendTarget === "native" ? selection.cloud === "azure" ? "azure-native" : undefined : selection.frontendTarget;
|
|
3596
|
+
for (const adapter of adapters) {
|
|
3597
|
+
if (adapter.target === selectedAdapter)
|
|
3598
|
+
continue;
|
|
3599
|
+
if (await removeIfPresent2(join5(repositoryRoot, adapter.path)))
|
|
3600
|
+
removed.push(adapter.path);
|
|
3316
3601
|
}
|
|
3317
|
-
|
|
3602
|
+
if (selection.frontendTarget !== "native") {
|
|
3603
|
+
const nativeScript = `scripts/deploy/frontend-${selection.cloud}.sh`;
|
|
3604
|
+
if (await removeIfPresent2(join5(repositoryRoot, nativeScript)))
|
|
3605
|
+
removed.push(nativeScript);
|
|
3606
|
+
}
|
|
3607
|
+
return removed;
|
|
3318
3608
|
}
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3609
|
+
|
|
3610
|
+
// src/scaffold.ts
|
|
3611
|
+
import { execFile } from "node:child_process";
|
|
3612
|
+
import { createHash } from "node:crypto";
|
|
3613
|
+
import {
|
|
3614
|
+
cp,
|
|
3615
|
+
lstat,
|
|
3616
|
+
mkdir as mkdir4,
|
|
3617
|
+
mkdtemp,
|
|
3618
|
+
readFile as readFile5,
|
|
3619
|
+
readdir,
|
|
3620
|
+
rename as rename2,
|
|
3621
|
+
rm as rm4,
|
|
3622
|
+
stat as stat4,
|
|
3623
|
+
writeFile as writeFile4
|
|
3624
|
+
} from "node:fs/promises";
|
|
3625
|
+
import { tmpdir } from "node:os";
|
|
3626
|
+
import { basename, dirname as dirname2, join as join7, relative, resolve as resolve3 } from "node:path";
|
|
3627
|
+
import { promisify } from "node:util";
|
|
3628
|
+
|
|
3629
|
+
// src/delivery.ts
|
|
3630
|
+
import { createWriteStream } from "node:fs";
|
|
3631
|
+
import { rm as rm3 } from "node:fs/promises";
|
|
3632
|
+
import { join as join6 } from "node:path";
|
|
3633
|
+
import { Readable, Transform } from "node:stream";
|
|
3634
|
+
import { pipeline } from "node:stream/promises";
|
|
3635
|
+
var RELEASE_TAG_PATTERN2 = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
|
|
3636
|
+
var INDEX_TIMEOUT_MS = 15000;
|
|
3637
|
+
var DOWNLOAD_TIMEOUT_MS = 300000;
|
|
3638
|
+
function deliveryBaseUrl(environment = process.env) {
|
|
3639
|
+
const configured = environment[DELIVERY_URL_VARIABLE]?.trim();
|
|
3640
|
+
return (configured || DELIVERY_BASE_URL).replace(/\/+$/, "");
|
|
3330
3641
|
}
|
|
3331
|
-
function
|
|
3332
|
-
if (
|
|
3333
|
-
return
|
|
3334
|
-
|
|
3335
|
-
if (/internal\/|landing\/|studio-owned/i.test(rewritten)) {
|
|
3336
|
-
throw new Error("Buyer Claude instructions still contain seller-only guidance.");
|
|
3337
|
-
}
|
|
3338
|
-
return rewritten;
|
|
3642
|
+
function entitledEditions(index) {
|
|
3643
|
+
if (!Array.isArray(index.editions))
|
|
3644
|
+
return [];
|
|
3645
|
+
return index.editions.filter((edition) => typeof edition === "string" && edition.trim());
|
|
3339
3646
|
}
|
|
3340
|
-
function
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3647
|
+
function entitledSelection(index) {
|
|
3648
|
+
const editions = entitledEditions(index);
|
|
3649
|
+
const namesBaseEdition = editions.includes("web") || editions.includes("start");
|
|
3650
|
+
return selectionFromEditions(namesBaseEdition ? editions : ["web", ...editions]);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
class EditionNotLicensedError extends Error {
|
|
3654
|
+
constructor(edition) {
|
|
3655
|
+
super(`Your licence does not include the ${edition} edition. Drop --${edition} to continue, or contact support if you purchased it.`);
|
|
3656
|
+
this.name = "EditionNotLicensedError";
|
|
3347
3657
|
}
|
|
3348
|
-
return rewritten;
|
|
3349
3658
|
}
|
|
3350
|
-
function
|
|
3351
|
-
if (!
|
|
3352
|
-
return
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
if (
|
|
3356
|
-
throw new
|
|
3659
|
+
function resolveEdition(edition, request) {
|
|
3660
|
+
if (!request.licensed)
|
|
3661
|
+
return request.requested ?? false;
|
|
3662
|
+
if (request.requested === false)
|
|
3663
|
+
return false;
|
|
3664
|
+
if (request.requested && !request.entitled)
|
|
3665
|
+
throw new EditionNotLicensedError(edition);
|
|
3666
|
+
return request.entitled;
|
|
3667
|
+
}
|
|
3668
|
+
function releaseAssetNames(releaseTag) {
|
|
3669
|
+
const artifact = `foundation-source-${releaseTag}.tar.gz`;
|
|
3670
|
+
return {
|
|
3671
|
+
artifact,
|
|
3672
|
+
checksum: `${artifact}.sha256`,
|
|
3673
|
+
manifest: `foundation-source-${releaseTag}.manifest.json`
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
function describeFailure(status, subject) {
|
|
3677
|
+
if (status === 401) {
|
|
3678
|
+
return "That licence key was not accepted. Copy it exactly from your purchase receipt or the customer portal (polar.sh).";
|
|
3357
3679
|
}
|
|
3358
|
-
|
|
3680
|
+
if (status === 429) {
|
|
3681
|
+
return "Too many download requests for this licence key. Wait a minute and try again.";
|
|
3682
|
+
}
|
|
3683
|
+
if (status === 404)
|
|
3684
|
+
return `${subject} is not available for download.`;
|
|
3685
|
+
if (status >= 500) {
|
|
3686
|
+
return "Source delivery is temporarily unavailable. Wait a few minutes and try again.";
|
|
3687
|
+
}
|
|
3688
|
+
return `Source delivery refused the request (HTTP ${status}).`;
|
|
3359
3689
|
}
|
|
3360
|
-
function
|
|
3361
|
-
|
|
3362
|
-
|
|
3690
|
+
async function requestDelivery(url, init, subject) {
|
|
3691
|
+
let response;
|
|
3692
|
+
try {
|
|
3693
|
+
response = await fetch(url, init);
|
|
3694
|
+
} catch {
|
|
3695
|
+
throw new Error("Could not reach source delivery. Check your network connection and try again.");
|
|
3363
3696
|
}
|
|
3364
|
-
if (
|
|
3365
|
-
throw new Error(
|
|
3697
|
+
if (!response.ok)
|
|
3698
|
+
throw new Error(describeFailure(response.status, subject));
|
|
3699
|
+
return response;
|
|
3700
|
+
}
|
|
3701
|
+
async function resolveRelease(licenseKey, baseUrl = deliveryBaseUrl()) {
|
|
3702
|
+
const response = await requestDelivery(`${baseUrl}/v1/releases`, {
|
|
3703
|
+
method: "POST",
|
|
3704
|
+
headers: { "content-type": "application/json" },
|
|
3705
|
+
body: JSON.stringify({ key: licenseKey }),
|
|
3706
|
+
signal: AbortSignal.timeout(INDEX_TIMEOUT_MS)
|
|
3707
|
+
}, "The release index");
|
|
3708
|
+
let index;
|
|
3709
|
+
try {
|
|
3710
|
+
index = await response.json();
|
|
3711
|
+
} catch {
|
|
3712
|
+
index = undefined;
|
|
3366
3713
|
}
|
|
3367
|
-
if (!
|
|
3368
|
-
throw new Error(
|
|
3714
|
+
if (!index || !RELEASE_TAG_PATTERN2.test(index.latest) || !Array.isArray(index.versions)) {
|
|
3715
|
+
throw new Error(`Source delivery returned an unreadable release index.`);
|
|
3369
3716
|
}
|
|
3370
|
-
|
|
3371
|
-
return example.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}`).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${secret}`);
|
|
3717
|
+
return index;
|
|
3372
3718
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3719
|
+
function readContentLength(response) {
|
|
3720
|
+
const header = response.headers.get("content-length");
|
|
3721
|
+
if (!header)
|
|
3722
|
+
return;
|
|
3723
|
+
const total = Number(header);
|
|
3724
|
+
return Number.isSafeInteger(total) && total > 0 ? total : undefined;
|
|
3376
3725
|
}
|
|
3377
|
-
function
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
if (basename.endsWith(".tfvars") || basename === "backend.hcl")
|
|
3408
|
-
return true;
|
|
3409
|
-
if (basename.includes(".tfstate") || /^gha-creds-.*\.json$/.test(basename))
|
|
3410
|
-
return true;
|
|
3411
|
-
if (normalized === "PLAN.md" || normalized === "comments.md")
|
|
3412
|
-
return true;
|
|
3413
|
-
if (normalized === "packages/create-app/src/cli-brand-contract.test.ts")
|
|
3414
|
-
return true;
|
|
3415
|
-
if (normalized === "packages/create-app/src/commercial-contract.test.ts")
|
|
3416
|
-
return true;
|
|
3417
|
-
if (normalized === "packages/create-app/LICENSE")
|
|
3418
|
-
return true;
|
|
3419
|
-
if (normalized === "packages/create-app/README.md")
|
|
3420
|
-
return true;
|
|
3421
|
-
if (normalized === "apps/server/server")
|
|
3422
|
-
return true;
|
|
3423
|
-
if (basename.endsWith(".tsbuildinfo"))
|
|
3424
|
-
return true;
|
|
3425
|
-
if (basename === ".env" || basename.startsWith(".env.") && basename !== ".env.example")
|
|
3426
|
-
return true;
|
|
3427
|
-
return false;
|
|
3726
|
+
function countBytes(total, onBytes) {
|
|
3727
|
+
let received = 0;
|
|
3728
|
+
return new Transform({
|
|
3729
|
+
transform(chunk, _encoding, done) {
|
|
3730
|
+
received += chunk.length;
|
|
3731
|
+
onBytes(received, total);
|
|
3732
|
+
done(null, chunk);
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
async function downloadAsset(licenseKey, releaseTag, assetName, directory, baseUrl = deliveryBaseUrl(), onBytes) {
|
|
3737
|
+
const response = await requestDelivery(`${baseUrl}/v1/source/${encodeURIComponent(releaseTag)}/${encodeURIComponent(assetName)}`, {
|
|
3738
|
+
headers: { authorization: `Bearer ${licenseKey}` },
|
|
3739
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
|
|
3740
|
+
}, releaseTag);
|
|
3741
|
+
if (!response.body)
|
|
3742
|
+
throw new Error(`Source delivery returned an empty ${assetName}.`);
|
|
3743
|
+
const path = join6(directory, assetName);
|
|
3744
|
+
const source = Readable.fromWeb(response.body);
|
|
3745
|
+
try {
|
|
3746
|
+
if (onBytes) {
|
|
3747
|
+
await pipeline(source, countBytes(readContentLength(response), onBytes), createWriteStream(path));
|
|
3748
|
+
} else {
|
|
3749
|
+
await pipeline(source, createWriteStream(path));
|
|
3750
|
+
}
|
|
3751
|
+
} catch {
|
|
3752
|
+
await rm3(path, { force: true });
|
|
3753
|
+
throw new Error(`Downloading ${assetName} was interrupted. Try again in a few minutes.`);
|
|
3754
|
+
}
|
|
3755
|
+
return path;
|
|
3428
3756
|
}
|
|
3429
3757
|
|
|
3430
3758
|
// src/scaffold.ts
|
|
@@ -3508,9 +3836,30 @@ function verifyReleaseMetadata(checksum, manifest, artifactName, digest, templat
|
|
|
3508
3836
|
throw new Error(`${CLI_NAME2} release manifest does not match the downloaded source artifact.`);
|
|
3509
3837
|
}
|
|
3510
3838
|
}
|
|
3511
|
-
|
|
3839
|
+
function chooseEditions(requested, entitled) {
|
|
3840
|
+
const choose = (edition, localDefault) => entitled ? resolveEdition(edition, {
|
|
3841
|
+
requested: requested[edition],
|
|
3842
|
+
entitled: entitled[edition],
|
|
3843
|
+
licensed: true
|
|
3844
|
+
}) : requested[edition] ?? localDefault;
|
|
3845
|
+
const selection = normalizeEditions({
|
|
3846
|
+
web: choose("web", true),
|
|
3847
|
+
mobile: choose("mobile", false),
|
|
3848
|
+
start: choose("start", false)
|
|
3849
|
+
});
|
|
3850
|
+
if (requested.web === false && selection.mobile) {
|
|
3851
|
+
throw new Error("The mobile edition ships with the web foundation it runs on. Drop --no-web, or add --no-mobile.");
|
|
3852
|
+
}
|
|
3853
|
+
if (selectedEditions(selection).length === 0) {
|
|
3854
|
+
throw new Error("Nothing to scaffold: every edition was excluded.");
|
|
3855
|
+
}
|
|
3856
|
+
return selection;
|
|
3857
|
+
}
|
|
3858
|
+
async function downloadLicensedTemplate(target, licenseKey, requested, sourceVersion, onDownloadProgress) {
|
|
3512
3859
|
const baseUrl = deliveryBaseUrl();
|
|
3513
|
-
const
|
|
3860
|
+
const index = await resolveRelease(licenseKey, baseUrl);
|
|
3861
|
+
const selection = chooseEditions(requested, entitledSelection(index));
|
|
3862
|
+
const releaseTag = sourceVersion ?? index.latest;
|
|
3514
3863
|
const { artifact, checksum: checksumName, manifest: manifestName } = releaseAssetNames(releaseTag);
|
|
3515
3864
|
const temporaryDirectory = await mkdtemp(join7(tmpdir(), `${CLI_NAME2}-`));
|
|
3516
3865
|
const extractedPath = join7(temporaryDirectory, "template");
|
|
@@ -3526,6 +3875,7 @@ async function downloadLicensedTemplate(target, licenseKey, sourceVersion, onDow
|
|
|
3526
3875
|
await mkdir4(extractedPath);
|
|
3527
3876
|
await execFileAsync("tar", ["-xzf", archivePath, "-C", extractedPath, "--strip-components=1"]);
|
|
3528
3877
|
await copyLocalTemplate(extractedPath, target);
|
|
3878
|
+
return selection;
|
|
3529
3879
|
} finally {
|
|
3530
3880
|
await rm4(temporaryDirectory, { recursive: true, force: true });
|
|
3531
3881
|
}
|
|
@@ -3575,8 +3925,10 @@ async function applyScopeRewrite(target, scope) {
|
|
|
3575
3925
|
}
|
|
3576
3926
|
async function applyBrandRewrite(target, brandName) {
|
|
3577
3927
|
const report = { files: 0, replacements: 0 };
|
|
3578
|
-
for (const entry of BRAND_MANIFEST) {
|
|
3928
|
+
for (const entry of [...BRAND_MANIFEST, ...START_BRAND_MANIFEST]) {
|
|
3579
3929
|
const path = join7(target, entry.path);
|
|
3930
|
+
if (!await exists(path))
|
|
3931
|
+
continue;
|
|
3580
3932
|
const replacements = await rewriteFile(path, (content) => {
|
|
3581
3933
|
const result = rewriteBrandContent(content, brandName);
|
|
3582
3934
|
if (result.replacements > 0 && path.endsWith(".tsx")) {
|
|
@@ -3590,6 +3942,105 @@ async function applyBrandRewrite(target, brandName) {
|
|
|
3590
3942
|
}
|
|
3591
3943
|
return report;
|
|
3592
3944
|
}
|
|
3945
|
+
async function rewriteJsonFile(path, rewrite) {
|
|
3946
|
+
await writeFile4(path, rewrite(await readFile5(path, "utf8")));
|
|
3947
|
+
}
|
|
3948
|
+
async function exists(path) {
|
|
3949
|
+
return Boolean(await lstat(path).catch(() => {
|
|
3950
|
+
return;
|
|
3951
|
+
}));
|
|
3952
|
+
}
|
|
3953
|
+
async function rewriteIfPresent(path, rewrite) {
|
|
3954
|
+
const content = await readFile5(path, "utf8").catch(() => {
|
|
3955
|
+
return;
|
|
3956
|
+
});
|
|
3957
|
+
if (content === undefined)
|
|
3958
|
+
return;
|
|
3959
|
+
const rewritten = rewrite(content);
|
|
3960
|
+
if (rewritten !== content)
|
|
3961
|
+
await writeFile4(path, rewritten);
|
|
3962
|
+
}
|
|
3963
|
+
async function markdownDocuments(target) {
|
|
3964
|
+
const pages = await readdir(join7(target, "docs"), { recursive: true }).catch(() => []);
|
|
3965
|
+
return [
|
|
3966
|
+
"AGENTS.md",
|
|
3967
|
+
...pages.filter((page) => page.endsWith(".md")).map((page) => join7("docs", page))
|
|
3968
|
+
];
|
|
3969
|
+
}
|
|
3970
|
+
async function applyMobileIdentity(target, appName, brandName, bundleIdentifier, appDomain) {
|
|
3971
|
+
const identity = deriveMobileIdentity(appName, brandName, bundleIdentifier, appDomain);
|
|
3972
|
+
await rewriteJsonFile(join7(target, "apps/mobile/app.json"), (content) => rewriteMobileAppConfig(content, identity));
|
|
3973
|
+
await rewriteFile(join7(target, "apps/server/.env.example"), (content) => rewriteMobileAppScheme(content, identity.scheme));
|
|
3974
|
+
}
|
|
3975
|
+
async function removeMobileEdition(target) {
|
|
3976
|
+
for (const path of MOBILE_EDITION_PATHS) {
|
|
3977
|
+
await rm4(join7(target, path), { recursive: true, force: true });
|
|
3978
|
+
}
|
|
3979
|
+
await rewriteIfPresent(join7(target, "package.json"), stripMobileRootScripts);
|
|
3980
|
+
await rewriteIfPresent(join7(target, "knip.json"), stripMobileKnipWorkspace);
|
|
3981
|
+
await rewriteIfPresent(join7(target, "docs/meta.json"), stripMobileDocsPage);
|
|
3982
|
+
for (const page of await markdownDocuments(target)) {
|
|
3983
|
+
await rewriteIfPresent(join7(target, page), stripMobileDocsReferences);
|
|
3984
|
+
}
|
|
3985
|
+
await rewriteIfPresent(join7(target, "apps/docs/lib/source.ts"), stripMobileDocsSource);
|
|
3986
|
+
await rewriteIfPresent(join7(target, "pnpm-lock.yaml"), (content) => pruneLockfileImporters(content, ["apps/mobile"]));
|
|
3987
|
+
}
|
|
3988
|
+
async function removeStartEdition(target) {
|
|
3989
|
+
for (const path of START_EDITION_PATHS) {
|
|
3990
|
+
await rm4(join7(target, path), { recursive: true, force: true });
|
|
3991
|
+
}
|
|
3992
|
+
await rewriteIfPresent(join7(target, "knip.json"), (content) => stripKnipWorkspaces(content, ["apps/start"]));
|
|
3993
|
+
for (const page of await markdownDocuments(target)) {
|
|
3994
|
+
await rewriteIfPresent(join7(target, page), stripStartReferences);
|
|
3995
|
+
}
|
|
3996
|
+
await rewriteIfPresent(join7(target, "pnpm-lock.yaml"), (content) => pruneLockfileImporters(content, ["apps/start"]));
|
|
3997
|
+
}
|
|
3998
|
+
async function pruneToKeptPaths(target, relativeDirectory, kept, removedWorkspaces) {
|
|
3999
|
+
for (const entry of await readdir(join7(target, relativeDirectory))) {
|
|
4000
|
+
const relativePath = relativeDirectory ? `${relativeDirectory}/${entry}` : entry;
|
|
4001
|
+
if (kept.has(relativePath))
|
|
4002
|
+
continue;
|
|
4003
|
+
if ([...kept].some((path) => path.startsWith(`${relativePath}/`))) {
|
|
4004
|
+
await pruneToKeptPaths(target, relativePath, kept, removedWorkspaces);
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
if (relativeDirectory === "apps" || relativeDirectory === "packages") {
|
|
4008
|
+
removedWorkspaces.push(relativePath);
|
|
4009
|
+
}
|
|
4010
|
+
await rm4(join7(target, relativePath), { recursive: true, force: true });
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
async function reduceToStartEdition(target) {
|
|
4014
|
+
const overlay = join7(target, START_EDITION_OVERLAY);
|
|
4015
|
+
const overlayPackage = await readFile5(join7(overlay, "package.json"), "utf8");
|
|
4016
|
+
const rootPackage = await readFile5(join7(target, "package.json"), "utf8");
|
|
4017
|
+
const removedWorkspaces = [];
|
|
4018
|
+
await pruneToKeptPaths(target, "", new Set(START_ONLY_KEPT_PATHS), removedWorkspaces);
|
|
4019
|
+
await cp(overlay, target, { recursive: true, force: true });
|
|
4020
|
+
await writeFile4(join7(target, "package.json"), startOnlyRootPackage(rootPackage, overlayPackage));
|
|
4021
|
+
for (const [overlayName, rootName] of Object.entries(START_ROOT_CONTRACTS)) {
|
|
4022
|
+
await rename2(join7(target, overlayName), join7(target, rootName));
|
|
4023
|
+
}
|
|
4024
|
+
await rm4(overlay, { recursive: true, force: true });
|
|
4025
|
+
await rewriteIfPresent(join7(target, "knip.json"), (content) => stripKnipWorkspaces(content, removedWorkspaces));
|
|
4026
|
+
await rewriteIfPresent(join7(target, "pnpm-lock.yaml"), (content) => pruneLockfileImporters(content, removedWorkspaces));
|
|
4027
|
+
}
|
|
4028
|
+
async function isStartOnlyTree(target) {
|
|
4029
|
+
return await exists(join7(target, "apps/start")) && !await exists(join7(target, "apps/server"));
|
|
4030
|
+
}
|
|
4031
|
+
async function assembleEditions(target, selection, { keepStartOverlay = false } = {}) {
|
|
4032
|
+
if (!selection.mobile)
|
|
4033
|
+
await removeMobileEdition(target);
|
|
4034
|
+
if (!selection.start)
|
|
4035
|
+
await removeStartEdition(target);
|
|
4036
|
+
if (!selection.web && !await isStartOnlyTree(target))
|
|
4037
|
+
await reduceToStartEdition(target);
|
|
4038
|
+
if (!keepStartOverlay)
|
|
4039
|
+
await rm4(join7(target, START_EDITION_OVERLAY), { recursive: true, force: true });
|
|
4040
|
+
}
|
|
4041
|
+
async function applyStartIdentity(target, appName) {
|
|
4042
|
+
await rewriteIfPresent(join7(target, "apps/start/wrangler.jsonc"), (content) => rewriteStartWorkerIdentifiers(content, appName));
|
|
4043
|
+
}
|
|
3593
4044
|
async function renameFrontendWorker(target, appName) {
|
|
3594
4045
|
const workerPath = join7(target, "apps/web/wrangler.toml");
|
|
3595
4046
|
if (!await stat4(workerPath).catch(() => {
|
|
@@ -3598,6 +4049,10 @@ async function renameFrontendWorker(target, appName) {
|
|
|
3598
4049
|
return;
|
|
3599
4050
|
await rewriteFile(workerPath, (content) => rewriteWorkerName(content, appName));
|
|
3600
4051
|
}
|
|
4052
|
+
async function generateStartEnvironment(target) {
|
|
4053
|
+
const example = await readFile5(join7(target, "apps/start/.dev.vars.example"), "utf8");
|
|
4054
|
+
await writeFile4(join7(target, "apps/start/.dev.vars"), generateStartDevVars(example));
|
|
4055
|
+
}
|
|
3601
4056
|
async function generateEnvironmentFiles(target, databasePort = 5432) {
|
|
3602
4057
|
const serverExample = await readFile5(join7(target, "apps/server/.env.example"), "utf8");
|
|
3603
4058
|
await writeFile4(join7(target, "apps/server/.env"), generateServerEnv(serverExample, undefined, databasePort));
|
|
@@ -3674,28 +4129,48 @@ async function scaffold(options) {
|
|
|
3674
4129
|
const target = resolve3(options.targetDirectory);
|
|
3675
4130
|
await assertEmptyTarget(target);
|
|
3676
4131
|
try {
|
|
3677
|
-
|
|
4132
|
+
const requested = {
|
|
4133
|
+
web: options.includeWeb,
|
|
4134
|
+
mobile: options.includeMobile,
|
|
4135
|
+
start: options.includeStart
|
|
4136
|
+
};
|
|
4137
|
+
let selection;
|
|
4138
|
+
if (options.from) {
|
|
4139
|
+
selection = chooseEditions(requested, undefined);
|
|
3678
4140
|
await copyLocalTemplate(options.from, target);
|
|
3679
|
-
else if (options.licenseKey) {
|
|
3680
|
-
await downloadLicensedTemplate(target, options.licenseKey, options.sourceVersion, options.onDownloadProgress);
|
|
4141
|
+
} else if (options.licenseKey) {
|
|
4142
|
+
selection = await downloadLicensedTemplate(target, options.licenseKey, requested, options.sourceVersion, options.onDownloadProgress);
|
|
3681
4143
|
} else {
|
|
3682
4144
|
throw new Error(`A local source path or a ${CLI_NAME2} licence key is required.`);
|
|
3683
4145
|
}
|
|
4146
|
+
await assembleEditions(target, selection);
|
|
4147
|
+
if (selection.mobile) {
|
|
4148
|
+
await applyMobileIdentity(target, options.appName, options.brandName, options.mobileBundleIdentifier, options.mobileAppDomain);
|
|
4149
|
+
}
|
|
4150
|
+
if (selection.start)
|
|
4151
|
+
await applyStartIdentity(target, options.appName);
|
|
3684
4152
|
const packagePath = join7(target, "package.json");
|
|
3685
4153
|
const packageReplacements = await rewriteFile(packagePath, (content) => rewriteRootPackageName(content, options.appName));
|
|
3686
4154
|
await renameFrontendWorker(target, options.appName);
|
|
3687
4155
|
const scope = await applyScopeRewrite(target, options.scope);
|
|
3688
|
-
|
|
3689
|
-
|
|
4156
|
+
if (selection.web) {
|
|
4157
|
+
await neutralizeAgentInstructions(target);
|
|
4158
|
+
await neutralizeSourceCli(target, options.scope, options.brandName);
|
|
4159
|
+
}
|
|
3690
4160
|
const brand = await applyBrandRewrite(target, options.brandName);
|
|
3691
|
-
|
|
4161
|
+
if (selection.web)
|
|
4162
|
+
await generateEnvironmentFiles(target, options.databasePort);
|
|
4163
|
+
if (selection.start)
|
|
4164
|
+
await generateStartEnvironment(target);
|
|
3692
4165
|
if (options.initializeGit)
|
|
3693
4166
|
await initializeRepository(target);
|
|
3694
4167
|
return {
|
|
3695
4168
|
targetDirectory: target,
|
|
3696
4169
|
packageName: { files: packageReplacements > 0 ? 1 : 0, replacements: packageReplacements },
|
|
3697
4170
|
scope,
|
|
3698
|
-
brand
|
|
4171
|
+
brand,
|
|
4172
|
+
editions: selectedEditions(selection),
|
|
4173
|
+
mobile: selection.mobile
|
|
3699
4174
|
};
|
|
3700
4175
|
} catch (error) {
|
|
3701
4176
|
if ((await stat4(target).catch(() => {
|
|
@@ -3723,6 +4198,15 @@ Options:
|
|
|
3723
4198
|
--license-key <key> Licence key from your purchase (or set ${LICENSE_KEY_VARIABLE})
|
|
3724
4199
|
--source-version <version> Pin an immutable release (default: the latest you are entitled to)
|
|
3725
4200
|
--from <path> Use a local Git checkout, tracked files only (maintainer/testing)
|
|
4201
|
+
--web / --no-web Include or exclude the web foundation: the React app and the API
|
|
4202
|
+
--mobile / --no-mobile Include or exclude the mobile edition, which ships with the web
|
|
4203
|
+
foundation it runs on
|
|
4204
|
+
--start-edition / --no-start-edition Include or exclude the TanStack Start edition
|
|
4205
|
+
Editions default to whatever your licence entitles you to;
|
|
4206
|
+
--from defaults to the web foundation alone
|
|
4207
|
+
--bundle-id <id> Reverse-DNS iOS/Android identifier (default: com.<app-name>.app)
|
|
4208
|
+
--app-domain <host> Host serving your web app, wired as an iOS/Android universal link
|
|
4209
|
+
so emailed sign-in and invite links open the app (default: none)
|
|
3726
4210
|
--scope <namespace> Advanced workspace namespace override (default: derived from brand)
|
|
3727
4211
|
--brand <name> Display name (default: derived from app name)
|
|
3728
4212
|
--yes Accept recommended defaults for omitted options
|
|
@@ -3766,7 +4250,23 @@ function parseArguments(argv) {
|
|
|
3766
4250
|
const argument = argv[index];
|
|
3767
4251
|
if (argument === "--from")
|
|
3768
4252
|
parsed.from = takeValue3(argv, index++, argument);
|
|
3769
|
-
else if (argument === "--
|
|
4253
|
+
else if (argument === "--web")
|
|
4254
|
+
parsed.includeWeb = true;
|
|
4255
|
+
else if (argument === "--no-web")
|
|
4256
|
+
parsed.includeWeb = false;
|
|
4257
|
+
else if (argument === "--start-edition")
|
|
4258
|
+
parsed.includeStart = true;
|
|
4259
|
+
else if (argument === "--no-start-edition")
|
|
4260
|
+
parsed.includeStart = false;
|
|
4261
|
+
else if (argument === "--mobile")
|
|
4262
|
+
parsed.includeMobile = true;
|
|
4263
|
+
else if (argument === "--no-mobile")
|
|
4264
|
+
parsed.includeMobile = false;
|
|
4265
|
+
else if (argument === "--bundle-id") {
|
|
4266
|
+
parsed.mobileBundleIdentifier = takeValue3(argv, index++, argument);
|
|
4267
|
+
} else if (argument === "--app-domain") {
|
|
4268
|
+
parsed.mobileAppDomain = takeValue3(argv, index++, argument);
|
|
4269
|
+
} else if (argument === "--license-key")
|
|
3770
4270
|
parsed.licenseKey = takeValue3(argv, index++, argument);
|
|
3771
4271
|
else if (argument === "--source-version") {
|
|
3772
4272
|
parsed.sourceVersion = normalizeSourceVersion(takeValue3(argv, index++, argument));
|
|
@@ -3916,7 +4416,7 @@ async function collectOptions(parsed) {
|
|
|
3916
4416
|
message: "Start local development now?",
|
|
3917
4417
|
initialValue: true
|
|
3918
4418
|
})));
|
|
3919
|
-
const setupDeployment = parsed.setupDeployment ?? (parsed.yes ? false : await unwrapPrompt(await confirm({
|
|
4419
|
+
const setupDeployment = parsed.setupDeployment ?? (parsed.yes || parsed.includeWeb === false ? false : await unwrapPrompt(await confirm({
|
|
3920
4420
|
message: "Set up cloud deployment now?",
|
|
3921
4421
|
initialValue: false
|
|
3922
4422
|
})));
|
|
@@ -3931,7 +4431,12 @@ async function collectOptions(parsed) {
|
|
|
3931
4431
|
initializeGit,
|
|
3932
4432
|
installDependencies,
|
|
3933
4433
|
startDevelopment,
|
|
3934
|
-
deployment
|
|
4434
|
+
deployment,
|
|
4435
|
+
includeWeb: parsed.includeWeb,
|
|
4436
|
+
includeMobile: parsed.includeMobile,
|
|
4437
|
+
includeStart: parsed.includeStart,
|
|
4438
|
+
mobileBundleIdentifier: parsed.mobileBundleIdentifier,
|
|
4439
|
+
mobileAppDomain: parsed.mobileAppDomain
|
|
3935
4440
|
};
|
|
3936
4441
|
}
|
|
3937
4442
|
async function readCliVersion() {
|
|
@@ -4096,7 +4601,7 @@ async function createApplication(options) {
|
|
|
4096
4601
|
const preparing = "Preparing your application";
|
|
4097
4602
|
const reporter = createScaffoldReporter(preparing, Boolean(options.licenseKey));
|
|
4098
4603
|
try {
|
|
4099
|
-
await runStep(preparing, "Application source prepared", () => scaffold({
|
|
4604
|
+
const report = await runStep(preparing, "Application source prepared", () => scaffold({
|
|
4100
4605
|
appName: options.appName,
|
|
4101
4606
|
scope: options.scope,
|
|
4102
4607
|
brandName: options.brandName,
|
|
@@ -4106,9 +4611,14 @@ async function createApplication(options) {
|
|
|
4106
4611
|
sourceVersion: options.sourceVersion,
|
|
4107
4612
|
initializeGit: options.initializeGit,
|
|
4108
4613
|
databasePort,
|
|
4109
|
-
onDownloadProgress: reporter.onDownloadProgress
|
|
4614
|
+
onDownloadProgress: reporter.onDownloadProgress,
|
|
4615
|
+
includeWeb: options.includeWeb,
|
|
4616
|
+
includeMobile: options.includeMobile,
|
|
4617
|
+
includeStart: options.includeStart,
|
|
4618
|
+
mobileBundleIdentifier: options.mobileBundleIdentifier,
|
|
4619
|
+
mobileAppDomain: options.mobileAppDomain
|
|
4110
4620
|
}), reporter.handle);
|
|
4111
|
-
if (options.deployment) {
|
|
4621
|
+
if (options.deployment && report.editions.includes("web")) {
|
|
4112
4622
|
await runStep("Configuring deployment", "Deployment target locked", () => applyDeploymentSelection2(staging, options.appName, options.deployment), spinner());
|
|
4113
4623
|
}
|
|
4114
4624
|
if (options.installDependencies) {
|
|
@@ -4118,19 +4628,20 @@ async function createApplication(options) {
|
|
|
4118
4628
|
return;
|
|
4119
4629
|
}))
|
|
4120
4630
|
await rmdir(target);
|
|
4121
|
-
await
|
|
4122
|
-
return { target, databasePort };
|
|
4631
|
+
await rename3(staging, target);
|
|
4632
|
+
return { target, databasePort, editions: report.editions };
|
|
4123
4633
|
} catch (error) {
|
|
4124
4634
|
await rm5(staging, { recursive: true, force: true });
|
|
4125
4635
|
throw error;
|
|
4126
4636
|
}
|
|
4127
4637
|
}
|
|
4128
|
-
function completionDetails(target, options, databasePort) {
|
|
4638
|
+
function completionDetails(target, options, databasePort, editions) {
|
|
4129
4639
|
const targetName = relative2(process.cwd(), target) || options.appName;
|
|
4130
4640
|
const install = options.installDependencies ? [] : ["vp install --frozen-lockfile"];
|
|
4641
|
+
const run = editions.includes("web") ? ["pnpm dev"] : ["pnpm db:migrate", "pnpm dev"];
|
|
4131
4642
|
return {
|
|
4132
4643
|
directory: `./${targetName}`,
|
|
4133
|
-
commands: [`cd ${targetName}`, ...install,
|
|
4644
|
+
commands: [`cd ${targetName}`, ...install, ...run].join(`
|
|
4134
4645
|
`),
|
|
4135
4646
|
database: `Postgres localhost:${databasePort}`
|
|
4136
4647
|
};
|
|
@@ -4166,14 +4677,28 @@ async function main() {
|
|
|
4166
4677
|
}
|
|
4167
4678
|
intro(`${suggestBrandName(CLI_NAME)} ${version} \xB7 production SaaS foundation`);
|
|
4168
4679
|
const options = await collectOptions(parsed);
|
|
4169
|
-
const { target, databasePort } = await createApplication(options);
|
|
4170
|
-
const completion = completionDetails(target, options, databasePort);
|
|
4680
|
+
const { target, databasePort, editions } = await createApplication(options);
|
|
4681
|
+
const completion = completionDetails(target, options, databasePort, editions);
|
|
4171
4682
|
log.success(`Created ${completion.directory}`);
|
|
4683
|
+
if (!editions.includes("web")) {
|
|
4684
|
+
log.step("Run locally");
|
|
4685
|
+
log.info("Start http://localhost:3003. apps/start/.dev.vars holds a fresh auth secret.\n Deploy with `pnpm deploy:worker`; see apps/start/docs/deploy.md.");
|
|
4686
|
+
outro(`${options.brandName} is ready.`);
|
|
4687
|
+
printRunnableCommands(completion.commands);
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
const mobile = editions.includes("mobile");
|
|
4172
4691
|
if (!options.startDevelopment) {
|
|
4173
4692
|
log.step("Run locally");
|
|
4174
4693
|
log.info(`Web http://localhost:5173
|
|
4175
4694
|
API http://localhost:8000
|
|
4176
4695
|
${completion.database}`);
|
|
4696
|
+
if (editions.includes("start")) {
|
|
4697
|
+
log.info("Start vp run --filter ./apps/start dev \u2014 http://localhost:3003. See apps/start/README.md.");
|
|
4698
|
+
}
|
|
4699
|
+
if (mobile) {
|
|
4700
|
+
log.info("Mobile pnpm dev:mobile \u2014 run `npx eas init` inside apps/mobile to claim your own\n Expo project before the first build. See docs/mobile.md.");
|
|
4701
|
+
}
|
|
4177
4702
|
if (options.deployment) {
|
|
4178
4703
|
log.info(`Deployment ${options.deployment.cloud.toUpperCase()} + ${options.deployment.storageProvider} + ${options.deployment.frontendTarget} locked. Review deployment/README.md.`);
|
|
4179
4704
|
} else {
|