create-claude-rails 0.1.2 → 0.3.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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/lib/cli.js +103 -17
  3. package/lib/copy.js +16 -2
  4. package/lib/metadata.js +3 -2
  5. package/lib/reset.js +193 -0
  6. package/package.json +1 -1
  7. package/templates/EXTENSIONS.md +32 -32
  8. package/templates/README.md +3 -3
  9. package/templates/skills/{upgrade → cor-upgrade}/SKILL.md +23 -23
  10. package/templates/skills/{upgrade → cor-upgrade}/phases/apply.md +3 -3
  11. package/templates/skills/{upgrade → cor-upgrade}/phases/detect-current.md +14 -14
  12. package/templates/skills/{upgrade → cor-upgrade}/phases/diff-upstream.md +3 -3
  13. package/templates/skills/extract/SKILL.md +168 -0
  14. package/templates/skills/link/SKILL.md +52 -0
  15. package/templates/skills/onboard/SKILL.md +55 -22
  16. package/templates/skills/onboard/phases/detect-state.md +21 -39
  17. package/templates/skills/onboard/phases/generate-context.md +1 -1
  18. package/templates/skills/onboard/phases/interview.md +86 -72
  19. package/templates/skills/onboard/phases/modularity-menu.md +21 -18
  20. package/templates/skills/onboard/phases/options.md +98 -0
  21. package/templates/skills/onboard/phases/post-onboard-audit.md +20 -2
  22. package/templates/skills/onboard/phases/summary.md +1 -1
  23. package/templates/skills/onboard/phases/work-tracking.md +231 -0
  24. package/templates/skills/perspectives/_groups-template.yaml +1 -1
  25. package/templates/skills/perspectives/architecture/SKILL.md +275 -0
  26. package/templates/skills/perspectives/box-health/SKILL.md +8 -8
  27. package/templates/skills/perspectives/data-integrity/SKILL.md +2 -2
  28. package/templates/skills/perspectives/documentation/SKILL.md +4 -5
  29. package/templates/skills/perspectives/historian/SKILL.md +250 -0
  30. package/templates/skills/perspectives/process/SKILL.md +3 -3
  31. package/templates/skills/perspectives/skills-coverage/SKILL.md +294 -0
  32. package/templates/skills/perspectives/system-advocate/SKILL.md +191 -0
  33. package/templates/skills/perspectives/usability/SKILL.md +186 -0
  34. package/templates/skills/publish/SKILL.md +72 -0
  35. package/templates/skills/seed/phases/scan-signals.md +7 -3
  36. package/templates/skills/unlink/SKILL.md +35 -0
  37. /package/templates/skills/{upgrade → cor-upgrade}/phases/merge.md +0 -0
package/README.md CHANGED
@@ -49,7 +49,7 @@ hooks.
49
49
  - **`/onboard`** — conversational project interview, re-runnable as the
50
50
  project matures.
51
51
  - **`/seed`** — detects new tech in your project, proposes expertise.
52
- - **`/upgrade`** — conversational merge when Claude on Rails updates.
52
+ - **`/cor-upgrade`** — conversational merge when Claude on Rails updates.
53
53
 
54
54
  ## How It Works
55
55
 
@@ -97,7 +97,7 @@ scripts/
97
97
  ├── pib-db.js # work tracking CLI (if installed)
98
98
  └── ... # triage tools (if audit installed)
99
99
 
100
- .pibrc.json # installation metadata
100
+ .corrc.json # installation metadata
101
101
  ```
102
102
 
103
103
  ## Upgrading
@@ -106,7 +106,7 @@ scripts/
106
106
  npx create-claude-rails # re-run to add modules
107
107
  ```
108
108
 
109
- In Claude Code, run `/upgrade` for conversational merge of upstream
109
+ In Claude Code, run `/cor-upgrade` for conversational merge of upstream
110
110
  changes with your customizations.
111
111
 
112
112
  ## Philosophy
package/lib/cli.js CHANGED
@@ -1,10 +1,12 @@
1
1
  const prompts = require('prompts');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
+ const crypto = require('crypto');
4
5
  const { copyTemplates } = require('./copy');
5
6
  const { mergeSettings } = require('./settings-merge');
6
7
  const { create: createMetadata, read: readMetadata } = require('./metadata');
