cnippet-stack 0.1.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 +395 -92
  2. package/package.json +6 -5
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,12 +5927,20 @@ 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";
5933
+
5934
+ // src/commands/doctor.ts
5935
+ function runDoctor(recipe) {
5936
+ console.log(
5937
+ `\`cnippet-stack doctor ${recipe}\` isn't implemented yet \u2014 post-install verification lands in Phase 7.`
5938
+ );
5939
+ }
5932
5940
 
5933
5941
  // src/commands/init.ts
5934
5942
  import * as clack2 from "@clack/prompts";
5935
- import pc2 from "picocolors";
5943
+ import pc3 from "picocolors";
5936
5944
 
5937
5945
  // src/compat.ts
5938
5946
  var CompatibilityError = class extends Error {
@@ -5972,10 +5980,12 @@ function checkCompatibility(detection, supports) {
5972
5980
  );
5973
5981
  }
5974
5982
  }
5983
+ function isRequireSatisfied(required, detection) {
5984
+ return required === "prisma" && detection.orm === "prisma";
5985
+ }
5975
5986
  function checkRequires(recipeName, requires, detection) {
5976
5987
  for (const required of requires) {
5977
- const satisfied = required === "prisma" && detection.orm === "prisma";
5978
- if (!satisfied) {
5988
+ if (!isRequireSatisfied(required, detection)) {
5979
5989
  throw new CompatibilityError(
5980
5990
  `"${recipeName}" requires "${required}" to be installed first. Run \`cnippet-stack init ${required}\` first, then re-run this command.`
5981
5991
  );
@@ -6215,6 +6225,7 @@ function toTemplateContext(detection) {
6215
6225
  import fs9 from "fs";
6216
6226
  import { spawnSync } from "child_process";
6217
6227
  import path9 from "path";
6228
+ import { parseDocument, Document } from "yaml";
6218
6229
  function parseDependencySpec(spec) {
6219
6230
  const atIndex = spec.lastIndexOf("@");
6220
6231
  if (atIndex <= 0) return { name: spec, range: "latest" };
@@ -6256,6 +6267,68 @@ function mergePackageJsonDeps(cwd, dependencies, devDependencies) {
6256
6267
  }
6257
6268
  return changed;
6258
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
+ }
6259
6332
  function envWithLocalBin(cwd) {
6260
6333
  const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
6261
6334
  const existing = process.env[pathKey] ?? "";
@@ -6266,13 +6339,14 @@ function envWithLocalBin(cwd) {
6266
6339
  };
6267
6340
  }
6268
6341
  var defaultCommandRunner = (command, args, cwd) => {
6269
- const result = spawnSync(command, args, {
6342
+ const result = spawnSync([command, ...args].join(" "), {
6270
6343
  cwd,
6344
+ encoding: "utf8",
6271
6345
  env: envWithLocalBin(cwd),
6272
- shell: true,
6273
- stdio: "inherit"
6346
+ shell: true
6274
6347
  });
6275
- return { status: result.status };
6348
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
6349
+ return { output, status: result.status };
6276
6350
  };
6277
6351
  function runInstallCommand(cwd, packageManager, runner = defaultCommandRunner) {
6278
6352
  const { command, args } = installCommandFor(packageManager);
@@ -6540,6 +6614,7 @@ function applyCreate(cwd, file, options) {
6540
6614
  // src/installer/index.ts
6541
6615
  function applyInstall(plan, options) {
6542
6616
  const runner = options.runner ?? defaultCommandRunner;
6617
+ const progress = options.progress ?? {};
6543
6618
  const files = [];
6544
6619
  for (const file of plan.renderedFiles) {
6545
6620
  if (file.strategy === "create") {
@@ -6572,25 +6647,58 @@ function applyInstall(plan, options) {
6572
6647
  plan.manifest.dependencies,
6573
6648
  plan.manifest.devDependencies
6574
6649
  );
6575
- let depsInstallFailed = false;
6650
+ let depsInstall = {
6651
+ ignoredBuilds: [],
6652
+ status: "not-needed"
6653
+ };
6576
6654
  if (depsChanged) {
6655
+ mergeTrustedBuildScripts(
6656
+ options.cwd,
6657
+ options.detection.packageManager,
6658
+ plan.manifest.trustedBuildScripts ?? []
6659
+ );
6660
+ progress.onDepsStart?.(options.detection.packageManager);
6577
6661
  const depsResult = runInstallCommand(
6578
6662
  options.cwd,
6579
6663
  options.detection.packageManager,
6580
6664
  runner
6581
6665
  );
6582
- 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);
6583
6676
  }
6677
+ const depsUsable = depsInstall.status !== "failed";
6584
6678
  const postInstall = plan.postInstall.map((step) => {
6585
- if (depsInstallFailed) {
6586
- return { ...step, status: "skipped" };
6679
+ if (!depsUsable) {
6680
+ const skipped = { ...step, status: "skipped" };
6681
+ progress.onStepDone?.(skipped);
6682
+ return skipped;
6587
6683
  }
6588
6684
  const [command, ...args] = step.command.split(" ");
6589
- 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);
6590
6691
  const result = runner(command, args, options.cwd);
6591
- 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;
6592
6699
  });
6593
6700
  return {
6701
+ depsInstall,
6594
6702
  envVarsWritten,
6595
6703
  files,
6596
6704
  manualSteps: plan.manualSteps.map((s) => ({
@@ -6598,6 +6706,7 @@ function applyInstall(plan, options) {
6598
6706
  description: s.description,
6599
6707
  title: s.title
6600
6708
  })),
6709
+ packageManager: options.detection.packageManager,
6601
6710
  postInstall,
6602
6711
  recipe: options.recipe
6603
6712
  };
@@ -10791,6 +10900,13 @@ var RecipeManifest = external_exports.object({
10791
10900
  router: external_exports.array(external_exports.enum(["app"]))
10792
10901
  }),
10793
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(),
10794
10910
  title: external_exports.string(),
10795
10911
  version: external_exports.string()
10796
10912
  });
@@ -11489,11 +11605,189 @@ var HttpRegistrySource = class {
11489
11605
  }
11490
11606
  };
11491
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
+
11492
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
+ }
11493
11749
  async function runInit(options) {
11494
11750
  const source = options.registrySource ?? new LocalRegistrySource();
11495
- const detection = detectProject(options.cwd);
11751
+ let detection = detectProject(options.cwd);
11496
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
+ }
11497
11791
  checkCompatibility(detection, manifest.supports);
11498
11792
  checkRequires(options.recipe, manifest.requires, detection);
11499
11793
  const suppliedAnswers = options.answers !== void 0 ? JSON.parse(options.answers) : void 0;
@@ -11511,15 +11805,21 @@ async function runInit(options) {
11511
11805
  if (options.dryRun) {
11512
11806
  if (!options.json) {
11513
11807
  clack2.log.info(
11514
- `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):`
11515
11809
  );
11516
- console.log(renderPreview(preview));
11810
+ clack2.log.message(renderPreview(preview));
11517
11811
  }
11518
- 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
+ };
11519
11819
  }
11520
11820
  if (!options.yes && !options.json) {
11521
- clack2.log.info(`Install plan for ${pc2.bold(options.recipe)}:`);
11522
- console.log(renderPreview(preview));
11821
+ clack2.log.step(`Install plan for ${pc3.bold(options.recipe)}`);
11822
+ clack2.log.message(renderPreview(preview));
11523
11823
  const confirmed = await clack2.confirm({ message: "Apply these changes?" });
11524
11824
  if (clack2.isCancel(confirmed) || !confirmed) {
11525
11825
  throw new PromptCancelledError();
@@ -11529,22 +11829,28 @@ async function runInit(options) {
11529
11829
  cwd: options.cwd,
11530
11830
  detection,
11531
11831
  overwrite: options.overwrite ?? false,
11832
+ progress: options.json ? void 0 : clackProgress(),
11532
11833
  recipe: options.recipe,
11533
11834
  runner: options.runner,
11534
11835
  timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")
11535
11836
  });
11536
- return { applied: true, preview, recipeVersion: plan.manifest.version, result };
11537
- }
11538
-
11539
- // src/commands/doctor.ts
11540
- function runDoctor(recipe) {
11541
- console.log(
11542
- `\`cnippet-stack doctor ${recipe}\` isn't implemented yet \u2014 post-install verification lands in Phase 7.`
11543
- );
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,23 +11963,24 @@ 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
 
11672
11978
  // src/program.ts
11673
11979
  function resolveRegistrySource(opts) {
11674
11980
  if (opts.localRegistry) return new LocalRegistrySource();
11675
- const url = opts.registry ?? process.env.CNIPPET_REGISTRY_URL;
11676
- return url ? new HttpRegistrySource(url) : void 0;
11981
+ return new HttpRegistrySource(
11982
+ opts.registry ?? process.env.CNIPPET_REGISTRY_URL ?? DEFAULT_REGISTRY_URL
11983
+ );
11677
11984
  }
11678
11985
  function sendTelemetry(registrySource, event) {
11679
11986
  if (process.env.CNIPPET_TELEMETRY === "0") return;
@@ -11686,46 +11993,31 @@ function sendTelemetry(registrySource, event) {
11686
11993
  }).catch(() => {
11687
11994
  });
11688
11995
  }
11996
+ function collectResults(outcome) {
11997
+ return [
11998
+ ...outcome.chained.flatMap(collectResults),
11999
+ ...outcome.applied && outcome.result ? [outcome.result] : []
12000
+ ];
12001
+ }
11689
12002
  function reportOutcome(outcome, json) {
11690
12003
  if (json) {
11691
12004
  console.log(JSON.stringify(outcome, null, 2));
11692
12005
  return;
11693
12006
  }
11694
- if (!outcome.applied || !outcome.result) return;
11695
- console.log(`
11696
- ${pc7.green("Done.")}`);
11697
- for (const file of outcome.result.files) {
11698
- console.log(` ${pc7.dim(file.status)} ${file.target}`);
11699
- }
11700
- if (outcome.result.envVarsWritten.length > 0) {
11701
- console.log(
11702
- ` env vars written: ${outcome.result.envVarsWritten.join(", ")}`
11703
- );
11704
- }
11705
- if (outcome.result.postInstall.length > 0) {
11706
- console.log(`
11707
- ${pc7.bold("Post-install:")}`);
11708
- for (const step of outcome.result.postInstall) {
11709
- if (step.status === "ok") {
11710
- console.log(` ${pc7.green("\u2713")} ${step.description} (${step.command})`);
11711
- } else {
11712
- const reason = step.status === "skipped" ? "skipped \u2014 dependency install failed" : "failed";
11713
- console.log(
11714
- ` ${pc7.red("\u2716")} ${step.description} \u2014 ${reason}. Run \`${step.command}\` yourself once dependencies are installed.`
11715
- );
11716
- }
11717
- }
12007
+ if (!outcome.applied || !outcome.result) {
12008
+ clack3.outro(pc8.dim("Dry run \u2014 nothing was written."));
12009
+ return;
11718
12010
  }
11719
- if (outcome.result.manualSteps.length > 0) {
11720
- console.log(`
11721
- ${pc7.bold("Manual steps:")}`);
11722
- for (const step of outcome.result.manualSteps) {
11723
- console.log(
11724
- ` ${step.blocking ? pc7.red("[required]") : pc7.dim("[optional]")} ${step.title}`
11725
- );
11726
- console.log(` ${step.description}`);
11727
- }
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));
11728
12015
  }
12016
+ const installed = [
12017
+ ...outcome.chained.map((sub) => sub.recipe),
12018
+ outcome.recipe
12019
+ ];
12020
+ clack3.outro(pc8.green(`${installed.join(" + ")} installed \u2714`));
11729
12021
  }
11730
12022
  function createProgram() {
11731
12023
  const program = new Command();
@@ -11752,12 +12044,17 @@ function createProgram() {
11752
12044
  ).action(async (recipe, opts) => {
11753
12045
  const registrySource = resolveRegistrySource(opts);
11754
12046
  const startedAt = Date.now();
11755
- if (!opts.json && !opts.dryRun) {
11756
- console.log(
11757
- pc7.dim(
11758
- "Anonymous usage data is sent to help improve cnippet \u2014 set CNIPPET_TELEMETRY=0 to opt out."
11759
- )
12047
+ if (!opts.json) {
12048
+ clack3.intro(
12049
+ `${pc8.bgCyan(pc8.black(" cnippet-stack "))} ${pc8.dim(`v${cliVersion()}`)}`
11760
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
+ }
11761
12058
  }
11762
12059
  try {
11763
12060
  const outcome = await runInit({
@@ -11788,17 +12085,20 @@ function createProgram() {
11788
12085
  });
11789
12086
  }
11790
12087
  if (error instanceof CompatibilityError) {
11791
- console.error(pc7.red(`\u2716 ${error.message}`));
12088
+ if (opts.json) console.error(pc8.red(`\u2716 ${error.message}`));
12089
+ else clack3.cancel(error.message);
11792
12090
  process.exitCode = 1;
11793
12091
  return;
11794
12092
  }
11795
12093
  if (error instanceof PromptCancelledError) {
11796
- 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.");
11797
12096
  process.exitCode = 1;
11798
12097
  return;
11799
12098
  }
11800
12099
  if (error instanceof RegistryHttpError) {
11801
- console.error(pc7.red(`\u2716 ${error.message}`));
12100
+ if (opts.json) console.error(pc8.red(`\u2716 ${error.message}`));
12101
+ else clack3.cancel(error.message);
11802
12102
  process.exitCode = 1;
11803
12103
  return;
11804
12104
  }
@@ -11814,10 +12114,13 @@ function createProgram() {
11814
12114
  false
11815
12115
  ).action(async (opts) => {
11816
12116
  try {
11817
- await runList({ json: opts.json, registrySource: resolveRegistrySource(opts) });
12117
+ await runList({
12118
+ json: opts.json,
12119
+ registrySource: resolveRegistrySource(opts)
12120
+ });
11818
12121
  } catch (error) {
11819
12122
  if (error instanceof RegistryHttpError) {
11820
- console.error(pc7.red(`\u2716 ${error.message}`));
12123
+ console.error(pc8.red(`\u2716 ${error.message}`));
11821
12124
  process.exitCode = 1;
11822
12125
  return;
11823
12126
  }
@@ -11835,7 +12138,7 @@ function createProgram() {
11835
12138
  await runLogin({ registry: opts.registry });
11836
12139
  } catch (error) {
11837
12140
  if (error instanceof LoginError) {
11838
- console.error(pc7.red(`\u2716 ${error.message}`));
12141
+ console.error(pc8.red(`\u2716 ${error.message}`));
11839
12142
  process.exitCode = 1;
11840
12143
  return;
11841
12144
  }
package/package.json CHANGED
@@ -4,13 +4,14 @@
4
4
  "cnippet-stack": "./dist/index.js"
5
5
  },
6
6
  "bugs": "https://github.com/cnippet-dev/cnippet-stack/issues",
7
- "description": "CLI that installs full integrations (auth, payments, email, storage) into your existing Next.js app — detection, prompts, safe file injection, env setup.",
8
7
  "dependencies": {
9
8
  "@clack/prompts": "^0.10.0",
10
9
  "commander": "^13.1.0",
11
10
  "picocolors": "^1.1.1",
12
- "ts-morph": "^25.0.1"
11
+ "ts-morph": "^25.0.1",
12
+ "yaml": "^2.9.0"
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": {
15
16
  "@cnippet/registry": "workspace:*",
16
17
  "@cnippet/validators": "workspace:*",
@@ -49,9 +50,9 @@
49
50
  "access": "public"
50
51
  },
51
52
  "repository": {
53
+ "directory": "packages/cli",
52
54
  "type": "git",
53
- "url": "git+https://github.com/cnippet-dev/cnippet-stack.git",
54
- "directory": "packages/cli"
55
+ "url": "git+https://github.com/cnippet-dev/cnippet-stack.git"
55
56
  },
56
57
  "scripts": {
57
58
  "build": "tsup",
@@ -62,5 +63,5 @@
62
63
  },
63
64
  "type": "module",
64
65
  "types": "./dist/index.d.ts",
65
- "version": "0.1.0"
66
+ "version": "0.2.1"
66
67
  }