cnippet-stack 0.2.0 → 0.2.1

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/index.js +381 -82
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -557,7 +557,7 @@ var require_logger = __commonJS({
557
557
  return level;
558
558
  },
559
559
  // Can be overridden in the host environment
560
- log: function log2(level) {
560
+ log: function log3(level) {
561
561
  level = logger.lookupLevel(level);
562
562
  if (typeof console !== "undefined" && logger.lookupLevel(logger.level) <= level) {
563
563
  var method = logger.methodMap[level];
@@ -738,8 +738,8 @@ var require_base = __commonJS({
738
738
  _internalProtoAccess.resetLoggedProperties();
739
739
  }
740
740
  };
741
- var log2 = _logger2["default"].log;
742
- exports.log = log2;
741
+ var log3 = _logger2["default"].log;
742
+ exports.log = log3;
743
743
  exports.createFrame = _utils.createFrame;
744
744
  exports.logger = _logger2["default"];
745
745
  }
@@ -5927,8 +5927,9 @@ var require_lib = __commonJS({
5927
5927
  });
5928
5928
 
5929
5929
  // src/program.ts
5930
+ import * as clack3 from "@clack/prompts";
5930
5931
  import { Command } from "commander";
5931
- import pc7 from "picocolors";
5932
+ import pc8 from "picocolors";
5932
5933
 
5933
5934
  // src/commands/doctor.ts
5934
5935
  function runDoctor(recipe) {
@@ -5939,7 +5940,7 @@ function runDoctor(recipe) {
5939
5940
 
5940
5941
  // src/commands/init.ts
5941
5942
  import * as clack2 from "@clack/prompts";
5942
- import pc2 from "picocolors";
5943
+ import pc3 from "picocolors";
5943
5944
 
5944
5945
  // src/compat.ts
5945
5946
  var CompatibilityError = class extends Error {
@@ -5979,10 +5980,12 @@ function checkCompatibility(detection, supports) {
5979
5980
  );
5980
5981
  }
5981
5982
  }
5983
+ function isRequireSatisfied(required, detection) {
5984
+ return required === "prisma" && detection.orm === "prisma";
5985
+ }
5982
5986
  function checkRequires(recipeName, requires, detection) {
5983
5987
  for (const required of requires) {
5984
- const satisfied = required === "prisma" && detection.orm === "prisma";
5985
- if (!satisfied) {
5988
+ if (!isRequireSatisfied(required, detection)) {
5986
5989
  throw new CompatibilityError(
5987
5990
  `"${recipeName}" requires "${required}" to be installed first. Run \`cnippet-stack init ${required}\` first, then re-run this command.`
5988
5991
  );
@@ -6222,6 +6225,7 @@ function toTemplateContext(detection) {
6222
6225
  import fs9 from "fs";
6223
6226
  import { spawnSync } from "child_process";
6224
6227
  import path9 from "path";
6228
+ import { parseDocument, Document } from "yaml";
6225
6229
  function parseDependencySpec(spec) {
6226
6230
  const atIndex = spec.lastIndexOf("@");
6227
6231
  if (atIndex <= 0) return { name: spec, range: "latest" };
@@ -6263,6 +6267,68 @@ function mergePackageJsonDeps(cwd, dependencies, devDependencies) {
6263
6267
  }
6264
6268
  return changed;
6265
6269
  }
6270
+ function findPnpmWorkspaceFile(cwd) {
6271
+ let dir = path9.resolve(cwd);
6272
+ for (; ; ) {
6273
+ const candidate = path9.join(dir, "pnpm-workspace.yaml");
6274
+ if (fs9.existsSync(candidate)) return candidate;
6275
+ const parent = path9.dirname(dir);
6276
+ if (parent === dir) return path9.join(path9.resolve(cwd), "pnpm-workspace.yaml");
6277
+ dir = parent;
6278
+ }
6279
+ }
6280
+ function mergeTrustedBuildScripts(cwd, packageManager, trusted) {
6281
+ if (trusted.length === 0) return false;
6282
+ if (packageManager === "pnpm") {
6283
+ const file = findPnpmWorkspaceFile(cwd);
6284
+ const doc = fs9.existsSync(file) ? parseDocument(fs9.readFileSync(file, "utf8")) : new Document({});
6285
+ const contents = doc.toJS() ?? {};
6286
+ const list = new Set(contents.onlyBuiltDependencies ?? []);
6287
+ const allow = { ...contents.allowBuilds };
6288
+ let changed = !fs9.existsSync(file);
6289
+ for (const name of trusted) {
6290
+ if (!list.has(name)) {
6291
+ list.add(name);
6292
+ changed = true;
6293
+ }
6294
+ if (typeof allow[name] !== "boolean") {
6295
+ allow[name] = true;
6296
+ changed = true;
6297
+ }
6298
+ }
6299
+ if (!changed) return false;
6300
+ doc.set("onlyBuiltDependencies", [...list]);
6301
+ doc.set("allowBuilds", allow);
6302
+ fs9.writeFileSync(file, doc.toString(), "utf8");
6303
+ return true;
6304
+ }
6305
+ if (packageManager === "bun") {
6306
+ const file = path9.join(cwd, "package.json");
6307
+ const pkg = JSON.parse(fs9.readFileSync(file, "utf8"));
6308
+ pkg.trustedDependencies ??= [];
6309
+ let changed = false;
6310
+ for (const name of trusted) {
6311
+ if (!pkg.trustedDependencies.includes(name)) {
6312
+ pkg.trustedDependencies.push(name);
6313
+ changed = true;
6314
+ }
6315
+ }
6316
+ if (changed) {
6317
+ fs9.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}
6318
+ `, "utf8");
6319
+ }
6320
+ return changed;
6321
+ }
6322
+ return false;
6323
+ }
6324
+ function parseIgnoredBuilds(output) {
6325
+ const match = /Ignored build scripts: ([^\n]+)/.exec(output);
6326
+ if (!match?.[1]) return [];
6327
+ return match[1].split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
6328
+ const atIndex = entry.lastIndexOf("@");
6329
+ return atIndex > 0 ? entry.slice(0, atIndex) : entry;
6330
+ });
6331
+ }
6266
6332
  function envWithLocalBin(cwd) {
6267
6333
  const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
6268
6334
  const existing = process.env[pathKey] ?? "";
@@ -6273,13 +6339,14 @@ function envWithLocalBin(cwd) {
6273
6339
  };
6274
6340
  }
6275
6341
  var defaultCommandRunner = (command, args, cwd) => {
6276
- const result = spawnSync(command, args, {
6342
+ const result = spawnSync([command, ...args].join(" "), {
6277
6343
  cwd,
6344
+ encoding: "utf8",
6278
6345
  env: envWithLocalBin(cwd),
6279
- shell: true,
6280
- stdio: "inherit"
6346
+ shell: true
6281
6347
  });
6282
- return { status: result.status };
6348
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
6349
+ return { output, status: result.status };
6283
6350
  };
6284
6351
  function runInstallCommand(cwd, packageManager, runner = defaultCommandRunner) {
6285
6352
  const { command, args } = installCommandFor(packageManager);
@@ -6547,6 +6614,7 @@ function applyCreate(cwd, file, options) {
6547
6614
  // src/installer/index.ts
6548
6615
  function applyInstall(plan, options) {
6549
6616
  const runner = options.runner ?? defaultCommandRunner;
6617
+ const progress = options.progress ?? {};
6550
6618
  const files = [];
6551
6619
  for (const file of plan.renderedFiles) {
6552
6620
  if (file.strategy === "create") {
@@ -6579,25 +6647,58 @@ function applyInstall(plan, options) {
6579
6647
  plan.manifest.dependencies,
6580
6648
  plan.manifest.devDependencies
6581
6649
  );
6582
- let depsInstallFailed = false;
6650
+ let depsInstall = {
6651
+ ignoredBuilds: [],
6652
+ status: "not-needed"
6653
+ };
6583
6654
  if (depsChanged) {
6655
+ mergeTrustedBuildScripts(
6656
+ options.cwd,
6657
+ options.detection.packageManager,
6658
+ plan.manifest.trustedBuildScripts ?? []
6659
+ );
6660
+ progress.onDepsStart?.(options.detection.packageManager);
6584
6661
  const depsResult = runInstallCommand(
6585
6662
  options.cwd,
6586
6663
  options.detection.packageManager,
6587
6664
  runner
6588
6665
  );
6589
- depsInstallFailed = depsResult.status !== 0;
6666
+ const output = depsResult.output;
6667
+ const ignoredBuilds = output ? parseIgnoredBuilds(output) : [];
6668
+ if (depsResult.status === 0) {
6669
+ depsInstall = { ignoredBuilds, output, status: "ok" };
6670
+ } else if (ignoredBuilds.length > 0) {
6671
+ depsInstall = { ignoredBuilds, output, status: "ignored-builds" };
6672
+ } else {
6673
+ depsInstall = { ignoredBuilds: [], output, status: "failed" };
6674
+ }
6675
+ progress.onDepsDone?.(depsInstall);
6590
6676
  }
6677
+ const depsUsable = depsInstall.status !== "failed";
6591
6678
  const postInstall = plan.postInstall.map((step) => {
6592
- if (depsInstallFailed) {
6593
- return { ...step, status: "skipped" };
6679
+ if (!depsUsable) {
6680
+ const skipped = { ...step, status: "skipped" };
6681
+ progress.onStepDone?.(skipped);
6682
+ return skipped;
6594
6683
  }
6595
6684
  const [command, ...args] = step.command.split(" ");
6596
- if (!command) return { ...step, status: "skipped" };
6685
+ if (!command) {
6686
+ const skipped = { ...step, status: "skipped" };
6687
+ progress.onStepDone?.(skipped);
6688
+ return skipped;
6689
+ }
6690
+ progress.onStepStart?.(step);
6597
6691
  const result = runner(command, args, options.cwd);
6598
- return { ...step, status: result.status === 0 ? "ok" : "failed" };
6692
+ const done = {
6693
+ ...step,
6694
+ output: result.output,
6695
+ status: result.status === 0 ? "ok" : "failed"
6696
+ };
6697
+ progress.onStepDone?.(done);
6698
+ return done;
6599
6699
  });
6600
6700
  return {
6701
+ depsInstall,
6601
6702
  envVarsWritten,
6602
6703
  files,
6603
6704
  manualSteps: plan.manualSteps.map((s) => ({
@@ -6605,6 +6706,7 @@ function applyInstall(plan, options) {
6605
6706
  description: s.description,
6606
6707
  title: s.title
6607
6708
  })),
6709
+ packageManager: options.detection.packageManager,
6608
6710
  postInstall,
6609
6711
  recipe: options.recipe
6610
6712
  };
@@ -10798,6 +10900,13 @@ var RecipeManifest = external_exports.object({
10798
10900
  router: external_exports.array(external_exports.enum(["app"]))
10799
10901
  }),
10800
10902
  tier: external_exports.enum(["free", "pro"]),
10903
+ // Packages whose install-time build scripts this recipe vouches for.
10904
+ // pnpm 10+ and bun block postinstall scripts by default; the CLI merges
10905
+ // these into `pnpm.onlyBuiltDependencies` / `trustedDependencies` in the
10906
+ // target package.json so e.g. `prisma generate` works out of the box.
10907
+ // Optional (not defaulted) so manifests published before this field parse
10908
+ // unchanged over the wire.
10909
+ trustedBuildScripts: external_exports.array(external_exports.string()).optional(),
10801
10910
  title: external_exports.string(),
10802
10911
  version: external_exports.string()
10803
10912
  });
@@ -11496,11 +11605,189 @@ var HttpRegistrySource = class {
11496
11605
  }
11497
11606
  };
11498
11607
 
11608
+ // src/ui.ts
11609
+ import pc2 from "picocolors";
11610
+ var FILE_STATUS_LABEL = {
11611
+ appended: "appended",
11612
+ created: "created",
11613
+ injected: "injected",
11614
+ overwritten: "overwritten",
11615
+ skipped: "skipped"
11616
+ };
11617
+ var FILE_STATUS_COLOR = {
11618
+ appended: pc2.yellow,
11619
+ created: pc2.green,
11620
+ injected: pc2.yellow,
11621
+ overwritten: pc2.yellow,
11622
+ skipped: pc2.dim
11623
+ };
11624
+ function renderAppliedFiles(result) {
11625
+ const lines = [];
11626
+ for (const file of result.files) {
11627
+ const color = FILE_STATUS_COLOR[file.status];
11628
+ lines.push(
11629
+ `${color(FILE_STATUS_LABEL[file.status].padEnd(16))} ${file.target}`
11630
+ );
11631
+ }
11632
+ if (result.envVarsWritten.length > 0) {
11633
+ lines.push(
11634
+ `${pc2.cyan("env vars".padEnd(16))} ${result.envVarsWritten.join(", ")}`
11635
+ );
11636
+ }
11637
+ return lines.join("\n");
11638
+ }
11639
+ function tailOf(output, maxLines) {
11640
+ const lines = output.trimEnd().split(/\r?\n/);
11641
+ return lines.slice(-maxLines).join("\n");
11642
+ }
11643
+ function execPrefix(packageManager) {
11644
+ const table = {
11645
+ bun: "bunx",
11646
+ npm: "npx",
11647
+ pnpm: "pnpm exec",
11648
+ yarn: "yarn"
11649
+ };
11650
+ return table[packageManager];
11651
+ }
11652
+ function buildNextSteps(results) {
11653
+ const steps = [];
11654
+ const ignoredBuilds = [
11655
+ ...new Set(results.flatMap((r) => r.depsInstall.ignoredBuilds))
11656
+ ];
11657
+ const pm = results[0]?.packageManager ?? "npm";
11658
+ if (ignoredBuilds.length > 0 && pm === "pnpm") {
11659
+ steps.push({
11660
+ detail: `pnpm blocked install scripts for ${ignoredBuilds.join(", ")}. Approve the ones you trust, then re-run \`pnpm install\`.`,
11661
+ required: false,
11662
+ title: `Run ${pc2.bold("pnpm approve-builds")} to allow blocked build scripts`
11663
+ });
11664
+ }
11665
+ for (const result of results) {
11666
+ for (const step of result.postInstall) {
11667
+ if (step.status === "ok") continue;
11668
+ const command = `${execPrefix(result.packageManager)} ${step.command}`;
11669
+ steps.push({
11670
+ detail: step.status === "failed" ? `The command failed during install \u2014 fix the error above and re-run it.` : `Skipped because dependency install failed. Install dependencies, then run it.`,
11671
+ required: true,
11672
+ title: `Run ${pc2.bold(command)} \u2014 ${step.description}`
11673
+ });
11674
+ }
11675
+ }
11676
+ const seen = /* @__PURE__ */ new Set();
11677
+ const manual = results.flatMap((r) => r.manualSteps).filter((s) => seen.has(s.title) ? false : (seen.add(s.title), true));
11678
+ for (const step of manual.filter((s) => s.blocking)) {
11679
+ steps.push({ detail: step.description, required: true, title: step.title });
11680
+ }
11681
+ for (const step of manual.filter((s) => !s.blocking)) {
11682
+ steps.push({
11683
+ detail: step.description,
11684
+ required: false,
11685
+ title: step.title
11686
+ });
11687
+ }
11688
+ return steps;
11689
+ }
11690
+ function renderNextSteps(steps) {
11691
+ const lines = [];
11692
+ steps.forEach((step, index) => {
11693
+ const number = `${index + 1}.`.padEnd(3);
11694
+ const badge = step.required ? pc2.red("required") : pc2.dim("optional");
11695
+ lines.push(`${pc2.bold(number)}${step.title} ${badge}`);
11696
+ if (step.detail) {
11697
+ lines.push(pc2.dim(` ${step.detail}`));
11698
+ }
11699
+ });
11700
+ return lines.join("\n");
11701
+ }
11702
+
11499
11703
  // src/commands/init.ts
11704
+ function clackProgress() {
11705
+ const spin = clack2.spinner();
11706
+ let depsPm = "";
11707
+ return {
11708
+ onDepsStart: (packageManager) => {
11709
+ depsPm = packageManager;
11710
+ spin.start(`Installing dependencies with ${packageManager}`);
11711
+ },
11712
+ onDepsDone: (outcome) => {
11713
+ if (outcome.status === "ok") {
11714
+ spin.stop(`Dependencies installed with ${depsPm}`);
11715
+ return;
11716
+ }
11717
+ if (outcome.status === "ignored-builds") {
11718
+ spin.stop(
11719
+ `Dependencies installed \u2014 build scripts blocked for ${pc3.bold(
11720
+ outcome.ignoredBuilds.join(", ")
11721
+ )} (see next steps)`
11722
+ );
11723
+ return;
11724
+ }
11725
+ spin.stop(pc3.red("Dependency install failed"), 2);
11726
+ if (outcome.output) {
11727
+ clack2.log.error(pc3.dim(tailOf(outcome.output, 20)));
11728
+ }
11729
+ },
11730
+ onStepStart: (step) => {
11731
+ spin.start(step.description);
11732
+ },
11733
+ onStepDone: (step) => {
11734
+ if (step.status === "ok") {
11735
+ spin.stop(`${step.description} ${pc3.dim(`(${step.command})`)}`);
11736
+ } else if (step.status === "failed") {
11737
+ spin.stop(
11738
+ pc3.red(`${step.description} failed ${pc3.dim(`(${step.command})`)}`),
11739
+ 2
11740
+ );
11741
+ } else {
11742
+ clack2.log.warn(
11743
+ `Skipped: ${step.description} ${pc3.dim(`(${step.command})`)}`
11744
+ );
11745
+ }
11746
+ }
11747
+ };
11748
+ }
11500
11749
  async function runInit(options) {
11501
11750
  const source = options.registrySource ?? new LocalRegistrySource();
11502
- const detection = detectProject(options.cwd);
11751
+ let detection = detectProject(options.cwd);
11503
11752
  const manifest = await source.getManifest(options.recipe);
11753
+ const interactive = !options.yes && !options.json && !options.dryRun;
11754
+ const chained = [];
11755
+ const missing = manifest.requires.filter(
11756
+ (required) => !isRequireSatisfied(required, detection)
11757
+ );
11758
+ if (missing.length > 0 && interactive) {
11759
+ for (const required of missing) {
11760
+ if (options.chain?.includes(required)) {
11761
+ throw new CompatibilityError(
11762
+ `Recipe dependency cycle detected: ${[...options.chain, options.recipe, required].join(" -> ")}.`
11763
+ );
11764
+ }
11765
+ }
11766
+ clack2.log.warn(
11767
+ `${pc3.bold(options.recipe)} requires ${pc3.bold(missing.join(", "))}, which isn't set up in this project yet.`
11768
+ );
11769
+ const install = await clack2.confirm({
11770
+ message: `Set up ${missing.join(", ")} now, then continue with ${options.recipe}?`
11771
+ });
11772
+ if (clack2.isCancel(install) || !install) {
11773
+ throw new CompatibilityError(
11774
+ `"${options.recipe}" requires "${missing.join('", "')}" to be installed first. Run \`cnippet-stack init ${missing[0]}\` first, then re-run this command.`
11775
+ );
11776
+ }
11777
+ for (const required of missing) {
11778
+ const sub = await runInit({
11779
+ ...options,
11780
+ answers: void 0,
11781
+ chain: [...options.chain ?? [], options.recipe],
11782
+ recipe: required
11783
+ });
11784
+ chained.push(sub);
11785
+ }
11786
+ detection = detectProject(options.cwd);
11787
+ clack2.log.success(
11788
+ `${pc3.bold(missing.join(", "))} installed \u2014 continuing with ${pc3.bold(options.recipe)}.`
11789
+ );
11790
+ }
11504
11791
  checkCompatibility(detection, manifest.supports);
11505
11792
  checkRequires(options.recipe, manifest.requires, detection);
11506
11793
  const suppliedAnswers = options.answers !== void 0 ? JSON.parse(options.answers) : void 0;
@@ -11518,15 +11805,21 @@ async function runInit(options) {
11518
11805
  if (options.dryRun) {
11519
11806
  if (!options.json) {
11520
11807
  clack2.log.info(
11521
- `Install plan for ${pc2.bold(options.recipe)} (dry run \u2014 nothing written):`
11808
+ `Install plan for ${pc3.bold(options.recipe)} (dry run \u2014 nothing written):`
11522
11809
  );
11523
- console.log(renderPreview(preview));
11810
+ clack2.log.message(renderPreview(preview));
11524
11811
  }
11525
- return { applied: false, preview, recipeVersion: plan.manifest.version };
11812
+ return {
11813
+ applied: false,
11814
+ chained,
11815
+ preview,
11816
+ recipe: options.recipe,
11817
+ recipeVersion: plan.manifest.version
11818
+ };
11526
11819
  }
11527
11820
  if (!options.yes && !options.json) {
11528
- clack2.log.info(`Install plan for ${pc2.bold(options.recipe)}:`);
11529
- console.log(renderPreview(preview));
11821
+ clack2.log.step(`Install plan for ${pc3.bold(options.recipe)}`);
11822
+ clack2.log.message(renderPreview(preview));
11530
11823
  const confirmed = await clack2.confirm({ message: "Apply these changes?" });
11531
11824
  if (clack2.isCancel(confirmed) || !confirmed) {
11532
11825
  throw new PromptCancelledError();
@@ -11536,15 +11829,28 @@ async function runInit(options) {
11536
11829
  cwd: options.cwd,
11537
11830
  detection,
11538
11831
  overwrite: options.overwrite ?? false,
11832
+ progress: options.json ? void 0 : clackProgress(),
11539
11833
  recipe: options.recipe,
11540
11834
  runner: options.runner,
11541
11835
  timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")
11542
11836
  });
11543
- return { applied: true, preview, recipeVersion: plan.manifest.version, result };
11837
+ if (!options.json) {
11838
+ clack2.log.success(`${pc3.bold(options.recipe)} applied`);
11839
+ const fileLines = renderAppliedFiles(result);
11840
+ if (fileLines) clack2.log.message(fileLines);
11841
+ }
11842
+ return {
11843
+ applied: true,
11844
+ chained,
11845
+ preview,
11846
+ recipe: options.recipe,
11847
+ recipeVersion: plan.manifest.version,
11848
+ result
11849
+ };
11544
11850
  }
11545
11851
 
11546
11852
  // src/commands/list.ts
11547
- import pc3 from "picocolors";
11853
+ import pc4 from "picocolors";
11548
11854
  async function runList(options = {}) {
11549
11855
  const source = options.registrySource ?? new LocalRegistrySource();
11550
11856
  const recipes = await source.list();
@@ -11557,15 +11863,15 @@ async function runList(options = {}) {
11557
11863
  return;
11558
11864
  }
11559
11865
  for (const recipe of recipes) {
11560
- const tier = recipe.tier === "pro" ? pc3.magenta("[pro]") : pc3.green("[free]");
11561
- console.log(`${pc3.bold(recipe.name)} ${tier} \u2014 ${recipe.title}`);
11562
- console.log(` ${pc3.dim(recipe.description)}`);
11866
+ const tier = recipe.tier === "pro" ? pc4.magenta("[pro]") : pc4.green("[free]");
11867
+ console.log(`${pc4.bold(recipe.name)} ${tier} \u2014 ${recipe.title}`);
11868
+ console.log(` ${pc4.dim(recipe.description)}`);
11563
11869
  }
11564
11870
  }
11565
11871
 
11566
11872
  // src/commands/login.ts
11567
11873
  import { spawn } from "child_process";
11568
- import pc4 from "picocolors";
11874
+ import pc5 from "picocolors";
11569
11875
  function resolveBaseUrl(options) {
11570
11876
  return options.registry ?? process.env.CNIPPET_REGISTRY_URL ?? DEFAULT_REGISTRY_URL;
11571
11877
  }
@@ -11598,11 +11904,11 @@ async function runLogin(options = {}) {
11598
11904
  }
11599
11905
  const start = await startRes.json();
11600
11906
  console.log(`
11601
- Go to: ${pc4.bold(pc4.cyan(start.verificationUrl))}`);
11602
- console.log(`Confirm this code: ${pc4.bold(start.code)}
11907
+ Go to: ${pc5.bold(pc5.cyan(start.verificationUrl))}`);
11908
+ console.log(`Confirm this code: ${pc5.bold(start.code)}
11603
11909
  `);
11604
11910
  openBrowser(start.verificationUrl);
11605
- console.log(pc4.dim("Waiting for approval..."));
11911
+ console.log(pc5.dim("Waiting for approval..."));
11606
11912
  const deadline = Date.now() + start.expiresIn * 1e3;
11607
11913
  while (Date.now() < deadline) {
11608
11914
  await sleep(2e3);
@@ -11618,7 +11924,7 @@ Go to: ${pc4.bold(pc4.cyan(start.verificationUrl))}`);
11618
11924
  const poll = await pollRes.json();
11619
11925
  if (poll.status === "approved" && poll.token) {
11620
11926
  saveCliConfig({ baseUrl, token: poll.token });
11621
- console.log(pc4.green("\u2714 Logged in."));
11927
+ console.log(pc5.green("\u2714 Logged in."));
11622
11928
  return;
11623
11929
  }
11624
11930
  }
@@ -11626,7 +11932,7 @@ Go to: ${pc4.bold(pc4.cyan(start.verificationUrl))}`);
11626
11932
  }
11627
11933
 
11628
11934
  // src/commands/logout.ts
11629
- import pc5 from "picocolors";
11935
+ import pc6 from "picocolors";
11630
11936
  async function runLogout() {
11631
11937
  const config = loadCliConfig();
11632
11938
  if (!config) {
@@ -11641,11 +11947,11 @@ async function runLogout() {
11641
11947
  } catch {
11642
11948
  }
11643
11949
  clearCliConfig();
11644
- console.log(pc5.green("\u2714 Logged out."));
11950
+ console.log(pc6.green("\u2714 Logged out."));
11645
11951
  }
11646
11952
 
11647
11953
  // src/commands/whoami.ts
11648
- import pc6 from "picocolors";
11954
+ import pc7 from "picocolors";
11649
11955
  async function runWhoami() {
11650
11956
  const config = loadCliConfig();
11651
11957
  if (!config) {
@@ -11657,15 +11963,15 @@ async function runWhoami() {
11657
11963
  });
11658
11964
  if (!res.ok) {
11659
11965
  console.log(
11660
- pc6.red("\u2716 Session is no longer valid."),
11966
+ pc7.red("\u2716 Session is no longer valid."),
11661
11967
  "Run `cnippet-stack login` again."
11662
11968
  );
11663
11969
  return;
11664
11970
  }
11665
11971
  const whoami = await res.json();
11666
- console.log(`${pc6.bold(whoami.name)} <${whoami.email}>`);
11972
+ console.log(`${pc7.bold(whoami.name)} <${whoami.email}>`);
11667
11973
  console.log(
11668
- whoami.hasActiveLicense ? pc6.green("License: active") : pc6.dim("License: none \u2014 pro recipes require a license.")
11974
+ whoami.hasActiveLicense ? pc7.green("License: active") : pc7.dim("License: none \u2014 pro recipes require a license.")
11669
11975
  );
11670
11976
  }
11671
11977
 
@@ -11687,46 +11993,31 @@ function sendTelemetry(registrySource, event) {
11687
11993
  }).catch(() => {
11688
11994
  });
11689
11995
  }
11996
+ function collectResults(outcome) {
11997
+ return [
11998
+ ...outcome.chained.flatMap(collectResults),
11999
+ ...outcome.applied && outcome.result ? [outcome.result] : []
12000
+ ];
12001
+ }
11690
12002
  function reportOutcome(outcome, json) {
11691
12003
  if (json) {
11692
12004
  console.log(JSON.stringify(outcome, null, 2));
11693
12005
  return;
11694
12006
  }
11695
- if (!outcome.applied || !outcome.result) return;
11696
- console.log(`
11697
- ${pc7.green("Done.")}`);
11698
- for (const file of outcome.result.files) {
11699
- console.log(` ${pc7.dim(file.status)} ${file.target}`);
11700
- }
11701
- if (outcome.result.envVarsWritten.length > 0) {
11702
- console.log(
11703
- ` env vars written: ${outcome.result.envVarsWritten.join(", ")}`
11704
- );
11705
- }
11706
- if (outcome.result.postInstall.length > 0) {
11707
- console.log(`
11708
- ${pc7.bold("Post-install:")}`);
11709
- for (const step of outcome.result.postInstall) {
11710
- if (step.status === "ok") {
11711
- console.log(` ${pc7.green("\u2713")} ${step.description} (${step.command})`);
11712
- } else {
11713
- const reason = step.status === "skipped" ? "skipped \u2014 dependency install failed" : "failed";
11714
- console.log(
11715
- ` ${pc7.red("\u2716")} ${step.description} \u2014 ${reason}. Run \`${step.command}\` yourself once dependencies are installed.`
11716
- );
11717
- }
11718
- }
12007
+ if (!outcome.applied || !outcome.result) {
12008
+ clack3.outro(pc8.dim("Dry run \u2014 nothing was written."));
12009
+ return;
11719
12010
  }
11720
- if (outcome.result.manualSteps.length > 0) {
11721
- console.log(`
11722
- ${pc7.bold("Manual steps:")}`);
11723
- for (const step of outcome.result.manualSteps) {
11724
- console.log(
11725
- ` ${step.blocking ? pc7.red("[required]") : pc7.dim("[optional]")} ${step.title}`
11726
- );
11727
- console.log(` ${step.description}`);
11728
- }
12011
+ const steps = buildNextSteps(collectResults(outcome));
12012
+ if (steps.length > 0) {
12013
+ clack3.log.step(pc8.bold("Next steps"));
12014
+ clack3.log.message(renderNextSteps(steps));
11729
12015
  }
12016
+ const installed = [
12017
+ ...outcome.chained.map((sub) => sub.recipe),
12018
+ outcome.recipe
12019
+ ];
12020
+ clack3.outro(pc8.green(`${installed.join(" + ")} installed \u2714`));
11730
12021
  }
11731
12022
  function createProgram() {
11732
12023
  const program = new Command();
@@ -11753,12 +12044,17 @@ function createProgram() {
11753
12044
  ).action(async (recipe, opts) => {
11754
12045
  const registrySource = resolveRegistrySource(opts);
11755
12046
  const startedAt = Date.now();
11756
- if (!opts.json && !opts.dryRun) {
11757
- console.log(
11758
- pc7.dim(
11759
- "Anonymous usage data is sent to help improve cnippet \u2014 set CNIPPET_TELEMETRY=0 to opt out."
11760
- )
12047
+ if (!opts.json) {
12048
+ clack3.intro(
12049
+ `${pc8.bgCyan(pc8.black(" cnippet-stack "))} ${pc8.dim(`v${cliVersion()}`)}`
11761
12050
  );
12051
+ if (!opts.dryRun) {
12052
+ clack3.log.message(
12053
+ pc8.dim(
12054
+ "Anonymous usage data is sent to help improve cnippet \u2014 set CNIPPET_TELEMETRY=0 to opt out."
12055
+ )
12056
+ );
12057
+ }
11762
12058
  }
11763
12059
  try {
11764
12060
  const outcome = await runInit({
@@ -11789,17 +12085,20 @@ function createProgram() {
11789
12085
  });
11790
12086
  }
11791
12087
  if (error instanceof CompatibilityError) {
11792
- console.error(pc7.red(`\u2716 ${error.message}`));
12088
+ if (opts.json) console.error(pc8.red(`\u2716 ${error.message}`));
12089
+ else clack3.cancel(error.message);
11793
12090
  process.exitCode = 1;
11794
12091
  return;
11795
12092
  }
11796
12093
  if (error instanceof PromptCancelledError) {
11797
- console.log(pc7.dim("Aborted \u2014 nothing was changed."));
12094
+ if (opts.json) console.log(pc8.dim("Aborted \u2014 nothing was changed."));
12095
+ else clack3.cancel("Aborted \u2014 nothing was changed.");
11798
12096
  process.exitCode = 1;
11799
12097
  return;
11800
12098
  }
11801
12099
  if (error instanceof RegistryHttpError) {
11802
- console.error(pc7.red(`\u2716 ${error.message}`));
12100
+ if (opts.json) console.error(pc8.red(`\u2716 ${error.message}`));
12101
+ else clack3.cancel(error.message);
11803
12102
  process.exitCode = 1;
11804
12103
  return;
11805
12104
  }
@@ -11821,7 +12120,7 @@ function createProgram() {
11821
12120
  });
11822
12121
  } catch (error) {
11823
12122
  if (error instanceof RegistryHttpError) {
11824
- console.error(pc7.red(`\u2716 ${error.message}`));
12123
+ console.error(pc8.red(`\u2716 ${error.message}`));
11825
12124
  process.exitCode = 1;
11826
12125
  return;
11827
12126
  }
@@ -11839,7 +12138,7 @@ function createProgram() {
11839
12138
  await runLogin({ registry: opts.registry });
11840
12139
  } catch (error) {
11841
12140
  if (error instanceof LoginError) {
11842
- console.error(pc7.red(`\u2716 ${error.message}`));
12141
+ console.error(pc8.red(`\u2716 ${error.message}`));
11843
12142
  process.exitCode = 1;
11844
12143
  return;
11845
12144
  }
package/package.json CHANGED
@@ -8,7 +8,8 @@
8
8
  "@clack/prompts": "^0.10.0",
9
9
  "commander": "^13.1.0",
10
10
  "picocolors": "^1.1.1",
11
- "ts-morph": "^25.0.1"
11
+ "ts-morph": "^25.0.1",
12
+ "yaml": "^2.9.0"
12
13
  },
13
14
  "description": "CLI that installs full integrations (auth, payments, email, storage) into your existing Next.js app — detection, prompts, safe file injection, env setup.",
14
15
  "devDependencies": {
@@ -62,5 +63,5 @@
62
63
  },
63
64
  "type": "module",
64
65
  "types": "./dist/index.d.ts",
65
- "version": "0.2.0"
66
+ "version": "0.2.1"
66
67
  }