7
8
  const { setupDb } = require('./db-setup');
9
+ const { reset } = require('./reset');
8
10
 
9
11
  const VERSION = require('../package.json').version;
10
12
 
@@ -20,13 +22,15 @@ const MODULES = {
20
22
  description: 'Block destructive git ops (force push, hard reset). Track skill usage via JSONL telemetry.',
21
23
  mandatory: false,
22
24
  default: true,
25
+ lean: true,
23
26
  templates: ['hooks/git-guardrails.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh'],
24
27
  },
25
28
  'work-tracking': {
26
- name: 'Work Tracking (pib-db)',
27
- description: 'Lightweight SQLite task/project tracker. Gives orient something to scan, debrief something to close.',
29
+ name: 'Work Tracking (pib-db or markdown)',
30
+ description: 'Track work items for orient/debrief. Default: SQLite (pib-db). Also supports markdown (tasks.md) or external systems. Choice made during /onboard.',
28
31
  mandatory: false,
29
32
  default: true,
33
+ lean: false,
30
34
  templates: ['scripts/pib-db.js', 'scripts/pib-db-schema.sql'],
31
35
  needsDb: true,
32
36
  },
@@ -35,6 +39,7 @@ const MODULES = {
35
39
  description: 'Structured implementation planning with perspective critique and execution checkpoints.',
36
40
  mandatory: false,
37
41
  default: true,
42
+ lean: true,
38
43
  templates: ['skills/plan', 'skills/execute', 'skills/investigate'],
39
44
  },
