Skip to content

Commit 44049d8

Browse files
Harden release readiness checks
1 parent de45c5a commit 44049d8

6 files changed

Lines changed: 277 additions & 46 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,16 @@ npm run smoke:fixtures:tui
504504

505505
That does not replace manual iTerm2/Ghostty visual QA, but it catches obvious TUI launch regressions.
506506

507+
For a local package/install smoke before publishing:
508+
509+
```bash
510+
npm pack
511+
npm exec --yes --package ./setupr-1.0.0.tgz -- setupr --version
512+
npx --yes file:$(pwd)/setupr-1.0.0.tgz --version
513+
```
514+
515+
Use `file:` or `--package` for tarball checks. A bare `npx ./setupr-1.0.0.tgz` is treated like an executable file path and will fail with a permission error.
516+
507517
## License
508518

509519
MIT

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
"version": "1.0.0",
44
"description": "Intelligent project setup & management CLI. Auto-detects your stack, installs dependencies, configures environments, and keeps projects healthy.",
55
"type": "module",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/Evan1108-Coder/Setupr.git"
9+
},
10+
"bugs": {
11+
"url": "https://github.com/Evan1108-Coder/Setupr/issues"
12+
},
13+
"homepage": "https://github.com/Evan1108-Coder/Setupr#readme",
614
"bin": {
715
"setupr": "./dist/setup.js",
816
"setup": "./dist/setup.js"

src/cli/plain.ts

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -196,38 +196,14 @@ async function plainDoctor(cwd: string, options: PlainOptions = {}): Promise<voi
196196

197197
console.log(chalk.blue.bold("\n Setupr Doctor\n"));
198198

199-
// Runtime
199+
const checkedCommands = new Set<string>();
200+
200201
if (scan.runtime) {
201-
const result = await runCommand(`${scan.runtime.name} --version`, cwd);
202-
if (result.exitCode === 0) {
203-
console.log(chalk.green(` ✓ ${scan.runtime.name}: ${result.stdout.trim()}`));
204-
} else {
205-
printPlainError(classifyCommandFailure({
206-
command: `${scan.runtime.name} --version`,
207-
cwd,
208-
exitCode: result.exitCode,
209-
stdout: result.stdout,
210-
stderr: result.stderr,
211-
stepLabel: `${scan.runtime.name} runtime`,
212-
}));
213-
}
202+
await printVersionCheck(`${scan.runtime.name} runtime`, runtimeVersionCommands(scan.runtime.name), cwd, checkedCommands);
214203
}
215204

216-
// PM
217205
if (scan.packageManager) {
218-
const result = await runCommand(`${scan.packageManager} --version`, cwd);
219-
if (result.exitCode === 0) {
220-
console.log(chalk.green(` ✓ ${scan.packageManager}: ${result.stdout.trim()}`));
221-
} else {
222-
printPlainError(classifyCommandFailure({
223-
command: `${scan.packageManager} --version`,
224-
cwd,
225-
exitCode: result.exitCode,
226-
stdout: result.stdout,
227-
stderr: result.stderr,
228-
stepLabel: `${scan.packageManager} package manager`,
229-
}));
230-
}
206+
await printVersionCheck(`${scan.packageManager} package manager`, packageManagerVersionCommands(scan.packageManager), cwd, checkedCommands);
231207
}
232208

233209
console.log(chalk.dim(`\n Language: ${scan.language || "unknown"}`));
@@ -295,6 +271,58 @@ async function plainDoctor(cwd: string, options: PlainOptions = {}): Promise<voi
295271
}
296272
}
297273

