stackpack-cli 0.3.1 → 0.3.2
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 +2 -0
- package/dist/cli.js +1 -1
- package/dist/{program-D9mgsLIa.js → program-CqWM02UT.js} +120 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,8 @@ StackPack requires **no sign-up, no login, no server, no telemetry**. Presets ar
|
|
|
137
137
|
|
|
138
138
|
Presets never contain shell commands, executable code, absolute paths, credentials, or `.env` values — the schema rejects anything unexpected. Internet access is only needed to run official creators/initializers and install packages.
|
|
139
139
|
|
|
140
|
+
One small exception, in the open: the interactive menu checks the npm registry (at most once per day, cached locally) to tell you when a newer StackPack version exists. It is an anonymous read-only request that sends nothing about you or your projects, never updates anything by itself, fails silently offline, and can be disabled completely by setting the `STACKPACK_NO_UPDATE_CHECK` environment variable.
|
|
141
|
+
|
|
140
142
|
## Safety
|
|
141
143
|
|
|
142
144
|
- Nothing is installed before you confirm the reviewed plan.
|
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-CqWM02UT.js");
|
|
5
5
|
await runCli(process.argv);
|
|
6
6
|
process.exit(process.exitCode ?? 0);
|
|
7
7
|
//#endregion
|
|
@@ -3828,16 +3828,50 @@ function buildPresetFromSelection(params) {
|
|
|
3828
3828
|
versionOverrides: selection.versionOverrides
|
|
3829
3829
|
};
|
|
3830
3830
|
}
|
|
3831
|
+
/**
|
|
3832
|
+
* True when the current selection still matches what the preset describes —
|
|
3833
|
+
* a save-and-load flow should not re-ask to save an unchanged setup.
|
|
3834
|
+
*/
|
|
3835
|
+
function selectionMatchesPreset(preset, selection) {
|
|
3836
|
+
const dependencies = {};
|
|
3837
|
+
const devDependencies = {};
|
|
3838
|
+
for (const pkg of selection.customPackages) (pkg.dependencyType === "dependency" ? dependencies : devDependencies)[pkg.name] = pkg.version;
|
|
3839
|
+
const current = {
|
|
3840
|
+
integrations: selectedRecipes(selection).map(({ recipe, options }) => ({
|
|
3841
|
+
id: recipe.id,
|
|
3842
|
+
options
|
|
3843
|
+
})),
|
|
3844
|
+
dependencies,
|
|
3845
|
+
devDependencies,
|
|
3846
|
+
versionOverrides: selection.versionOverrides
|
|
3847
|
+
};
|
|
3848
|
+
const saved = {
|
|
3849
|
+
integrations: preset.integrations.map(({ id, options }) => ({
|
|
3850
|
+
id,
|
|
3851
|
+
options
|
|
3852
|
+
})),
|
|
3853
|
+
dependencies: preset.customPackages.dependencies,
|
|
3854
|
+
devDependencies: preset.customPackages.devDependencies,
|
|
3855
|
+
versionOverrides: preset.versionOverrides
|
|
3856
|
+
};
|
|
3857
|
+
return JSON.stringify(current) === JSON.stringify(saved);
|
|
3858
|
+
}
|
|
3831
3859
|
/** Post-install offer to save the applied setup as a preset. */
|
|
3832
|
-
async function offerToSavePreset(context, selection) {
|
|
3860
|
+
async function offerToSavePreset(context, selection, options = {}) {
|
|
3833
3861
|
if (context.framework === "unknown" || context.language === "unknown") return;
|
|
3862
|
+
const source = options.sourcePreset;
|
|
3863
|
+
if (source && selectionMatchesPreset(source, selection)) {
|
|
3864
|
+
p.log.success(`Installed from preset "${source.displayName ?? source.name}" — nothing new to save. Good to go!`);
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3834
3867
|
if (!guard(await p.confirm({
|
|
3835
|
-
message: "Save this setup as a preset?",
|
|
3868
|
+
message: source ? `This setup was changed after loading preset "${source.name}". Save the updated setup as a preset?` : "Save this setup as a preset?",
|
|
3836
3869
|
initialValue: false
|
|
3837
3870
|
}))) return;
|
|
3838
3871
|
const name = guard(await p.text({
|
|
3839
3872
|
message: "Preset name",
|
|
3840
3873
|
placeholder: "my-react-stack",
|
|
3874
|
+
initialValue: source?.name,
|
|
3841
3875
|
validate(value) {
|
|
3842
3876
|
const result = validatePresetName(value ?? "");
|
|
3843
3877
|
return result.ok ? void 0 : result.reason;
|
|
@@ -3902,7 +3936,7 @@ async function dashboardInstallLoop(params) {
|
|
|
3902
3936
|
continue;
|
|
3903
3937
|
}
|
|
3904
3938
|
if (result === "installed") {
|
|
3905
|
-
await offerToSavePreset(params.context, params.selection);
|
|
3939
|
+
await offerToSavePreset(params.context, params.selection, { sourcePreset: params.sourcePreset });
|
|
3906
3940
|
return "installed";
|
|
3907
3941
|
}
|
|
3908
3942
|
return result === "dry-run" ? "dry-run" : "cancelled";
|
|
@@ -3968,6 +4002,25 @@ async function directoryState(target) {
|
|
|
3968
4002
|
return "missing";
|
|
3969
4003
|
}
|
|
3970
4004
|
}
|
|
4005
|
+
/**
|
|
4006
|
+
* Creating a project inside an existing project is allowed but easy to mix up
|
|
4007
|
+
* later (two nested package.json trees), so it needs an explicit confirmation.
|
|
4008
|
+
*/
|
|
4009
|
+
async function confirmIfInsideExistingProject() {
|
|
4010
|
+
if (!await fs.access(path.join(process.cwd(), "package.json")).then(() => true).catch(() => false)) return true;
|
|
4011
|
+
p.log.warn(`This folder is already a project (package.json found):\n ${process.cwd()}\nThe new project would be created inside it — nested projects are easy to confuse later.`);
|
|
4012
|
+
return guard(await p.select({
|
|
4013
|
+
message: "Create the new project inside this existing project anyway?",
|
|
4014
|
+
options: [{
|
|
4015
|
+
value: "cancel",
|
|
4016
|
+
label: "No, cancel",
|
|
4017
|
+
hint: "cd to another folder first, then run stackpack again"
|
|
4018
|
+
}, {
|
|
4019
|
+
value: "continue",
|
|
4020
|
+
label: "Yes, create it nested inside this project"
|
|
4021
|
+
}]
|
|
4022
|
+
})) === "continue";
|
|
4023
|
+
}
|
|
3971
4024
|
async function askProjectName(initial) {
|
|
3972
4025
|
let candidate = initial;
|
|
3973
4026
|
for (;;) {
|
|
@@ -4081,6 +4134,7 @@ async function ensureDependenciesInstalled(destination, packageManager) {
|
|
|
4081
4134
|
async function runNew(projectNameArg, options) {
|
|
4082
4135
|
p.intro("StackPack — new project");
|
|
4083
4136
|
p.log.message(pc.dim("Official project tooling with real-world integrations."));
|
|
4137
|
+
if (!await confirmIfInsideExistingProject()) throw new CancelledError();
|
|
4084
4138
|
let preset;
|
|
4085
4139
|
if (options.preset) preset = (await loadPreset(options.preset, { projectRoot: process.cwd() })).preset;
|
|
4086
4140
|
else {
|
|
@@ -4179,14 +4233,16 @@ async function runNew(projectNameArg, options) {
|
|
|
4179
4233
|
context,
|
|
4180
4234
|
selection: preset ? presetToSelection(preset) : createEmptySelection(),
|
|
4181
4235
|
dryRun: false,
|
|
4182
|
-
startAtReview: preset !== void 0
|
|
4236
|
+
startAtReview: preset !== void 0,
|
|
4237
|
+
sourcePreset: preset
|
|
4183
4238
|
}) === "cancelled") {
|
|
4184
4239
|
await ensureDependenciesInstalled(destination, packageManager);
|
|
4185
4240
|
p.outro(`Base project created at ${destination}. No integrations were installed.`);
|
|
4186
4241
|
return;
|
|
4187
4242
|
}
|
|
4188
4243
|
await ensureDependenciesInstalled(destination, packageManager);
|
|
4189
|
-
p.
|
|
4244
|
+
p.note(`cd ${name}\n${packageManager} run dev`, "Next steps");
|
|
4245
|
+
p.outro(`Project ready at ${destination} — good to go!`);
|
|
4190
4246
|
}
|
|
4191
4247
|
//#endregion
|
|
4192
4248
|
//#region src/project/git-status.ts
|
|
@@ -4436,7 +4492,8 @@ async function runApply(name, options) {
|
|
|
4436
4492
|
context,
|
|
4437
4493
|
selection: presetToSelection(preset),
|
|
4438
4494
|
dryRun: options.dryRun === true,
|
|
4439
|
-
startAtReview: true
|
|
4495
|
+
startAtReview: true,
|
|
4496
|
+
sourcePreset: preset
|
|
4440
4497
|
});
|
|
4441
4498
|
if (outcome === "cancelled") throw new CancelledError();
|
|
4442
4499
|
p.outro(outcome === "dry-run" ? "Dry run finished." : `Preset "${name}" applied.`);
|
|
@@ -4581,8 +4638,62 @@ async function runPresetsDelete(name, options = {}) {
|
|
|
4581
4638
|
p.outro(`Deleted ${location.scope} preset: ${location.filePath}`);
|
|
4582
4639
|
}
|
|
4583
4640
|
//#endregion
|
|
4641
|
+
//#region src/utils/update-check.ts
|
|
4642
|
+
const PACKAGE_NAME = "stackpack-cli";
|
|
4643
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
4644
|
+
const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
4645
|
+
const FETCH_TIMEOUT_MS = 2e3;
|
|
4646
|
+
function cacheFilePath() {
|
|
4647
|
+
return path.join(getStoragePaths().cacheDir, "update-check.json");
|
|
4648
|
+
}
|
|
4649
|
+
async function readCachedLatest() {
|
|
4650
|
+
try {
|
|
4651
|
+
const raw = JSON.parse(await fs.readFile(cacheFilePath(), "utf8"));
|
|
4652
|
+
return Date.now() - Date.parse(raw.checkedAt) < CHECK_INTERVAL_MS && semver.valid(raw.latest) ? raw.latest : null;
|
|
4653
|
+
} catch {
|
|
4654
|
+
return null;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
async function fetchLatestFromRegistry() {
|
|
4658
|
+
const controller = new AbortController();
|
|
4659
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
4660
|
+
try {
|
|
4661
|
+
const response = await fetch(REGISTRY_URL, { signal: controller.signal });
|
|
4662
|
+
if (!response.ok) return null;
|
|
4663
|
+
const data = await response.json();
|
|
4664
|
+
if (typeof data.version !== "string" || !semver.valid(data.version)) return null;
|
|
4665
|
+
const cache = {
|
|
4666
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4667
|
+
latest: data.version
|
|
4668
|
+
};
|
|
4669
|
+
await fs.mkdir(path.dirname(cacheFilePath()), { recursive: true });
|
|
4670
|
+
await fs.writeFile(cacheFilePath(), JSON.stringify(cache), "utf8");
|
|
4671
|
+
return data.version;
|
|
4672
|
+
} catch {
|
|
4673
|
+
return null;
|
|
4674
|
+
} finally {
|
|
4675
|
+
clearTimeout(timer);
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
/**
|
|
4679
|
+
* Anonymous, best-effort check for a newer published version. Never blocks
|
|
4680
|
+
* for more than ~2s, runs at most once per day (cached in ~/.stackpack/cache),
|
|
4681
|
+
* fails silently offline, and is disabled entirely by STACKPACK_NO_UPDATE_CHECK.
|
|
4682
|
+
* It never updates anything by itself — it only returns a notice to display.
|
|
4683
|
+
*/
|
|
4684
|
+
async function getUpdateNotice(currentVersion) {
|
|
4685
|
+
if (process.env.STACKPACK_NO_UPDATE_CHECK) return null;
|
|
4686
|
+
try {
|
|
4687
|
+
const latest = await readCachedLatest() ?? await fetchLatestFromRegistry();
|
|
4688
|
+
if (!latest || !semver.valid(currentVersion) || !semver.gt(latest, currentVersion)) return null;
|
|
4689
|
+
return `Update available: ${currentVersion} → ${latest}\nRun: npm install -g ${PACKAGE_NAME}`;
|
|
4690
|
+
} catch {
|
|
4691
|
+
return null;
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
//#endregion
|
|
4584
4695
|
//#region src/version.ts
|
|
4585
|
-
const VERSION = "0.3.
|
|
4696
|
+
const VERSION = "0.3.2";
|
|
4586
4697
|
//#endregion
|
|
4587
4698
|
//#region src/program.ts
|
|
4588
4699
|
const packageManagerOption = new Option("--package-manager <manager>", "package manager to use").choices([
|
|
@@ -4596,6 +4707,8 @@ async function runMainMenu() {
|
|
|
4596
4707
|
p.log.message("Official project tooling with real-world integrations.");
|
|
4597
4708
|
p.log.message("Presets stay on this device.");
|
|
4598
4709
|
p.log.message(pc.dim("Ctrl+C on any question brings you back to this menu instead of exiting."));
|
|
4710
|
+
const updateNotice = await getUpdateNotice(VERSION);
|
|
4711
|
+
if (updateNotice) p.log.warn(pc.yellow(updateNotice));
|
|
4599
4712
|
for (;;) {
|
|
4600
4713
|
const choice = await p.select({
|
|
4601
4714
|
message: "What would you like to do?",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stackpack-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|