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.
- package/README.md +3 -3
- package/lib/cli.js +103 -17
- package/lib/copy.js +16 -2
- package/lib/metadata.js +3 -2
- package/lib/reset.js +193 -0
- package/package.json +1 -1
- package/templates/EXTENSIONS.md +32 -32
- package/templates/README.md +3 -3
- package/templates/skills/{upgrade → cor-upgrade}/SKILL.md +23 -23
- package/templates/skills/{upgrade → cor-upgrade}/phases/apply.md +3 -3
- package/templates/skills/{upgrade → cor-upgrade}/phases/detect-current.md +14 -14
- package/templates/skills/{upgrade → cor-upgrade}/phases/diff-upstream.md +3 -3
- package/templates/skills/extract/SKILL.md +168 -0
- package/templates/skills/link/SKILL.md +52 -0
- package/templates/skills/onboard/SKILL.md +55 -22
- package/templates/skills/onboard/phases/detect-state.md +21 -39
- package/templates/skills/onboard/phases/generate-context.md +1 -1
- package/templates/skills/onboard/phases/interview.md +86 -72
- package/templates/skills/onboard/phases/modularity-menu.md +21 -18
- package/templates/skills/onboard/phases/options.md +98 -0
- package/templates/skills/onboard/phases/post-onboard-audit.md +20 -2
- package/templates/skills/onboard/phases/summary.md +1 -1
- package/templates/skills/onboard/phases/work-tracking.md +231 -0
- package/templates/skills/perspectives/_groups-template.yaml +1 -1
- package/templates/skills/perspectives/architecture/SKILL.md +275 -0
- package/templates/skills/perspectives/box-health/SKILL.md +8 -8
- package/templates/skills/perspectives/data-integrity/SKILL.md +2 -2
- package/templates/skills/perspectives/documentation/SKILL.md +4 -5
- package/templates/skills/perspectives/historian/SKILL.md +250 -0
- package/templates/skills/perspectives/process/SKILL.md +3 -3
- package/templates/skills/perspectives/skills-coverage/SKILL.md +294 -0
- package/templates/skills/perspectives/system-advocate/SKILL.md +191 -0
- package/templates/skills/perspectives/usability/SKILL.md +186 -0
- package/templates/skills/publish/SKILL.md +72 -0
- package/templates/skills/seed/phases/scan-signals.md +7 -3
- package/templates/skills/unlink/SKILL.md +35 -0
- /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
|
-
.
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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, '.
|
|
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.
|
|
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 {
|
|
233
|
-
type: '
|
|
234
|
-
name: '
|
|
235
|
-
message: '
|
|
236
|
-
|
|
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 (
|
|
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 .
|
|
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 = '.
|
|
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 };
|