274+
async function printVersionCheck(label: string, commands: string[], cwd: string, checkedCommands: Set<string>): Promise<void> {
275+
const candidates = commands.filter((command) => !checkedCommands.has(command));
276+
if (candidates.length === 0) return;
277+
278+
let lastResult: Awaited<ReturnType<typeof runCommand>> | null = null;
279+
for (const command of candidates) {
280+
checkedCommands.add(command);
281+
const result = await runCommand(command, cwd);
282+
lastResult = result;
283+
if (result.exitCode === 0) {
284+
console.log(chalk.green(` ✓ ${label}: ${firstLine(result.stdout || result.stderr)}`));
285+
return;
286+
}
287+
}
288+
289+
const command = candidates[0];
290+
printPlainError(classifyCommandFailure({
291+
command,
292+
cwd,
293+
exitCode: lastResult?.exitCode ?? 1,
294+
stdout: lastResult?.stdout ?? "",
295+
stderr: lastResult?.stderr ?? "",
296+
stepLabel: label,
297+
}));
298+
}
299+
300+
function runtimeVersionCommands(runtime: string): string[] {
301+
switch (runtime.toLowerCase()) {
302+
case "python":
303+
return ["python3 --version", "python --version"];
304+
case "rust":
305+
return ["rustc --version"];
306+
case "go":
307+
return ["go version"];
308+
default:
309+
return [`${runtime} --version`];
310+
}
311+
}
312+
313+
function packageManagerVersionCommands(packageManager: string): string[] {
314+
switch (packageManager) {
315+
case "go":
316+
return ["go version"];
317+
default:
318+
return [`${packageManager} --version`];
319+
}
320+
}
321+
322+
function firstLine(value: string): string {
323+
return value.trim().split(/\r?\n/)[0] || "available";
324+
}
325+
298326
async function plainStart(cwd: string, target: string | undefined, options: PlainOptions): Promise<void> {
299327
const { startManagedProcess } = await import("../processes/manager.js");
300328
try {

src/commands/plain/router.ts

Lines changed: 167 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ interface LockPackageInfo {
700700
interface DependencySnapshot {
701701
packageJson?: PackageJsonInfo;
702702
lockfile?: any;
703+
lockfileError?: string;
703704
packages: LockPackageInfo[];
704705
}
705706

@@ -729,23 +730,151 @@ async function cmdDepsList(cwd: string) {
729730
const scan = await scanProject(cwd);
730731
const pm = scan.packageManager || "npm";
731732
console.log(chalk.blue(`Dependencies (${pm}):`));
732-
const result = await runCommand(`${pm} list --depth=0`, cwd);
733+
734+
const listCommand = dependencyListCommand(pm);
735+
if (!listCommand) {
736+
await printDeclaredDependencies(cwd, scan, `No live dependency tree command is configured for ${pm}.`);
737+
return;
738+
}
739+
740+
const result = await runCommand(listCommand, cwd);
733741
if (result.exitCode !== 0) {
734-
console.log(chalk.yellow(" Package manager dependency tree is unavailable; showing package.json declarations instead."));
735-
const snapshot = await loadDependencySnapshot(cwd);
736-
const declared = declaredDependencyRows(snapshot.packageJson);
737-
if (declared.length === 0) {
738-
printPlainError(classifyCommandFailure({ command: `${pm} list --depth=0`, cwd, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }));
742+
const shown = await printDeclaredDependencies(cwd, scan, "Package manager dependency tree is unavailable; showing package.json declarations or ecosystem declarations instead.");
743+
if (!shown) {
744+
printPlainError(classifyCommandFailure({ command: listCommand, cwd, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }));
739745
return;
740746
}
741-
for (const row of declared) {
742-
console.log(` ${row.kind.padEnd(22)} ${row.name}@${row.range}`);
743-
}
744747
} else {
745748
console.log(result.stdout || result.stderr);
746749
}
747750
}
748751

752+
function dependencyListCommand(packageManager: string): string | null {
753+
switch (packageManager) {
754+
case "npm":
755+
case "yarn":
756+
case "pnpm":
757+
case "bun":
758+
return `${packageManager} list --depth=0`;
759+
case "pip":
760+
return "python3 -m pip list || python -m pip list || pip list";
761+
case "poetry":
762+
return "poetry show --no-dev";
763+
case "pipenv":
764+
return "pipenv graph";
765+
case "go":
766+
return "go list -m all";
767+
case "cargo":
768+
return null;
769+
default:
770+
return null;
771+
}
772+
}
773+
774+
async function printDeclaredDependencies(cwd: string, scan: Awaited<ReturnType<typeof scanProject>>, reason: string): Promise<boolean> {
775+
console.log(chalk.yellow(` ${reason}`));
776+
const rows = await declaredDependencyRowsForProject(cwd, scan);
777+
if (rows.length === 0) return false;
778+
for (const row of rows) {
779+
console.log(` ${row.kind.padEnd(22)} ${row.name}${row.range ? `@${row.range}` : ""}`);
780+
}
781+
return true;
782+
}
783+
784+
async function declaredDependencyRowsForProject(
785+
cwd: string,
786+
scan: Awaited<ReturnType<typeof scanProject>>
787+
): Promise<Array<{ kind: string; name: string; range: string }>> {
788+
const snapshot = await loadDependencySnapshot(cwd);
789+
const nodeRows = declaredDependencyRows(snapshot.packageJson);
790+
if (nodeRows.length > 0) return nodeRows;
791+
792+
if (scan.packageManager === "cargo" || scan.language === "Rust") {
793+
return parseCargoDependencies(await readTextFile(join(cwd, "Cargo.toml")));
794+
}
795+
if (scan.packageManager === "go" || scan.language === "Go") {
796+
return parseGoDependencies(await readTextFile(join(cwd, "go.mod")));
797+
}
798+
if (scan.language === "Python") {
799+
return [
800+
...parseRequirementsDependencies(await readTextFile(join(cwd, "requirements.txt"))),
801+
...parsePyprojectDependencies(await readTextFile(join(cwd, "pyproject.toml"))),
802+
];
803+
}
804+
return [];
805+
}
806+
807+
async function readTextFile(path: string): Promise<string> {
808+
try {
809+
return await readFile(path, "utf-8");
810+
} catch {
811+
return "";
812+
}
813+
}
814+
815+
function parseCargoDependencies(content: string): Array<{ kind: string; name: string; range: string }> {
816+
const rows: Array<{ kind: string; name: string; range: string }> = [];
817+
let section = "";
818+
for (const line of content.split(/\r?\n/)) {
819+
const trimmed = line.trim();
820+
if (!trimmed || trimmed.startsWith("#")) continue;
821+
if (/^\[.+\]$/.test(trimmed)) {
822+
section = trimmed;
823+
continue;
824+
}
825+
if (section !== "[dependencies]" && section !== "[dev-dependencies]" && section !== "[build-dependencies]") continue;
826+
const match = trimmed.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
827+
if (!match) continue;
828+
rows.push({ kind: section.slice(1, -1), name: match[1], range: match[2].replace(/^"|"$/g, "") });
829+
}
830+
return rows.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
831+
}
832+
833+
function parseGoDependencies(content: string): Array<{ kind: string; name: string; range: string }> {
834+
const rows: Array<{ kind: string; name: string; range: string }> = [];
835+
let inRequireBlock = false;
836+
for (const line of content.split(/\r?\n/)) {
837+
const trimmed = line.trim();
838+
if (!trimmed || trimmed.startsWith("//")) continue;
839+
if (trimmed.startsWith("require (")) {
840+
inRequireBlock = true;
841+
continue;
842+
}
843+
if (inRequireBlock && trimmed === ")") {
844+
inRequireBlock = false;
845+
continue;
846+
}
847+
const depLine = inRequireBlock ? trimmed : trimmed.startsWith("require ") ? trimmed.slice("require ".length).trim() : "";
848+
if (!depLine) continue;
849+
const [name, range] = depLine.split(/\s+/, 2);
850+
if (name && range) rows.push({ kind: "require", name, range });
851+
}
852+
return rows.sort((a, b) => a.name.localeCompare(b.name));
853+
}
854+
855+
function parseRequirementsDependencies(content: string): Array<{ kind: string; name: string; range: string }> {
856+
return content.split(/\r?\n/)
857+
.map((line) => line.trim())
858+
.filter((line) => line && !line.startsWith("#") && !line.startsWith("-"))
859+
.map((line) => {
860+
const match = line.match(/^([A-Za-z0-9_.-]+)\s*([<>=!~].*)?$/);
861+
return { kind: "requirements", name: match?.[1] || line, range: match?.[2] || "" };
862+
})
863+
.sort((a, b) => a.name.localeCompare(b.name));
864+
}
865+
866+
function parsePyprojectDependencies(content: string): Array<{ kind: string; name: string; range: string }> {
867+
const rows: Array<{ kind: string; name: string; range: string }> = [];
868+
const dependenciesBlock = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/m)?.[1] || "";
869+
for (const raw of dependenciesBlock.split(",")) {
870+
const value = raw.trim().replace(/^["']|["']$/g, "");
871+
if (!value) continue;
872+
const match = value.match(/^([A-Za-z0-9_.-]+)\s*(.*)$/);
873+
rows.push({ kind: "pyproject", name: match?.[1] || value, range: match?.[2] || "" });
874+
}
875+
return rows.sort((a, b) => a.name.localeCompare(b.name));
876+
}
877+
749878
async function cmdDepsAudit(cwd: string): Promise<void> {
750879
console.log(chalk.blue.bold("\n Dependency Audit\n"));
751880
const result = await runCommand("npm audit --json", cwd);
@@ -760,6 +889,13 @@ async function cmdDepsAudit(cwd: string): Promise<void> {
760889
}
761890
return;
762891
}
892+
if (audit.error) {
893+
console.log(chalk.yellow(" Audit unavailable."));
894+
console.log(chalk.dim(` npm audit returned ${audit.error.code || "an error"}${audit.error.summary ? `: ${audit.error.summary}` : ""}`));
895+
if (audit.error.detail) console.log(chalk.dim(` ${audit.error.detail}`));
896+
process.exitCode = result.exitCode || 1;
897+
return;
898+
}
763899

764900
const counts = auditCounts(audit);
765901
const total = counts.total ?? Object.entries(counts)
@@ -824,6 +960,8 @@ async function cmdDepsWhy(cwd: string, packageName: string | undefined): Promise
824960
for (const pkg of locked) {
825961
console.log(` - ${pkg.name}${pkg.version ? `@${pkg.version}` : ""} (${pkg.path || "root"})`);
826962
}
963+
} else if (snapshot.lockfileError) {
964+
console.log(chalk.yellow(`\n package-lock.json could not be parsed; transitive signal is limited. ${snapshot.lockfileError}`));
827965
} else if (!snapshot.lockfile) {
828966
console.log(chalk.yellow("\n No package-lock.json found; transitive signal is limited."));
829967
} else {
@@ -845,7 +983,9 @@ async function cmdDepsLicenses(cwd: string): Promise<void> {
845983
const snapshot = await loadDependencySnapshot(cwd);
846984
console.log(chalk.blue.bold("\n Dependency Licenses\n"));
847985

848-
if (!snapshot.lockfile && snapshot.packages.length === 0) {
986+
if (snapshot.lockfileError) {
987+
console.log(chalk.yellow(` package-lock.json could not be parsed; license signal is limited. ${snapshot.lockfileError}`));
988+
} else if (!snapshot.lockfile && snapshot.packages.length === 0) {
849989
console.log(chalk.yellow(" No package-lock.json found; license signal is limited."));
850990
}
851991

@@ -872,10 +1012,12 @@ async function cmdDepsLicenses(cwd: string): Promise<void> {
8721012

8731013
async function loadDependencySnapshot(cwd: string): Promise<DependencySnapshot> {
8741014
const packageJson = await readJsonFile<PackageJsonInfo>(join(cwd, "package.json"));
875-
const lockfile = await readJsonFile<any>(join(cwd, "package-lock.json"));
1015+
const lockfileResult = await readJsonFileWithError<any>(join(cwd, "package-lock.json"));
1016+
const lockfile = lockfileResult.value;
8761017
return {
8771018
packageJson,
8781019
lockfile,
1020+
lockfileError: lockfileResult.exists && !lockfileResult.ok ? lockfileResult.error : undefined,
8791021
packages: collectLockPackages(lockfile),
8801022
};
8811023
}
@@ -888,6 +1030,20 @@ async function readJsonFile<T>(path: string): Promise<T | undefined> {
8881030
}
8891031
}
8901032

1033+
async function readJsonFileWithError<T>(path: string): Promise<{ ok: boolean; exists: boolean; value?: T; error?: string }> {
1034+
try {
1035+
return { ok: true, exists: true, value: JSON.parse(await readFile(path, "utf-8")) as T };
1036+
} catch (err) {
1037+
const code = (err as { code?: string } | undefined)?.code;
1038+
if (code === "ENOENT") return { ok: false, exists: false };
1039+
return {
1040+
ok: false,
1041+
exists: true,
1042+
error: err instanceof Error ? err.message : String(err),
1043+
};
1044+
}
1045+
}
1046+
8911047
function collectLockPackages(lockfile: any): LockPackageInfo[] {
8921048
if (!lockfile) return [];
8931049

0 commit comments

Comments
 (0)