superslate 0.3.0 → 0.8.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 +246 -71
  2. package/package.json +4 -3
package/dist/cli.js CHANGED
@@ -1329,6 +1329,7 @@ function isUnicodeSupported() {
1329
1329
  }
1330
1330
  var unicode = isUnicodeSupported();
1331
1331
  var isCI = () => process.env.CI === "true";
1332
+ var isTTY = (o2) => o2.isTTY === true;
1332
1333
  var unicodeOr = (o2, e) => unicode ? o2 : e;
1333
1334
  var S_STEP_ACTIVE = unicodeOr("◆", "*");
1334
1335
  var S_STEP_CANCEL = unicodeOr("■", "x");
@@ -1591,6 +1592,46 @@ var u4 = {
1591
1592
  heavy: unicodeOr("━", "="),
1592
1593
  block: unicodeOr("█", "#")
1593
1594
  };
1595
+ function progress({
1596
+ style: o2 = "heavy",
1597
+ max: d = 100,
1598
+ size: v = 40,
1599
+ ...x
1600
+ } = {}) {
1601
+ const r2 = spinner(x);
1602
+ let a2 = 0, n3 = "";
1603
+ const c2 = Math.max(1, d), l2 = Math.max(1, v), S = (t2) => {
1604
+ switch (t2) {
1605
+ case "initial":
1606
+ case "active":
1607
+ return (e) => styleText2("magenta", e);
1608
+ case "error":
1609
+ case "cancel":
1610
+ return (e) => styleText2("red", e);
1611
+ case "submit":
1612
+ return (e) => styleText2("green", e);
1613
+ default:
1614
+ return (e) => styleText2("magenta", e);
1615
+ }
1616
+ }, p2 = (t2, e) => {
1617
+ const m2 = Math.floor(a2 / c2 * l2);
1618
+ return `${S(t2)(u4[o2].repeat(m2))}${styleText2("dim", u4[o2].repeat(l2 - m2))} ${e}`;
1619
+ }, h2 = (t2 = "") => {
1620
+ n3 = t2, r2.start(p2("initial", t2));
1621
+ }, g2 = (t2 = 1, e) => {
1622
+ a2 = Math.min(c2, t2 + a2), r2.message(p2("active", e ?? n3)), n3 = e ?? n3;
1623
+ };
1624
+ return {
1625
+ start: h2,
1626
+ stop: r2.stop,
1627
+ cancel: r2.cancel,
1628
+ error: r2.error,
1629
+ clear: r2.clear,
1630
+ advance: g2,
1631
+ isCancelled: r2.isCancelled,
1632
+ message: (t2) => g2(0, t2)
1633
+ };
1634
+ }
1594
1635
  var SELECT_INSTRUCTIONS = [
1595
1636
  `${styleText2("dim", "↑/↓")} to navigate`,
1596
1637
  `${styleText2("dim", "Enter:")} confirm`
@@ -1640,7 +1681,7 @@ import { existsSync, readdirSync, statSync } from "fs";
1640
1681
  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";
1641
1682
  import { createConnection, createServer } from "net";
1642
1683
  import { fileURLToPath as fileURLToPath2 } from "url";
1643
- import { dirname as dirname2, join as join8, relative as relative2, resolve as resolve4, sep } from "path";
1684
+ import { dirname as dirname3, join as join8, relative as relative2, resolve as resolve4, sep } from "path";
1644
1685
 
1645
1686
  // src/constants.ts
1646
1687
  var CLI_NAME = "superslate";
@@ -1653,6 +1694,7 @@ var BUYER_CLI_NAME = "create-app";
1653
1694
  var BUYER_CLI_PACKAGE_NAME = "create-app";
1654
1695
  var DEFAULT_SCOPE = "@app";
1655
1696
  var SOURCE_BRAND_NAME = "superslate";
1697
+ var DOCS_BASE_URL = "https://docs.superslate.dev";
1656
1698
  var DELIVERY_BASE_URL = "https://dl.superslate.dev";
1657
1699
  var DELIVERY_URL_VARIABLE = "SUPERSLATE_DELIVERY_URL";
1658
1700
  var LICENSE_KEY_VARIABLE2 = "SUPERSLATE_LICENSE_KEY";
@@ -2278,9 +2320,17 @@ function isExecutable(path) {
2278
2320
 
2279
2321
  // src/agent-setup.ts
2280
2322
  var CLIENTS = ["codex", "claude", "cursor"];
2281
- var SERVER_NAME = "superslate-docs";
2282
- var CODEX_START = "# superslate-agent-context:start";
2283
- var CODEX_END = "# superslate-agent-context:end";
2323
+ var SERVER_NAME = "agent-docs";
2324
+ var CODEX_START = "# agent-context:start";
2325
+ var CODEX_END = "# agent-context:end";
2326
+ var AGENT_CONTEXT_TOOLS = [
2327
+ "search_docs",
2328
+ "search_implementation",
2329
+ "get_repository_map",
2330
+ "plan_change",
2331
+ "get_invariants",
2332
+ "get_verification"
2333
+ ];
2284
2334
  function takeValue(args, index, flag) {
2285
2335
  const value = args[index + 1];
2286
2336
  if (!value || value.startsWith("-"))
@@ -2311,7 +2361,7 @@ function parseAgentSetupArguments(argv) {
2311
2361
  function agentSetupUsage(cliName) {
2312
2362
  return `Usage: ${cliName} agent setup [options]
2313
2363
 
2314
- Connect local coding clients to this checkout's read-only superslate documentation server.
2364
+ Connect local coding clients to this checkout's read-only project context server.
2315
2365
 
2316
2366
  Options:
2317
2367
  --clients <list> Comma-separated clients: codex,claude,cursor
@@ -2332,12 +2382,12 @@ async function findAgentRepositoryRoot(start = process.cwd()) {
2332
2382
  }
2333
2383
  const parent = dirname(current);
2334
2384
  if (parent === current)
2335
- throw new Error("Run agent setup inside a superslate repository.");
2385
+ throw new Error("Run agent setup inside a project repository.");
2336
2386
  current = parent;
2337
2387
  }
2338
2388
  }
2339
2389
  function serverCommand(root) {
2340
- return { command: "pnpm", args: ["--dir", resolve(root), "superslate:mcp"] };
2390
+ return { command: "pnpm", args: ["--dir", resolve(root), "agent:mcp"] };
2341
2391
  }
2342
2392
  function codexManagedBlock(root) {
2343
2393
  const server = serverCommand(root);
@@ -2347,7 +2397,7 @@ function codexManagedBlock(root) {
2347
2397
  command = ${JSON.stringify(server.command)}
2348
2398
  args = [${args}]
2349
2399
  required = false
2350
- enabled_tools = ["search_docs", "get_repository_map", "plan_change", "get_invariants", "get_verification"]
2400
+ enabled_tools = ${JSON.stringify(AGENT_CONTEXT_TOOLS)}
2351
2401
  ${CODEX_END}`;
2352
2402
  }
2353
2403
  function mergeCodexConfig(existing, root) {
@@ -2356,14 +2406,14 @@ function mergeCodexConfig(existing, root) {
2356
2406
  const end = existing.indexOf(CODEX_END);
2357
2407
  if (start >= 0 || end >= 0) {
2358
2408
  if (start < 0 || end < start)
2359
- throw new Error("Codex config has an incomplete superslate block.");
2409
+ throw new Error("Codex config has an incomplete agent-context block.");
2360
2410
  const after = end + CODEX_END.length;
2361
2411
  return `${existing.slice(0, start).trimEnd()}
2362
2412
 
2363
2413
  ${managed}${existing.slice(after)}`.trimStart();
2364
2414
  }
2365
- if (/^\[mcp_servers\.(?:superslate-docs|superslate_docs|"superslate-docs")\]$/m.test(existing)) {
2366
- throw new Error("Codex already has an unmanaged superslate-docs server; remove or rename it first.");
2415
+ if (/^\[mcp_servers\.(?:agent-docs|agent_docs|"agent-docs")\]$/m.test(existing)) {
2416
+ throw new Error("Codex already has an unmanaged agent-docs server; remove or rename it first.");
2367
2417
  }
2368
2418
  return `${existing.trimEnd()}${existing.trim() ? `
2369
2419
 
@@ -2389,7 +2439,7 @@ function mergeCursorConfig(existing, root) {
2389
2439
  const current = servers[SERVER_NAME];
2390
2440
  const desired = serverCommand(root);
2391
2441
  if (current !== undefined && JSON.stringify(current) !== JSON.stringify(desired)) {
2392
- throw new Error("Cursor already has a different superslate-docs server; remove or rename it first.");
2442
+ throw new Error("Cursor already has a different agent-docs server; remove or rename it first.");
2393
2443
  }
2394
2444
  config.mcpServers = { ...servers, [SERVER_NAME]: desired };
2395
2445
  return `${JSON.stringify(config, null, 2)}
@@ -2419,7 +2469,7 @@ async function addGitInfoExcludes(root, paths) {
2419
2469
  const prefix = existing && !existing.endsWith(`
2420
2470
  `) ? `
2421
2471
  ` : "";
2422
- const marker = "# superslate local agent configuration";
2472
+ const marker = "# Local agent configuration";
2423
2473
  const heading = existing.split(/\r?\n/).includes(marker) ? "" : `${marker}
2424
2474
  `;
2425
2475
  await atomicWrite(excludePath, `${existing}${prefix}${heading}${missing.join(`
@@ -2450,10 +2500,10 @@ async function configureClaude(root) {
2450
2500
  if (existing.exitCode === 0) {
2451
2501
  const combined = `${existing.stdout}
2452
2502
  ${existing.stderr}`;
2453
- if (combined.includes("superslate:mcp") && combined.includes(root)) {
2503
+ if (combined.includes("agent:mcp") && combined.includes(root)) {
2454
2504
  return "Claude Code already has the matching local server.";
2455
2505
  }
2456
- throw new Error("Claude Code already has a different superslate-docs server; remove it first.");
2506
+ throw new Error("Claude Code already has a different agent-docs server; remove it first.");
2457
2507
  }
2458
2508
  const server = serverCommand(root);
2459
2509
  const result = await capture(["claude", "mcp", "add", SERVER_NAME, "--scope", "local", "--", server.command, ...server.args], root);
@@ -2464,14 +2514,15 @@ ${existing.stderr}`;
2464
2514
  return "Configured Claude Code local project scope.";
2465
2515
  }
2466
2516
  async function smokeServer(root) {
2467
- const result = await capture(["pnpm", "--dir", root, "superslate:mcp", "--check", "--root", root], root);
2517
+ const result = await capture(["pnpm", "--dir", root, "agent:mcp", "--check", "--root", root], root);
2468
2518
  if (result.exitCode !== 0) {
2469
2519
  const output = result.stderr.trim() || result.stdout.trim();
2470
- throw new Error(`superslate MCP self-check failed${output ? `: ${output}` : "."}`);
2520
+ throw new Error(`Agent context MCP self-check failed${output ? `: ${output}` : "."}`);
2471
2521
  }
2472
2522
  const report = JSON.parse(result.stdout);
2473
- if (!report.ok || report.tools !== 5)
2474
- throw new Error("superslate MCP self-check was incomplete.");
2523
+ if (!report.ok || report.tools !== AGENT_CONTEXT_TOOLS.length) {
2524
+ throw new Error("Agent context MCP self-check was incomplete.");
2525
+ }
2475
2526
  }
2476
2527
  function detectedClients() {
2477
2528
  return CLIENTS.filter((client) => {
@@ -2541,7 +2592,7 @@ async function runAgentSetup(argv, cliName) {
2541
2592
  const clients = await chooseClients(parsed);
2542
2593
  if (!clients)
2543
2594
  return;
2544
- intro2("superslate local agent context");
2595
+ intro2("Local agent context");
2545
2596
  if (parsed.dryRun) {
2546
2597
  for (const client of clients) {
2547
2598
  log2.info(`${client}
@@ -2550,16 +2601,16 @@ ${dryRunDescription(client, root)}`);
2550
2601
  outro2("Dry run complete. No client configuration was changed.");
2551
2602
  return;
2552
2603
  }
2553
- const progress = spinner2();
2554
- progress.start("Testing the local MCP server");
2604
+ const progress2 = spinner2();
2605
+ progress2.start("Testing the local MCP server");
2555
2606
  await smokeServer(root);
2556
- progress.stop("Local MCP server passed its protocol self-check");
2607
+ progress2.stop("Local MCP server passed its protocol self-check");
2557
2608
  for (const client of clients) {
2558
2609
  const result = client === "codex" ? await configureCodex(root) : client === "cursor" ? await configureCursor(root) : await configureClaude(root);
2559
2610
  log2.success(`${client}: ${result}`);
2560
2611
  }
2561
2612
  log2.info("Restart configured clients, then ask: “Use plan_change for my next feature.”");
2562
- outro2("superslate agent context is ready.");
2613
+ outro2("Agent context is ready.");
2563
2614
  }
2564
2615
 
2565
2616
  // src/deployment-cli.ts
@@ -2694,7 +2745,8 @@ Create a GitHub environment named \`${config.environment}\` and configure:
2694
2745
  - a \`BACKEND_HCL\` secret containing the selected remote-state backend configuration;
2695
2746
  - a \`TFVARS_JSON\` secret containing only:
2696
2747
  - ${providerInputs};
2697
- - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`.
2748
+ - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a
2749
+ public marketing site.
2698
2750
 
2699
2751
  ## Cloud preparation
2700
2752
 
@@ -3203,7 +3255,8 @@ Create a GitHub environment named \`${config.environment}\` and configure:
3203
3255
  - a \`BACKEND_HCL\` secret containing the selected remote-state backend configuration;
3204
3256
  - a \`TFVARS_JSON\` secret containing only:
3205
3257
  - ${providerInputs};
3206
- - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`.
3258
+ - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a
3259
+ public marketing site.
3207
3260
 
3208
3261
  ## Cloud preparation
3209
3262
 
@@ -3290,14 +3343,14 @@ import { execFile } from "node:child_process";
3290
3343
  import { createHash } from "node:crypto";
3291
3344
  import { cp, lstat, mkdir as mkdir4, mkdtemp, readFile as readFile5, readdir, rm as rm4, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
3292
3345
  import { tmpdir } from "node:os";
3293
- import { basename, join as join7, relative, resolve as resolve3 } from "node:path";
3346
+ import { basename, dirname as dirname2, join as join7, relative, resolve as resolve3 } from "node:path";
3294
3347
  import { promisify } from "node:util";
3295
3348
 
3296
3349
  // src/delivery.ts
3297
3350
  import { createWriteStream } from "node:fs";
3298
3351
  import { rm as rm3 } from "node:fs/promises";
3299
3352
  import { join as join6 } from "node:path";
3300
- import { Readable } from "node:stream";
3353
+ import { Readable, Transform } from "node:stream";
3301
3354
  import { pipeline } from "node:stream/promises";
3302
3355
  var RELEASE_TAG_PATTERN2 = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
3303
3356
  var INDEX_TIMEOUT_MS = 15000;
@@ -3357,7 +3410,24 @@ async function resolveRelease(licenseKey, baseUrl = deliveryBaseUrl()) {
3357
3410
  }
3358
3411
  return index;
3359
3412
  }
3360
- async function downloadAsset(licenseKey, releaseTag, assetName, directory, baseUrl = deliveryBaseUrl()) {
3413
+ function readContentLength(response) {
3414
+ const header = response.headers.get("content-length");
3415
+ if (!header)
3416
+ return;
3417
+ const total = Number(header);
3418
+ return Number.isSafeInteger(total) && total > 0 ? total : undefined;
3419
+ }
3420
+ function countBytes(total, onBytes) {
3421
+ let received = 0;
3422
+ return new Transform({
3423
+ transform(chunk, _encoding, done) {
3424
+ received += chunk.length;
3425
+ onBytes(received, total);
3426
+ done(null, chunk);
3427
+ }
3428
+ });
3429
+ }
3430
+ async function downloadAsset(licenseKey, releaseTag, assetName, directory, baseUrl = deliveryBaseUrl(), onBytes) {
3361
3431
  const response = await requestDelivery(`${baseUrl}/v1/source/${encodeURIComponent(releaseTag)}/${encodeURIComponent(assetName)}`, {
3362
3432
  headers: { authorization: `Bearer ${licenseKey}` },
3363
3433
  signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
@@ -3365,8 +3435,13 @@ async function downloadAsset(licenseKey, releaseTag, assetName, directory, baseU
3365
3435
  if (!response.body)
3366
3436
  throw new Error(`Source delivery returned an empty ${assetName}.`);
3367
3437
  const path = join6(directory, assetName);
3438
+ const source = Readable.fromWeb(response.body);
3368
3439
  try {
3369
- await pipeline(Readable.fromWeb(response.body), createWriteStream(path));
3440
+ if (onBytes) {
3441
+ await pipeline(source, countBytes(readContentLength(response), onBytes), createWriteStream(path));
3442
+ } else {
3443
+ await pipeline(source, createWriteStream(path));
3444
+ }
3370
3445
  } catch {
3371
3446
  await rm3(path, { force: true });
3372
3447
  throw new Error(`Downloading ${assetName} was interrupted. Try again in a few minutes.`);
@@ -3383,6 +3458,9 @@ var BRAND_MANIFEST = [
3383
3458
  { path: "apps/web/public/favicon/site.webmanifest", occurrences: 2 },
3384
3459
  { path: "apps/web/public/offline.html", occurrences: 6 },
3385
3460
  { path: "apps/docs/lib/shared.ts", occurrences: 1 },
3461
+ { path: "apps/docs/README.md", occurrences: 1 },
3462
+ { path: "README.md", occurrences: 1 },
3463
+ { path: "docs/meta.json", occurrences: 1 },
3386
3464
  { path: "apps/server/.env.example", occurrences: 1 },
3387
3465
  { path: "apps/server/src/config/env.ts", occurrences: 2 },
3388
3466
  { path: "apps/server/src/config/env.test.ts", occurrences: 1 },
@@ -3406,6 +3484,7 @@ var BRAND_MANIFEST = [
3406
3484
  { path: "apps/server/email-templates/security-alert.html", occurrences: 4 },
3407
3485
  { path: "apps/server/email-templates/verify-email.html", occurrences: 5 },
3408
3486
  { path: "apps/server/email-templates/reset-password.html", occurrences: 5 },
3487
+ { path: "apps/server/email-templates/organization-invitation.html", occurrences: 3 },
3409
3488
  { path: "apps/server/email-templates/welcome-email.html", occurrences: 8 }
3410
3489
  ];
3411
3490
  function validateAppName2(value) {
@@ -3454,7 +3533,7 @@ function rewriteBrandContent(content, brandName) {
3454
3533
  if (error)
3455
3534
  throw new Error(error);
3456
3535
  const escapedSourceBrand = SOURCE_BRAND_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3457
- const sourceBrandPattern = new RegExp(`\\b${escapedSourceBrand}\\b`, "g");
3536
+ const sourceBrandPattern = new RegExp(`\\b${escapedSourceBrand}\\b`, "gi");
3458
3537
  const matches = content.match(sourceBrandPattern);
3459
3538
  if (brandName === SOURCE_BRAND_NAME || !matches)
3460
3539
  return { content, replacements: 0 };
@@ -3552,6 +3631,20 @@ function rewriteRootPackageName(content, appName) {
3552
3631
  return { content: `${JSON.stringify(parsed, null, 2)}
3553
3632
  `, replacements: 1 };
3554
3633
  }
3634
+ var WORKER_NAME_DECLARATION = /^name = "[^"]*"$/m;
3635
+ function rewriteWorkerName(content, appName) {
3636
+ const error = validateAppName2(appName);
3637
+ if (error)
3638
+ throw new Error(error);
3639
+ const declaration = content.match(WORKER_NAME_DECLARATION)?.[0];
3640
+ if (!declaration) {
3641
+ throw new Error("Cloudflare Worker configuration is missing a name declaration.");
3642
+ }
3643
+ const replacement = `name = "${appName}"`;
3644
+ if (declaration === replacement)
3645
+ return { content, replacements: 0 };
3646
+ return { content: content.replace(WORKER_NAME_DECLARATION, replacement), replacements: 1 };
3647
+ }
3555
3648
  function replaceMarkdownSection(content, heading, replacement) {
3556
3649
  const marker = `## ${heading}
3557
3650
  `;
@@ -3604,6 +3697,12 @@ into this repository.
3604
3697
  }
3605
3698
  return rewritten;
3606
3699
  }
3700
+ var BUYER_README_HEADING = "## Create your product";
3701
+ function stripDistributionReadmeBootstrap(content) {
3702
+ if (!content.includes(BUYER_README_HEADING))
3703
+ return content;
3704
+ return replaceMarkdownSection(content, "Create your product", "");
3705
+ }
3607
3706
  var REQUIRED_BUYER_CLI_CONSTANTS = ["CLI_NAME", "CLI_PACKAGE_NAME", "DEFAULT_SCOPE"];
3608
3707
  function rewriteBuyerCliConstants(content, scope, brandName) {
3609
3708
  const buyerVariablePrefix = BUYER_CLI_NAME.toUpperCase().replaceAll("-", "_");
@@ -3614,6 +3713,7 @@ function rewriteBuyerCliConstants(content, scope, brandName) {
3614
3713
  ["SOURCE_BRAND_NAME", brandName],
3615
3714
  ["TEMPLATE_REPOSITORY", "OWNER/REPOSITORY"],
3616
3715
  ["DELIVERY_BASE_URL", "https://releases.example.com"],
3716
+ ["DOCS_BASE_URL", "https://docs.example.com"],
3617
3717
  ["DELIVERY_URL_VARIABLE", `${buyerVariablePrefix}_DELIVERY_URL`],
3618
3718
  ["LICENSE_KEY_VARIABLE", `${buyerVariablePrefix}_LICENSE_KEY`],
3619
3719
  ["LICENSE_KEY_PREFIX", "LICENSE-"]
@@ -3633,6 +3733,7 @@ function rewriteBuyerCliConstants(content, scope, brandName) {
3633
3733
  CLI_PACKAGE_NAME,
3634
3734
  SOURCE_BRAND_NAME,
3635
3735
  DELIVERY_BASE_URL,
3736
+ DOCS_BASE_URL,
3636
3737
  DELIVERY_URL_VARIABLE,
3637
3738
  LICENSE_KEY_VARIABLE2,
3638
3739
  LICENSE_KEY_PREFIX
@@ -3716,12 +3817,16 @@ function isTemplatePathExcluded(relativePath) {
3716
3817
  return true;
3717
3818
  if (normalized === "internal" || normalized.startsWith("internal/"))
3718
3819
  return true;
3820
+ if (normalized === "marketing" || normalized.startsWith("marketing/"))
3821
+ return true;
3719
3822
  if (normalized === ".github/workflows/internal-commercial.yml")
3720
3823
  return true;
3721
3824
  if (normalized === "apps/worker" || normalized.startsWith("apps/worker/"))
3722
3825
  return true;
3723
3826
  if (parts.includes("dist") || parts.includes("out"))
3724
3827
  return true;
3828
+ if (parts.includes(".next") || parts.includes(".source") || parts.includes(".turbo"))
3829
+ return true;
3725
3830
  if (normalized === "bun.lock")
3726
3831
  return true;
3727
3832
  if (normalized === ".vercelignore")
@@ -3734,6 +3839,8 @@ function isTemplatePathExcluded(relativePath) {
3734
3839
  return true;
3735
3840
  if (normalized === "packages/create-app/src/cli-brand-contract.test.ts")
3736
3841
  return true;
3842
+ if (normalized === "packages/create-app/src/commercial-contract.test.ts")
3843
+ return true;
3737
3844
  if (normalized === "packages/create-app/LICENSE")
3738
3845
  return true;
3739
3846
  if (normalized === "packages/create-app/README.md")
@@ -3762,6 +3869,29 @@ async function assertEmptyTarget(targetDirectory) {
3762
3869
  throw error;
3763
3870
  }
3764
3871
  }
3872
+ async function trackedTemplatePaths(sourceDirectory) {
3873
+ const topLevel = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
3874
+ cwd: sourceDirectory
3875
+ }).then((result) => resolve3(result.stdout.trim())).catch(() => {
3876
+ return;
3877
+ });
3878
+ if (topLevel !== sourceDirectory)
3879
+ return;
3880
+ const { stdout: stdout2 } = await execFileAsync("git", ["ls-files", "-z"], {
3881
+ cwd: sourceDirectory,
3882
+ maxBuffer: 64 * 1024 * 1024
3883
+ });
3884
+ const tracked = new Set;
3885
+ for (const path of stdout2.split("\x00")) {
3886
+ if (!path)
3887
+ continue;
3888
+ tracked.add(path);
3889
+ for (let parent = dirname2(path);parent !== "."; parent = dirname2(parent)) {
3890
+ tracked.add(parent);
3891
+ }
3892
+ }
3893
+ return tracked;
3894
+ }
3765
3895
  async function copyLocalTemplate(source, target) {
3766
3896
  const sourceDirectory = resolve3(source);
3767
3897
  const sourceStat = await lstat(sourceDirectory).catch(() => {
@@ -3769,29 +3899,28 @@ async function copyLocalTemplate(source, target) {
3769
3899
  });
3770
3900
  if (!sourceStat?.isDirectory())
3771
3901
  throw new Error(`Template path is not a directory: ${sourceDirectory}`);
3772
- await assertNoSymbolicLinks(sourceDirectory, sourceDirectory, true);
3902
+ const tracked = await trackedTemplatePaths(sourceDirectory);
3903
+ const isTemplatePathIncluded = (relativePath) => relativePath === "" || !isTemplatePathExcluded(relativePath) && (!tracked || tracked.has(relativePath));
3904
+ await assertNoSymbolicLinks(sourceDirectory, sourceDirectory, isTemplatePathIncluded);
3773
3905
  await mkdir4(target, { recursive: true });
3774
3906
  await cp(sourceDirectory, target, {
3775
3907
  recursive: true,
3776
- filter: (path) => {
3777
- const relativePath = relative(sourceDirectory, path);
3778
- return relativePath === "" || !isTemplatePathExcluded(relativePath);
3779
- }
3908
+ filter: (path) => isTemplatePathIncluded(relative(sourceDirectory, path).replaceAll("\\", "/"))
3780
3909
  });
3781
3910
  await assertNoSymbolicLinks(target);
3782
3911
  }
3783
- async function assertNoSymbolicLinks(directory, root = directory, skipExcludedPaths = false) {
3912
+ async function assertNoSymbolicLinks(directory, root = directory, isIncluded) {
3784
3913
  const entries = await readdir(directory, { withFileTypes: true });
3785
3914
  for (const entry of entries) {
3786
3915
  const path = join7(directory, entry.name);
3787
- const relativePath = relative(root, path);
3788
- if (skipExcludedPaths && isTemplatePathExcluded(relativePath))
3916
+ const relativePath = relative(root, path).replaceAll("\\", "/");
3917
+ if (isIncluded && !isIncluded(relativePath))
3789
3918
  continue;
3790
3919
  if (entry.isSymbolicLink()) {
3791
3920
  throw new Error(`Template contains a symbolic link, which is not supported: ${path}`);
3792
3921
  }
3793
3922
  if (entry.isDirectory())
3794
- await assertNoSymbolicLinks(path, root, skipExcludedPaths);
3923
+ await assertNoSymbolicLinks(path, root, isIncluded);
3795
3924
  }
3796
3925
  }
3797
3926
  function verifyReleaseMetadata(checksum, manifest, artifactName, digest, templateRef) {
@@ -3806,14 +3935,14 @@ function verifyReleaseMetadata(checksum, manifest, artifactName, digest, templat
3806
3935
  throw new Error(`${CLI_NAME2} release manifest does not match the downloaded source artifact.`);
3807
3936
  }
3808
3937
  }
3809
- async function downloadLicensedTemplate(target, licenseKey, sourceVersion) {
3938
+ async function downloadLicensedTemplate(target, licenseKey, sourceVersion, onDownloadProgress) {
3810
3939
  const baseUrl = deliveryBaseUrl();
3811
3940
  const releaseTag = sourceVersion ?? (await resolveRelease(licenseKey, baseUrl)).latest;
3812
3941
  const { artifact, checksum: checksumName, manifest: manifestName } = releaseAssetNames(releaseTag);
3813
3942
  const temporaryDirectory = await mkdtemp(join7(tmpdir(), `${CLI_NAME2}-`));
3814
3943
  const extractedPath = join7(temporaryDirectory, "template");
3815
3944
  try {
3816
- const archivePath = await downloadAsset(licenseKey, releaseTag, artifact, temporaryDirectory, baseUrl);
3945
+ const archivePath = await downloadAsset(licenseKey, releaseTag, artifact, temporaryDirectory, baseUrl, onDownloadProgress);
3817
3946
  await downloadAsset(licenseKey, releaseTag, checksumName, temporaryDirectory, baseUrl);
3818
3947
  await downloadAsset(licenseKey, releaseTag, manifestName, temporaryDirectory, baseUrl);
3819
3948
  const archive = await readFile5(archivePath);
@@ -3888,6 +4017,14 @@ async function applyBrandRewrite(target, brandName) {
3888
4017
  }
3889
4018
  return report;
3890
4019
  }
4020
+ async function renameFrontendWorker(target, appName) {
4021
+ const workerPath = join7(target, "apps/web/wrangler.toml");
4022
+ if (!await stat4(workerPath).catch(() => {
4023
+ return;
4024
+ }))
4025
+ return;
4026
+ await rewriteFile(workerPath, (content) => rewriteWorkerName(content, appName));
4027
+ }
3891
4028
  async function generateEnvironmentFiles(target, databasePort = 5432) {
3892
4029
  const serverExample = await readFile5(join7(target, "apps/server/.env.example"), "utf8");
3893
4030
  await writeFile4(join7(target, "apps/server/.env"), generateServerEnv(serverExample, undefined, databasePort));
@@ -3901,8 +4038,10 @@ async function generateEnvironmentFiles(target, databasePort = 5432) {
3901
4038
  async function neutralizeAgentInstructions(target) {
3902
4039
  const agentPath = join7(target, "AGENTS.md");
3903
4040
  const claudePath = join7(target, "CLAUDE.md");
4041
+ const readmePath = join7(target, "README.md");
3904
4042
  await writeFile4(agentPath, rewriteBuyerAgentInstructions(await readFile5(agentPath, "utf8")));
3905
4043
  await writeFile4(claudePath, rewriteBuyerClaudeInstructions(await readFile5(claudePath, "utf8")));
4044
+ await writeFile4(readmePath, stripDistributionReadmeBootstrap(await readFile5(readmePath, "utf8")));
3906
4045
  const rootPackagePath = join7(target, "package.json");
3907
4046
  await writeFile4(rootPackagePath, stripSellerOnlyRootScripts(await readFile5(rootPackagePath, "utf8")));
3908
4047
  }
@@ -3965,12 +4104,13 @@ async function scaffold(options) {
3965
4104
  if (options.from)
3966
4105
  await copyLocalTemplate(options.from, target);
3967
4106
  else if (options.licenseKey) {
3968
- await downloadLicensedTemplate(target, options.licenseKey, options.sourceVersion);
4107
+ await downloadLicensedTemplate(target, options.licenseKey, options.sourceVersion, options.onDownloadProgress);
3969
4108
  } else {
3970
4109
  throw new Error(`A local source path or a ${CLI_NAME2} licence key is required.`);
3971
4110
  }
3972
4111
  const packagePath = join7(target, "package.json");
3973
4112
  const packageReplacements = await rewriteFile(packagePath, (content) => rewriteRootPackageName(content, options.appName));
4113
+ await renameFrontendWorker(target, options.appName);
3974
4114
  const scope = await applyScopeRewrite(target, options.scope);
3975
4115
  await neutralizeAgentInstructions(target);
3976
4116
  await neutralizeSourceCli(target, options.scope, options.brandName);
@@ -4009,7 +4149,7 @@ Create a buyer-owned SaaS application from your licensed ${suggestBrandName(CLI_
4009
4149
  Options:
4010
4150
  --license-key <key> Licence key from your purchase (or set ${LICENSE_KEY_VARIABLE})
4011
4151
  --source-version <version> Pin an immutable release (default: the latest you are entitled to)
4012
- --from <path> Use a local source checkout (maintainer/testing)
4152
+ --from <path> Use a local Git checkout, tracked files only (maintainer/testing)
4013
4153
  --scope <namespace> Advanced workspace namespace override (default: derived from brand)
4014
4154
  --brand <name> Display name (default: derived from app name)
4015
4155
  --yes Accept recommended defaults for omitted options
@@ -4222,7 +4362,7 @@ async function collectOptions(parsed) {
4222
4362
  };
4223
4363
  }
4224
4364
  async function readCliVersion() {
4225
- const packageJson = JSON.parse(await readFile6(resolve4(dirname2(fileURLToPath2(import.meta.url)), "../package.json"), "utf8"));
4365
+ const packageJson = JSON.parse(await readFile6(resolve4(dirname3(fileURLToPath2(import.meta.url)), "../package.json"), "utf8"));
4226
4366
  if (!packageJson.version)
4227
4367
  throw new Error(`${CLI_NAME} package version is missing.`);
4228
4368
  return packageJson.version;
@@ -4305,35 +4445,71 @@ async function installDependencies(target) {
4305
4445
  throw new Error(`Dependency installation failed.${commandFailureOutput(stdout2, stderr)}`);
4306
4446
  }
4307
4447
  }
4308
- async function runCapturedCommand(command, target) {
4309
- const { stdout: stdout2, stderr, exitCode } = await runCaptured2(command, { cwd: target });
4310
- if (exitCode !== 0) {
4311
- throw new Error(`${command.join(" ")} failed.${commandFailureOutput(stdout2, stderr)}`);
4312
- }
4313
- }
4314
4448
  async function occupiedDevelopmentPorts() {
4315
4449
  const results = await Promise.all([5173, 8000].map(async (port) => ({ port, available: await isPortAvailable(port) })));
4316
4450
  return results.filter((result) => !result.available).map((result) => result.port);
4317
4451
  }
4318
4452
  async function startDevelopment(target) {
4319
- await runStep("Starting local Postgres", "Local Postgres is ready", () => runCapturedCommand(["docker", "compose", "up", "-d", "--wait", "postgres"], target));
4320
4453
  log.info(`Web http://localhost:5173
4321
4454
  API http://localhost:8000`);
4322
4455
  outro("Development servers are starting. Ctrl+C stops app servers; Postgres stays running.");
4323
- const exitCode = await runInherited(["vp", "run", "-r", "--parallel", "dev"], { cwd: target });
4456
+ const exitCode = await runInherited(["pnpm", "run", "dev"], { cwd: target });
4324
4457
  if (exitCode !== 0 && exitCode !== 130) {
4325
4458
  throw new Error(`Development servers exited with code ${exitCode}.`);
4326
4459
  }
4327
4460
  }
4328
- async function runStep(pending, complete, action, indicator = "dots") {
4329
- const progress = spinner({ indicator });
4330
- progress.start(pending);
4461
+ var BYTE_UNITS = ["B", "KB", "MB", "GB"];
4462
+ var BYTE_SCALE = 1024;
4463
+ var PROGRESS_MAX = 100;
4464
+ var PROGRESS_BAR_WIDTH = 40;
4465
+ var PROGRESS_MIN_INTERVAL_MS = 80;
4466
+ var PROGRESS_MIN_DELTA = 0.5;
4467
+ function formatBytes(bytes) {
4468
+ let value = bytes;
4469
+ let unit = 0;
4470
+ while (value >= BYTE_SCALE && unit < BYTE_UNITS.length - 1) {
4471
+ value /= BYTE_SCALE;
4472
+ unit += 1;
4473
+ }
4474
+ return `${unit === 0 ? value : value.toFixed(1)} ${BYTE_UNITS[unit]}`;
4475
+ }
4476
+ function supportsAnimation() {
4477
+ return isTTY(process.stdout) && !isCI();
4478
+ }
4479
+ function createScaffoldReporter(pending, expectsDownload) {
4480
+ if (!expectsDownload || !supportsAnimation())
4481
+ return { handle: spinner() };
4482
+ const bar = progress({
4483
+ style: "heavy",
4484
+ size: PROGRESS_BAR_WIDTH,
4485
+ max: PROGRESS_MAX
4486
+ });
4487
+ let shown = 0;
4488
+ let emittedAt = 0;
4489
+ return {
4490
+ handle: bar,
4491
+ onDownloadProgress: (received, total) => {
4492
+ const percent = total ? Math.min(PROGRESS_MAX, received / total * PROGRESS_MAX) : shown;
4493
+ const finished = total !== undefined && received >= total;
4494
+ const now = performance.now();
4495
+ if (!finished && now - emittedAt < PROGRESS_MIN_INTERVAL_MS && percent - shown < PROGRESS_MIN_DELTA) {
4496
+ return;
4497
+ }
4498
+ emittedAt = now;
4499
+ const label = finished ? pending : total === undefined ? `Downloading source ${formatBytes(received)}` : `Downloading source ${formatBytes(received)} / ${formatBytes(total)}`;
4500
+ bar.advance(percent - shown, label);
4501
+ shown = percent;
4502
+ }
4503
+ };
4504
+ }
4505
+ async function runStep(pending, complete, action, handle) {
4506
+ handle.start(pending);
4331
4507
  try {
4332
4508
  const result = await action();
4333
- progress.stop(complete);
4509
+ handle.stop(complete);
4334
4510
  return result;
4335
4511
  } catch (error) {
4336
- progress.error(`${pending} failed`);
4512
+ handle.error(`${pending} failed`);
4337
4513
  throw error;
4338
4514
  }
4339
4515
  }
@@ -4342,10 +4518,12 @@ async function createApplication(options) {
4342
4518
  await assertTargetAvailable(target);
4343
4519
  assertSiblingTarget(options.from, target);
4344
4520
  const databasePort = await findAvailablePort();
4345
- await mkdir5(dirname2(target), { recursive: true });
4346
- const staging = await mkdtemp2(join8(dirname2(target), `.${options.appName}-${CLI_NAME}-`));
4521
+ await mkdir5(dirname3(target), { recursive: true });
4522
+ const staging = await mkdtemp2(join8(dirname3(target), `.${options.appName}-${CLI_NAME}-`));
4523
+ const preparing = "Preparing your application";
4524
+ const reporter = createScaffoldReporter(preparing, Boolean(options.licenseKey));
4347
4525
  try {
4348
- await runStep("Preparing your application", "Application source prepared", () => scaffold({
4526
+ await runStep(preparing, "Application source prepared", () => scaffold({
4349
4527
  appName: options.appName,
4350
4528
  scope: options.scope,
4351
4529
  brandName: options.brandName,
@@ -4354,13 +4532,14 @@ async function createApplication(options) {
4354
4532
  licenseKey: options.licenseKey,
4355
4533
  sourceVersion: options.sourceVersion,
4356
4534
  initializeGit: options.initializeGit,
4357
- databasePort
4358
- }));
4535
+ databasePort,
4536
+ onDownloadProgress: reporter.onDownloadProgress
4537
+ }), reporter.handle);
4359
4538
  if (options.deployment) {
4360
- await runStep("Configuring deployment", "Deployment target locked", () => applyDeploymentSelection2(staging, options.appName, options.deployment));
4539
+ await runStep("Configuring deployment", "Deployment target locked", () => applyDeploymentSelection2(staging, options.appName, options.deployment), spinner());
4361
4540
  }
4362
4541
  if (options.installDependencies) {
4363
- await runStep("Installing dependencies with pnpm", "Dependencies installed with pnpm", () => installDependencies(staging), "timer");
4542
+ await runStep("Installing dependencies with pnpm", "Dependencies installed with pnpm", () => installDependencies(staging), spinner({ indicator: "timer" }));
4364
4543
  }
4365
4544
  if (await stat5(target).catch(() => {
4366
4545
  return;
@@ -4378,12 +4557,7 @@ function completionDetails(target, options, databasePort) {
4378
4557
  const install = options.installDependencies ? [] : ["vp install --frozen-lockfile"];
4379
4558
  return {
4380
4559
  directory: `./${targetName}`,
4381
- commands: [
4382
- `cd ${targetName}`,
4383
- ...install,
4384
- "docker compose up -d postgres",
4385
- "vp run -r --parallel dev"
4386
- ].join(`
4560
+ commands: [`cd ${targetName}`, ...install, "pnpm dev"].join(`
4387
4561
  `),
4388
4562
  database: `Postgres localhost:${databasePort}`
4389
4563
  };
@@ -4470,6 +4644,7 @@ export {
4470
4644
  printRunnableCommands,
4471
4645
  parseArguments,
4472
4646
  occupiedDevelopmentPorts,
4647
+ formatBytes,
4473
4648
  findAvailablePort,
4474
4649
  describeTargetConflict,
4475
4650
  collectLicenseKey
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superslate",
3
- "version": "0.3.0",
3
+ "version": "0.8.0",
4
4
  "description": "Create a production-oriented SaaS application from superslate.",
5
5
  "keywords": [
6
6
  "bun",
@@ -39,8 +39,9 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@clack/prompts": "^1.7.0",
42
- "@types/bun": "^1.3.14",
43
- "typescript": "^7.0.2"
42
+ "@types/node": "^26.2.0",
43
+ "typescript": "^7.0.2",
44
+ "vitest": "^4.1.10"
44
45
  },
45
46
  "engines": {
46
47
  "node": ">=20"