@jgiox/goodvibes 1.0.0 → 1.1.0
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/dist/index.js +203 -7
- package/package.json +10 -7
- package/templates/.github/dependabot.yml +20 -0
- package/templates/.github/workflows/ci-both.yml +66 -0
- package/templates/.github/workflows/ci-node.yml +36 -0
- package/templates/.github/workflows/ci-python.yml +38 -0
- package/templates/.github/workflows/dependency-review.yml +15 -0
- package/templates/.github/workflows/security.yml +46 -0
- package/templates/CLAUDE.md +28 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { intro, outro, note, tasks } from "@clack/prompts";
|
|
|
8
8
|
|
|
9
9
|
// src/steps/copy-templates.ts
|
|
10
10
|
import { copy } from "fs-extra";
|
|
11
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
11
|
+
import { readFile as readFile2, rename } from "fs/promises";
|
|
12
12
|
import { readdir } from "fs/promises";
|
|
13
13
|
import { existsSync } from "fs";
|
|
14
14
|
import { join, relative } from "path";
|
|
@@ -86,9 +86,25 @@ async function listTemplateFiles(templateDir) {
|
|
|
86
86
|
await walk(templateDir);
|
|
87
87
|
return results.sort();
|
|
88
88
|
}
|
|
89
|
-
async function
|
|
89
|
+
async function walkDir(dir, base) {
|
|
90
|
+
const results = [];
|
|
91
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const fullPath = join(dir, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
results.push(...await walkDir(fullPath, base));
|
|
96
|
+
} else {
|
|
97
|
+
results.push(relative(base, fullPath));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
async function copyTemplates(templateDir, destDir, dryRun, minimal, projectType = "both") {
|
|
103
|
+
const ciVariants = ["ci-node.yml", "ci-python.yml", "ci-both.yml"];
|
|
104
|
+
const selectedVariant = `ci-${projectType}.yml`;
|
|
90
105
|
if (dryRun) {
|
|
91
|
-
|
|
106
|
+
const all = await listTemplateFiles(templateDir);
|
|
107
|
+
return all.filter((p) => !ciVariants.some((v) => p.endsWith(v) && v !== selectedVariant));
|
|
92
108
|
}
|
|
93
109
|
await copy(templateDir, destDir, {
|
|
94
110
|
overwrite: false,
|
|
@@ -97,14 +113,25 @@ async function copyTemplates(templateDir, destDir, dryRun, minimal) {
|
|
|
97
113
|
if (src.endsWith("CLAUDE.md")) return false;
|
|
98
114
|
if (minimal && src.includes(".github/workflows")) return false;
|
|
99
115
|
if (relative(templateDir, src).includes("..")) return false;
|
|
116
|
+
for (const variant of ciVariants) {
|
|
117
|
+
if (src.endsWith(variant) && variant !== selectedVariant) return false;
|
|
118
|
+
}
|
|
100
119
|
return true;
|
|
101
120
|
}
|
|
102
121
|
});
|
|
122
|
+
if (!minimal) {
|
|
123
|
+
const variantPath = join(destDir, ".github", "workflows", selectedVariant);
|
|
124
|
+
const ciPath = join(destDir, ".github", "workflows", "ci.yml");
|
|
125
|
+
if (existsSync(variantPath)) {
|
|
126
|
+
await rename(variantPath, ciPath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
103
129
|
const claudeSrc = join(templateDir, "CLAUDE.md");
|
|
104
130
|
const claudeDest = join(destDir, "CLAUDE.md");
|
|
105
131
|
const templateContent = await readFile2(claudeSrc, "utf-8");
|
|
106
132
|
await mergeClaude(claudeDest, templateContent);
|
|
107
|
-
|
|
133
|
+
const destFiles = await walkDir(destDir, destDir);
|
|
134
|
+
return destFiles.sort();
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
// src/steps/install-headroom.ts
|
|
@@ -215,12 +242,25 @@ async function configureMcp(log) {
|
|
|
215
242
|
}
|
|
216
243
|
}
|
|
217
244
|
|
|
245
|
+
// src/utils/detect-project-type.ts
|
|
246
|
+
import { existsSync as existsSync2 } from "fs";
|
|
247
|
+
import { join as join2 } from "path";
|
|
248
|
+
function detectProjectType(cwd) {
|
|
249
|
+
const hasNode = existsSync2(join2(cwd, "package.json"));
|
|
250
|
+
const hasPython = existsSync2(join2(cwd, "pyproject.toml")) || existsSync2(join2(cwd, "requirements.txt"));
|
|
251
|
+
if (hasNode && hasPython) return "both";
|
|
252
|
+
if (hasNode) return "node";
|
|
253
|
+
if (hasPython) return "python";
|
|
254
|
+
return "both";
|
|
255
|
+
}
|
|
256
|
+
|
|
218
257
|
// src/commands/init.ts
|
|
219
258
|
function registerInitCommand(program2) {
|
|
220
259
|
program2.command("init").description("Bootstrap a project with goodvibes configuration").option("--dry-run", "Preview files without writing to disk").option("--minimal", "Skip headroom install and CI workflows").action(async (options) => {
|
|
221
260
|
const dryRun = options.dryRun ?? false;
|
|
222
261
|
const minimal = options.minimal ?? false;
|
|
223
262
|
const cwd = process.cwd();
|
|
263
|
+
const projectType = detectProjectType(cwd);
|
|
224
264
|
const templateDir = resolveTemplatesDir();
|
|
225
265
|
intro("goodvibes init");
|
|
226
266
|
if (dryRun) {
|
|
@@ -229,7 +269,7 @@ function registerInitCommand(program2) {
|
|
|
229
269
|
note(
|
|
230
270
|
[
|
|
231
271
|
"1. Open this project in Claude Code",
|
|
232
|
-
"2.
|
|
272
|
+
"2. In Claude Code CLI: /plugin marketplace add DietrichGebert/ponytail",
|
|
233
273
|
"3. Start coding \u2014 CLAUDE.md rules are already active"
|
|
234
274
|
].join("\n"),
|
|
235
275
|
"Next steps"
|
|
@@ -242,7 +282,7 @@ function registerInitCommand(program2) {
|
|
|
242
282
|
{
|
|
243
283
|
title: "Copying template files",
|
|
244
284
|
task: async (message) => {
|
|
245
|
-
const files = await copyTemplates(templateDir, cwd, false, minimal);
|
|
285
|
+
const files = await copyTemplates(templateDir, cwd, false, minimal, projectType);
|
|
246
286
|
createdFiles.push(...files);
|
|
247
287
|
return `Copied ${files.length} files`;
|
|
248
288
|
}
|
|
@@ -270,7 +310,7 @@ function registerInitCommand(program2) {
|
|
|
270
310
|
note(createdFiles.join("\n"), "Files created");
|
|
271
311
|
const nextSteps = [
|
|
272
312
|
"1. Open this project in Claude Code",
|
|
273
|
-
"2.
|
|
313
|
+
"2. In Claude Code CLI: /plugin marketplace add DietrichGebert/ponytail",
|
|
274
314
|
"3. Start coding \u2014 CLAUDE.md rules are already active"
|
|
275
315
|
].join("\n");
|
|
276
316
|
note(nextSteps, "Next steps");
|
|
@@ -278,6 +318,161 @@ function registerInitCommand(program2) {
|
|
|
278
318
|
});
|
|
279
319
|
}
|
|
280
320
|
|
|
321
|
+
// src/commands/upgrade.ts
|
|
322
|
+
import { intro as intro2, outro as outro2, note as note2, tasks as tasks2 } from "@clack/prompts";
|
|
323
|
+
import { copy as copy2, pathExists as pathExists2 } from "fs-extra";
|
|
324
|
+
import { readFile as readFile3, rename as rename2 } from "fs/promises";
|
|
325
|
+
import { existsSync as existsSync3 } from "fs";
|
|
326
|
+
import { join as join3, relative as relative2 } from "path";
|
|
327
|
+
import { execa as execa4 } from "execa";
|
|
328
|
+
import { createRequire } from "module";
|
|
329
|
+
var _require = createRequire(import.meta.url);
|
|
330
|
+
var _GV_UPGRADING = "_GV_UPGRADING";
|
|
331
|
+
async function checkLatestNpmVersion() {
|
|
332
|
+
try {
|
|
333
|
+
const { stdout } = await execa4("npm", ["view", "@jgiox/goodvibes", "version"]);
|
|
334
|
+
return stdout.trim() || null;
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function getInstalledVersion() {
|
|
340
|
+
try {
|
|
341
|
+
const pkg = _require("../../package.json");
|
|
342
|
+
return pkg.version ?? null;
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function selfUpdateNpm(version) {
|
|
348
|
+
await execa4("npm", ["install", "-g", `@jgiox/goodvibes@${version}`], { stdio: "inherit" });
|
|
349
|
+
}
|
|
350
|
+
var MANAGED_FIXED = /* @__PURE__ */ new Set([
|
|
351
|
+
"CLAUDE.md",
|
|
352
|
+
".github/workflows/ci.yml",
|
|
353
|
+
".github/workflows/security.yml",
|
|
354
|
+
".github/workflows/dependency-review.yml",
|
|
355
|
+
".github/dependabot.yml"
|
|
356
|
+
]);
|
|
357
|
+
async function detectInstalledVersion(cwd) {
|
|
358
|
+
const claudePath = join3(cwd, "CLAUDE.md");
|
|
359
|
+
if (!await pathExists2(claudePath)) return null;
|
|
360
|
+
const content = await readFile3(claudePath, "utf-8");
|
|
361
|
+
return extractVersion(content);
|
|
362
|
+
}
|
|
363
|
+
async function detectBundledVersion(templateDir) {
|
|
364
|
+
if (!templateDir) return null;
|
|
365
|
+
const claudeSrc = join3(templateDir, "CLAUDE.md");
|
|
366
|
+
if (!await pathExists2(claudeSrc)) return null;
|
|
367
|
+
const content = await readFile3(claudeSrc, "utf-8");
|
|
368
|
+
return extractVersion(content);
|
|
369
|
+
}
|
|
370
|
+
async function computeChanges(templateDir, destDir, projectType) {
|
|
371
|
+
const allFiles = await listTemplateFiles(templateDir) ?? [];
|
|
372
|
+
const managedFiles = allFiles.filter(
|
|
373
|
+
(f) => f.startsWith(".claude/skills/") || MANAGED_FIXED.has(f) || // ci.yml variant maps to the fixed set already — include it for comparison
|
|
374
|
+
f === `.github/workflows/ci-${projectType}.yml`
|
|
375
|
+
);
|
|
376
|
+
const results = [];
|
|
377
|
+
for (const rel of managedFiles) {
|
|
378
|
+
const destRel = rel === `.github/workflows/ci-${projectType}.yml` ? ".github/workflows/ci.yml" : rel;
|
|
379
|
+
const srcPath = join3(templateDir, rel);
|
|
380
|
+
const destPath = join3(destDir, destRel);
|
|
381
|
+
if (!await pathExists2(destPath)) {
|
|
382
|
+
results.push({ path: destRel, status: "new" });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const srcContent = await readFile3(srcPath, "utf-8");
|
|
386
|
+
const destContent = await readFile3(destPath, "utf-8");
|
|
387
|
+
results.push({ path: destRel, status: srcContent === destContent ? "unchanged" : "changed" });
|
|
388
|
+
}
|
|
389
|
+
return results.sort((a, b) => a.path.localeCompare(b.path));
|
|
390
|
+
}
|
|
391
|
+
function formatChangeSummary(changes) {
|
|
392
|
+
if (changes.length === 0) return "(no managed files found)";
|
|
393
|
+
const symbol = { changed: "~", unchanged: "=", new: "+" };
|
|
394
|
+
return changes.map((c) => `${symbol[c.status] ?? "?"} ${c.path}`).join("\n");
|
|
395
|
+
}
|
|
396
|
+
async function upgradeTemplates(templateDir, destDir, projectType) {
|
|
397
|
+
const ciVariants = ["ci-node.yml", "ci-python.yml", "ci-both.yml"];
|
|
398
|
+
const selectedVariant = `ci-${projectType}.yml`;
|
|
399
|
+
const claudeDest = join3(destDir, "CLAUDE.md");
|
|
400
|
+
await copy2(templateDir, destDir, {
|
|
401
|
+
overwrite: true,
|
|
402
|
+
errorOnExist: false,
|
|
403
|
+
filter: (src) => {
|
|
404
|
+
if (src.endsWith("CLAUDE.md")) return false;
|
|
405
|
+
if (relative2(templateDir, src).includes("..")) return false;
|
|
406
|
+
if (src.includes(".claude/skills/")) return true;
|
|
407
|
+
for (const v of ciVariants) {
|
|
408
|
+
if (src.endsWith(v) && v !== selectedVariant) return false;
|
|
409
|
+
}
|
|
410
|
+
if (src.includes(".github/workflows/")) return true;
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
const variantPath = join3(destDir, ".github", "workflows", selectedVariant);
|
|
415
|
+
const ciPath = join3(destDir, ".github", "workflows", "ci.yml");
|
|
416
|
+
if (existsSync3(variantPath)) {
|
|
417
|
+
await rename2(variantPath, ciPath);
|
|
418
|
+
}
|
|
419
|
+
const claudeSrc = join3(templateDir, "CLAUDE.md");
|
|
420
|
+
const templateContent = await readFile3(claudeSrc, "utf-8");
|
|
421
|
+
await mergeClaude(claudeDest, templateContent);
|
|
422
|
+
const allDest = await listTemplateFiles(destDir) ?? [];
|
|
423
|
+
return allDest.filter((f) => f.startsWith(".claude/skills/") || f.startsWith(".github/workflows/") || f === "CLAUDE.md").sort();
|
|
424
|
+
}
|
|
425
|
+
function registerUpgradeCommand(program2) {
|
|
426
|
+
program2.command("upgrade").description("Update goodvibes-managed files to the latest version").option("--dry-run", "Preview what would change without writing").action(async (options) => {
|
|
427
|
+
const dryRun = options.dryRun ?? false;
|
|
428
|
+
const cwd = process.cwd();
|
|
429
|
+
intro2("goodvibes upgrade");
|
|
430
|
+
if (!process.env[_GV_UPGRADING]) {
|
|
431
|
+
const current = getInstalledVersion();
|
|
432
|
+
const latest = await checkLatestNpmVersion();
|
|
433
|
+
if (latest && current && !versionGte(current, latest)) {
|
|
434
|
+
note2(`Updating goodvibes ${current} \u2192 ${latest}\u2026`, "New version available");
|
|
435
|
+
await selfUpdateNpm(latest);
|
|
436
|
+
note2(`Updated to ${latest} \u2014 re-applying templates\u2026`, "\u2713");
|
|
437
|
+
const { execa: execaFn } = await import("execa");
|
|
438
|
+
await execaFn(process.argv[1], process.argv.slice(2), {
|
|
439
|
+
stdio: "inherit",
|
|
440
|
+
env: { ...process.env, [_GV_UPGRADING]: "1" }
|
|
441
|
+
});
|
|
442
|
+
process.exit(0);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const templateDir = resolveTemplatesDir();
|
|
447
|
+
const projectType = detectProjectType(cwd);
|
|
448
|
+
const installedVersion = await detectInstalledVersion(cwd);
|
|
449
|
+
const bundledVersion = await detectBundledVersion(templateDir);
|
|
450
|
+
if (installedVersion && bundledVersion && versionGte(installedVersion, bundledVersion)) {
|
|
451
|
+
outro2(`Already up to date (v${installedVersion})`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const changes = await computeChanges(templateDir, cwd, projectType);
|
|
455
|
+
note2(formatChangeSummary(changes), "What will change");
|
|
456
|
+
if (dryRun) {
|
|
457
|
+
outro2("Run without --dry-run to apply these changes.");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const updated = [];
|
|
461
|
+
await tasks2([
|
|
462
|
+
{
|
|
463
|
+
title: "Upgrading managed files",
|
|
464
|
+
task: async () => {
|
|
465
|
+
const files = await upgradeTemplates(templateDir, cwd, projectType);
|
|
466
|
+
updated.push(...files);
|
|
467
|
+
return `Updated ${files.length} files`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
]);
|
|
471
|
+
note2(updated.join("\n") || "(no files changed)", "Files updated");
|
|
472
|
+
outro2("Upgrade complete!");
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
281
476
|
// src/index.ts
|
|
282
477
|
var [major] = process.version.replace("v", "").split(".").map(Number);
|
|
283
478
|
if (major < 20) {
|
|
@@ -292,4 +487,5 @@ Install the latest LTS from https://nodejs.org
|
|
|
292
487
|
var program = new Command();
|
|
293
488
|
program.name("goodvibes").version("1.0.0").description("One-command bootstrap for vibe coding projects");
|
|
294
489
|
registerInitCommand(program);
|
|
490
|
+
registerUpgradeCommand(program);
|
|
295
491
|
await program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgiox/goodvibes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "One-command bootstrap for vibe coding projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=20.
|
|
11
|
+
"node": ">=20.12.0"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
@@ -37,15 +37,18 @@
|
|
|
37
37
|
"test:watch": "vitest"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"commander": "^
|
|
41
|
-
"@clack/prompts": "^
|
|
40
|
+
"commander": "^15",
|
|
41
|
+
"@clack/prompts": "^1",
|
|
42
42
|
"fs-extra": "^11",
|
|
43
43
|
"execa": "^9"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"tsup": "^8",
|
|
47
|
-
"typescript": "^
|
|
48
|
-
"@types/node": "^
|
|
49
|
-
"vitest": "^
|
|
47
|
+
"typescript": "^6",
|
|
48
|
+
"@types/node": "^26",
|
|
49
|
+
"vitest": "^4"
|
|
50
|
+
},
|
|
51
|
+
"overrides": {
|
|
52
|
+
"esbuild": "^0.28.1"
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
updates:
|
|
4
|
+
- package-ecosystem: "github-actions"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
open-pull-requests-limit: 5
|
|
9
|
+
|
|
10
|
+
- package-ecosystem: "npm"
|
|
11
|
+
directory: "/"
|
|
12
|
+
schedule:
|
|
13
|
+
interval: "weekly"
|
|
14
|
+
open-pull-requests-limit: 5
|
|
15
|
+
|
|
16
|
+
- package-ecosystem: "pip"
|
|
17
|
+
directory: "/"
|
|
18
|
+
schedule:
|
|
19
|
+
interval: "weekly"
|
|
20
|
+
open-pull-requests-limit: 5
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test-node:
|
|
11
|
+
name: Node.js tests (Node ${{ matrix.node }})
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node: ['20', '22']
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v6
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node }}
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
# ponytail: no cache-dependency-path — beginner projects are single-package, not monorepos
|
|
24
|
+
|
|
25
|
+
# npm install (not npm ci) — new projects may lack package-lock.json on first push
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm install
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: npm run build --if-present
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
run: npm test --if-present
|
|
34
|
+
|
|
35
|
+
- name: Lint
|
|
36
|
+
run: npm run lint --if-present
|
|
37
|
+
|
|
38
|
+
test-python:
|
|
39
|
+
name: Python tests (Python ${{ matrix.python }})
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
strategy:
|
|
42
|
+
matrix:
|
|
43
|
+
python: ['3.10', '3.11', '3.12']
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v7
|
|
46
|
+
|
|
47
|
+
# python-version must be explicit — setup-uv does not auto-read matrix.python (Pitfall 4)
|
|
48
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
49
|
+
with:
|
|
50
|
+
python-version: ${{ matrix.python }}
|
|
51
|
+
|
|
52
|
+
- name: Install dependencies
|
|
53
|
+
run: |
|
|
54
|
+
if [ -f "pyproject.toml" ]; then
|
|
55
|
+
uv sync --all-extras 2>/dev/null || uv sync
|
|
56
|
+
elif [ -f "requirements.txt" ]; then
|
|
57
|
+
uv pip install -r requirements.txt
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
- name: Run tests
|
|
61
|
+
run: |
|
|
62
|
+
if find . -name "test_*.py" -not -path "./.venv/*" | grep -q .; then
|
|
63
|
+
uv run --extra dev pytest -x -q
|
|
64
|
+
else
|
|
65
|
+
echo "No tests found — add a tests/ directory to get started"
|
|
66
|
+
fi
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Node.js tests (Node ${{ matrix.node }})
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node: ['20', '22']
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v6
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node }}
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
# ponytail: no cache-dependency-path — beginner projects are single-package, not monorepos
|
|
24
|
+
|
|
25
|
+
# npm install (not npm ci) — new projects may lack package-lock.json on first push
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm install
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: npm run build --if-present
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
run: npm test --if-present
|
|
34
|
+
|
|
35
|
+
- name: Lint
|
|
36
|
+
run: npm run lint --if-present
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Python tests (Python ${{ matrix.python }})
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python: ['3.10', '3.11', '3.12']
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
|
|
19
|
+
# python-version must be explicit — setup-uv does not auto-read matrix.python (Pitfall 4)
|
|
20
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
if [ -f "pyproject.toml" ]; then
|
|
27
|
+
uv sync --all-extras 2>/dev/null || uv sync
|
|
28
|
+
elif [ -f "requirements.txt" ]; then
|
|
29
|
+
uv pip install -r requirements.txt
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: |
|
|
34
|
+
if find . -name "test_*.py" -not -path "./.venv/*" | grep -q .; then
|
|
35
|
+
uv run --extra dev pytest -x -q
|
|
36
|
+
else
|
|
37
|
+
echo "No tests found — add a tests/ directory to get started"
|
|
38
|
+
fi
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Security scan
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
schedule:
|
|
9
|
+
# Weekly scan on Monday at 08:00 UTC
|
|
10
|
+
- cron: '0 8 * * 1'
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
analyze:
|
|
14
|
+
name: Analyze
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
security-events: write
|
|
18
|
+
actions: read
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
|
+
|
|
24
|
+
- name: Detect languages
|
|
25
|
+
id: langs
|
|
26
|
+
run: |
|
|
27
|
+
py=$(find . -name "*.py" -not -path "./.venv/*" | head -1)
|
|
28
|
+
js=$(find . \( -name "*.js" -o -name "*.ts" \) -not -path "*/node_modules/*" | head -1)
|
|
29
|
+
langs=""
|
|
30
|
+
[ -n "$py" ] && langs="python"
|
|
31
|
+
[ -n "$js" ] && [ -n "$langs" ] && langs="python,javascript-typescript"
|
|
32
|
+
[ -n "$js" ] && [ -z "$langs" ] && langs="javascript-typescript"
|
|
33
|
+
[ -z "$langs" ] && langs="python"
|
|
34
|
+
echo "value=$langs" >> $GITHUB_OUTPUT
|
|
35
|
+
|
|
36
|
+
- name: Initialize CodeQL
|
|
37
|
+
uses: github/codeql-action/init@v4
|
|
38
|
+
with:
|
|
39
|
+
languages: ${{ steps.langs.outputs.value }}
|
|
40
|
+
queries: security-extended
|
|
41
|
+
|
|
42
|
+
- name: Autobuild
|
|
43
|
+
uses: github/codeql-action/autobuild@v4
|
|
44
|
+
|
|
45
|
+
- name: Perform CodeQL Analysis
|
|
46
|
+
uses: github/codeql-action/analyze@v4
|
package/templates/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CLAUDE.md
|
|
2
2
|
|
|
3
3
|
<!-- goodvibes:start -->
|
|
4
|
-
# goodvibes: v1.
|
|
4
|
+
# goodvibes: v1.1.0
|
|
5
5
|
|
|
6
6
|
## Engineering Rules
|
|
7
7
|
|
|
@@ -49,6 +49,10 @@ Must flag immediately: SQL injection, XSS, command injection, path traversal, br
|
|
|
49
49
|
Each entry must include: date, task summary, files changed, why the change was made, tests run, docs updated.
|
|
50
50
|
Rules: do not rewrite history. Additive entries only. Keep it short, factual, and readable.
|
|
51
51
|
|
|
52
|
+
### Push to remote
|
|
53
|
+
**Push to GitHub after every completed task or end of session.**
|
|
54
|
+
A commit that only exists locally is one machine failure away from being lost. Run `git push origin <branch>` after each commit, or at minimum before stopping for the day. Never leave completed work unpushed for more than one session.
|
|
55
|
+
|
|
52
56
|
## Ponytail — Minimalism Ruleset
|
|
53
57
|
|
|
54
58
|
You are a lazy senior developer. Lazy means efficient, not careless. You have
|
|
@@ -94,4 +98,27 @@ that prevents data loss, security measures, accessibility basics, anything
|
|
|
94
98
|
explicitly requested. User insists on the full version → build it.
|
|
95
99
|
|
|
96
100
|
Never lazy about understanding the problem. Trace the whole thing first.
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
**Inline comments:** Only write a comment when WHY is non-obvious. Never describe what the
|
|
105
|
+
code does. No docstrings for self-evident functions. One line max.
|
|
106
|
+
|
|
107
|
+
**Unit tests:** Mock all external calls (subprocess, network, filesystem). Test one function
|
|
108
|
+
in isolation. File: `src/steps/foo.ts` → `src/steps/foo.test.ts` (TS) or `tests/test_foo.py`
|
|
109
|
+
(Python). Every public function gets at least one test. Use vitest (TS) or pytest + pytest-mock
|
|
110
|
+
(Python). Never run real uv/pip/claude/npm in a unit test.
|
|
111
|
+
|
|
112
|
+
**Integration tests:** Use a real temporary directory, no mocks for file ops. Verify that
|
|
113
|
+
modules work together. Live in `tests/integration/` or `src/**/*.integration.test.ts`.
|
|
114
|
+
Run with: `npm run test:integration` or `pytest tests/integration/`.
|
|
115
|
+
|
|
116
|
+
**Regression tests:** For every bug fix — write a failing test reproducing the bug BEFORE the
|
|
117
|
+
fix. Commit the failing test first (RED), then the fix (GREEN), in separate commits.
|
|
118
|
+
Test name must reference the symptom: `test_install_headroom_does_not_throw_on_cpp_failure`.
|
|
119
|
+
|
|
120
|
+
**Test naming:** Test names are sentences describing the expected behavior.
|
|
121
|
+
- TS: `it('returns null when Python version is below 3.10')`
|
|
122
|
+
- Python: `def test_returns_none_when_python_below_3_10()`
|
|
123
|
+
Never name tests `test_1`, `test_happy_path`, or `test_works`.
|
|
97
124
|
<!-- goodvibes:end -->
|