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.
Files changed (2) hide show
  1. package/dist/cli.js +2445 -1845
  2. 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 rename2, rm as rm5, rmdir, stat as stat5 } from "fs/promises";
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 (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
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 (/\s/.test(scope)) {
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 (!/^@[a-z0-9][a-z0-9._-]*$/.test(scope)) {
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 (/\r|\n/.test(value))
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 (!/^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u.test(value)) {
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/delivery.ts
1304
- var RELEASE_TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
1305
- function validateLicenseKey(value) {
1306
- const key = value.trim();
1307
- if (!key)
1308
- return "Enter the licence key from your purchase receipt.";
1309
- if (/\s/.test(key))
1310
- return "A licence key does not contain spaces.";
1311
- if (!key.startsWith(LICENSE_KEY_PREFIX) || key.length === LICENSE_KEY_PREFIX.length) {
1312
- return `A ${CLI_NAME2} licence key starts with ${LICENSE_KEY_PREFIX}. Copy it from your purchase receipt or the customer portal.`;
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 readLicenseKeyFromEnvironment(environment = process.env) {
1317
- return environment[LICENSE_KEY_VARIABLE2]?.trim() || undefined;
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 normalizeSourceVersion(value) {
1320
- const trimmed = value.trim();
1321
- const releaseTag = trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
1322
- if (!RELEASE_TAG_PATTERN.test(releaseTag)) {
1323
- throw new Error(`Source version must be an immutable release such as 1.2.3: ${value}`);
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 releaseTag;
1423
+ return;
1326
1424
  }
1327
-
1328
- // node_modules/@clack/prompts/dist/index.mjs
1329
- import { styleText as styleText3, stripVTControlCharacters as stripVTControlCharacters2 } from "node:util";
1330
- import process$12 from "node:process";
1331
- var import_sisteransi3 = __toESM(require_src(), 1);
1332
- function isUnicodeSupported2() {
1333
- if (process$12.platform !== "win32") {
1334
- return process$12.env.TERM !== "linux";
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 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";
1466
+ return;
1337
1467
  }
1338
- var unicode2 = isUnicodeSupported2();
1339
- var isCI2 = () => process.env.CI === "true";
1340
- var unicodeOr2 = (o, e) => unicode2 ? o : e;
1341
- var S_STEP_ACTIVE2 = unicodeOr2("◆", "*");
1342
- var S_STEP_CANCEL2 = unicodeOr2("■", "x");
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
- var symbolBar = (o) => {
1382
- switch (o) {
1383
- case "initial":
1384
- case "active":
1385
- return styleText3("cyan", S_BAR2);
1386
- case "cancel":
1387
- return styleText3("red", S_BAR2);
1388
- case "error":
1389
- return styleText3("yellow", S_BAR2);
1390
- case "submit":
1391
- return styleText3("green", S_BAR2);
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
- function formatInstructionFooter(o, e) {
1395
- const r = [`${e ? `${styleText3("cyan", S_BAR2)} ` : ""}${o.join(" • ")}`];
1396
- return e && r.push(styleText3("cyan", S_BAR_END2)), r;
1490
+ return {
1491
+ displayName: brandName,
1492
+ slug: appName,
1493
+ scheme: mobileSchemeFor(appName),
1494
+ bundleIdentifier: resolvedBundleId,
1495
+ ...domain ? { appDomain: domain } : {}
1496
+ };
1397
1497
  }
1398
- var I = (l, e, w, p, b, C = false) => {
1399
- let r = e, O = 0;
1400
- if (C)
1401
- for (let i = p - 1;i >= w; i--) {
1402
- const m = l[i];
1403
- if (m && (r -= m.length), O++, r <= b)
1404
- break;
1405
- }
1406
- else
1407
- for (let i = w;i < p; i++) {
1408
- const m = l[i];
1409
- if (m && (r -= m.length), O++, r <= b)
1410
- break;
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
- return { lineCount: r, removals: O };
1413
- };
1414
- var limitOptions = ({
1415
- cursor: l,
1416
- options: e,
1417
- style: w,
1418
- output: p = process.stdout,
1419
- maxItems: b = Number.POSITIVE_INFINITY,
1420
- columnPadding: C = 0,
1421
- rowPadding: r = 4
1422
- }) => {
1423
- 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);
1424
- let f = 0;
1425
- l >= a - 3 && (f = Math.max(Math.min(l - a + 3, e.length - a), 0));
1426
- let d = a < e.length && f > 0, c = a < e.length && f + a < e.length;
1427
- const W = Math.min(f + a, e.length), s = [];
1428
- let g = 0;
1429
- d && g++, c && g++;
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
- if (g > v) {
1440
- let t = 0, n = 0, o = g;
1441
- const h = l - T;
1442
- let u = v;
1443
- const L = () => I(s, o, 0, h, u), E = () => I(s, o, h + 1, s.length, u, true);
1444
- 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));
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
- const x = [];
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
- }).prompt();
1487
- };
1488
- var MULTISELECT_INSTRUCTIONS2 = [
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
- default: {
1556
- const n = t ? `${styleText3("cyan", S_BAR2)} ` : "", l = r.split(`
1557
- `).length, s = x ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS2, t) : t ? [styleText3("cyan", S_BAR_END2)] : [], h = s.join(`
1558
- `), $ = s.length + 1;
1559
- return `${r}${n}${limitOptions({
1560
- output: i.output,
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
- }).prompt();
1575
- };
1576
- var log2 = {
1577
- message: (s = [], {
1578
- symbol: e = styleText3("gray", S_BAR2),
1579
- secondarySymbol: r = styleText3("gray", S_BAR2),
1580
- output: m = process.stdout,
1581
- spacing: l = 1,
1582
- withGuide: c
1583
- } = {}) => {
1584
- const t = [], o = c ?? settings.withGuide, f = o ? r : "", O = o ? `${e} ` : "", u = o ? `${r} ` : "";
1585
- for (let i = 0;i < l; i++)
1586
- t.push(f);
1587
- const g = Array.isArray(s) ? s : s.split(`
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
- var cancel2 = (o = "", t) => {
1619
- const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText3("gray", S_BAR_END2)} ` : "";
1620
- i.write(`${e}${styleText3("red", o)}
1621
-
1622
- `);
1623
- };
1624
- var intro2 = (o = "", t) => {
1625
- const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText3("gray", S_BAR_START2)} ` : "";
1626
- i.write(`${e}${o}
1627
- `);
1628
- };
1629
- var outro2 = (o = "", t) => {
1630
- const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText3("gray", S_BAR2)}
1631
- ${styleText3("gray", S_BAR_END2)} ` : "";
1632
- i.write(`${e}${o}
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
- const r = wrapAnsi(p, x, {
1663
- hard: true,
1664
- trim: false
1665
- }).split(`
1654
+ }).join(`
1666
1655
  `);
1667
- 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());
1668
- }, C = (e) => e.replace(/\.+$/, ""), _ = (e) => {
1669
- const r = (performance.now() - e) / 1000, t = Math.floor(r / 60), o = Math.floor(r % 60);
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
- let r = 0, t = 0;
1675
- A(), T = setInterval(() => {
1676
- if (u && s === p)
1677
- return;
1678
- y(), p = s;
1679
- const o = k(E[r]);
1680
- let v;
1681
- if (u)
1682
- v = `${o} ${s}...`;
1683
- else if (l === "timer")
1684
- v = `${o} ${s} ${_(w)}`;
1685
- else {
1686
- const B = ".".repeat(Math.floor(t)).slice(0, 3);
1687
- v = `${o} ${s}${B}`;
1688
- }
1689
- const j = wrapAnsi(v, x, {
1690
- hard: true,
1691
- trim: false
1692
- });
1693
- n.write(j), r = r + 1 < E.length ? r + 1 : 0, t = t < 4 ? t + 0.125 : 0;
1694
- }, F);
1695
- }, a = (e = "", r = 0, t = false) => {
1696
- if (!d)
1697
- return;
1698
- d = false, clearInterval(T), y();
1699
- const o = r === 0 ? styleText3("green", S_STEP_SUBMIT2) : r === 1 ? styleText3("red", S_STEP_CANCEL2) : styleText3("red", S_STEP_ERROR2);
1700
- s = e ?? s, t || (l === "timer" ? n.write(`${o} ${s} ${_(w)}
1701
- `) : n.write(`${o} ${s}
1702
- `)), H(), M();
1703
- };
1704
- return {
1705
- start: P,
1706
- stop: (e = "") => a(e, 0),
1707
- message: (e = "") => {
1708
- s = C(e ?? s);
1709
- },
1710
- cancel: (e = "") => a(e, 1),
1711
- error: (e = "") => a(e, 2),
1712
- clear: () => a("", 0, true),
1713
- get isCancelled() {
1714
- return S;
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
- var u3 = {
1719
- light: unicodeOr2("─", "-"),
1720
- heavy: unicodeOr2("━", "="),
1721
- block: unicodeOr2("█", "#")
1722
- };
1723
- var SELECT_INSTRUCTIONS2 = [
1724
- `${styleText3("dim", "↑/↓")} to navigate`,
1725
- `${styleText3("dim", "Enter:")} confirm`
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
- }, d = t.showInstructions ?? true;
1749
- return new n$1({
1750
- options: t.options,
1751
- signal: t.signal,
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
- }).prompt();
1789
- };
1790
- var i2 = `${styleText3("gray", S_BAR2)} `;
1791
- var text2 = (t) => new n({
1792
- validate: t.validate,
1793
- placeholder: t.placeholder,
1794
- defaultValue: t.defaultValue,
1795
- initialValue: t.initialValue,
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
- }).prompt();
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 which(command) {
1871
- if (command.includes("/"))
1872
- return isExecutable(command) ? command : null;
1873
- const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? "").split(";") : [""];
1874
- for (const directory of (process.env.PATH ?? "").split(delimiter)) {
1875
- if (!directory)
1876
- continue;
1877
- for (const extension of extensions) {
1878
- const candidate = join(directory, `${command}${extension}`);
1879
- if (isExecutable(candidate))
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
- function isExecutable(path) {
1886
- try {
1887
- accessSync(path, constants.X_OK);
1888
- return true;
1889
- } catch {
1890
- return false;
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
- // src/agent-setup.ts
1895
- var CLIENTS = ["codex", "claude", "cursor"];
1896
- var SERVER_NAME = "agent-docs";
1897
- var CODEX_START = "# agent-context:start";
1898
- var CODEX_END = "# agent-context:end";
1899
- var AGENT_CONTEXT_TOOLS = [
1900
- "search_docs",
1901
- "search_implementation",
1902
- "get_repository_map",
1903
- "plan_change",
1904
- "get_invariants",
1905
- "get_verification"
1906
- ];
1907
- function takeValue(args, index, flag) {
1908
- const value = args[index + 1];
1909
- if (!value || value.startsWith("-"))
1910
- throw new Error(`${flag} requires a value.`);
1911
- return value;
1912
- }
1913
- function parseAgentSetupArguments(argv) {
1914
- const parsed = { yes: false, dryRun: false, help: false };
1915
- for (let index = 0;index < argv.length; index += 1) {
1916
- const argument = argv[index];
1917
- if (argument === "--clients") {
1918
- const values = takeValue(argv, index++, argument).split(",").map((value) => value.trim()).filter(Boolean);
1919
- const invalid = values.filter((value) => !CLIENTS.includes(value));
1920
- if (invalid.length > 0)
1921
- throw new Error(`Unknown agent client: ${invalid.join(", ")}`);
1922
- parsed.clients = [...new Set(values)];
1923
- } else if (argument === "--yes" || argument === "-y")
1924
- parsed.yes = true;
1925
- else if (argument === "--dry-run")
1926
- parsed.dryRun = true;
1927
- else if (argument === "--help" || argument === "-h")
1928
- parsed.help = true;
1929
- else
1930
- throw new Error(`Unknown agent setup option: ${argument}`);
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 parsed;
1733
+ return content.replace(JSONC_WORKER_NAME_FIELD, `$1${appName}-site$2`);
1933
1734
  }
1934
- function agentSetupUsage(cliName) {
1935
- return `Usage: ${cliName} agent setup [options]
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
- Connect local coding clients to this checkout's read-only project context server.
1748
+ `)}
1749
+ `;
1750
+ }
1751
+ function removeParagraphContaining(content, fragment) {
1752
+ const paragraphs = content.split(`
1938
1753
 
1939
- Options:
1940
- --clients <list> Comma-separated clients: codex,claude,cursor
1941
- --yes Configure every detected client without prompting
1942
- --dry-run Show the selected configuration without changing client settings
1943
- -h, --help Show this help`;
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
- async function pathExists(path) {
1946
- return Boolean(await stat(path).catch(() => {
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
- async function findAgentRepositoryRoot(start = process.cwd()) {
1951
- let current = resolve(start);
1952
- while (true) {
1953
- if (await pathExists(join2(current, "AGENTS.md")) && await pathExists(join2(current, "packages/agent-context/package.json")) && await pathExists(join2(current, "agent-tooling/manifest.json"))) {
1954
- return current;
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 parent = dirname(current);
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
- function serverCommand(root) {
1963
- return { command: "pnpm", args: ["--dir", resolve(root), "agent:mcp"] };
1964
- }
1965
- function codexManagedBlock(root) {
1966
- const server = serverCommand(root);
1967
- const args = server.args.map((argument) => JSON.stringify(argument)).join(", ");
1968
- return `${CODEX_START}
1969
- [mcp_servers.${SERVER_NAME}]
1970
- command = ${JSON.stringify(server.command)}
1971
- args = [${args}]
1972
- required = false
1973
- enabled_tools = ${JSON.stringify(AGENT_CONTEXT_TOOLS)}
1974
- ${CODEX_END}`;
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 mergeCodexConfig(existing, root) {
1977
- const managed = codexManagedBlock(root);
1978
- const start = existing.indexOf(CODEX_START);
1979
- const end = existing.indexOf(CODEX_END);
1980
- if (start >= 0 || end >= 0) {
1981
- if (start < 0 || end < start)
1982
- throw new Error("Codex config has an incomplete agent-context block.");
1983
- const after = end + CODEX_END.length;
1984
- return `${existing.slice(0, start).trimEnd()}
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
- if (/^\[mcp_servers\.(?:agent-docs|agent_docs|"agent-docs")\]$/m.test(existing)) {
1989
- throw new Error("Codex already has an unmanaged agent-docs server; remove or rename it first.");
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 `${existing.trimEnd()}${existing.trim() ? `
1992
-
1993
- ` : ""}${managed}
1994
- `;
1861
+ return rewritten;
1995
1862
  }
1996
- function mergeCursorConfig(existing, root) {
1997
- let config = {};
1998
- if (existing?.trim()) {
1999
- try {
2000
- const parsed = JSON.parse(existing);
2001
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
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
- const servers = config.mcpServers ?? {};
2012
- const current = servers[SERVER_NAME];
2013
- const desired = serverCommand(root);
2014
- if (current !== undefined && JSON.stringify(current) !== JSON.stringify(desired)) {
2015
- throw new Error("Cursor already has a different agent-docs server; remove or rename it first.");
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
- config.mcpServers = { ...servers, [SERVER_NAME]: desired };
2018
- return `${JSON.stringify(config, null, 2)}
2019
- `;
1876
+ return example.replace(BETTER_AUTH_SECRET_LINE_RE, `BETTER_AUTH_SECRET=${secret}`);
2020
1877
  }
2021
- async function atomicWrite(path, content) {
2022
- await mkdir(dirname(path), { recursive: true });
2023
- const temporary = `${path}.${process.pid}.tmp`;
2024
- await writeFile(temporary, content, { mode: 384 });
2025
- await rename(temporary, path);
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
- async function capture(command, cwd) {
2028
- return runCaptured(command, { cwd });
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
- async function addGitInfoExcludes(root, paths) {
2031
- const result = await capture(["git", "rev-parse", "--git-path", "info/exclude"], root);
2032
- if (result.exitCode !== 0)
2033
- return false;
2034
- const rawPath = result.stdout.trim();
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
- const prefix = existing && !existing.endsWith(`
2043
- `) ? `
2044
- ` : "";
2045
- const marker = "# Local agent configuration";
2046
- const heading = existing.split(/\r?\n/).includes(marker) ? "" : `${marker}
2047
- `;
2048
- await atomicWrite(excludePath, `${existing}${prefix}${heading}${missing.join(`
2049
- `)}
2050
- `);
2051
- return true;
2052
- }
2053
- async function configureCodex(root) {
2054
- const path = join2(root, ".codex/config.toml");
2055
- const existing = await readFile(path, "utf8").catch(() => "");
2056
- await atomicWrite(path, mergeCodexConfig(existing, root));
2057
- const excluded = await addGitInfoExcludes(root, [".codex/config.toml"]);
2058
- return excluded ? "Configured project-local .codex/config.toml (excluded from Git)." : "Configured project-local .codex/config.toml; review Git status before committing it.";
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
- async function configureCursor(root) {
2061
- const path = join2(root, ".cursor/mcp.json");
2062
- const existing = await readFile(path, "utf8").catch(() => {
2063
- return;
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
- async function configureClaude(root) {
2070
- if (!which("claude"))
2071
- throw new Error("Claude Code CLI is not installed or not on PATH.");
2072
- const existing = await capture(["claude", "mcp", "get", SERVER_NAME], root);
2073
- if (existing.exitCode === 0) {
2074
- const combined = `${existing.stdout}
2075
- ${existing.stderr}`;
2076
- if (combined.includes("agent:mcp") && combined.includes(root)) {
2077
- return "Claude Code already has the matching local server.";
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
- throw new Error("Claude Code already has a different agent-docs server; remove it first.");
2080
- }
2081
- const server = serverCommand(root);
2082
- const result = await capture(["claude", "mcp", "add", SERVER_NAME, "--scope", "local", "--", server.command, ...server.args], root);
2083
- if (result.exitCode !== 0) {
2084
- const output = result.stderr.trim() || result.stdout.trim();
2085
- throw new Error(`Claude Code MCP setup failed${output ? `: ${output}` : "."}`);
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 "Configured Claude Code local project scope.";
2027
+ return output.join(`
2028
+ `).replace(EXTRA_BLANK_LINES_RE, `
2029
+
2030
+ `);
2088
2031
  }
2089
- async function smokeServer(root) {
2090
- const result = await capture(["pnpm", "--dir", root, "agent:mcp", "--check", "--root", root], root);
2091
- if (result.exitCode !== 0) {
2092
- const output = result.stderr.trim() || result.stdout.trim();
2093
- throw new Error(`Agent context MCP self-check failed${output ? `: ${output}` : "."}`);
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 detectedClients() {
2101
- return CLIENTS.filter((client) => {
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
- async function chooseClients(parsed) {
2108
- if (parsed.clients) {
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 dryRunDescription(client, root) {
2140
- if (client === "codex")
2141
- return mergeCodexConfig("", root).trim();
2142
- if (client === "cursor")
2143
- return mergeCursorConfig(undefined, root).trim();
2144
- const server = serverCommand(root);
2145
- return [
2146
- "claude",
2147
- "mcp",
2148
- "add",
2149
- SERVER_NAME,
2150
- "--scope",
2151
- "local",
2152
- "--",
2153
- server.command,
2154
- ...server.args
2155
- ].map((value) => value.includes(" ") ? JSON.stringify(value) : value).join(" ");
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
- async function runAgentSetup(argv, cliName) {
2158
- const parsed = parseAgentSetupArguments(argv);
2159
- if (parsed.help) {
2160
- process.stdout.write(`${agentSetupUsage(cliName)}
2161
- `);
2162
- return;
2163
- }
2164
- const root = await findAgentRepositoryRoot();
2165
- const clients = await chooseClients(parsed);
2166
- if (!clients)
2167
- return;
2168
- intro2("Local agent context");
2169
- if (parsed.dryRun) {
2170
- for (const client of clients) {
2171
- log2.info(`${client}
2172
- ${dryRunDescription(client, root)}`);
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
- const progress = spinner2();
2178
- progress.start("Testing the local MCP server");
2179
- await smokeServer(root);
2180
- progress.stop("Local MCP server passed its protocol self-check");
2181
- for (const client of clients) {
2182
- const result = client === "codex" ? await configureCodex(root) : client === "cursor" ? await configureCursor(root) : await configureClaude(root);
2183
- log2.success(`${client}: ${result}`);
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
- log2.info("Restart configured clients, then ask: “Use plan_change for my next feature.”");
2186
- outro2("Agent context is ready.");
2078
+ return releaseTag;
2187
2079
  }
2188
2080
 
2189
- // src/deployment-cli.ts
2190
- import { readFile as readFile3 } from "node:fs/promises";
2191
- import { resolve as resolve2 } from "node:path";
2192
-
2193
- // src/deployment.ts
2194
- import { mkdir as mkdir2, readFile as readFile2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
2195
- import { join as join3 } from "node:path";
2196
- var deploymentClouds = ["aws", "gcp", "azure"];
2197
- var frontendTargets = ["native", "vercel", "cloudflare"];
2198
- var storageProviders = ["r2", "s3", "gcs"];
2199
- var defaultRegions = {
2200
- aws: "us-east-1",
2201
- gcp: "us-central1",
2202
- azure: "eastus2"
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
- function validateRegion(value) {
2246
- if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(value)) {
2247
- return "Region must use 2-32 lowercase letters, numbers, or hyphens.";
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
- return;
2250
- }
2251
- function validateDomain(value) {
2252
- if (value.length > 80 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value)) {
2253
- return "Enter a lowercase hostname such as api.example.com, without a protocol or path.";
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
- return;
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
- function validateDeploymentSelection(selection) {
2258
- if (!storageProvidersByCloud[selection.cloud].includes(selection.storageProvider)) {
2259
- throw new Error(`${selection.storageProvider} storage is not supported for ${selection.cloud} deployments.`);
2260
- }
2261
- const environmentError = validateDeploymentEnvironment(selection.environment);
2262
- if (environmentError)
2263
- throw new Error(environmentError);
2264
- const regionError = validateRegion(selection.region);
2265
- if (regionError)
2266
- throw new Error(regionError);
2267
- const apiError = validateDomain(selection.apiDomain);
2268
- if (apiError)
2269
- throw new Error(`API domain: ${apiError}`);
2270
- const frontendError = validateDomain(selection.frontendDomain);
2271
- if (frontendError)
2272
- throw new Error(`Frontend domain: ${frontendError}`);
2273
- if (selection.apiDomain === selection.frontendDomain) {
2274
- throw new Error("API and frontend domains must be different hostnames.");
2275
- }
2276
- const apiSite = selection.apiDomain.split(".").slice(1).join(".");
2277
- const frontendSite = selection.frontendDomain.split(".").slice(1).join(".");
2278
- if (apiSite !== frontendSite) {
2279
- throw new Error("API and frontend domains must be sibling hosts under the same site.");
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
- function createDeploymentConfig(projectName, selection) {
2283
- validateDeploymentSelection(selection);
2284
- return {
2285
- schema_version: 2,
2286
- cloud: selection.cloud,
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
- await mkdir2(join3(repositoryRoot, "deployment"), { recursive: true });
2363
- await writeFile2(configPath, `${JSON.stringify(config, null, 2)}
2364
- `);
2365
- await writeFile2(join3(repositoryRoot, "deployment/README.md"), deploymentReadme(config));
2366
- const removed = [];
2367
- for (const cloud of deploymentClouds) {
2368
- if (cloud === selection.cloud)
2369
- continue;
2370
- for (const relativePath of [
2371
- `infra/bootstrap/${cloud}`,
2372
- `infra/modules/${cloud}`,
2373
- `infra/roots/${cloud}`,
2374
- `scripts/deploy/release-${cloud}.sh`,
2375
- `scripts/deploy/frontend-${cloud}.sh`
2376
- ]) {
2377
- if (await removeIfPresent(join3(repositoryRoot, relativePath)))
2378
- removed.push(relativePath);
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
- const adapters = [
2382
- { target: "vercel", path: "apps/web/vercel.json" },
2383
- { target: "cloudflare", path: "apps/web/wrangler.toml" },
2384
- {
2385
- target: "azure-native",
2386
- path: "apps/web/public/staticwebapp.config.json"
2387
- }
2388
- ];
2389
- const selectedAdapter = selection.frontendTarget === "native" ? selection.cloud === "azure" ? "azure-native" : undefined : selection.frontendTarget;
2390
- for (const adapter of adapters) {
2391
- if (adapter.target === selectedAdapter)
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
- if (await removeIfPresent(join3(repositoryRoot, adapter.path)))
2394
- removed.push(adapter.path);
2630
+ for (const extension of extensions) {
2631
+ const candidate = join(directory, `${command}${extension}`);
2632
+ if (isExecutable(candidate))
2633
+ return candidate;
2634
+ }
2395
2635
  }
2396
- if (selection.frontendTarget !== "native") {
2397
- const nativeScript = `scripts/deploy/frontend-${selection.cloud}.sh`;
2398
- if (await removeIfPresent(join3(repositoryRoot, nativeScript)))
2399
- removed.push(nativeScript);
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/deployment-prompts.ts
2405
- class DeploymentPromptCancelled extends Error {
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
- async function unwrap(value) {
2408
- if (isCancel(value)) {
2409
- cancel2("Deployment configuration was not changed.");
2410
- throw new DeploymentPromptCancelled;
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 value;
2687
+ return parsed;
2413
2688
  }
2414
- async function collectDeploymentSelection(defaults = {}, nonInteractive = false) {
2415
- if (nonInteractive) {
2416
- if (!defaults.cloud || !defaults.apiDomain || !defaults.frontendDomain) {
2417
- throw new Error("Non-interactive deployment setup requires --cloud, --api-domain, and --frontend-domain.");
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
- return {
2420
- cloud: defaults.cloud,
2421
- storageProvider: defaults.storageProvider ?? defaultStorageProviders[defaults.cloud],
2422
- frontendTarget: defaults.frontendTarget ?? "native",
2423
- environment: defaults.environment ?? "production",
2424
- region: defaults.region ?? defaultRegions[defaults.cloud],
2425
- apiDomain: defaults.apiDomain,
2426
- frontendDomain: defaults.frontendDomain,
2427
- haEnabled: defaults.haEnabled ?? false
2428
- };
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
- // src/deployment-cli.ts
2497
- function takeValue2(args, index, flag) {
2498
- const value = args[index + 1];
2499
- if (!value || value.startsWith("--"))
2500
- throw new Error(`${flag} requires a value.`);
2501
- return value;
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 parseDeploymentArguments(argv) {
2504
- const parsed = { yes: false };
2505
- for (let index = 0;index < argv.length; index += 1) {
2506
- const argument = argv[index];
2507
- if (argument === "--cloud") {
2508
- parsed.cloud = parseDeploymentCloud(takeValue2(argv, index++, argument));
2509
- } else if (argument === "--frontend") {
2510
- parsed.frontendTarget = parseFrontendTarget(takeValue2(argv, index++, argument));
2511
- } else if (argument === "--storage") {
2512
- parsed.storageProvider = parseStorageProvider(takeValue2(argv, index++, argument));
2513
- } else if (argument === "--environment") {
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
- return parsed;
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
- async function runDeploymentConfigure(argv, productName, repositoryRoot = process.cwd()) {
2533
- const parsed = parseDeploymentArguments(argv);
2534
- const root = resolve2(repositoryRoot);
2535
- const rootPackage = JSON.parse(await readFile3(`${root}/package.json`, "utf8"));
2536
- if (!rootPackage.name)
2537
- throw new Error("Root package name is required for deployment setup.");
2538
- intro2(`${productName} deployment setup`);
2539
- try {
2540
- const selection = await collectDeploymentSelection(parsed, parsed.yes);
2541
- const removed = await applyDeploymentSelection(root, rootPackage.name, selection);
2542
- log2.success(`${selection.cloud.toUpperCase()} + ${selection.storageProvider} + ${selection.frontendTarget} is locked for ${selection.environment}.`);
2543
- log2.info(`${removed.length} unused deployment paths removed.`);
2544
- log2.step("Next");
2545
- log2.message(`Review deployment/config.json
2546
- Configure the protected GitHub environment
2547
- Dispatch .github/workflows/deploy-cloud.yml`);
2548
- outro2("Deployment configuration is ready to commit.");
2549
- } catch (error) {
2550
- if (error instanceof DeploymentPromptCancelled)
2551
- return;
2552
- throw error;
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
- // src/runtime.ts
2557
- import { spawn as spawn2 } from "node:child_process";
2558
- import { accessSync as accessSync2, constants as constants2, realpathSync as realpathSync2 } from "node:fs";
2559
- import { delimiter as delimiter2, join as join4, resolve as resolvePath2 } from "node:path";
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 runInherited(command, options) {
2588
- const [file, ...args] = command;
2589
- if (!file)
2590
- throw new Error("runInherited requires a command");
2591
- return new Promise((resolve, reject) => {
2592
- const child = spawn2(file, args, {
2593
- cwd: options.cwd,
2594
- env: options.env ?? process.env,
2595
- stdio: "inherit"
2596
- });
2597
- child.on("error", reject);
2598
- child.on("close", (code, signal) => {
2599
- resolve(code ?? (signal === "SIGINT" ? 130 : 1));
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 canonicalPath(path) {
2604
- try {
2605
- return realpathSync2(path);
2606
- } catch {
2607
- return resolvePath2(path);
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 isEntrypoint(moduleUrl) {
2611
- const entry = process.argv[1];
2612
- if (!entry)
2613
- return false;
2614
- try {
2615
- return canonicalPath(fileURLToPath(moduleUrl)) === canonicalPath(entry);
2616
- } catch {
2617
- return false;
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
- // src/deployment-prompts.ts
2622
- class DeploymentPromptCancelled2 extends Error {
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 unwrap2(value) {
2625
- if (isCancel(value)) {
2626
- cancel2("Deployment configuration was not changed.");
2627
- throw new DeploymentPromptCancelled2;
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
- return value;
2630
- }
2631
- async function collectDeploymentSelection2(defaults = {}, nonInteractive = false) {
2632
- if (nonInteractive) {
2633
- if (!defaults.cloud || !defaults.apiDomain || !defaults.frontendDomain) {
2634
- throw new Error("Non-interactive deployment setup requires --cloud, --api-domain, and --frontend-domain.");
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
- return {
2637
- cloud: defaults.cloud,
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 cloud = defaults.cloud ?? parseDeploymentCloud(await unwrap2(await select({
2648
- message: "Backend cloud",
2649
- options: [
2650
- { value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL" },
2651
- { value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL" },
2652
- { value: "azure", label: "Azure", hint: "Container Apps + Flexible Server" }
2653
- ]
2654
- })));
2655
- const frontendTarget = defaults.frontendTarget ?? parseFrontendTarget(await unwrap2(await select({
2656
- message: "Frontend host",
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 mkdir3, readFile as readFile4, rm as rm2, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
2715
- import { join as join5 } from "node:path";
2716
- var deploymentClouds2 = ["aws", "gcp", "azure"];
2717
- var frontendTargets2 = ["native", "vercel", "cloudflare"];
2718
- var storageProviders2 = ["r2", "s3", "gcs"];
2719
- var storageProvidersByCloud2 = {
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 isDeploymentCloud2(value) {
2725
- return deploymentClouds2.some((cloud) => cloud === value);
2972
+ function isDeploymentCloud(value) {
2973
+ return deploymentClouds.some((cloud) => cloud === value);
2726
2974
  }
2727
- function isFrontendTarget2(value) {
2728
- return frontendTargets2.some((target) => target === value);
2975
+ function isFrontendTarget(value) {
2976
+ return frontendTargets.some((target) => target === value);
2729
2977
  }
2730
- function isStorageProvider2(value) {
2731
- return storageProviders2.some((provider) => provider === value);
2978
+ function isStorageProvider(value) {
2979
+ return storageProviders.some((provider) => provider === value);
2732
2980
  }
2733
- function parseDeploymentCloud2(value) {
2734
- if (!isDeploymentCloud2(value))
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 parseFrontendTarget2(value) {
2739
- if (!isFrontendTarget2(value)) {
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 parseStorageProvider2(value) {
2745
- if (!isStorageProvider2(value))
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 validateDeploymentEnvironment2(value) {
2750
- if (!/^[a-z][a-z0-9-]{1,30}$/.test(value)) {
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 validateRegion2(value) {
2756
- if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(value)) {
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 validateDomain2(value) {
2762
- if (value.length > 80 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value)) {
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 validateDeploymentSelection2(selection) {
2768
- if (!storageProvidersByCloud2[selection.cloud].includes(selection.storageProvider)) {
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 = validateDeploymentEnvironment2(selection.environment);
3019
+ const environmentError = validateDeploymentEnvironment(selection.environment);
2772
3020
  if (environmentError)
2773
3021
  throw new Error(environmentError);
2774
- const regionError = validateRegion2(selection.region);
3022
+ const regionError = validateRegion(selection.region);
2775
3023
  if (regionError)
2776
3024
  throw new Error(regionError);
2777
- const apiError = validateDomain2(selection.apiDomain);
3025
+ const apiError = validateDomain(selection.apiDomain);
2778
3026
  if (apiError)
2779
3027
  throw new Error(`API domain: ${apiError}`);
2780
- const frontendError = validateDomain2(selection.frontendDomain);
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 createDeploymentConfig2(projectName, selection) {
2793
- validateDeploymentSelection2(selection);
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 deploymentReadme2(config) {
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 removeIfPresent2(target) {
2857
- if (!await stat3(target).catch(() => {
3104
+ async function removeIfPresent(target) {
3105
+ if (!await stat2(target).catch(() => {
2858
3106
  return;
2859
3107
  }))
2860
3108
  return false;
2861
- await rm2(target, { recursive: true, force: true });
3109
+ await rm(target, { recursive: true, force: true });
2862
3110
  return true;
2863
3111
  }
2864
- async function applyDeploymentSelection2(repositoryRoot, projectName, selection) {
2865
- const config = createDeploymentConfig2(projectName, selection);
2866
- const configPath = join5(repositoryRoot, "deployment/config.json");
2867
- if (await stat3(configPath).catch(() => {
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 mkdir3(join5(repositoryRoot, "deployment"), { recursive: true });
2873
- await writeFile3(configPath, `${JSON.stringify(config, null, 2)}
3120
+ await mkdir2(join3(repositoryRoot, "deployment"), { recursive: true });
3121
+ await writeFile2(configPath, `${JSON.stringify(config, null, 2)}
2874
3122
  `);
2875
- await writeFile3(join5(repositoryRoot, "deployment/README.md"), deploymentReadme2(config));
3123
+ await writeFile2(join3(repositoryRoot, "deployment/README.md"), deploymentReadme(config));
2876
3124
  const removed = [];
2877
- for (const cloud of deploymentClouds2) {
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 removeIfPresent2(join5(repositoryRoot, relativePath)))
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 removeIfPresent2(join5(repositoryRoot, adapter.path)))
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 removeIfPresent2(join5(repositoryRoot, nativeScript)))
3156
+ if (await removeIfPresent(join3(repositoryRoot, nativeScript)))
2909
3157
  removed.push(nativeScript);
2910
3158
  }
2911
3159
  return removed;
2912
3160
  }
2913
3161
 
2914
- // src/scaffold.ts
2915
- import { execFile } from "node:child_process";
2916
- import { createHash } from "node:crypto";
2917
- import { cp, lstat, mkdir as mkdir4, mkdtemp, readFile as readFile5, readdir, rm as rm4, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
2918
- import { tmpdir } from "node:os";
2919
- import { basename, dirname as dirname2, join as join7, relative, resolve as resolve3 } from "node:path";
2920
- import { promisify } from "node:util";
2921
-
2922
- // src/delivery.ts
2923
- import { createWriteStream } from "node:fs";
2924
- import { rm as rm3 } from "node:fs/promises";
2925
- import { join as join6 } from "node:path";
2926
- import { Readable, Transform } from "node:stream";
2927
- import { pipeline } from "node:stream/promises";
2928
- var RELEASE_TAG_PATTERN2 = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
2929
- var INDEX_TIMEOUT_MS = 15000;
2930
- var DOWNLOAD_TIMEOUT_MS = 300000;
2931
- function deliveryBaseUrl(environment = process.env) {
2932
- const configured = environment[DELIVERY_URL_VARIABLE]?.trim();
2933
- return (configured || DELIVERY_BASE_URL).replace(/\/+$/, "");
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 releaseAssetNames(releaseTag) {
2936
- const artifact = `foundation-source-${releaseTag}.tar.gz`;
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
- artifact,
2939
- checksum: `${artifact}.sha256`,
2940
- manifest: `foundation-source-${releaseTag}.manifest.json`
3243
+ cloud,
3244
+ storageProvider,
3245
+ frontendTarget,
3246
+ environment,
3247
+ region,
3248
+ apiDomain,
3249
+ frontendDomain,
3250
+ haEnabled
2941
3251
  };
2942
3252
  }
2943
- function describeFailure(status, subject) {
2944
- if (status === 401) {
2945
- return "That licence key was not accepted. Copy it exactly from your purchase receipt or the customer portal (polar.sh).";
2946
- }
2947
- if (status === 429) {
2948
- return "Too many download requests for this licence key. Wait a minute and try again.";
2949
- }
2950
- if (status === 404)
2951
- return `${subject} is not available for download.`;
2952
- if (status >= 500) {
2953
- return "Source delivery is temporarily unavailable. Wait a few minutes and try again.";
2954
- }
2955
- return `Source delivery refused the request (HTTP ${status}).`;
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
- async function requestDelivery(url, init, subject) {
2958
- let response;
2959
- try {
2960
- response = await fetch(url, init);
2961
- } catch {
2962
- throw new Error("Could not reach source delivery. Check your network connection and try again.");
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
- if (!response.ok)
2965
- throw new Error(describeFailure(response.status, subject));
2966
- return response;
3288
+ return parsed;
2967
3289
  }
2968
- async function resolveRelease(licenseKey, baseUrl = deliveryBaseUrl()) {
2969
- const response = await requestDelivery(`${baseUrl}/v1/releases`, {
2970
- method: "POST",
2971
- headers: { "content-type": "application/json" },
2972
- body: JSON.stringify({ key: licenseKey }),
2973
- signal: AbortSignal.timeout(INDEX_TIMEOUT_MS)
2974
- }, "The release index");
2975
- let index;
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
- index = await response.json();
2978
- } catch {
2979
- index = undefined;
2980
- }
2981
- if (!index || !RELEASE_TAG_PATTERN2.test(index.latest) || !Array.isArray(index.versions)) {
2982
- throw new Error(`Source delivery returned an unreadable release index.`);
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
- function readContentLength(response) {
2987
- const header = response.headers.get("content-length");
2988
- if (!header)
2989
- return;
2990
- const total = Number(header);
2991
- return Number.isSafeInteger(total) && total > 0 ? total : undefined;
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 countBytes(total, onBytes) {
2994
- let received = 0;
2995
- return new Transform({
2996
- transform(chunk, _encoding, done) {
2997
- received += chunk.length;
2998
- onBytes(received, total);
2999
- done(null, chunk);
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
- async function downloadAsset(licenseKey, releaseTag, assetName, directory, baseUrl = deliveryBaseUrl(), onBytes) {
3004
- const response = await requestDelivery(`${baseUrl}/v1/source/${encodeURIComponent(releaseTag)}/${encodeURIComponent(assetName)}`, {
3005
- headers: { authorization: `Bearer ${licenseKey}` },
3006
- signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
3007
- }, releaseTag);
3008
- if (!response.body)
3009
- throw new Error(`Source delivery returned an empty ${assetName}.`);
3010
- const path = join6(directory, assetName);
3011
- const source = Readable.fromWeb(response.body);
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
- if (onBytes) {
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
- await rm3(path, { force: true });
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/core.ts
3026
- import { randomBytes } from "node:crypto";
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 normalizeScope2(value) {
3072
- const scope = value.startsWith("@") ? value : `@${value}`;
3073
- if (/\s/.test(scope)) {
3074
- throw new Error("Use hyphens instead of spaces, for example test-package.");
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 scope;
3387
+ return value;
3080
3388
  }
3081
- function validateBrandName2(value) {
3082
- if (!value.trim())
3083
- return "Brand name cannot be empty.";
3084
- if (/\r|\n/.test(value))
3085
- return "Brand name must fit on one line.";
3086
- if (value !== value.trim())
3087
- return "Brand name cannot start or end with whitespace.";
3088
- if (value.length > 48)
3089
- return "Brand name must be 48 characters or less.";
3090
- if (!/^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u.test(value)) {
3091
- return "Brand name may contain letters, numbers, spaces, periods, underscores, and hyphens.";
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
- return;
3094
- }
3095
- function replaceLiteral(content, search, replacement) {
3096
- if (search === replacement || !content.includes(search))
3097
- return { content, replacements: 0 };
3098
- const parts = content.split(search);
3099
- return { content: parts.join(replacement), replacements: parts.length - 1 };
3100
- }
3101
- function rewriteScopeContent(content, scope, originalScope = DEFAULT_SCOPE) {
3102
- return replaceLiteral(content, `${originalScope}/`, `${normalizeScope2(scope)}/`);
3103
- }
3104
- function rewriteBrandContent(content, brandName) {
3105
- const error = validateBrandName2(brandName);
3106
- if (error)
3107
- throw new Error(error);
3108
- const escapedSourceBrand = SOURCE_BRAND_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3109
- const sourceBrandPattern = new RegExp(`\\b${escapedSourceBrand}\\b`, "gi");
3110
- const matches = content.match(sourceBrandPattern);
3111
- if (brandName === SOURCE_BRAND_NAME || !matches)
3112
- return { content, replacements: 0 };
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
- content: content.replace(sourceBrandPattern, () => brandName),
3115
- replacements: matches.length
3460
+ cloud,
3461
+ storageProvider,
3462
+ frontendTarget,
3463
+ environment,
3464
+ region,
3465
+ apiDomain,
3466
+ frontendDomain,
3467
+ haEnabled
3116
3468
  };
3117
3469
  }
3118
- function formatRewrittenPackageJson(content) {
3119
- const parsed = JSON.parse(content);
3120
- for (const key of [
3121
- "dependencies",
3122
- "devDependencies",
3123
- "optionalDependencies",
3124
- "peerDependencies"
3125
- ]) {
3126
- const values = parsed[key];
3127
- if (values && typeof values === "object" && !Array.isArray(values)) {
3128
- parsed[key] = Object.fromEntries(Object.entries(values).sort(([left], [right]) => left.localeCompare(right)));
3129
- }
3130
- }
3131
- return `${JSON.stringify(parsed, null, 2)}
3132
- `;
3133
- }
3134
- function wrapLongNamedImports(content, width = 100) {
3135
- return content.split(`
3136
- `).map((line) => {
3137
- if (line.length <= width)
3138
- return line;
3139
- const match = line.match(/^(\s*)(import(?:\s+type)?\s+)\{([^}]+)\}(\s+from\s+.+)$/);
3140
- if (!match)
3141
- return line;
3142
- const [, indentation = "", prefix = "", names = "", suffix = ""] = match;
3143
- const members = names.split(",").map((name) => name.trim());
3144
- return [
3145
- `${indentation}${prefix}{`,
3146
- ...members.map((name) => `${indentation} ${name},`),
3147
- `${indentation}}${suffix}`
3148
- ].join(`
3149
- `);
3150
- }).join(`
3151
- `);
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 output.join(`
3194
- `);
3503
+ return value;
3195
3504
  }
3196
- function rewriteRootPackageName(content, appName) {
3197
- const error = validateAppName2(appName);
3198
- if (error)
3199
- throw new Error(error);
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
- var WORKER_NAME_DECLARATION = /^name = "[^"]*"$/m;
3208
- function rewriteWorkerName(content, appName) {
3209
- const error = validateAppName2(appName);
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
- const replacement = `name = "${appName}"`;
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 replaceMarkdownSection(content, heading, replacement) {
3222
- const marker = `## ${heading}
3223
- `;
3224
- const start = content.indexOf(marker);
3225
- if (start === -1)
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 removeParagraphContaining(content, fragment) {
3238
- const paragraphs = content.split(`
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 rewriteBuyerAgentInstructions(content) {
3250
- if (content.includes("Audit and remediate the complete buyer-owned SaaS application") && !/internal\/|landing\/|seller-surface|studio-owned/i.test(content)) {
3251
- return content;
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
- let rewritten = removeParagraphContaining(content, "`internal/` holds every seller-only surface");
3254
- rewritten = removeParagraphContaining(rewritten, "The root pnpm workspace includes only `apps/*` and `packages/*`.");
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
- rewritten = replaceMarkdownSection(rewritten, "Landing boundary", "");
3265
- rewritten = rewritten.split(`
3266
- `).filter((line) => !line.includes("| `internal`")).join(`
3267
- `).replace("real env files, `internal/`, or\n `bun.lock`", "real env files, or `bun.lock`");
3268
- if (/internal\/|landing\/|seller-surface|studio-owned/i.test(rewritten)) {
3269
- throw new Error("Buyer agent instructions still contain seller-only guidance.");
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
- return rewritten;
3272
- }
3273
- var BUYER_README_HEADING = "## Create your product";
3274
- function stripDistributionReadmeBootstrap(content) {
3275
- if (!content.includes(BUYER_README_HEADING))
3276
- return content;
3277
- return replaceMarkdownSection(content, "Create your product", "");
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
- let rewritten = content;
3295
- for (const [name, value] of buyerConstants) {
3296
- const declaration = new RegExp(`^export const ${name} = '[^']*'$`, "m");
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
- rewritten = rewritten.replace(declaration, `export const ${name} = '${value}'`);
3664
+ if (await removeIfPresent2(join5(repositoryRoot, adapter.path)))
3665
+ removed.push(adapter.path);
3304
3666
  }
3305
- const leaked = [
3306
- CLI_PACKAGE_NAME,
3307
- SOURCE_BRAND_NAME,
3308
- DELIVERY_BASE_URL,
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 rewritten;
3672
+ return removed;
3318
3673
  }
3319
- var SELLER_ONLY_ROOT_SCRIPTS = [
3320
- "release:distribute",
3321
- "release:publish-cli",
3322
- "release:upload"
3323
- ];
3324
- function stripSellerOnlyRootScripts(content) {
3325
- const manifest = JSON.parse(content);
3326
- for (const script of SELLER_ONLY_ROOT_SCRIPTS)
3327
- delete manifest.scripts?.[script];
3328
- return `${JSON.stringify(manifest, null, 2)}
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 rewriteBuyerClaudeInstructions(content) {
3332
- if (!/internal\/|landing\/|studio-owned/i.test(content))
3333
- return content;
3334
- const rewritten = removeParagraphContaining(content, "`internal/landing/AGENTS.md`");
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 rewriteBuyerSetupInstructions(content) {
3341
- if (!content.includes("## Create a product repository"))
3342
- return content;
3343
- const rewritten = replaceMarkdownSection(content, "Create a product repository", "");
3344
- const escapedCliName = CLI_NAME2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3345
- if (new RegExp(`pnpm dlx ${escapedCliName}|bunx ${escapedCliName}|${escapedCliName} repository`, "i").test(rewritten)) {
3346
- throw new Error("Buyer setup instructions still contain source-delivery guidance.");
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 rewriteBuyerReleaseInstructions(content) {
3351
- if (!content.includes("## npm bootstrapper"))
3352
- return content;
3353
- const rewritten = replaceMarkdownSection(content, "npm bootstrapper", "");
3354
- const escapedCliName = CLI_NAME2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3355
- if (new RegExp(`${escapedCliName}@\\d|public \`${escapedCliName}\` npm package`, "i").test(rewritten)) {
3356
- throw new Error("Buyer release instructions still contain vendor bootstrapper guidance.");
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
- return rewritten;
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 generateServerEnv(example, secret = randomBytes(32).toString("hex"), databasePort = 5432) {
3361
- if (!/^[a-f0-9]{64}$/.test(secret)) {
3362
- throw new Error("Better Auth secret must be 32 bytes encoded as hex.");
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 (!/^DATABASE_URL=.*$/m.test(example) || !/^BETTER_AUTH_SECRET=.*$/m.test(example)) {
3365
- throw new Error("Server .env.example must contain DATABASE_URL and BETTER_AUTH_SECRET entries.");
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 (!Number.isInteger(databasePort) || databasePort < 1 || databasePort > 65535) {
3368
- throw new Error("Database port must be an integer between 1 and 65535.");
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
- const databaseUrl = `postgresql://app:app@localhost:${databasePort}/app?sslmode=disable`;
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
- var SCOPE_EXTENSIONS2 = new Set([".json", ".jsonc", ".md", ".ts", ".tsx", ".yaml", ".yml"]);
3374
- function isScopeRewriteFile(path) {
3375
- return SCOPE_EXTENSIONS2.has(extname(path)) || path.replaceAll("\\", "/").endsWith("/.vite-hooks/pre-commit");
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 isTemplatePathExcluded(relativePath) {
3378
- const normalized = relativePath.replaceAll("\\", "/");
3379
- const parts = normalized.split("/").filter(Boolean);
3380
- const basename = parts.at(-1) ?? "";
3381
- if (parts.includes(".git") || parts.includes(".worktrees") || parts.includes("node_modules"))
3382
- return true;
3383
- if (parts.includes(".terraform"))
3384
- return true;
3385
- if (parts.includes(".impeccable"))
3386
- return true;
3387
- if (parts.includes(".agent-evals") || parts.includes(".agents") || parts.includes(".claude"))
3388
- return true;
3389
- if (normalized === "skills-lock.json")
3390
- return true;
3391
- if (normalized === "internal" || normalized.startsWith("internal/"))
3392
- return true;
3393
- if (normalized === "marketing" || normalized.startsWith("marketing/"))
3394
- return true;
3395
- if (normalized === ".github/workflows/internal-commercial.yml")
3396
- return true;
3397
- if (normalized === "apps/worker" || normalized.startsWith("apps/worker/"))
3398
- return true;
3399
- if (parts.includes("dist") || parts.includes("out"))
3400
- return true;
3401
- if (parts.includes(".next") || parts.includes(".source") || parts.includes(".turbo"))
3402
- return true;
3403
- if (normalized === "bun.lock")
3404
- return true;
3405
- if (normalized === ".vercelignore")
3406
- return true;
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(/^([a-f0-9]{64})\s+\*?([^\s]+)$/i);
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
- async function downloadLicensedTemplate(target, licenseKey, sourceVersion, onDownloadProgress) {
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 releaseTag = sourceVersion ?? (await resolveRelease(licenseKey, baseUrl)).latest;
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
- if (options.from)
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
- await neutralizeAgentInstructions(target);
3689
- await neutralizeSourceCli(target, options.scope, options.brandName);
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
- await generateEnvironmentFiles(target, options.databasePort);
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 === "--license-key")
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(/\p{Mark}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
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 rename2(staging, target);
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, "pnpm dev"].join(`
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 {