stackpack-cli 0.3.2 → 0.3.3
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 +3 -0
- package/dist/cli.js +1 -1
- package/dist/{program-CqWM02UT.js → program-Bq8-8HvQ.js} +150 -9
- package/package.json +2 -2
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-
|
|
4
|
+
const { runCli } = await import("./program-Bq8-8HvQ.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
|
-
/**
|
|
2481
|
-
|
|
2482
|
-
|
|
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
|
|
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:
|
|
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.
|
|
4831
|
+
const VERSION = "0.3.3";
|
|
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.
|
|
3
|
+
"version": "0.3.3",
|
|
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": ">=
|
|
23
|
+
"node": ">=22"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"dev": "tsx src/cli.ts",
|