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