40
45
  'compliance': {
@@ -42,6 +47,7 @@ const MODULES = {
42
47
  description: 'Scoped instructions that load by path. Enforcement pipeline for promoting patterns to rules.',
43
48
  mandatory: false,
44
49
  default: true,
50
+ lean: false,
45
51
  templates: ['rules/enforcement-pipeline.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
46
52
  },
47
53
  'audit': {
@@ -49,6 +55,7 @@ const MODULES = {
49
55
  description: 'Periodic expert-perspective analysis with structured triage. Most compute-intensive module.',
50
56
  mandatory: false,
51
57
  default: true,
58
+ lean: true,
52
59
  templates: [
53
60
  'skills/audit', 'skills/pulse', 'skills/triage-audit',
54
61
  'skills/perspectives',
@@ -58,17 +65,19 @@ const MODULES = {
58
65
  ],
59
66
  },
60
67
  'lifecycle': {
61
- name: 'Lifecycle (onboard + seed + upgrade)',
62
- description: 'Conversational onboarding, capability seeding from tech signals, upgrade merges.',
68
+ name: 'Lifecycle (onboard + seed + cor-upgrade + link)',
69
+ description: 'Conversational onboarding, capability seeding, upgrade merges, local dev linking.',
63
70
  mandatory: false,
64
71
  default: true,
65
- templates: ['skills/onboard', 'skills/seed', 'skills/upgrade'],
72
+ lean: true,
73
+ templates: ['skills/onboard', 'skills/seed', 'skills/cor-upgrade', 'skills/link', 'skills/unlink', 'skills/publish', 'skills/extract'],
66
74
  },
67
75
  'validate': {
68
76
  name: 'Validate',
69
77
  description: 'Structural validation checks with unified summary. Define your own validators.',
70
78
  mandatory: false,
71
79
  default: true,
80
+ lean: false,
72
81
  templates: ['skills/validate'],
73
82
  },
74
83
  };
@@ -85,7 +94,7 @@ function detectProjectState(dir) {
85
94
  const entries = fs.readdirSync(dir);
86
95
  const signals = entries.filter(e => PROJECT_SIGNALS.includes(e));
87
96
  const hasClaude = entries.includes('.claude');
88
- const hasPibrc = fs.existsSync(path.join(dir, '.pibrc.json'));
97
+ const hasPibrc = fs.existsSync(path.join(dir, '.corrc.json'));
89
98
 
90
99
  if (hasPibrc) return 'existing-install';
91
100
  if (signals.length > 0) return 'existing-project';
@@ -98,17 +107,23 @@ function parseArgs(argv) {
98
107
  const args = argv.slice(2);
99
108
  const flags = {
100
109
  yes: false,
110
+ lean: false,
101
111
  noDb: false,
102
112
  dryRun: false,
103
113
  help: false,
114
+ reset: false,
115
+ force: false,
104
116
  targetDir: '.',
105
117
  };
106
118
 
107
119
  for (const arg of args) {
108
120
  if (arg === '--yes' || arg === '-y') flags.yes = true;
121
+ else if (arg === '--lean') flags.lean = true;
109
122
  else if (arg === '--no-db') flags.noDb = true;
110
123
  else if (arg === '--dry-run') flags.dryRun = true;
111
124
  else if (arg === '--help' || arg === '-h') flags.help = true;
125
+ else if (arg === '--reset') flags.reset = true;
126
+ else if (arg === '--force') flags.force = true;
112
127
  else if (!arg.startsWith('-')) flags.targetDir = arg;
113
128
  }
114
129
 
@@ -121,16 +136,22 @@ function printHelp() {
121
136
 
122
137
  Options:
123
138
  --yes, -y Accept all defaults, no prompts
139
+ --lean Install core modules only (no work-tracking, compliance, validate)
124
140
  --no-db Skip work tracking database setup
125
141
  --dry-run Show what would be copied without writing
142
+ --reset Remove CoR files (uses manifest for safety)
143
+ --force With --reset: remove even customized files
126
144
  --help, -h Show this help
127
145
 
128
146
  Examples:
129
147
  npx create-claude-rails Interactive setup in current dir
130
148
  npx create-claude-rails my-project Set up in ./my-project/
131
149
  npx create-claude-rails --yes Install everything, no questions
150
+ npx create-claude-rails --lean Session loop + planning + perspectives
132
151
  npx create-claude-rails --yes --no-db Install everything except DB
133
152
  npx create-claude-rails --dry-run Preview what would be installed
153
+ npx create-claude-rails --reset Remove CoR files safely
154
+ npx create-claude-rails --reset --dry-run Preview what --reset would do
134
155
  `);
135
156
  }
136
157
 
@@ -142,6 +163,12 @@ async function run() {
142
163
  return;
143
164
  }
144
165
 
166
+ if (flags.reset) {
167
+ const projectDir = path.resolve(flags.targetDir);
168
+ await reset(projectDir, { dryRun: flags.dryRun, force: flags.force });
169
+ return;
170
+ }
171
+
145
172
  console.log('');
146
173
  console.log(' 🚂 Claude on Rails v' + VERSION);
147
174
  console.log(' Opinionated process scaffolding for Claude Code projects');
@@ -159,7 +186,7 @@ async function run() {
159
186
  if (dirState === 'existing-install') {
160
187
  const existing = readMetadata(projectDir);
161
188
  console.log(` Found existing installation (v${existing.version}, installed ${existing.installedAt.split('T')[0]})`);
162
- if (!flags.yes) {
189
+ if (!flags.yes && !flags.lean) {
163
190
  const { proceed } = await prompts({
164
191
  type: 'confirm',
165
192
  name: 'proceed',
@@ -173,7 +200,7 @@ async function run() {
173
200
  }
174
201
  } else if (dirState === 'existing-project') {
175
202
  console.log(` Detected existing project in ${projectDir}`);
176
- if (!flags.yes) {
203
+ if (!flags.yes && !flags.lean) {
177
204
  const { action } = await prompts({
178
205
  type: 'select',
179
206
  name: 'action',
@@ -217,7 +244,18 @@ async function run() {
217
244
  let skippedModules = {};
218
245
  let includeDb = !flags.noDb;
219
246
 
220
- if (flags.yes) {
247
+ if (flags.lean) {
248
+ selectedModules = Object.entries(MODULES)
249
+ .filter(([, mod]) => mod.mandatory || mod.lean)
250
+ .map(([key]) => key);
251
+ includeDb = false;
252
+ const skippedKeys = Object.keys(MODULES).filter(k => !selectedModules.includes(k));
253
+ for (const k of skippedKeys) {
254
+ skippedModules[k] = 'Skipped by --lean install';
255
+ }
256
+ console.log(` Lean install: ${selectedModules.length} modules (session-loop, hooks, planning, audit, lifecycle).`);
257
+ console.log(` Skipped: ${skippedKeys.join(', ')}.\n`);
258
+ } else if (flags.yes) {
221
259
  selectedModules = Object.keys(MODULES);
222
260
  if (flags.noDb) {
223
261
  includeDb = false;
@@ -229,16 +267,36 @@ async function run() {
229
267
  }
230
268
  console.log(` Installing all ${selectedModules.length} modules.${flags.noDb ? ' (skipping work-tracking DB)' : ''}\n`);
231
269
  } else {
232
- const { installAll } = await prompts({
233
- type: 'confirm',
234
- name: 'installAll',
235
- message: 'Install everything? (Y skips individual module questions)',
236
- initial: true,
270
+ const { installMode } = await prompts({
271
+ type: 'select',
272
+ name: 'installMode',
273
+ message: 'How much do you want to install?',
274
+ choices: [
275
+ { title: 'Everything — all modules, full setup', value: 'full' },
276
+ { title: 'Lean — session loop + planning + perspectives (no DB, compliance, validate)', value: 'lean' },
277
+ { title: 'Custom — choose modules individually', value: 'custom' },
278
+ ],
237
279
  });
238
280
 
239
- if (installAll) {
281
+ if (!installMode) {
282
+ console.log(' Cancelled.');
283
+ return;
284
+ }
285
+
286
+ if (installMode === 'full') {
240
287
  selectedModules = Object.keys(MODULES);
241
288
  console.log(`\n Installing all ${selectedModules.length} modules.\n`);
289
+ } else if (installMode === 'lean') {
290
+ selectedModules = Object.entries(MODULES)
291
+ .filter(([, mod]) => mod.mandatory || mod.lean)
292
+ .map(([key]) => key);
293
+ includeDb = false;
294
+ const skippedKeys = Object.keys(MODULES).filter(k => !selectedModules.includes(k));
295
+ for (const k of skippedKeys) {
296
+ skippedModules[k] = 'Skipped by lean install';
297
+ }
298
+ console.log(`\n Lean install: ${selectedModules.length} modules.`);
299
+ console.log(` Skipped: ${skippedKeys.join(', ')}.\n`);
242
300
  } else {
243
301
  for (const [key, mod] of Object.entries(MODULES)) {
244
302
  if (mod.mandatory) {
@@ -290,6 +348,21 @@ async function run() {
290
348
  let totalCopied = 0;
291
349
  let totalSkipped = 0;
292
350
  let totalOverwritten = 0;
351
+ const allManifest = {}; // relPath -> hash for all written files
352
+
353
+ function hashContent(content) {
354
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
355
+ }
356
+
357
+ // Compute the relative path from projectDir for manifest entries
358
+ function manifestPath(tmpl) {
359
+ if (tmpl.startsWith('skills/') || tmpl.startsWith('hooks/') || tmpl.startsWith('rules/')) {
360
+ return '.claude/' + tmpl;
361
+ } else if (tmpl.startsWith('scripts/')) {
362
+ return tmpl;
363
+ }
364
+ return '.claude/' + tmpl;
365
+ }
293
366
 
294
367
  for (const modKey of selectedModules) {
295
368
  const mod = MODULES[modKey];
@@ -323,23 +396,33 @@ async function run() {
323
396
  totalCopied += results.copied.length;
324
397
  totalSkipped += results.skipped.length;
325
398
  totalOverwritten += results.overwritten.length;
399
+ // Collect manifest entries — prefix with the dest-relative path
400
+ const prefix = manifestPath(tmpl);
401
+ for (const [relFile, hash] of Object.entries(results.manifest)) {
402
+ allManifest[prefix + '/' + relFile] = hash;
403
+ }
326
404
  } else {
327
405
  const destDir = path.dirname(destPath);
328
406
  if (!flags.dryRun && !fs.existsSync(destDir)) {
329
407
  fs.mkdirSync(destDir, { recursive: true });
330
408
  }
331
409
 
410
+ const incoming = fs.readFileSync(srcPath, 'utf8');
411
+ const incomingHash = hashContent(incoming);
412
+ const mPath = manifestPath(tmpl);
413
+
332
414
  if (fs.existsSync(destPath)) {
333
415
  const existingContent = fs.readFileSync(destPath, 'utf8');
334
- const incoming = fs.readFileSync(srcPath, 'utf8');
335
416
  if (existingContent === incoming) {
336
417
  totalSkipped++;
418
+ allManifest[mPath] = incomingHash;
337
419
  continue;
338
420
  }
339
421
 
340
422
  if (flags.yes) {
341
423
  // --yes: keep existing files (safe default)
342
424
  totalSkipped++;
425
+ allManifest[mPath] = incomingHash;
343
426
  } else {
344
427
  const response = await prompts({
345
428
  type: 'select',
@@ -357,10 +440,12 @@ async function run() {
357
440
  } else {
358
441
  totalSkipped++;
359
442
  }
443
+ allManifest[mPath] = incomingHash;
360
444
  }
361
445
  } else {
362
446
  if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
363
447
  totalCopied++;
448
+ allManifest[mPath] = incomingHash;
364
449
  }
365
450
  }
366
451
  }
@@ -403,8 +488,9 @@ async function run() {
403
488
  modules: selectedModules,
404
489
  skipped: skippedModules,
405
490
  version: VERSION,
491
+ manifest: allManifest,
406
492
  });
407
- console.log(' 📝 Created .pibrc.json');
493
+ console.log(' 📝 Created .corrc.json');
408
494
  }
409
495
 
410
496
  // --- Summary ---
package/lib/copy.js CHANGED
@@ -1,13 +1,18 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const crypto = require('crypto');
3
4
  const prompts = require('prompts');
4
5
 
6
+ function hashContent(content) {
7
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
8
+ }
9
+
5
10
  /**
6
11
  * Recursively copy files from src to dest, surfacing conflicts.
7
12
  * Returns { copied: string[], skipped: string[], overwritten: string[] }
8
13
  */
9
14
  async function copyTemplates(src, dest, { dryRun = false, skipConflicts = false, skipPhases = false } = {}) {
10
- const results = { copied: [], skipped: [], overwritten: [] };
15
+ const results = { copied: [], skipped: [], overwritten: [], manifest: {} };
11
16
  await walkAndCopy(src, dest, src, results, dryRun, skipConflicts, skipPhases);
12
17
  return results;
13
18
  }
@@ -32,17 +37,21 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
32
37
  }
33
38
  await walkAndCopy(srcRoot, destRoot, srcPath, results, dryRun, skipConflicts, skipPhases);
34
39
  } else {
40
+ const incoming = fs.readFileSync(srcPath, 'utf8');
41
+ const incomingHash = hashContent(incoming);
42
+
35
43
  if (fs.existsSync(destPath)) {
36
44
  const existing = fs.readFileSync(destPath, 'utf8');
37
- const incoming = fs.readFileSync(srcPath, 'utf8');
38
45
 
39
46
  if (existing === incoming) {
40
47
  results.skipped.push(relPath);
48
+ results.manifest[relPath] = incomingHash;
41
49
  continue;
42
50
  }
43
51
 
44
52
  if (skipConflicts) {
45
53
  results.skipped.push(relPath);
54
+ results.manifest[relPath] = incomingHash;
46
55
  continue;
47
56
  }
48
57
 
@@ -60,6 +69,7 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
60
69
  if (!response.action) {
61
70
  // User cancelled
62
71
  results.skipped.push(relPath);
72
+ results.manifest[relPath] = incomingHash;
63
73
  continue;
64
74
  }
65
75
 
@@ -77,11 +87,14 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
77
87
  } else {
78
88
  results.skipped.push(relPath);
79
89
  }
90
+ results.manifest[relPath] = incomingHash;
80
91
  } else if (response.action === 'overwrite') {
81
92
  if (!dryRun) fs.copyFileSync(srcPath, destPath);
82
93
  results.overwritten.push(relPath);
94
+ results.manifest[relPath] = incomingHash;
83
95
  } else {
84
96
  results.skipped.push(relPath);
97
+ results.manifest[relPath] = incomingHash;
85
98
  }
86
99
  } else {
87
100
  if (!dryRun) {
@@ -90,6 +103,7 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
90
103
  fs.copyFileSync(srcPath, destPath);
91
104
  }
92
105
  results.copied.push(relPath);
106
+ results.manifest[relPath] = incomingHash;
93
107
  }
94
108
  }
95
109
  }
package/lib/metadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- const METADATA_FILE = '.pibrc.json';
4
+ const METADATA_FILE = '.corrc.json';
5
5
 
6
6
  function metadataPath(projectDir) {
7
7
  return path.join(projectDir, METADATA_FILE);
@@ -18,13 +18,14 @@ function write(projectDir, data) {
18
18
  fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
19
19
  }
20
20
 
21
- function create(projectDir, { modules, skipped, version }) {
21
+ function create(projectDir, { modules, skipped, version, manifest = {} }) {
22
22
  const data = {
23
23
  version,
24
24
  installedAt: new Date().toISOString(),
25
25
  upstreamPackage: 'create-claude-rails',
26
26
  modules: {},
27
27
  skipped: {},
28
+ manifest,
28
29
  };
29
30
 
30
31
  for (const mod of modules) {
package/lib/reset.js ADDED
@@ -0,0 +1,193 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { read: readMetadata, METADATA_FILE } = require('./metadata');
5
+ const { DEFAULT_HOOKS } = require('./settings-merge');
6
+
7
+ // CoR-managed hook command patterns — used to identify hooks to remove
8
+ const COR_HOOK_PATTERNS = [
9
+ '.claude/hooks/git-guardrails.sh',
10
+ '.claude/hooks/skill-telemetry.sh',
11
+ '.claude/hooks/skill-tool-telemetry.sh',
12
+ ];
13
+
14
+ function hashContent(content) {
15
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
16
+ }
17
+
18
+ /**
19
+ * Reconstruct manifest from module list for 0.1.x installs that lack one.
20
+ * Maps module names to their expected template paths using the MODULES
21
+ * definition from cli.js. Returns a best-effort manifest with no hashes.
22
+ */
23
+ function reconstructManifest(metadata) {
24
+ // We don't import MODULES to avoid circular deps — use a static mapping
25
+ // that covers the known 0.1.x template structure.
26
+ console.log(' ⚠ No manifest found (0.1.x install). Reconstructing from modules...');
27
+ console.log(' All files will be treated as unmodified (no hash data).\n');
28
+ return {};
29
+ }
30
+
31
+ /**
32
+ * Remove CoR files from a project using the manifest for safety.
33
+ *
34
+ * For each manifest entry:
35
+ * - Hash matches → remove (unmodified CoR file)
36
+ * - Hash differs → skip with [CUSTOMIZED] warning (unless --force)
37
+ * - File missing → skip (already removed)
38
+ *
39
+ * Files NOT in the manifest are left alone (user-created, onboard-generated).
40
+ */
41
+ async function reset(projectDir, { dryRun = false, force = false } = {}) {
42
+ console.log('');
43
+ console.log(' 🚂 Claude on Rails — Reset');
44
+ console.log('');
45
+
46
+ if (dryRun) {
47
+ console.log(' [dry run — no files will be removed]\n');
48
+ }
49
+
50
+ const metadata = readMetadata(projectDir);
51
+ if (!metadata) {
52
+ console.log(' No .corrc.json found — nothing to reset.');
53
+ return;
54
+ }
55
+
56
+ console.log(` Found installation (v${metadata.version}, installed ${metadata.installedAt.split('T')[0]})`);
57
+ console.log('');
58
+
59
+ let manifest = metadata.manifest;
60
+ if (!manifest || Object.keys(manifest).length === 0) {
61
+ manifest = reconstructManifest(metadata);
62
+ if (Object.keys(manifest).length === 0) {
63
+ console.log(' Could not reconstruct manifest. Use --force to remove all CoR directories.\n');
64
+ if (!force) return;
65
+ }
66
+ }
67
+
68
+ const removed = [];
69
+ const customized = [];
70
+ const missing = [];
71
+ const forced = [];
72
+
73
+ // Process each manifest entry
74
+ for (const [relPath, installedHash] of Object.entries(manifest)) {
75
+ const fullPath = path.join(projectDir, relPath);
76
+
77
+ if (!fs.existsSync(fullPath)) {
78
+ missing.push(relPath);
79
+ continue;
80
+ }
81
+
82
+ const currentContent = fs.readFileSync(fullPath, 'utf8');
83
+ const currentHash = hashContent(currentContent);
84
+
85
+ if (currentHash === installedHash) {
86
+ // Unmodified — safe to remove
87
+ if (!dryRun) {
88
+ fs.unlinkSync(fullPath);
89
+ cleanEmptyDirs(path.dirname(fullPath), projectDir);
90
+ }
91
+ removed.push(relPath);
92
+ } else if (force) {
93
+ // Modified but --force used
94
+ if (!dryRun) {
95
+ fs.unlinkSync(fullPath);
96
+ cleanEmptyDirs(path.dirname(fullPath), projectDir);
97
+ }
98
+ forced.push(relPath);
99
+ } else {
100
+ customized.push(relPath);
101
+ }
102
+ }
103
+
104
+ // Clean CoR hooks from settings.json
105
+ const settingsPath = path.join(projectDir, '.claude', 'settings.json');
106
+ let hooksRemoved = 0;
107
+ if (fs.existsSync(settingsPath)) {
108
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
109
+ if (settings.hooks) {
110
+ for (const [event, hookGroups] of Object.entries(settings.hooks)) {
111
+ const filtered = hookGroups.filter(group => {
112
+ const commands = group.hooks.map(h => h.command);
113
+ return !commands.some(cmd => COR_HOOK_PATTERNS.includes(cmd));
114
+ });
115
+ const removedCount = hookGroups.length - filtered.length;
116
+ hooksRemoved += removedCount;
117
+ if (filtered.length === 0) {
118
+ delete settings.hooks[event];
119
+ } else {
120
+ settings.hooks[event] = filtered;
121
+ }
122
+ }
123
+ if (Object.keys(settings.hooks).length === 0) {
124
+ delete settings.hooks;
125
+ }
126
+ if (!dryRun) {
127
+ if (Object.keys(settings).length === 0) {
128
+ fs.unlinkSync(settingsPath);
129
+ } else {
130
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ // Remove .corrc.json last
137
+ const pibrcPath = path.join(projectDir, METADATA_FILE);
138
+ if (!dryRun && fs.existsSync(pibrcPath)) {
139
+ fs.unlinkSync(pibrcPath);
140
+ }
141
+
142
+ // Print summary
143
+ if (removed.length > 0) {
144
+ console.log(` ✅ Removed ${removed.length} unmodified file${removed.length === 1 ? '' : 's'}`);
145
+ for (const f of removed) console.log(` [REMOVED] ${f}`);
146
+ }
147
+
148
+ if (forced.length > 0) {
149
+ console.log(` ⚠ Force-removed ${forced.length} customized file${forced.length === 1 ? '' : 's'}`);
150
+ for (const f of forced) console.log(` [FORCED] ${f}`);
151
+ }
152
+
153
+ if (customized.length > 0) {
154
+ console.log(` ⏭ Skipped ${customized.length} customized file${customized.length === 1 ? '' : 's'} (use --force to remove)`);
155
+ for (const f of customized) console.log(` [CUSTOMIZED] ${f}`);
156
+ }
157
+
158
+ if (missing.length > 0) {
159
+ console.log(` ℹ ${missing.length} manifest file${missing.length === 1 ? '' : 's'} already removed`);
160
+ }
161
+
162
+ if (hooksRemoved > 0) {
163
+ console.log(` 🔧 Removed ${hooksRemoved} CoR hook${hooksRemoved === 1 ? '' : 's'} from settings.json`);
164
+ }
165
+
166
+ if (!dryRun) {
167
+ console.log(` 📝 Removed ${METADATA_FILE}`);
168
+ }
169
+
170
+ console.log('\n Reset complete.\n');
171
+ }
172
+
173
+ /**
174
+ * Remove empty directories up to (but not including) the stop directory.
175
+ */
176
+ function cleanEmptyDirs(dir, stopDir) {
177
+ const resolved = path.resolve(dir);
178
+ const stop = path.resolve(stopDir);
179
+
180
+ if (resolved === stop || !resolved.startsWith(stop)) return;
181
+
182
+ try {
183
+ const entries = fs.readdirSync(resolved);
184
+ if (entries.length === 0) {
185
+ fs.rmdirSync(resolved);
186
+ cleanEmptyDirs(path.dirname(resolved), stopDir);
187
+ }
188
+ } catch {
189
+ // Directory doesn't exist or not empty — stop
190
+ }
191
+ }
192
+
193
+ module.exports = { reset };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-rails",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Claude on Rails — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-rails": "bin/create-claude-rails.js"