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 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.copyFileSync(here(src), target);
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.1",
4
- "description": "Scaffold the Cadmo method into your project \u2014 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",
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
+ }
@@ -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 { frontMatter, parseWatches as watchesOf, parseReviewed as reviewedOf, globToRegex } from './cadmo-grammar.mjs';
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
- // (including the file's EOL style a CRLF file must not gain a lone LF line).
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 parsed = frontMatter(text);
102
- const rest = text.slice(parsed.end); // '\n---' onwards
103
- let lines = parsed.fm.split(/\r?\n/);
104
- let found = false;
105
- lines = lines.map((l) => {
106
- if (/^reviewed:\s*/.test(l)) { found = true; return `reviewed: ${hash}`; }
107
- return l;
108
- });
109
- if (!found) {
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 '---' + lines.join(eol) + rest;
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);