create-cadmo 0.4.1 → 0.4.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/LICENSE +25 -0
- package/index.js +19 -1
- package/package.json +37 -37
- package/templates/spec-drift.mjs +39 -16
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Cadmo — licensing
|
|
2
|
+
|
|
3
|
+
The methodology and documentation in this repository (everything under docs/, templates/,
|
|
4
|
+
and the Markdown files at the root, except where noted) are licensed under the
|
|
5
|
+
|
|
6
|
+
Creative Commons Attribution 4.0 International License (CC BY 4.0)
|
|
7
|
+
https://creativecommons.org/licenses/by/4.0/
|
|
8
|
+
Full legal code: https://creativecommons.org/licenses/by/4.0/legalcode
|
|
9
|
+
|
|
10
|
+
You are free to share and adapt the material for any purpose, even commercially, provided you
|
|
11
|
+
give appropriate credit, link to the license, and indicate if changes were made.
|
|
12
|
+
|
|
13
|
+
Any source code added to this repository (scripts, CLI, tooling) is licensed under the MIT
|
|
14
|
+
License unless a different license is stated in that file's directory:
|
|
15
|
+
|
|
16
|
+
MIT License — Copyright (c) 2026 Tiago Torres
|
|
17
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
18
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
19
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
20
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
21
|
+
to whom the Software is furnished to do so, subject to the above copyright notice and this
|
|
22
|
+
permission notice being included in all copies or substantial portions of the Software.
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
|
24
|
+
|
|
25
|
+
The "Cadmo" name and marks are not licensed here — see TRADEMARK.md.
|
package/index.js
CHANGED
|
@@ -40,6 +40,24 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
40
40
|
|
|
41
41
|
const here = (...p) => path.join(__dirname, 'templates', ...p);
|
|
42
42
|
const cwd = process.cwd();
|
|
43
|
+
const VERSION = require('./package.json').version;
|
|
44
|
+
|
|
45
|
+
// Every copied MECHANISM carries a sync marker (origin + version) — "which version
|
|
46
|
+
// does this instance run?" must be answerable without diffing (the method's own
|
|
47
|
+
// return-lane rule, applied to its own scaffolder).
|
|
48
|
+
const MARKED = {
|
|
49
|
+
'.mjs': `// synced from create-cadmo@${VERSION} — files are never overwritten; for fixes, re-run \`npm create cadmo\` in a fresh dir and diff\n`,
|
|
50
|
+
'.yml': `# synced from create-cadmo@${VERSION} — files are never overwritten; for fixes, re-run \`npm create cadmo\` in a fresh dir and diff\n`,
|
|
51
|
+
};
|
|
52
|
+
function withSyncMarker(src, body) {
|
|
53
|
+
const marker = MARKED[path.extname(src)];
|
|
54
|
+
if (!marker) return body;
|
|
55
|
+
if (body.startsWith('#!')) {
|
|
56
|
+
const nl = body.indexOf('\n') + 1;
|
|
57
|
+
return body.slice(0, nl) + marker + body.slice(nl);
|
|
58
|
+
}
|
|
59
|
+
return marker + body;
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
const FILES = [
|
|
45
63
|
['AGENTS.md', 'AGENTS.md'],
|
|
@@ -77,7 +95,7 @@ for (const [src, dest] of plan) {
|
|
|
77
95
|
console.log(` ~ would create ${dest}`);
|
|
78
96
|
} else {
|
|
79
97
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
80
|
-
fs.
|
|
98
|
+
fs.writeFileSync(target, withSyncMarker(src, fs.readFileSync(here(src), 'utf8')));
|
|
81
99
|
console.log(` + created ${dest}`);
|
|
82
100
|
}
|
|
83
101
|
placed++;
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-cadmo",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Scaffold the Cadmo method into your project
|
|
5
|
-
"keywords": [
|
|
6
|
-
"cadmo",
|
|
7
|
-
"create-cadmo",
|
|
8
|
-
"spec-driven-development",
|
|
9
|
-
"ai",
|
|
10
|
-
"methodology",
|
|
11
|
-
"framework",
|
|
12
|
-
"agents",
|
|
13
|
-
"claude",
|
|
14
|
-
"scaffold"
|
|
15
|
-
],
|
|
16
|
-
"homepage": "https://github.com/tiagotorres91/cadmo",
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/tiagotorres91/cadmo.git"
|
|
20
|
-
},
|
|
21
|
-
"author": "Tiago Torres",
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"files": [
|
|
24
|
-
"index.js",
|
|
25
|
-
"templates/",
|
|
26
|
-
"README.md"
|
|
27
|
-
],
|
|
28
|
-
"bin": {
|
|
29
|
-
"create-cadmo": "index.js"
|
|
30
|
-
},
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=18"
|
|
33
|
-
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"test": "node --test"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "create-cadmo",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Scaffold the Cadmo method into your project — a right-sized method for building software with AI as your pair.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cadmo",
|
|
7
|
+
"create-cadmo",
|
|
8
|
+
"spec-driven-development",
|
|
9
|
+
"ai",
|
|
10
|
+
"methodology",
|
|
11
|
+
"framework",
|
|
12
|
+
"agents",
|
|
13
|
+
"claude",
|
|
14
|
+
"scaffold"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/tiagotorres91/cadmo",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/tiagotorres91/cadmo.git"
|
|
20
|
+
},
|
|
21
|
+
"author": "Tiago Torres",
|
|
22
|
+
"license": "(MIT AND CC-BY-4.0)",
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"templates/",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"bin": {
|
|
29
|
+
"create-cadmo": "index.js"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/templates/spec-drift.mjs
CHANGED
|
@@ -46,7 +46,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
46
46
|
import crypto from 'node:crypto';
|
|
47
47
|
import fs from 'node:fs';
|
|
48
48
|
import path from 'node:path';
|
|
49
|
-
import {
|
|
49
|
+
import { parseWatches as watchesOf, parseReviewed as reviewedOf, globToRegex } from './cadmo-grammar.mjs';
|
|
50
50
|
|
|
51
51
|
const BS = String.fromCharCode(92);
|
|
52
52
|
const args = process.argv.slice(2);
|
|
@@ -64,12 +64,21 @@ function refExists(ref) {
|
|
|
64
64
|
} catch { return false; }
|
|
65
65
|
}
|
|
66
66
|
function resolveBase() {
|
|
67
|
+
// the remote's declared default branch beats any name guess (main-only was a
|
|
68
|
+
// round-6 finding: a master/trunk repo silently fell through to HEAD~1)
|
|
69
|
+
try {
|
|
70
|
+
const ref = execFileSync('git', ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
71
|
+
const name = ref.replace('refs/remotes/', '');
|
|
72
|
+
if (name && refExists(name)) return name;
|
|
73
|
+
} catch { /* origin/HEAD not set */ }
|
|
67
74
|
if (refExists('origin/main')) return 'origin/main';
|
|
75
|
+
if (refExists('origin/master')) return 'origin/master';
|
|
68
76
|
let branch = '';
|
|
69
77
|
try {
|
|
70
78
|
branch = execFileSync('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
71
79
|
} catch { /* detached HEAD */ }
|
|
72
80
|
if (branch !== 'main' && refExists('main')) return 'main';
|
|
81
|
+
if (branch !== 'master' && refExists('master')) return 'master';
|
|
73
82
|
if (refExists('HEAD~1')) return 'HEAD~1';
|
|
74
83
|
return EMPTY_TREE;
|
|
75
84
|
}
|
|
@@ -94,23 +103,24 @@ function* mdFiles(dir) {
|
|
|
94
103
|
|
|
95
104
|
const readSpec = (file) => fs.readFileSync(file, 'utf8');
|
|
96
105
|
|
|
97
|
-
// Add or update a top-level `reviewed:` key, preserving everything else
|
|
98
|
-
//
|
|
106
|
+
// Add or update a top-level `reviewed:` key, preserving everything else —
|
|
107
|
+
// including the file's EOL style. This edits the raw front-matter block in place
|
|
108
|
+
// instead of splitting/re-joining lines: the join approach leaked lone LF/CR into
|
|
109
|
+
// CRLF files (round-6 finding — the round-4 test covered CRLF in the WATCHED
|
|
110
|
+
// files, not in the spec file receiving the stamp).
|
|
99
111
|
function setReviewed(text, hash) {
|
|
100
112
|
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (lines.length && lines[lines.length - 1] === '') lines[lines.length - 1] = `reviewed: ${hash}`;
|
|
111
|
-
else lines.push(`reviewed: ${hash}`);
|
|
113
|
+
const m = text.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
114
|
+
if (!m) return text; // no front matter — caller already guards this
|
|
115
|
+
const block = m[0];
|
|
116
|
+
let newBlock;
|
|
117
|
+
if (/^reviewed:[^\r\n]*/m.test(block)) {
|
|
118
|
+
newBlock = block.replace(/^reviewed:[^\r\n]*/m, 'reviewed: ' + hash);
|
|
119
|
+
} else {
|
|
120
|
+
const close = block.match(/\r?\n---$/)[0];
|
|
121
|
+
newBlock = block.slice(0, block.length - close.length) + eol + 'reviewed: ' + hash + eol + '---';
|
|
112
122
|
}
|
|
113
|
-
return
|
|
123
|
+
return newBlock + text.slice(block.length);
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
// --- reviewed-state hashing ---
|
|
@@ -172,10 +182,12 @@ if (STAMP) {
|
|
|
172
182
|
|
|
173
183
|
// --- changed files vs base ---
|
|
174
184
|
// The empty tree has no merge base, so it gets a two-dot diff; refs get three-dot.
|
|
185
|
+
// --no-renames: a rename must surface BOTH sides — reporting only the destination
|
|
186
|
+
// let `git mv src/a.js other/` escape a spec watching src/** (round-6 finding).
|
|
175
187
|
const RANGE = BASE === EMPTY_TREE ? [BASE, 'HEAD'] : [`${BASE}...HEAD`];
|
|
176
188
|
let changed;
|
|
177
189
|
try {
|
|
178
|
-
changed = execFileSync('git', ['diff', '--name-only', ...RANGE], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
190
|
+
changed = execFileSync('git', ['diff', '--name-only', '--no-renames', ...RANGE], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
179
191
|
.split('\n').filter(Boolean);
|
|
180
192
|
if (!BASE_GIVEN) {
|
|
181
193
|
console.log(`spec-drift: no --base given — diffing against ${BASE === EMPTY_TREE ? 'the empty tree (single-commit repo: the whole initial commit)' : BASE}.`);
|
|
@@ -187,6 +199,17 @@ try {
|
|
|
187
199
|
process.exit(2);
|
|
188
200
|
}
|
|
189
201
|
|
|
202
|
+
// Uncommitted work counts too: /cadmo:done runs this guard BEFORE the commit —
|
|
203
|
+
// a working-tree-only change reporting "in sync" was false safety (round-6 finding).
|
|
204
|
+
// In CI the checkout is clean, so this adds nothing there.
|
|
205
|
+
try {
|
|
206
|
+
const wt = execFileSync('git', ['status', '--porcelain', '--no-renames'], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
207
|
+
.split('\n').filter(Boolean)
|
|
208
|
+
.map((l) => l.slice(3).trim())
|
|
209
|
+
.map((p) => (p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p));
|
|
210
|
+
for (const p of wt) if (p && !changed.includes(p)) changed.push(p);
|
|
211
|
+
} catch { /* status unavailable — commit-range diff already covers CI */ }
|
|
212
|
+
|
|
190
213
|
const specs = [];
|
|
191
214
|
for (const f of mdFiles(ROOT)) {
|
|
192
215
|
const text = readSpec(f);
|