superslate 0.8.1 → 0.9.0

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