stackpack-cli 0.3.2 → 0.3.4

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.
package/README.md CHANGED
@@ -112,6 +112,8 @@ Framework-specific filtering applies automatically — e.g. React Router is hidd
112
112
  stackpack # interactive main menu
113
113
  stackpack new <project-name> # create a project with an official creator
114
114
  stackpack new my-app --preset jc-react-stack
115
+ stackpack install <preset> [name] # express mode: whole stack in one shot
116
+ stackpack i <preset> [name] # same, shorter
115
117
  stackpack add # add integrations to the current project
116
118
  stackpack add --dry-run # full plan, zero changes
117
119
  stackpack add --package-manager pnpm
@@ -120,6 +122,7 @@ stackpack save <name> [--local|--global]
120
122
  stackpack apply <name> [--dry-run] # apply a preset to the current project
121
123
  stackpack presets list
122
124
  stackpack presets show <name>
125
+ stackpack presets edit <name> # swap integrations, save back — no project touched
123
126
  stackpack presets delete <name>
124
127
  stackpack --no-color
125
128
  ```
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  //#region src/cli.ts
3
3
  if (process.argv.includes("--no-color")) process.env.NO_COLOR = "1";
4
- const { runCli } = await import("./program-CqWM02UT.js");
4
+ const { runCli } = await import("./program-Cis7OMvk.js");
5
5
  await runCli(process.argv);
6
6
  process.exit(process.exitCode ?? 0);
7
7
  //#endregion
@@ -2477,16 +2477,45 @@ function testingLabel(selection) {
2477
2477
  if (selection.testing.length === 0) return void 0;
2478
2478
  return selection.testing.map((entry) => getRecipe(entry.id)?.name ?? entry.id).join(" + ");
2479
2479
  }
2480
- /** Category rows turn green with a check once something is selected. */
2481
- function categoryOption(value, label, selected) {
2482
- return selected ? {
2480
+ /**
2481
+ * Project inventory per category: what is already installed in the project
2482
+ * (detected from real files/packages), independent of this session's picks.
2483
+ */
2484
+ function installedSummary(availabilities, category) {
2485
+ const entries = availabilities.filter((a) => a.recipe.category === category && (a.compatibility === "already-installed" || a.compatibility === "partially-configured"));
2486
+ if (entries.length === 0) return void 0;
2487
+ return {
2488
+ label: entries.map((a) => {
2489
+ const version = a.detection.installedVersion;
2490
+ const base = version ? `${a.recipe.name} ${version}` : a.recipe.name;
2491
+ return a.compatibility === "partially-configured" ? `${base} — partial setup` : base;
2492
+ }).join(" + "),
2493
+ partial: entries.some((a) => a.compatibility === "partially-configured")
2494
+ };
2495
+ }
2496
+ /**
2497
+ * Category rows show this session's picks first (green check), otherwise what
2498
+ * the project already has installed, otherwise that the slot is open.
2499
+ */
2500
+ function categoryOption(value, label, selected, installed) {
2501
+ if (selected) return {
2483
2502
  value,
2484
2503
  label: `${label} ${pc.green("✓")}`,
2485
2504
  hint: pc.green(selected)
2505
+ };
2506
+ if (installed) return installed.partial ? {
2507
+ value,
2508
+ label: `${label} ${pc.yellow("⚠")}`,
2509
+ hint: pc.yellow(`installed: ${installed.label}`)
2486
2510
  } : {
2511
+ value,
2512
+ label: `${label} ${pc.green("●")}`,
2513
+ hint: pc.dim(`installed: ${installed.label}`)
2514
+ };
2515
+ return {
2487
2516
  value,
2488
2517
  label,
2489
- hint: "Not selected"
2518
+ hint: "Not installed"
2490
2519
  };
2491
2520
  }
2492
2521
  function categoryHasVisibleRecipes(availabilities, category) {
@@ -2496,7 +2525,8 @@ function categoryHasVisibleRecipes(availabilities, category) {
2496
2525
  * The jumpable category dashboard. Selections persist in memory while the
2497
2526
  * user moves between categories; nothing is installed here.
2498
2527
  */
2499
- async function runDashboard(context, selection) {
2528
+ async function runDashboard(context, selection, options = {}) {
2529
+ const reviewLabel = options.reviewLabel ?? "Review and Install";
2500
2530
  for (;;) {
2501
2531
  const availabilities = filterIntegrations(context, allRecipes);
2502
2532
  const count = selectedIntegrationCount(selection);
@@ -2504,13 +2534,13 @@ async function runDashboard(context, selection) {
2504
2534
  const overrideCount = Object.keys(selection.versionOverrides).length;
2505
2535
  p.log.info(`Selected integrations: ${count} Additional packages: ${customCount}${overrideCount > 0 ? ` Version overrides: ${overrideCount}` : ""}`);
2506
2536
  const options = [];
2507
- if (categoryHasVisibleRecipes(availabilities, "routing")) options.push(categoryOption("routing", "Routing", selectionLabel(selection.routing)));
2508
- options.push(categoryOption("state-management", "State Management", selectionLabel(selection.stateManagement)), categoryOption("data-fetching", "Data Fetching and API", selectionLabel(selection.dataFetching)), categoryOption("forms-validation", "Forms and Validation", selectionLabel(selection.formsAndValidation)), categoryOption("ui", "UI Components", selectionLabel(selection.ui)), categoryOption("orm", "Database / ORM", selectionLabel(selection.orm)), categoryOption("testing", "Testing", testingLabel(selection)), categoryOption("custom-packages", "Custom Packages", customCount > 0 ? `${customCount} package${customCount === 1 ? "" : "s"}` : void 0), {
2537
+ if (categoryHasVisibleRecipes(availabilities, "routing")) options.push(categoryOption("routing", "Routing", selectionLabel(selection.routing), installedSummary(availabilities, "routing")));
2538
+ options.push(categoryOption("state-management", "State Management", selectionLabel(selection.stateManagement), installedSummary(availabilities, "state-management")), categoryOption("data-fetching", "Data Fetching and API", selectionLabel(selection.dataFetching), installedSummary(availabilities, "data-fetching")), categoryOption("forms-validation", "Forms and Validation", selectionLabel(selection.formsAndValidation), installedSummary(availabilities, "forms-validation")), categoryOption("ui", "UI Components", selectionLabel(selection.ui), installedSummary(availabilities, "ui")), categoryOption("orm", "Database / ORM", selectionLabel(selection.orm), installedSummary(availabilities, "orm")), categoryOption("testing", "Testing", testingLabel(selection), installedSummary(availabilities, "testing")), categoryOption("custom-packages", "Custom Packages", customCount > 0 ? `${customCount} package${customCount === 1 ? "" : "s"}` : void 0), {
2509
2539
  value: "versions",
2510
2540
  label: "Edit Package Versions"
2511
2541
  }, {
2512
2542
  value: "review",
2513
- label: "Review and Install"
2543
+ label: reviewLabel
2514
2544
  });
2515
2545
  switch (guard(await p.select({
2516
2546
  message: "Choose a category",
@@ -4131,6 +4161,43 @@ async function ensureDependenciesInstalled(destination, packageManager) {
4131
4161
  const result = await realCommandRunner(baseInstallCommand(packageManager, destination));
4132
4162
  if (result.exitCode !== 0) p.log.warn(`Dependency installation failed (exit code ${result.exitCode}). Run "${packageManager} install" inside the project to finish setup.`);
4133
4163
  }
4164
+ /**
4165
+ * Express install: create a project from a saved preset in one shot, like the
4166
+ * official create-* tools but with the whole stack. Skips the intermediate
4167
+ * confirmations of runNew; the review screen is the single decision point.
4168
+ */
4169
+ async function runExpressInstall(presetName, projectNameArg, options = {}) {
4170
+ p.intro("StackPack — express install");
4171
+ const { preset, location } = await loadPreset(presetName, { projectRoot: process.cwd() });
4172
+ p.log.info(`Preset: ${preset.displayName ?? preset.name} ${pc.dim(`(${location.scope} — ${preset.integrations.length} integrations)`)}`);
4173
+ if (!await confirmIfInsideExistingProject()) throw new CancelledError();
4174
+ const { name, destination } = await askProjectName(projectNameArg);
4175
+ const creator = getCreator(preset.base.creator);
4176
+ const config = await loadConfig();
4177
+ const packageManager = options.packageManager ?? config.defaultPackageManager ?? "npm";
4178
+ await runOfficialCreator(creator, name, { language: preset.base.language }, packageManager);
4179
+ const context = await detectProject(destination, { packageManagerOverride: packageManager });
4180
+ if (context.detection === "unsupported") {
4181
+ p.log.warn("The generated project is not a supported React or Next.js project, so the preset's integrations cannot be applied.");
4182
+ await ensureDependenciesInstalled(destination, packageManager);
4183
+ p.outro(`Base project created at ${destination}. No integrations were installed.`);
4184
+ return;
4185
+ }
4186
+ if (await dashboardInstallLoop({
4187
+ context,
4188
+ selection: presetToSelection(preset),
4189
+ dryRun: false,
4190
+ startAtReview: true,
4191
+ sourcePreset: preset
4192
+ }) === "cancelled") {
4193
+ await ensureDependenciesInstalled(destination, packageManager);
4194
+ p.outro(`Base project created at ${destination}. No integrations were installed.`);
4195
+ return;
4196
+ }
4197
+ await ensureDependenciesInstalled(destination, packageManager);
4198
+ p.note(`cd ${name}\n${packageManager} run dev`, "Next steps");
4199
+ p.outro(`Project ready at ${destination} — good to go!`);
4200
+ }
4134
4201
  async function runNew(projectNameArg, options) {
4135
4202
  p.intro("StackPack — new project");
4136
4203
  p.log.message(pc.dim("Official project tooling with real-world integrations."));
@@ -4602,6 +4669,11 @@ async function runPresetsBrowser(options = {}) {
4602
4669
  value: "list",
4603
4670
  label: "Back to preset list"
4604
4671
  },
4672
+ {
4673
+ value: "edit",
4674
+ label: "Edit this preset",
4675
+ hint: "swap integrations, save back"
4676
+ },
4605
4677
  {
4606
4678
  value: "delete",
4607
4679
  label: "Delete this preset"
@@ -4613,6 +4685,10 @@ async function runPresetsBrowser(options = {}) {
4613
4685
  ]
4614
4686
  }));
4615
4687
  if (action === "menu") return;
4688
+ if (action === "edit") {
4689
+ await runPresetsEdit(selected.name, { cwd: projectRoot });
4690
+ continue;
4691
+ }
4616
4692
  if (action === "delete") {
4617
4693
  if (guard(await p.confirm({
4618
4694
  message: `Delete preset "${selected.name}"?`,
@@ -4624,6 +4700,65 @@ async function runPresetsBrowser(options = {}) {
4624
4700
  }
4625
4701
  }
4626
4702
  }
4703
+ /**
4704
+ * Editing a preset never touches a real project, so the dashboard runs
4705
+ * against a synthetic context built from the preset's own project shape.
4706
+ */
4707
+ function contextForPresetEditing(preset) {
4708
+ return {
4709
+ rootDirectory: process.cwd(),
4710
+ framework: preset.project.framework,
4711
+ buildTool: preset.project.buildTool,
4712
+ language: preset.project.language,
4713
+ packageManager: "npm",
4714
+ packageManagerCandidates: [],
4715
+ detection: "detected",
4716
+ packageJson: {},
4717
+ detectedFiles: [],
4718
+ installedPackages: {}
4719
+ };
4720
+ }
4721
+ /**
4722
+ * Save-and-load editing: open a preset in the dashboard, change integrations,
4723
+ * and write it back to the same file. No project is created or modified.
4724
+ */
4725
+ async function runPresetsEdit(name, options = {}) {
4726
+ const projectRoot = options.cwd ?? process.cwd();
4727
+ const { preset, location } = await loadPreset(name, { projectRoot });
4728
+ p.log.info(`Editing ${location.scope} preset: ${location.filePath}`);
4729
+ p.log.message(pc.dim("Changes apply to the preset file only — no project is created or modified."));
4730
+ const context = contextForPresetEditing(preset);
4731
+ const selection = presetToSelection(preset);
4732
+ if (await runDashboard(context, selection, { reviewLabel: "Finish editing" }) !== "review") return;
4733
+ if (selectionMatchesPreset(preset, selection)) {
4734
+ p.log.info(`No changes made — preset "${preset.name}" is untouched.`);
4735
+ return;
4736
+ }
4737
+ const rebuilt = buildPresetFromSelection({
4738
+ name: preset.name,
4739
+ scope: location.scope,
4740
+ context,
4741
+ selection
4742
+ });
4743
+ if (!rebuilt) {
4744
+ p.log.warn("This preset cannot be rebuilt (unknown framework or language). Nothing saved.");
4745
+ return;
4746
+ }
4747
+ const updated = {
4748
+ ...rebuilt,
4749
+ displayName: preset.displayName ?? preset.name,
4750
+ createdAt: preset.createdAt
4751
+ };
4752
+ if (!guard(await p.confirm({
4753
+ message: `Save changes to preset "${preset.name}"?`,
4754
+ initialValue: true
4755
+ }))) {
4756
+ p.log.info("Changes discarded — the preset file was not modified.");
4757
+ return;
4758
+ }
4759
+ const saved = await savePreset(updated, location.scope, { projectRoot });
4760
+ p.log.success(`Preset updated: ${saved.filePath}`);
4761
+ }
4627
4762
  async function runPresetsDelete(name, options = {}) {
4628
4763
  const storeOptions = { projectRoot: options.cwd ?? process.cwd() };
4629
4764
  p.intro("StackPack — delete preset");
@@ -4693,7 +4828,7 @@ async function getUpdateNotice(currentVersion) {
4693
4828
  }
4694
4829
  //#endregion
4695
4830
  //#region src/version.ts
4696
- const VERSION = "0.3.2";
4831
+ const VERSION = "0.3.4";
4697
4832
  //#endregion
4698
4833
  //#region src/program.ts
4699
4834
  const packageManagerOption = new Option("--package-manager <manager>", "package manager to use").choices([
@@ -4789,6 +4924,9 @@ async function runCli(argv) {
4789
4924
  program.command("apply").description("Apply a saved preset's integrations to the current project").argument("<preset-name>", "preset to apply").option("--dry-run", "show the full plan without changing anything").action(async (name, options) => {
4790
4925
  await runApply(name, options);
4791
4926
  });
4927
+ program.command("install").alias("i").description("Create a project from a saved preset in one shot (express mode)").argument("<preset-name>", "saved preset to install from").argument("[project-name]", "project folder to create (asked interactively if omitted)").addOption(packageManagerOption).action(async (presetName, projectName, options) => {
4928
+ await runExpressInstall(presetName, projectName, options);
4929
+ });
4792
4930
  const presets = program.command("presets").description("Manage saved presets");
4793
4931
  presets.command("list").description("List saved presets (global and project-local)").action(async () => {
4794
4932
  await runPresetsList();
@@ -4796,6 +4934,9 @@ async function runCli(argv) {
4796
4934
  presets.command("show").description("Show a preset's contents").argument("<preset-name>").action(async (name) => {
4797
4935
  await runPresetsShow(name);
4798
4936
  });
4937
+ presets.command("edit").description("Edit a saved preset's integrations (no project is touched)").argument("<preset-name>").action(async (name) => {
4938
+ await runPresetsEdit(name);
4939
+ });
4799
4940
  presets.command("delete").description("Delete a saved preset").argument("<preset-name>").action(async (name) => {
4800
4941
  await runPresetsDelete(name);
4801
4942
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackpack-cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Local-first, privacy-focused terminal integration builder for JavaScript and TypeScript projects. Official tooling first, presets stay on your device.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "dist"
21
21
  ],
22
22
  "engines": {
23
- "node": ">=18.17"
23
+ "node": ">=22"
24
24
  },
25
25
  "scripts": {
26
26
  "dev": "tsx src/cli.ts",