worclaude 2.4.5 → 2.4.7
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/CHANGELOG.md +38 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/commands/diff.js +12 -3
- package/src/commands/doctor.js +3 -19
- package/src/commands/init.js +1 -145
- package/src/commands/upgrade.js +356 -104
- package/src/core/config.js +8 -0
- package/src/core/drift-checks.js +63 -0
- package/src/core/file-categorizer.js +105 -37
- package/src/core/remover.js +1 -0
- package/src/core/scaffolder.js +1 -0
- package/src/core/variables.js +196 -0
- package/src/index.js +4 -1
package/src/commands/upgrade.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
+
import fs from 'fs-extra';
|
|
3
4
|
import inquirer from 'inquirer';
|
|
4
5
|
import ora from 'ora';
|
|
5
6
|
import { requireWorkflowMeta, writeWorkflowMeta, getPackageVersion } from '../core/config.js';
|
|
6
7
|
import { createBackup } from '../core/backup.js';
|
|
7
|
-
import { categorizeFiles } from '../core/file-categorizer.js';
|
|
8
|
+
import { categorizeFiles, resolveKeyPath } from '../core/file-categorizer.js';
|
|
8
9
|
import { buildSettingsJson, mergeSettingsPermissionsAndHooks } from '../core/merger.js';
|
|
9
|
-
import { readTemplate, updateGitignore } from '../core/scaffolder.js';
|
|
10
|
-
import {
|
|
10
|
+
import { readTemplate, substituteVariables, updateGitignore } from '../core/scaffolder.js';
|
|
11
|
+
import { buildAgentsMdVariables } from '../core/variables.js';
|
|
12
|
+
import {
|
|
13
|
+
hasClaudeMdMemoryGuidance,
|
|
14
|
+
ensureLearningsDir,
|
|
15
|
+
writeMemoryGuidanceSidecar,
|
|
16
|
+
readClaudeMd,
|
|
17
|
+
} from '../core/drift-checks.js';
|
|
18
|
+
import { writeFile, readFile, fileExists } from '../utils/file.js';
|
|
11
19
|
import { hashFile } from '../utils/hash.js';
|
|
12
20
|
import { getLatestNpmVersion } from '../utils/npm.js';
|
|
13
21
|
import * as display from '../utils/display.js';
|
|
14
22
|
import { semverLessThan, migrateSkillFormat, patchAgentDescriptions } from '../core/migration.js';
|
|
15
23
|
|
|
24
|
+
const CONFLICT_CHECK_TYPES = new Set(['hook', 'root-file']);
|
|
25
|
+
|
|
16
26
|
function selfUpdate(latestVersion) {
|
|
17
27
|
const spinner = ora(`Updating worclaude to v${latestVersion}...`).start();
|
|
18
28
|
try {
|
|
@@ -31,68 +41,100 @@ function selfUpdate(latestVersion) {
|
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const latestVersion = await getLatestNpmVersion();
|
|
44
|
+
function sidecarPathFor(dest) {
|
|
45
|
+
const ext = path.extname(dest);
|
|
46
|
+
const base = ext ? dest.slice(0, dest.length - ext.length) : dest;
|
|
47
|
+
return `${base}.workflow-ref${ext}`;
|
|
48
|
+
}
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
const { doUpdate } = await inquirer.prompt([
|
|
47
|
-
{
|
|
48
|
-
type: 'list',
|
|
49
|
-
name: 'doUpdate',
|
|
50
|
-
message: 'Update worclaude CLI?',
|
|
51
|
-
choices: [
|
|
52
|
-
{ name: 'Yes, update and continue', value: true },
|
|
53
|
-
{ name: 'No, continue with current version', value: false },
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
]);
|
|
50
|
+
async function renderTemplate({ templatePath, type }, variables) {
|
|
51
|
+
const templateContent = await readTemplate(templatePath);
|
|
52
|
+
return type === 'root-file' ? substituteVariables(templateContent, variables) : templateContent;
|
|
53
|
+
}
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
55
|
+
async function writeTemplateToDest(entry, dest, variables) {
|
|
56
|
+
await fs.ensureDir(path.dirname(dest));
|
|
57
|
+
await writeFile(dest, await renderTemplate(entry, variables));
|
|
58
|
+
}
|
|
66
59
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
if (error === 'corrupted') {
|
|
74
|
-
display.error('workflow-meta.json is corrupted. Run `worclaude init` to reinstall.');
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
60
|
+
async function writeSidecarFor(entry, dest, variables) {
|
|
61
|
+
const sidecarPath = sidecarPathFor(dest);
|
|
62
|
+
await writeFile(sidecarPath, await renderTemplate(entry, variables));
|
|
63
|
+
return sidecarPath;
|
|
64
|
+
}
|
|
77
65
|
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
66
|
+
async function diskContentMatchesTemplate(entry, dest, variables) {
|
|
67
|
+
const currentContent = await readFile(dest);
|
|
68
|
+
return currentContent === (await renderTemplate(entry, variables));
|
|
69
|
+
}
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
async function buildRepairPlan(projectRoot, categories, claudeMdContent) {
|
|
72
|
+
const migrationNewFiles = categories.newFiles.filter((f) => CONFLICT_CHECK_TYPES.has(f.type));
|
|
73
|
+
const templateNewFiles = categories.newFiles.filter((f) => !CONFLICT_CHECK_TYPES.has(f.type));
|
|
74
|
+
const learningsGitkeep = path.join(projectRoot, '.claude', 'learnings', '.gitkeep');
|
|
75
|
+
const learningsDirMissing = !(await fileExists(learningsGitkeep));
|
|
76
|
+
const claudeMdNeedsSidecar =
|
|
77
|
+
typeof claudeMdContent === 'string' && !hasClaudeMdMemoryGuidance(claudeMdContent);
|
|
78
|
+
return {
|
|
79
|
+
missingExpected: categories.missingExpected,
|
|
80
|
+
migrationNewFiles,
|
|
81
|
+
templateNewFiles,
|
|
82
|
+
learningsDirMissing,
|
|
83
|
+
claudeMdNeedsSidecar,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
function hasRepairWork(plan) {
|
|
88
|
+
return (
|
|
89
|
+
plan.missingExpected.length > 0 ||
|
|
90
|
+
plan.migrationNewFiles.length > 0 ||
|
|
91
|
+
plan.learningsDirMissing ||
|
|
92
|
+
plan.claudeMdNeedsSidecar
|
|
93
|
+
);
|
|
94
|
+
}
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
function hasTemplateWork(categories, plan) {
|
|
97
|
+
return (
|
|
98
|
+
categories.autoUpdate.length > 0 ||
|
|
99
|
+
categories.conflict.length > 0 ||
|
|
100
|
+
plan.templateNewFiles.length > 0
|
|
101
|
+
);
|
|
102
|
+
}
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
function renderRepairPreview(plan) {
|
|
105
|
+
if (plan.missingExpected.length > 0) {
|
|
106
|
+
display.barLine(`${display.green('+')} Restore (missing from disk):`);
|
|
107
|
+
for (const { key } of plan.missingExpected) {
|
|
108
|
+
display.barLine(` ${display.green('+')} ${key}`);
|
|
109
|
+
}
|
|
110
|
+
display.newline();
|
|
111
|
+
}
|
|
112
|
+
if (plan.migrationNewFiles.length > 0) {
|
|
113
|
+
display.barLine(`${display.green('+')} Track & install (new file type in this CLI version):`);
|
|
114
|
+
for (const { key } of plan.migrationNewFiles) {
|
|
115
|
+
display.barLine(` ${display.green('+')} ${key}`);
|
|
116
|
+
}
|
|
117
|
+
display.newline();
|
|
118
|
+
}
|
|
119
|
+
const sidecarLines = [];
|
|
120
|
+
if (plan.learningsDirMissing) {
|
|
121
|
+
sidecarLines.push(' ~ `.claude/learnings/` directory missing (will be created)');
|
|
122
|
+
}
|
|
123
|
+
if (plan.claudeMdNeedsSidecar) {
|
|
124
|
+
sidecarLines.push(
|
|
125
|
+
' ~ CLAUDE.md memory guidance missing (will write CLAUDE.md.workflow-ref.md)'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (sidecarLines.length > 0) {
|
|
129
|
+
display.barLine(`${display.yellow('~')} Also:`);
|
|
130
|
+
for (const line of sidecarLines) {
|
|
131
|
+
display.barLine(line);
|
|
132
|
+
}
|
|
133
|
+
display.newline();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
95
136
|
|
|
137
|
+
function renderTemplatePreview(categories, plan) {
|
|
96
138
|
if (categories.autoUpdate.length > 0) {
|
|
97
139
|
display.barLine(`${display.green('✓')} Auto-update (unchanged since install):`);
|
|
98
140
|
const showCount = Math.min(categories.autoUpdate.length, 3);
|
|
@@ -104,7 +146,6 @@ export async function upgradeCommand() {
|
|
|
104
146
|
}
|
|
105
147
|
display.newline();
|
|
106
148
|
}
|
|
107
|
-
|
|
108
149
|
if (categories.conflict.length > 0) {
|
|
109
150
|
display.barLine(`${display.yellow('~')} Needs review (you've customized these):`);
|
|
110
151
|
for (const { key } of categories.conflict) {
|
|
@@ -114,22 +155,19 @@ export async function upgradeCommand() {
|
|
|
114
155
|
}
|
|
115
156
|
display.newline();
|
|
116
157
|
}
|
|
117
|
-
|
|
118
|
-
if (categories.newFiles.length > 0) {
|
|
158
|
+
if (plan.templateNewFiles.length > 0) {
|
|
119
159
|
display.barLine(`${display.green('+')} New in this version:`);
|
|
120
|
-
for (const { key } of
|
|
160
|
+
for (const { key } of plan.templateNewFiles) {
|
|
121
161
|
display.barLine(` ${display.green('+')} ${key}`);
|
|
122
162
|
}
|
|
123
163
|
display.newline();
|
|
124
164
|
}
|
|
125
|
-
|
|
126
165
|
if (categories.unchanged.length > 0) {
|
|
127
166
|
display.barLine(
|
|
128
167
|
`${display.dimColor('=')} Unchanged: ${display.dimColor(`${categories.unchanged.length} files`)}`
|
|
129
168
|
);
|
|
130
169
|
display.newline();
|
|
131
170
|
}
|
|
132
|
-
|
|
133
171
|
if (categories.modified.length > 0) {
|
|
134
172
|
display.barLine(`${display.yellow('~')} Your customizations (no workflow updates available):`);
|
|
135
173
|
for (const { key } of categories.modified) {
|
|
@@ -137,43 +175,244 @@ export async function upgradeCommand() {
|
|
|
137
175
|
}
|
|
138
176
|
display.newline();
|
|
139
177
|
}
|
|
178
|
+
}
|
|
140
179
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
180
|
+
async function applyRepairPass(projectRoot, plan, variables) {
|
|
181
|
+
const result = {
|
|
182
|
+
restored: [],
|
|
183
|
+
migrated: [],
|
|
184
|
+
migrationConflicts: [],
|
|
185
|
+
createdDirs: [],
|
|
186
|
+
sidecars: [],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const entry of plan.missingExpected) {
|
|
190
|
+
const dest = resolveKeyPath(entry.key, projectRoot);
|
|
191
|
+
await writeTemplateToDest(entry, dest, variables);
|
|
192
|
+
result.restored.push({ key: entry.key, dest });
|
|
193
|
+
}
|
|
145
194
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
195
|
+
for (const entry of plan.migrationNewFiles) {
|
|
196
|
+
const dest = resolveKeyPath(entry.key, projectRoot);
|
|
197
|
+
if (await fileExists(dest)) {
|
|
198
|
+
const matches = await diskContentMatchesTemplate(entry, dest, variables);
|
|
199
|
+
if (!matches) {
|
|
200
|
+
const sidecarPath = await writeSidecarFor(entry, dest, variables);
|
|
201
|
+
result.migrationConflicts.push({ key: entry.key, dest, sidecarPath });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await writeTemplateToDest(entry, dest, variables);
|
|
206
|
+
result.migrated.push({ key: entry.key, dest });
|
|
149
207
|
}
|
|
150
208
|
|
|
151
|
-
|
|
209
|
+
if (plan.learningsDirMissing) {
|
|
210
|
+
const created = await ensureLearningsDir(projectRoot);
|
|
211
|
+
if (created) {
|
|
212
|
+
result.createdDirs.push(path.join(projectRoot, '.claude', 'learnings'));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (plan.claudeMdNeedsSidecar) {
|
|
217
|
+
const sidecarPath = await writeMemoryGuidanceSidecar(projectRoot);
|
|
218
|
+
result.sidecars.push(sidecarPath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function promptProceed(message) {
|
|
152
225
|
const { proceed } = await inquirer.prompt([
|
|
153
226
|
{
|
|
154
227
|
type: 'list',
|
|
155
228
|
name: 'proceed',
|
|
156
|
-
message
|
|
229
|
+
message,
|
|
157
230
|
choices: [
|
|
158
231
|
{ name: 'Yes', value: true },
|
|
159
232
|
{ name: 'No', value: false },
|
|
160
233
|
],
|
|
161
234
|
},
|
|
162
235
|
]);
|
|
236
|
+
return proceed;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function runRepairOnlyFlow({ projectRoot, meta, plan, variables, dryRun, yes }) {
|
|
240
|
+
display.sectionHeader(`WORCLAUDE REPAIR (v${meta.version})`);
|
|
241
|
+
display.newline();
|
|
242
|
+
display.barLine('Drift detected:');
|
|
243
|
+
renderRepairPreview(plan);
|
|
163
244
|
|
|
164
|
-
if (
|
|
165
|
-
display.info('
|
|
245
|
+
if (dryRun) {
|
|
246
|
+
display.info('Dry run — no changes written.');
|
|
166
247
|
return;
|
|
167
248
|
}
|
|
168
249
|
|
|
169
|
-
|
|
250
|
+
if (!yes) {
|
|
251
|
+
const proceed = await promptProceed('Repair drifted files?');
|
|
252
|
+
if (!proceed) {
|
|
253
|
+
display.info('Repair cancelled.');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const spinner = ora('Repairing...').start();
|
|
259
|
+
try {
|
|
260
|
+
const backupDir = await createBackup(projectRoot);
|
|
261
|
+
spinner.text = 'Backup created, restoring files...';
|
|
262
|
+
|
|
263
|
+
const result = await applyRepairPass(projectRoot, plan, variables);
|
|
264
|
+
|
|
265
|
+
const fileHashes = { ...meta.fileHashes };
|
|
266
|
+
for (const { key, dest } of [...result.restored, ...result.migrated]) {
|
|
267
|
+
fileHashes[key] = await hashFile(dest);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
meta.lastUpdated = new Date().toISOString();
|
|
271
|
+
meta.fileHashes = fileHashes;
|
|
272
|
+
await writeWorkflowMeta(projectRoot, meta);
|
|
273
|
+
|
|
274
|
+
spinner.succeed('Repair complete.');
|
|
275
|
+
|
|
276
|
+
display.newline();
|
|
277
|
+
if (result.restored.length > 0) {
|
|
278
|
+
display.barLine(`Restored: ${result.restored.length} files`);
|
|
279
|
+
}
|
|
280
|
+
if (result.migrated.length > 0) {
|
|
281
|
+
display.barLine(`Installed: ${result.migrated.length} files`);
|
|
282
|
+
}
|
|
283
|
+
if (result.migrationConflicts.length > 0) {
|
|
284
|
+
display.barLine(
|
|
285
|
+
`Conflicts: ${result.migrationConflicts.length} files ${display.dimColor('(saved as .workflow-ref)')}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (result.createdDirs.length > 0) {
|
|
289
|
+
display.barLine(`Created: ${result.createdDirs.length} directories`);
|
|
290
|
+
}
|
|
291
|
+
if (result.sidecars.length > 0) {
|
|
292
|
+
display.barLine(`Sidecar: ${result.sidecars.length} suggestion files`);
|
|
293
|
+
}
|
|
294
|
+
display.barLine(display.dimColor(`Backup: ${path.basename(backupDir)}/`));
|
|
295
|
+
|
|
296
|
+
if (result.migrationConflicts.length > 0 || result.sidecars.length > 0) {
|
|
297
|
+
display.newline();
|
|
298
|
+
display.barLine(`Review .workflow-ref files and merge what's useful.`);
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
spinner.fail('Repair failed.');
|
|
302
|
+
display.error(err.message);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function upgradeCommand(options = {}) {
|
|
307
|
+
const { dryRun = false, yes = false, repairOnly = false } = options;
|
|
308
|
+
const projectRoot = process.cwd();
|
|
309
|
+
|
|
310
|
+
// 1. Check for CLI self-update from npm
|
|
311
|
+
const cliVersion = await getPackageVersion();
|
|
312
|
+
const latestVersion = await getLatestNpmVersion();
|
|
313
|
+
|
|
314
|
+
if (latestVersion && latestVersion !== cliVersion) {
|
|
315
|
+
display.newline();
|
|
316
|
+
display.info(
|
|
317
|
+
`New worclaude version available: ${display.dimColor(`v${cliVersion}`)} → ${display.green(`v${latestVersion}`)}`
|
|
318
|
+
);
|
|
319
|
+
const { doUpdate } = await inquirer.prompt([
|
|
320
|
+
{
|
|
321
|
+
type: 'list',
|
|
322
|
+
name: 'doUpdate',
|
|
323
|
+
message: 'Update worclaude CLI?',
|
|
324
|
+
choices: [
|
|
325
|
+
{ name: 'Yes, update and continue', value: true },
|
|
326
|
+
{ name: 'No, continue with current version', value: false },
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
if (doUpdate) {
|
|
332
|
+
const updated = await selfUpdate(latestVersion);
|
|
333
|
+
if (updated) {
|
|
334
|
+
display.info('Re-run `worclaude upgrade` to apply the new workflow files.');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 2. Check prerequisite
|
|
341
|
+
const { meta, error } = await requireWorkflowMeta(projectRoot);
|
|
342
|
+
if (error === 'not-installed') {
|
|
343
|
+
display.info('Workflow is not installed. Run `worclaude init` to set up.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (error === 'corrupted') {
|
|
347
|
+
display.error('workflow-meta.json is corrupted. Run `worclaude init` to reinstall.');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 3. Version + categorize
|
|
352
|
+
const currentVersion = await getPackageVersion();
|
|
353
|
+
const installedVersion = meta.version;
|
|
354
|
+
const versionMatch = installedVersion === currentVersion;
|
|
355
|
+
|
|
356
|
+
const categories = await categorizeFiles(projectRoot, meta);
|
|
357
|
+
const claudeMdContent = await readClaudeMd(projectRoot);
|
|
358
|
+
const plan = await buildRepairPlan(projectRoot, categories, claudeMdContent);
|
|
359
|
+
|
|
360
|
+
const repairWork = hasRepairWork(plan);
|
|
361
|
+
const templateWork = hasTemplateWork(categories, plan);
|
|
362
|
+
|
|
363
|
+
// Version match + no repair + no template work → up to date.
|
|
364
|
+
// Early return keeps the clean-install fast path free of package.json I/O.
|
|
365
|
+
if (versionMatch && !repairWork && !templateWork) {
|
|
366
|
+
display.success(`Already up to date (v${currentVersion}).`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const variables = await buildAgentsMdVariables(meta, projectRoot);
|
|
371
|
+
|
|
372
|
+
// Version match + repair only OR explicit --repair-only → repair-only flow
|
|
373
|
+
if ((versionMatch && !templateWork) || repairOnly) {
|
|
374
|
+
await runRepairOnlyFlow({ projectRoot, meta, plan, variables, dryRun, yes });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Full upgrade flow (version mismatch)
|
|
379
|
+
display.sectionHeader(`WORCLAUDE UPGRADE (v${installedVersion} → v${currentVersion})`);
|
|
380
|
+
display.newline();
|
|
381
|
+
display.barLine('Changes:');
|
|
382
|
+
|
|
383
|
+
if (repairWork) {
|
|
384
|
+
renderRepairPreview(plan);
|
|
385
|
+
}
|
|
386
|
+
renderTemplatePreview(categories, plan);
|
|
387
|
+
|
|
388
|
+
if (!repairWork && !templateWork) {
|
|
389
|
+
display.info('No file changes needed — only updating version metadata.');
|
|
390
|
+
display.newline();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (dryRun) {
|
|
394
|
+
display.info('Dry run — no changes written.');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!yes) {
|
|
399
|
+
const proceed = await promptProceed('Proceed with upgrade?');
|
|
400
|
+
if (!proceed) {
|
|
401
|
+
display.info('Upgrade cancelled.');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
170
406
|
const spinner = ora('Upgrading...').start();
|
|
171
407
|
|
|
172
408
|
try {
|
|
173
|
-
// Create backup first
|
|
174
409
|
const backupDir = await createBackup(projectRoot);
|
|
175
410
|
spinner.text = 'Backup created, applying updates...';
|
|
176
411
|
|
|
412
|
+
// Repair pass (restoration + migration) runs first so hash-prune below
|
|
413
|
+
// cannot drop entries we just rewrote.
|
|
414
|
+
const repairResult = await applyRepairPass(projectRoot, plan, variables);
|
|
415
|
+
|
|
177
416
|
// v2.0.0 migrations (version-gated)
|
|
178
417
|
let skillReport = { migrated: 0, skipped: 0, names: [] };
|
|
179
418
|
let agentReport = { autoPatched: 0, prompted: 0, declined: 0, skipped: [] };
|
|
@@ -181,10 +420,8 @@ export async function upgradeCommand() {
|
|
|
181
420
|
if (semverLessThan(installedVersion, '2.0.0')) {
|
|
182
421
|
spinner.text = 'Running v2.0.0 migrations...';
|
|
183
422
|
|
|
184
|
-
// Item 14: Skill format migration (flat .md → skill-name/SKILL.md)
|
|
185
423
|
skillReport = await migrateSkillFormat(projectRoot, meta);
|
|
186
424
|
|
|
187
|
-
// Item 15: Agent frontmatter patch (add missing description)
|
|
188
425
|
spinner.stop();
|
|
189
426
|
agentReport = await patchAgentDescriptions(projectRoot, meta, async (agentName) => {
|
|
190
427
|
const { patch } = await inquirer.prompt([
|
|
@@ -206,20 +443,21 @@ export async function upgradeCommand() {
|
|
|
206
443
|
// Auto-update files
|
|
207
444
|
for (const { key, templatePath } of categories.autoUpdate) {
|
|
208
445
|
const content = await readTemplate(templatePath);
|
|
209
|
-
await writeFile(
|
|
446
|
+
await writeFile(resolveKeyPath(key, projectRoot), content);
|
|
210
447
|
}
|
|
211
448
|
|
|
212
449
|
// Conflict files: save as .workflow-ref.md
|
|
213
450
|
for (const { key, templatePath } of categories.conflict) {
|
|
214
451
|
const content = await readTemplate(templatePath);
|
|
215
452
|
const refKey = key.replace(/\.md$/, '.workflow-ref.md');
|
|
216
|
-
await writeFile(
|
|
453
|
+
await writeFile(resolveKeyPath(refKey, projectRoot), content);
|
|
217
454
|
}
|
|
218
455
|
|
|
219
|
-
// New files
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await
|
|
456
|
+
// New files (non-migration types handled here; migration types were
|
|
457
|
+
// processed in applyRepairPass with conflict safety)
|
|
458
|
+
for (const entry of plan.templateNewFiles) {
|
|
459
|
+
const content = await readTemplate(entry.templatePath);
|
|
460
|
+
await writeFile(resolveKeyPath(entry.key, projectRoot), content);
|
|
223
461
|
}
|
|
224
462
|
|
|
225
463
|
// Settings.json merge: append new permissions and hooks
|
|
@@ -236,29 +474,26 @@ export async function upgradeCommand() {
|
|
|
236
474
|
// Ensure sessions directory exists for session persistence
|
|
237
475
|
await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
|
|
238
476
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// the customization via the autoUpdate path.
|
|
477
|
+
// Hash refresh — files we just wrote (repair restored, repair migrated,
|
|
478
|
+
// autoUpdate, templateNewFiles). Modified / conflict / unchanged /
|
|
479
|
+
// userAdded / missingUntracked keep their prior hash; missingUntracked
|
|
480
|
+
// keys are dropped below.
|
|
244
481
|
const fileHashes = { ...meta.fileHashes };
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
482
|
+
const rehashTargets = [
|
|
483
|
+
...repairResult.restored,
|
|
484
|
+
...repairResult.migrated,
|
|
485
|
+
...categories.autoUpdate.map(({ key }) => ({ key, dest: resolveKeyPath(key, projectRoot) })),
|
|
486
|
+
...plan.templateNewFiles.map(({ key }) => ({ key, dest: resolveKeyPath(key, projectRoot) })),
|
|
487
|
+
];
|
|
488
|
+
for (const { key, dest } of rehashTargets) {
|
|
489
|
+
fileHashes[key] = await hashFile(dest);
|
|
252
490
|
}
|
|
253
|
-
for (const { key } of categories.
|
|
491
|
+
for (const { key } of categories.missingUntracked) {
|
|
254
492
|
delete fileHashes[key];
|
|
255
493
|
}
|
|
256
|
-
// modified, conflict, unchanged, userAdded: stored hash deliberately left alone
|
|
257
494
|
|
|
258
|
-
// Ensure .gitignore has worclaude entries
|
|
259
495
|
await updateGitignore(projectRoot);
|
|
260
496
|
|
|
261
|
-
// Update meta
|
|
262
497
|
meta.version = currentVersion;
|
|
263
498
|
meta.lastUpdated = new Date().toISOString();
|
|
264
499
|
meta.fileHashes = fileHashes;
|
|
@@ -266,8 +501,18 @@ export async function upgradeCommand() {
|
|
|
266
501
|
|
|
267
502
|
spinner.succeed(`Upgrade complete! (${installedVersion} → ${currentVersion})`);
|
|
268
503
|
|
|
269
|
-
// 8. Display report
|
|
270
504
|
display.newline();
|
|
505
|
+
if (repairResult.restored.length > 0) {
|
|
506
|
+
display.barLine(`Restored: ${repairResult.restored.length} files`);
|
|
507
|
+
}
|
|
508
|
+
if (repairResult.migrated.length > 0) {
|
|
509
|
+
display.barLine(`Installed: ${repairResult.migrated.length} files (migration)`);
|
|
510
|
+
}
|
|
511
|
+
if (repairResult.migrationConflicts.length > 0) {
|
|
512
|
+
display.barLine(
|
|
513
|
+
`Migration conflicts: ${repairResult.migrationConflicts.length} ${display.dimColor('(saved as .workflow-ref)')}`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
271
516
|
if (categories.autoUpdate.length > 0) {
|
|
272
517
|
display.barLine(`Updated: ${categories.autoUpdate.length} files`);
|
|
273
518
|
}
|
|
@@ -276,8 +521,8 @@ export async function upgradeCommand() {
|
|
|
276
521
|
`Conflicts: ${categories.conflict.length} files ${display.dimColor('(saved as .workflow-ref.md)')}`
|
|
277
522
|
);
|
|
278
523
|
}
|
|
279
|
-
if (
|
|
280
|
-
display.barLine(`New: ${
|
|
524
|
+
if (plan.templateNewFiles.length > 0) {
|
|
525
|
+
display.barLine(`New: ${plan.templateNewFiles.length} files added`);
|
|
281
526
|
}
|
|
282
527
|
display.barLine(`Unchanged: ${categories.unchanged.length} files`);
|
|
283
528
|
if (categories.modified.length > 0) {
|
|
@@ -285,6 +530,9 @@ export async function upgradeCommand() {
|
|
|
285
530
|
`Customized: ${categories.modified.length} files ${display.dimColor('(no updates needed)')}`
|
|
286
531
|
);
|
|
287
532
|
}
|
|
533
|
+
if (repairResult.sidecars.length > 0) {
|
|
534
|
+
display.barLine(`Sidecar: ${repairResult.sidecars.length} suggestion files`);
|
|
535
|
+
}
|
|
288
536
|
if (skillReport.migrated > 0) {
|
|
289
537
|
display.barLine(`Migrated: ${skillReport.migrated} skills to directory format`);
|
|
290
538
|
}
|
|
@@ -304,9 +552,13 @@ export async function upgradeCommand() {
|
|
|
304
552
|
display.newline();
|
|
305
553
|
display.barLine(display.dimColor(`Backup: ${path.basename(backupDir)}/`));
|
|
306
554
|
|
|
307
|
-
if (
|
|
555
|
+
if (
|
|
556
|
+
categories.conflict.length > 0 ||
|
|
557
|
+
repairResult.migrationConflicts.length > 0 ||
|
|
558
|
+
repairResult.sidecars.length > 0
|
|
559
|
+
) {
|
|
308
560
|
display.newline();
|
|
309
|
-
display.barLine(`Review .workflow-ref
|
|
561
|
+
display.barLine(`Review .workflow-ref files and merge what's useful.`);
|
|
310
562
|
}
|
|
311
563
|
} catch (err) {
|
|
312
564
|
spinner.fail('Upgrade failed.');
|
package/src/core/config.js
CHANGED
|
@@ -70,6 +70,8 @@ export async function requireWorkflowMeta(projectRoot) {
|
|
|
70
70
|
return { meta, error: null };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const ROOT_TRACKED_FILES = ['AGENTS.md'];
|
|
74
|
+
|
|
73
75
|
export async function computeFileHashes(projectRoot) {
|
|
74
76
|
const claudeDir = path.join(projectRoot, '.claude');
|
|
75
77
|
const allFiles = await listFilesRecursive(claudeDir);
|
|
@@ -84,5 +86,11 @@ export async function computeFileHashes(projectRoot) {
|
|
|
84
86
|
fileHashes[relKey] = await hashFile(filePath);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
89
|
+
for (const rel of ROOT_TRACKED_FILES) {
|
|
90
|
+
const filePath = path.join(projectRoot, rel);
|
|
91
|
+
if (await fileExists(filePath)) {
|
|
92
|
+
fileHashes[`root/${rel}`] = await hashFile(filePath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
87
95
|
return fileHashes;
|
|
88
96
|
}
|