Skip to content

Commit 1f2eb1b

Browse files
Polish and publish 1.0.4 release
1 parent 4c0c3f8 commit 1f2eb1b

8 files changed

Lines changed: 96 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ concurrency:
1111
group: ci-${{ github.workflow }}-${{ github.ref }}
1212
cancel-in-progress: true
1313

14+
permissions:
15+
contents: read
16+
1417
jobs:
1518
build-and-test:
1619
name: Build & test (Node ${{ matrix.node }})

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ dist/
55
!.env.example
66
!.env.template
77
.env.local
8+
.envrc
89
*.tgz
910
.npmrc
1011
*.pem
1112
*.key
1213
*.p12
1314
*.crt
15+
*.token
16+
*.credentials.json
17+
secrets.json
18+
secrets.local.json
19+
.secrets/
1420
.DS_Store
1521
coverage/
1622
.setupr/

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to Setupr will be documented in this file. Setupr keeps a changelog because it is a versioned developer tool with user-facing CLI behavior.
44

5+
## 1.0.4
6+
7+
### Changed
8+
- Promoted the public README from beta language to stable-release language now that the npm package, install flow, and release checks are in place.
9+
- Hardened repository ignore rules for additional local secret and credential file patterns that should never be committed during real-world CLI use.
10+
11+
### Validation
12+
- Re-ran the full release gate: unit tests, typecheck, lint, build, plain and TUI smoke fixtures, `release check`, npm pack/publish dry-runs, and fresh-install execution from the packed tarball.
13+
514
## 1.0.3
615

716
### Fixed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **AI-powered project-control CLI.** Setupr detects your project's stack, plans setup, installs dependencies, configures environments, verifies local health, and keeps the project running — all from one terminal-native command.
44
5-
![Status](https://img.shields.io/badge/status-beta-6b7280) ![License](https://img.shields.io/github/license/Evan1108-Coder/Setupr) ![Node](https://img.shields.io/badge/node-%3E%3D18-3c873a) ![npm](https://img.shields.io/badge/npm-%40evan--coder%2Fsetupr-cb3837)
5+
![Status](https://img.shields.io/badge/status-stable-2563eb) ![License](https://img.shields.io/github/license/Evan1108-Coder/Setupr) ![Node](https://img.shields.io/badge/node-%3E%3D18-3c873a) ![npm](https://img.shields.io/badge/npm-%40evan--coder%2Fsetupr-cb3837)
66

77
**TypeScript CLI · stack detection · setup automation · project doctor · terminal dashboard · AI director**
88

@@ -21,7 +21,7 @@
2121
| **CI / scripts** | add `--plain` (no TUI) and `--force` (skip safe prompts); `--json` for machine-readable output |
2222
| **Requires** | Node.js ≥ 18, a Unicode-capable terminal |
2323

24-
> **Status: beta.** Setupr is usable as a daily CLI today. Stack-specific setup paths keep getting real-world coverage before a 1.0-stable claim.
24+
> **Status: stable public release.** Setupr is ready for npm installation and normal daily CLI use on supported terminals and Node versions.
2525
2626
**Who it's for:** developers who clone projects often and don't want to manually guess install commands, runtime versions, env files, ports, or verification steps.
2727

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@evan-coder/setupr",
3-
"version": "1.0.3",
3+
"version": "1.0.4",
44
"description": "Intelligent project setup & management CLI. Auto-detects your stack, installs dependencies, configures environments, and keeps projects healthy.",
55
"type": "module",
66
"repository": {

src/security/index.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ async function runSingle(cwd: string, category: SecurityCategory, options: Secur
143143
async function scanSecrets(cwd: string, depth: "quick" | "deep"): Promise<SecurityFinding[]> {
144144
const findings: SecurityFinding[] = [];
145145
for (const file of await listFiles(cwd, depth === "quick" ? 120 : 500)) {
146+
if (isNonProductionPath(file)) continue;
146147
if (!/\.(env|js|jsx|ts|tsx|py|go|rs|json|ya?ml|toml|md|txt|sh)$/i.test(file)) continue;
147148
const content = await readFile(join(cwd, file), "utf-8").catch(() => "");
148149
const lines = content.split(/\r?\n/);
@@ -233,14 +234,15 @@ async function scanDeps(cwd: string): Promise<SecurityFinding[]> {
233234
async function scanCode(cwd: string, depth: "quick" | "deep"): Promise<SecurityFinding[]> {
234235
const findings: SecurityFinding[] = [];
235236
for (const file of await listFiles(cwd, depth === "quick" ? 150 : 700)) {
237+
if (isNonProductionPath(file)) continue;
236238
if (!/\.[jt]sx?$|\.py$|\.go$|\.rs$/i.test(file)) continue;
237239
const content = await readFile(join(cwd, file), "utf-8").catch(() => "");
238240
const checks: Array<[RegExp, string, SecuritySeverity, string]> = [
239241
[/\beval\s*\(|new Function\s*\(/, "Dynamic code execution", "high", "Avoid eval/new Function on user-controlled data."],
240242
[/child_process\.(exec|execSync)\s*\(/, "Shell execution", "medium", "Prefer spawn/execFile with argument arrays and validate inputs."],
241243
[/jwt\.sign\([^)]*['"]none['"]|algorithm\s*:\s*['"]none['"]/, "JWT none algorithm", "critical", "Never allow unsigned JWTs."],
242244
[/secure\s*:\s*false|httpOnly\s*:\s*false/, "Weak cookie option", "medium", "Use secure and httpOnly cookies for sessions."],
243-
[/md5|sha1/i, "Weak hash usage", "low", "Use modern password hashing or SHA-256+ for non-password integrity."],
245+
[/\b(createHash\s*\(\s*['"](?:md5|sha1)['"]|md5\s*\(|sha1\s*\()/i, "Weak hash usage", "low", "Use modern password hashing or SHA-256+ for non-password integrity."],
244246
];
245247
for (const [pattern, title, severity, recommendation] of checks) {
246248
const line = content.split(/\r?\n/).findIndex((row) => pattern.test(row));
@@ -253,10 +255,27 @@ async function scanCode(cwd: string, depth: "quick" | "deep"): Promise<SecurityF
253255
async function scanRoutes(cwd: string): Promise<SecurityFinding[]> {
254256
const findings: SecurityFinding[] = [];
255257
for (const file of await listFiles(cwd, 500)) {
258+
if (isNonProductionPath(file)) continue;
256259
if (!/\.[jt]sx?$|\.py$/i.test(file)) continue;
257260
const content = await readFile(join(cwd, file), "utf-8").catch(() => "");
258-
if (/(\/admin|\/internal|\/debug)/i.test(content) && !/(auth|authorize|session|permission|requireUser|middleware)/i.test(content)) {
259-
findings.push(finding(`route-admin:${file}`, "Sensitive route without nearby auth signal", "routes", "medium", file, "Confirm this route is protected by middleware or explicit authorization."));
261+
const lines = content.split(/\r?\n/);
262+
const line = lines.findIndex((row) => (
263+
/(\/admin|\/internal|\/debug)/i.test(row)
264+
&& /(router|app\.|route|handler|pathname|href|redirect|rewrite|navigate|fetch|axios|\bGET\b|\bPOST\b|\bPUT\b|\bPATCH\b|\bDELETE\b)/i.test(row)
265+
&& !/(auth|authorize|session|permission|requireUser|middleware)/i.test(row)
266+
));
267+
if (line >= 0) {
268+
findings.push({
269+
id: `route-admin:${file}:${line + 1}`,
270+
title: "Sensitive route without nearby auth signal",
271+
category: "routes",
272+
severity: "medium",
273+
confidence: "medium",
274+
file,
275+
line: line + 1,
276+
evidence: redact(lines[line]),
277+
recommendation: "Confirm this route is protected by middleware or explicit authorization.",
278+
});
260279
}
261280
}
262281
return findings;
@@ -265,10 +284,23 @@ async function scanRoutes(cwd: string): Promise<SecurityFinding[]> {
265284
async function scanAuth(cwd: string): Promise<SecurityFinding[]> {
266285
const findings: SecurityFinding[] = [];
267286
for (const file of await listFiles(cwd, 400)) {
287+
if (isNonProductionPath(file)) continue;
268288
if (!/\.[jt]sx?$|\.py$/i.test(file)) continue;
269289
const content = await readFile(join(cwd, file), "utf-8").catch(() => "");
270-
if (/password/i.test(content) && /==|===/.test(content) && !/hash|bcrypt|argon|scrypt/i.test(content)) {
271-
findings.push(finding(`auth-password-compare:${file}`, "Plain password comparison signal", "auth", "high", file, "Use a password hashing library and constant-time verification."));
290+
const lines = content.split(/\r?\n/);
291+
const line = lines.findIndex((row) => /password/i.test(row) && /(===|==)/.test(row) && !/hash|bcrypt|argon|scrypt/i.test(row));
292+
if (line >= 0) {
293+
findings.push({
294+
id: `auth-password-compare:${file}:${line + 1}`,
295+
title: "Plain password comparison signal",
296+
category: "auth",
297+
severity: "high",
298+
confidence: "medium",
299+
file,
300+
line: line + 1,
301+
evidence: redact(lines[line]),
302+
recommendation: "Use a password hashing library and constant-time verification.",
303+
});
272304
}
273305
}
274306
return findings;
@@ -382,7 +414,13 @@ function markdown(report: SecurityReport): string {
382414
async function applyIgnores(cwd: string, findings: SecurityFinding[]): Promise<SecurityFinding[]> {
383415
const ignored = new Set(await readProjectJson<string[]>(cwd, SECURITY_IGNORE_FILE, []));
384416
const baseline = new Set(await readProjectJson<string[]>(cwd, SECURITY_BASELINE_FILE, []));
385-
return findings.filter((finding) => !ignored.has(finding.id) && !baseline.has(finding.id));
417+
const seen = new Set<string>();
418+
return findings.filter((finding) => {
419+
const dedupeKey = [finding.category, finding.title, finding.file || "", finding.line || 0].join(":");
420+
if (seen.has(dedupeKey)) return false;
421+
seen.add(dedupeKey);
422+
return !ignored.has(finding.id) && !baseline.has(finding.id);
423+
});
386424
}
387425

388426
async function listFiles(cwd: string, max: number): Promise<string[]> {
@@ -407,6 +445,10 @@ async function listFiles(cwd: string, max: number): Promise<string[]> {
407445
return files;
408446
}
409447

448+
function isNonProductionPath(file: string): boolean {
449+
return /(^|\/)(test|tests|__tests__|__mocks__|fixtures|fixture|examples|example|docs)(\/|$)/i.test(file);
450+
}
451+
410452
function finding(id: string, title: string, category: SecurityCategory, severity: SecuritySeverity, evidence: string, recommendation: string): SecurityFinding {
411453
return { id, title, category, severity, confidence: "medium", evidence, recommendation };
412454
}

tests/verification-security.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2-
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
2+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
33
import { existsSync } from "fs";
44
import { join } from "path";
55
import { tmpdir } from "os";
@@ -104,6 +104,22 @@ describe("verification and security command groups", () => {
104104
expect(status.health.checks.some((check) => check.label === "Security")).toBe(true);
105105
});
106106

107+
it("ignores test fixtures for code heuristics and only reports real app auth issues", async () => {
108+
await writeFile(join(tempDir, "package.json"), JSON.stringify({ scripts: { test: "node -e \"console.log('ok')\"" } }, null, 2));
109+
await writeFile(join(tempDir, "app.ts"), "const password = input === passwordInput;\n");
110+
await mkdir(join(tempDir, "tests"), { recursive: true });
111+
await mkdir(join(tempDir, ".github", "workflows"), { recursive: true });
112+
await writeFile(join(tempDir, "tests", "security.test.ts"), "eval('1+1'); const password = input === passwordInput;\n");
113+
await writeFile(join(tempDir, ".github", "workflows", "ci.yml"), "name: ci\non: push\npermissions:\n contents: read\n");
114+
115+
const report = await runSecurityCommand(tempDir, "deep", {});
116+
117+
expect(report?.findings.some((finding) => finding.title === "Plain password comparison signal" && finding.file === "app.ts")).toBe(true);
118+
expect(report?.findings.some((finding) => finding.file?.includes("tests/security.test.ts"))).toBe(false);
119+
expect(report?.findings.some((finding) => finding.title === "Dynamic code execution")).toBe(false);
120+
expect(report?.findings.some((finding) => finding.title === "GitHub workflow has no explicit permissions block")).toBe(false);
121+
});
122+
107123
it("routes CLI test/security commands through the plain command router", async () => {
108124
await writeFile(join(tempDir, "package.json"), JSON.stringify({ scripts: { test: "node -e \"console.log('ok')\"" } }));
109125

0 commit comments

Comments
 (0)