worclaude 1.0.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/LICENSE +21 -0
- package/README.md +278 -0
- package/package.json +62 -0
- package/src/commands/backup.js +55 -0
- package/src/commands/diff.js +76 -0
- package/src/commands/init.js +628 -0
- package/src/commands/restore.js +95 -0
- package/src/commands/status.js +141 -0
- package/src/commands/upgrade.js +208 -0
- package/src/core/backup.js +94 -0
- package/src/core/config.js +54 -0
- package/src/core/detector.js +43 -0
- package/src/core/file-categorizer.js +177 -0
- package/src/core/merger.js +413 -0
- package/src/core/scaffolder.js +60 -0
- package/src/data/agents.js +164 -0
- package/src/index.js +51 -0
- package/src/prompts/agent-selection.js +99 -0
- package/src/prompts/claude-md-merge.js +153 -0
- package/src/prompts/conflict-resolution.js +24 -0
- package/src/prompts/project-type.js +75 -0
- package/src/prompts/tech-stack.js +35 -0
- package/src/utils/display.js +41 -0
- package/src/utils/file.js +70 -0
- package/src/utils/hash.js +13 -0
- package/src/utils/time.js +22 -0
- package/templates/agents/optional/backend/api-designer.md +61 -0
- package/templates/agents/optional/backend/auth-auditor.md +63 -0
- package/templates/agents/optional/backend/database-analyst.md +61 -0
- package/templates/agents/optional/data/data-pipeline-reviewer.md +68 -0
- package/templates/agents/optional/data/ml-experiment-tracker.md +67 -0
- package/templates/agents/optional/data/prompt-engineer.md +75 -0
- package/templates/agents/optional/devops/ci-fixer.md +64 -0
- package/templates/agents/optional/devops/dependency-manager.md +55 -0
- package/templates/agents/optional/devops/deploy-validator.md +68 -0
- package/templates/agents/optional/devops/docker-helper.md +63 -0
- package/templates/agents/optional/docs/changelog-generator.md +69 -0
- package/templates/agents/optional/docs/doc-writer.md +60 -0
- package/templates/agents/optional/frontend/style-enforcer.md +47 -0
- package/templates/agents/optional/frontend/ui-reviewer.md +51 -0
- package/templates/agents/optional/quality/bug-fixer.md +54 -0
- package/templates/agents/optional/quality/performance-auditor.md +65 -0
- package/templates/agents/optional/quality/refactorer.md +61 -0
- package/templates/agents/optional/quality/security-reviewer.md +74 -0
- package/templates/agents/universal/build-validator.md +15 -0
- package/templates/agents/universal/code-simplifier.md +17 -0
- package/templates/agents/universal/plan-reviewer.md +20 -0
- package/templates/agents/universal/test-writer.md +17 -0
- package/templates/agents/universal/verify-app.md +16 -0
- package/templates/claude-md.md +40 -0
- package/templates/commands/commit-push-pr.md +9 -0
- package/templates/commands/compact-safe.md +8 -0
- package/templates/commands/end.md +9 -0
- package/templates/commands/review-plan.md +10 -0
- package/templates/commands/setup.md +112 -0
- package/templates/commands/start.md +3 -0
- package/templates/commands/status.md +6 -0
- package/templates/commands/techdebt.md +9 -0
- package/templates/commands/update-claude-md.md +9 -0
- package/templates/commands/verify.md +8 -0
- package/templates/mcp-json.json +3 -0
- package/templates/progress-md.md +21 -0
- package/templates/settings/base.json +64 -0
- package/templates/settings/docker.json +9 -0
- package/templates/settings/go.json +10 -0
- package/templates/settings/node.json +17 -0
- package/templates/settings/python.json +16 -0
- package/templates/settings/rust.json +11 -0
- package/templates/skills/templates/backend-conventions.md +57 -0
- package/templates/skills/templates/frontend-design-system.md +48 -0
- package/templates/skills/templates/project-patterns.md +48 -0
- package/templates/skills/universal/claude-md-maintenance.md +110 -0
- package/templates/skills/universal/context-management.md +71 -0
- package/templates/skills/universal/git-conventions.md +95 -0
- package/templates/skills/universal/planning-with-files.md +114 -0
- package/templates/skills/universal/prompt-engineering.md +97 -0
- package/templates/skills/universal/review-and-handoff.md +106 -0
- package/templates/skills/universal/subagent-usage.md +108 -0
- package/templates/skills/universal/testing.md +116 -0
- package/templates/skills/universal/verification.md +120 -0
- package/templates/spec-md-backend.md +85 -0
- package/templates/spec-md-cli.md +79 -0
- package/templates/spec-md-data.md +74 -0
- package/templates/spec-md-devops.md +87 -0
- package/templates/spec-md-frontend.md +81 -0
- package/templates/spec-md-fullstack.md +81 -0
- package/templates/spec-md-library.md +87 -0
- package/templates/spec-md.md +22 -0
- package/templates/workflow-meta.json +10 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { listBackups, restoreBackup } from '../core/backup.js';
|
|
5
|
+
import { fileExists, dirExists } from '../utils/file.js';
|
|
6
|
+
import { relativeTime } from '../utils/time.js';
|
|
7
|
+
import * as display from '../utils/display.js';
|
|
8
|
+
|
|
9
|
+
export async function restoreCommand() {
|
|
10
|
+
const projectRoot = process.cwd();
|
|
11
|
+
|
|
12
|
+
const backups = await listBackups(projectRoot);
|
|
13
|
+
if (backups.length === 0) {
|
|
14
|
+
display.info('No backups found. Run `worclaude backup` to create one.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
display.header('Available Backups');
|
|
19
|
+
display.newline();
|
|
20
|
+
|
|
21
|
+
const choices = backups.map((b, i) => ({
|
|
22
|
+
name: `${path.basename(b.path)} (${relativeTime(b.dateString)})`,
|
|
23
|
+
value: i,
|
|
24
|
+
}));
|
|
25
|
+
choices.push(new inquirer.Separator());
|
|
26
|
+
choices.push({ name: '← Cancel', value: '__cancel__' });
|
|
27
|
+
|
|
28
|
+
const { selected } = await inquirer.prompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'list',
|
|
31
|
+
name: 'selected',
|
|
32
|
+
message: 'Select backup to restore:',
|
|
33
|
+
choices,
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
if (selected === '__cancel__') {
|
|
38
|
+
display.info('Restore cancelled.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const backup = backups[selected];
|
|
43
|
+
|
|
44
|
+
display.newline();
|
|
45
|
+
display.warn('This will replace your current Claude setup with the backup.');
|
|
46
|
+
display.dim(' Current files will be overwritten.');
|
|
47
|
+
display.newline();
|
|
48
|
+
|
|
49
|
+
const { confirm } = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'confirm',
|
|
52
|
+
name: 'confirm',
|
|
53
|
+
message: 'Confirm restore?',
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
if (!confirm) {
|
|
59
|
+
display.info('Restore cancelled.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const spinner = ora('Restoring...').start();
|
|
64
|
+
try {
|
|
65
|
+
await restoreBackup(projectRoot, backup.path);
|
|
66
|
+
spinner.succeed('Restore complete!');
|
|
67
|
+
} catch (err) {
|
|
68
|
+
spinner.fail('Restore failed.');
|
|
69
|
+
display.error(err.message);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
display.newline();
|
|
74
|
+
display.success(`Restored from ${path.basename(backup.path)}/`);
|
|
75
|
+
display.newline();
|
|
76
|
+
|
|
77
|
+
// Show what was restored
|
|
78
|
+
const restored = [];
|
|
79
|
+
if (await fileExists(path.join(projectRoot, 'CLAUDE.md'))) restored.push('CLAUDE.md');
|
|
80
|
+
if (await dirExists(path.join(projectRoot, '.claude'))) restored.push('.claude/ (full directory)');
|
|
81
|
+
if (await fileExists(path.join(projectRoot, '.mcp.json'))) restored.push('.mcp.json');
|
|
82
|
+
|
|
83
|
+
if (restored.length > 0) {
|
|
84
|
+
display.info('Restored:');
|
|
85
|
+
for (const item of restored) {
|
|
86
|
+
display.dim(` ${item}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
display.newline();
|
|
91
|
+
display.info(
|
|
92
|
+
'Note: workflow-meta.json has been restored to its backup state.'
|
|
93
|
+
);
|
|
94
|
+
display.dim(' Run `worclaude upgrade` if you want to update to the latest version.');
|
|
95
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readWorkflowMeta, workflowMetaExists } from '../core/config.js';
|
|
3
|
+
import { hashFile } from '../utils/hash.js';
|
|
4
|
+
import { fileExists, readFile, listFilesRecursive } from '../utils/file.js';
|
|
5
|
+
import { TECH_STACKS } from '../data/agents.js';
|
|
6
|
+
import * as display from '../utils/display.js';
|
|
7
|
+
|
|
8
|
+
const TECH_DISPLAY_NAMES = Object.fromEntries(TECH_STACKS.map((t) => [t.value, t.name]));
|
|
9
|
+
|
|
10
|
+
function countByPrefix(fileHashes, prefix) {
|
|
11
|
+
return Object.keys(fileHashes).filter((k) => k.startsWith(prefix)).length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function statusCommand() {
|
|
15
|
+
const projectRoot = process.cwd();
|
|
16
|
+
|
|
17
|
+
if (!(await workflowMetaExists(projectRoot))) {
|
|
18
|
+
display.info('Workflow is not installed. Run `worclaude init` to set up.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const meta = await readWorkflowMeta(projectRoot);
|
|
23
|
+
if (!meta) {
|
|
24
|
+
display.error('workflow-meta.json is corrupted. Run `worclaude init` to reinstall.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
display.header('Worclaude Status');
|
|
29
|
+
display.newline();
|
|
30
|
+
|
|
31
|
+
// Version and dates
|
|
32
|
+
display.dim(` Version: ${meta.version}`);
|
|
33
|
+
display.dim(` Installed: ${meta.installedAt?.split('T')[0] || 'unknown'}`);
|
|
34
|
+
display.dim(` Last updated: ${meta.lastUpdated?.split('T')[0] || 'unknown'}`);
|
|
35
|
+
display.newline();
|
|
36
|
+
|
|
37
|
+
// Project info
|
|
38
|
+
const projectTypes = (meta.projectTypes || []).join(', ');
|
|
39
|
+
if (projectTypes) display.dim(` Project type: ${projectTypes}`);
|
|
40
|
+
|
|
41
|
+
const techNames = (meta.techStack || [])
|
|
42
|
+
.map((t) => TECH_DISPLAY_NAMES[t] || t)
|
|
43
|
+
.join(', ');
|
|
44
|
+
if (techNames) display.dim(` Tech stack: ${techNames}`);
|
|
45
|
+
display.newline();
|
|
46
|
+
|
|
47
|
+
// Agents
|
|
48
|
+
const universalCount = (meta.universalAgents || []).length;
|
|
49
|
+
const optionalCount = (meta.optionalAgents || []).length;
|
|
50
|
+
const totalAgents = universalCount + optionalCount;
|
|
51
|
+
display.dim(` Agents: ${universalCount} universal + ${optionalCount} optional (${totalAgents} total)`);
|
|
52
|
+
if (optionalCount > 0) {
|
|
53
|
+
display.dim(` Optional: ${meta.optionalAgents.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
display.newline();
|
|
56
|
+
|
|
57
|
+
// Commands and skills counts
|
|
58
|
+
const commandCount = countByPrefix(meta.fileHashes || {}, 'commands/');
|
|
59
|
+
const skillCount = countByPrefix(meta.fileHashes || {}, 'skills/');
|
|
60
|
+
display.dim(` Commands: ${commandCount} installed`);
|
|
61
|
+
display.dim(` Skills: ${skillCount} installed`);
|
|
62
|
+
display.newline();
|
|
63
|
+
|
|
64
|
+
// Customized files
|
|
65
|
+
const customized = [];
|
|
66
|
+
for (const [key, storedHash] of Object.entries(meta.fileHashes || {})) {
|
|
67
|
+
const filePath = path.join(projectRoot, '.claude', ...key.split('/'));
|
|
68
|
+
if (await fileExists(filePath)) {
|
|
69
|
+
const currentHash = await hashFile(filePath);
|
|
70
|
+
if (currentHash !== storedHash) {
|
|
71
|
+
customized.push(`.claude/${key}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check CLAUDE.md separately
|
|
77
|
+
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
78
|
+
if (await fileExists(claudeMdPath)) {
|
|
79
|
+
// CLAUDE.md is always considered potentially customized
|
|
80
|
+
customized.push('CLAUDE.md');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (customized.length > 0) {
|
|
84
|
+
display.info('Customized files (differ from installed version):');
|
|
85
|
+
for (const f of customized) {
|
|
86
|
+
display.dim(` ~ ${f}`);
|
|
87
|
+
}
|
|
88
|
+
display.newline();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pending review files
|
|
92
|
+
const pendingReview = [];
|
|
93
|
+
try {
|
|
94
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
95
|
+
const allFiles = await listFilesRecursive(claudeDir);
|
|
96
|
+
for (const fp of allFiles) {
|
|
97
|
+
const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
|
|
98
|
+
if (rel.endsWith('.workflow-ref.md')) {
|
|
99
|
+
pendingReview.push(`.claude/${rel}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// .claude dir might not exist
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const suggestionsPath = path.join(projectRoot, 'CLAUDE.md.workflow-suggestions');
|
|
107
|
+
if (await fileExists(suggestionsPath)) {
|
|
108
|
+
pendingReview.push('CLAUDE.md.workflow-suggestions');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (pendingReview.length > 0) {
|
|
112
|
+
display.warn('Pending review:');
|
|
113
|
+
for (const f of pendingReview) {
|
|
114
|
+
display.dim(` ⚠ ${f}`);
|
|
115
|
+
}
|
|
116
|
+
display.newline();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Settings info
|
|
120
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
121
|
+
if (await fileExists(settingsPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const raw = await readFile(settingsPath);
|
|
124
|
+
const settings = JSON.parse(raw);
|
|
125
|
+
|
|
126
|
+
const allow = settings.permissions?.allow || [];
|
|
127
|
+
const permCount = allow.filter((p) => !p.trim().startsWith('//')).length;
|
|
128
|
+
|
|
129
|
+
const hooks = settings.hooks || {};
|
|
130
|
+
const hookCount = Object.values(hooks).reduce(
|
|
131
|
+
(sum, entries) => sum + entries.length,
|
|
132
|
+
0
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
display.dim(` Hooks: ${hookCount} active`);
|
|
136
|
+
display.dim(` Permissions: ${permCount} rules`);
|
|
137
|
+
} catch {
|
|
138
|
+
display.dim(' Settings: (could not parse settings.json)');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import {
|
|
5
|
+
readWorkflowMeta,
|
|
6
|
+
workflowMetaExists,
|
|
7
|
+
writeWorkflowMeta,
|
|
8
|
+
getPackageVersion,
|
|
9
|
+
} from '../core/config.js';
|
|
10
|
+
import { createBackup } from '../core/backup.js';
|
|
11
|
+
import { categorizeFiles } from '../core/file-categorizer.js';
|
|
12
|
+
import { buildSettingsJson, mergeSettingsPermissionsAndHooks } from '../core/merger.js';
|
|
13
|
+
import { readTemplate } from '../core/scaffolder.js';
|
|
14
|
+
import { hashFile } from '../utils/hash.js';
|
|
15
|
+
import { writeFile, fileExists, listFilesRecursive } from '../utils/file.js';
|
|
16
|
+
import * as display from '../utils/display.js';
|
|
17
|
+
|
|
18
|
+
export async function upgradeCommand() {
|
|
19
|
+
const projectRoot = process.cwd();
|
|
20
|
+
|
|
21
|
+
// 1. Check prerequisite
|
|
22
|
+
if (!(await workflowMetaExists(projectRoot))) {
|
|
23
|
+
display.error('No workflow installation found.');
|
|
24
|
+
display.info('Run `worclaude init` to set up the workflow first.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const meta = await readWorkflowMeta(projectRoot);
|
|
29
|
+
if (!meta) {
|
|
30
|
+
display.error('workflow-meta.json is corrupted or invalid.');
|
|
31
|
+
display.info('Run `worclaude init` to reinstall (a backup will be created first).');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Version comparison
|
|
36
|
+
const currentVersion = await getPackageVersion();
|
|
37
|
+
const installedVersion = meta.version;
|
|
38
|
+
|
|
39
|
+
if (installedVersion === currentVersion) {
|
|
40
|
+
display.success(`Already up to date (v${currentVersion}).`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Categorize files
|
|
45
|
+
const categories = await categorizeFiles(projectRoot, meta);
|
|
46
|
+
|
|
47
|
+
// 4. Preview
|
|
48
|
+
display.header('Worclaude Upgrade');
|
|
49
|
+
display.newline();
|
|
50
|
+
display.dim(` Current version: ${installedVersion}`);
|
|
51
|
+
display.dim(` New version: ${currentVersion}`);
|
|
52
|
+
display.newline();
|
|
53
|
+
|
|
54
|
+
display.info('Changes:');
|
|
55
|
+
|
|
56
|
+
if (categories.autoUpdate.length > 0) {
|
|
57
|
+
display.dim(' Auto-update (unchanged since install):');
|
|
58
|
+
const showCount = Math.min(categories.autoUpdate.length, 3);
|
|
59
|
+
for (let i = 0; i < showCount; i++) {
|
|
60
|
+
display.dim(` ✓ ${categories.autoUpdate[i].key}`);
|
|
61
|
+
}
|
|
62
|
+
if (categories.autoUpdate.length > 3) {
|
|
63
|
+
display.dim(` ✓ ${categories.autoUpdate.length - 3} more files`);
|
|
64
|
+
}
|
|
65
|
+
display.newline();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (categories.conflict.length > 0) {
|
|
69
|
+
display.dim(' Needs review (you\'ve customized these):');
|
|
70
|
+
for (const { key } of categories.conflict) {
|
|
71
|
+
display.dim(` ~ ${key} (modified since install)`);
|
|
72
|
+
}
|
|
73
|
+
display.newline();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (categories.newFiles.length > 0) {
|
|
77
|
+
display.dim(' New in this version:');
|
|
78
|
+
for (const { key } of categories.newFiles) {
|
|
79
|
+
display.dim(` + ${key}`);
|
|
80
|
+
}
|
|
81
|
+
display.newline();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (categories.unchanged.length > 0) {
|
|
85
|
+
display.dim(` Unchanged (no updates needed): ${categories.unchanged.length} files`);
|
|
86
|
+
display.newline();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (categories.modified.length > 0) {
|
|
90
|
+
display.dim(' Your customizations (no workflow updates available):');
|
|
91
|
+
for (const { key } of categories.modified) {
|
|
92
|
+
display.dim(` ~ ${key}`);
|
|
93
|
+
}
|
|
94
|
+
display.newline();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const hasWork =
|
|
98
|
+
categories.autoUpdate.length > 0 ||
|
|
99
|
+
categories.conflict.length > 0 ||
|
|
100
|
+
categories.newFiles.length > 0;
|
|
101
|
+
|
|
102
|
+
if (!hasWork) {
|
|
103
|
+
display.info('No file changes needed — only updating version metadata.');
|
|
104
|
+
display.newline();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 5. Confirm
|
|
108
|
+
const { proceed } = await inquirer.prompt([
|
|
109
|
+
{
|
|
110
|
+
type: 'list',
|
|
111
|
+
name: 'proceed',
|
|
112
|
+
message: 'Proceed with upgrade?',
|
|
113
|
+
choices: [
|
|
114
|
+
{ name: 'Yes', value: true },
|
|
115
|
+
{ name: 'No', value: false },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
if (!proceed) {
|
|
121
|
+
display.info('Upgrade cancelled.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 6. Execute
|
|
126
|
+
const spinner = ora('Upgrading...').start();
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Create backup first
|
|
130
|
+
const backupDir = await createBackup(projectRoot);
|
|
131
|
+
spinner.text = 'Backup created, applying updates...';
|
|
132
|
+
|
|
133
|
+
// Auto-update files
|
|
134
|
+
for (const { key, templatePath } of categories.autoUpdate) {
|
|
135
|
+
const content = await readTemplate(templatePath);
|
|
136
|
+
await writeFile(path.join(projectRoot, '.claude', ...key.split('/')), content);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Conflict files: save as .workflow-ref.md
|
|
140
|
+
for (const { key, templatePath } of categories.conflict) {
|
|
141
|
+
const content = await readTemplate(templatePath);
|
|
142
|
+
const refKey = key.replace(/\.md$/, '.workflow-ref.md');
|
|
143
|
+
await writeFile(path.join(projectRoot, '.claude', ...refKey.split('/')), content);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// New files: add directly
|
|
147
|
+
for (const { key, templatePath } of categories.newFiles) {
|
|
148
|
+
const content = await readTemplate(templatePath);
|
|
149
|
+
await writeFile(path.join(projectRoot, '.claude', ...key.split('/')), content);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Settings.json merge: append new permissions and hooks
|
|
153
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
154
|
+
if (await fileExists(settingsPath)) {
|
|
155
|
+
const techStack = meta.techStack || [];
|
|
156
|
+
const useDocker = meta.useDocker || false;
|
|
157
|
+
const { settingsObject: workflowSettings } = await buildSettingsJson(techStack, useDocker);
|
|
158
|
+
const settingsReport = { added: { permissions: 0, hooks: 0 }, hookConflicts: [] };
|
|
159
|
+
await mergeSettingsPermissionsAndHooks(projectRoot, workflowSettings, settingsReport);
|
|
160
|
+
spinner.text = 'Settings merged...';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Recompute file hashes
|
|
164
|
+
const fileHashes = {};
|
|
165
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
166
|
+
const allFiles = await listFilesRecursive(claudeDir);
|
|
167
|
+
for (const filePath of allFiles) {
|
|
168
|
+
const relKey = path.relative(claudeDir, filePath).split(path.sep).join('/');
|
|
169
|
+
if (relKey !== 'workflow-meta.json' && relKey !== 'settings.json') {
|
|
170
|
+
fileHashes[relKey] = await hashFile(filePath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Update meta
|
|
175
|
+
meta.version = currentVersion;
|
|
176
|
+
meta.lastUpdated = new Date().toISOString();
|
|
177
|
+
meta.fileHashes = fileHashes;
|
|
178
|
+
await writeWorkflowMeta(projectRoot, meta);
|
|
179
|
+
|
|
180
|
+
spinner.succeed(`Upgrade complete! (${installedVersion} → ${currentVersion})`);
|
|
181
|
+
|
|
182
|
+
// 7. Display report
|
|
183
|
+
display.newline();
|
|
184
|
+
if (categories.autoUpdate.length > 0) {
|
|
185
|
+
display.dim(` Updated: ${categories.autoUpdate.length} files`);
|
|
186
|
+
}
|
|
187
|
+
if (categories.conflict.length > 0) {
|
|
188
|
+
display.dim(` Conflicts: ${categories.conflict.length} files (saved as .workflow-ref.md)`);
|
|
189
|
+
}
|
|
190
|
+
if (categories.newFiles.length > 0) {
|
|
191
|
+
display.dim(` New: ${categories.newFiles.length} files added`);
|
|
192
|
+
}
|
|
193
|
+
display.dim(` Unchanged: ${categories.unchanged.length} files`);
|
|
194
|
+
if (categories.modified.length > 0) {
|
|
195
|
+
display.dim(` Customized: ${categories.modified.length} files (no updates needed)`);
|
|
196
|
+
}
|
|
197
|
+
display.newline();
|
|
198
|
+
display.dim(` Backup: ${path.basename(backupDir)}/`);
|
|
199
|
+
|
|
200
|
+
if (categories.conflict.length > 0) {
|
|
201
|
+
display.newline();
|
|
202
|
+
display.info('Review .workflow-ref.md files and merge what\'s useful.');
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
spinner.fail('Upgrade failed.');
|
|
206
|
+
display.error(err.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import {
|
|
4
|
+
fileExists,
|
|
5
|
+
dirExists,
|
|
6
|
+
copyFile,
|
|
7
|
+
copyDirectory,
|
|
8
|
+
removeDirectory,
|
|
9
|
+
ensureDir,
|
|
10
|
+
} from '../utils/file.js';
|
|
11
|
+
|
|
12
|
+
function generateTimestamp() {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
15
|
+
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
16
|
+
const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
17
|
+
return `${date}-${time}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createBackup(projectRoot) {
|
|
21
|
+
const timestamp = generateTimestamp();
|
|
22
|
+
const backupDir = path.join(projectRoot, `.claude-backup-${timestamp}`);
|
|
23
|
+
await ensureDir(backupDir);
|
|
24
|
+
|
|
25
|
+
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
26
|
+
if (await fileExists(claudeMdPath)) {
|
|
27
|
+
await copyFile(claudeMdPath, path.join(backupDir, 'CLAUDE.md'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
31
|
+
if (await dirExists(claudeDir)) {
|
|
32
|
+
await copyDirectory(claudeDir, path.join(backupDir, '.claude'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mcpPath = path.join(projectRoot, '.mcp.json');
|
|
36
|
+
if (await fileExists(mcpPath)) {
|
|
37
|
+
await copyFile(mcpPath, path.join(backupDir, '.mcp.json'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return backupDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function listBackups(projectRoot) {
|
|
44
|
+
const BACKUP_PATTERN = /^\.claude-backup-(\d{8}-\d{6})$/;
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const backups = entries
|
|
53
|
+
.filter((e) => e.isDirectory() && BACKUP_PATTERN.test(e.name))
|
|
54
|
+
.map((e) => {
|
|
55
|
+
const match = e.name.match(BACKUP_PATTERN);
|
|
56
|
+
const timestamp = match[1];
|
|
57
|
+
// Parse YYYYMMDD-HHMMSS into readable date
|
|
58
|
+
const year = timestamp.slice(0, 4);
|
|
59
|
+
const month = timestamp.slice(4, 6);
|
|
60
|
+
const day = timestamp.slice(6, 8);
|
|
61
|
+
const hour = timestamp.slice(9, 11);
|
|
62
|
+
const min = timestamp.slice(11, 13);
|
|
63
|
+
const sec = timestamp.slice(13, 15);
|
|
64
|
+
return {
|
|
65
|
+
path: path.join(projectRoot, e.name),
|
|
66
|
+
timestamp,
|
|
67
|
+
dateString: `${year}-${month}-${day} ${hour}:${min}:${sec}`,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
71
|
+
|
|
72
|
+
return backups;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function restoreBackup(projectRoot, backupPath) {
|
|
76
|
+
const backupClaudeMd = path.join(backupPath, 'CLAUDE.md');
|
|
77
|
+
if (await fileExists(backupClaudeMd)) {
|
|
78
|
+
await copyFile(backupClaudeMd, path.join(projectRoot, 'CLAUDE.md'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const backupClaudeDir = path.join(backupPath, '.claude');
|
|
82
|
+
if (await dirExists(backupClaudeDir)) {
|
|
83
|
+
const existingClaudeDir = path.join(projectRoot, '.claude');
|
|
84
|
+
if (await dirExists(existingClaudeDir)) {
|
|
85
|
+
await removeDirectory(existingClaudeDir);
|
|
86
|
+
}
|
|
87
|
+
await copyDirectory(backupClaudeDir, existingClaudeDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const backupMcp = path.join(backupPath, '.mcp.json');
|
|
91
|
+
if (await fileExists(backupMcp)) {
|
|
92
|
+
await copyFile(backupMcp, path.join(projectRoot, '.mcp.json'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readFile, writeFile, fileExists } from '../utils/file.js';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export async function getPackageVersion() {
|
|
8
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
9
|
+
const content = await readFile(pkgPath);
|
|
10
|
+
return JSON.parse(content).version;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createWorkflowMeta({
|
|
14
|
+
projectTypes,
|
|
15
|
+
techStack,
|
|
16
|
+
universalAgents,
|
|
17
|
+
optionalAgents,
|
|
18
|
+
fileHashes = {},
|
|
19
|
+
version,
|
|
20
|
+
useDocker = false,
|
|
21
|
+
}) {
|
|
22
|
+
const now = new Date().toISOString();
|
|
23
|
+
return {
|
|
24
|
+
version: version || '1.0.0',
|
|
25
|
+
installedAt: now,
|
|
26
|
+
lastUpdated: now,
|
|
27
|
+
projectTypes,
|
|
28
|
+
techStack,
|
|
29
|
+
universalAgents,
|
|
30
|
+
optionalAgents,
|
|
31
|
+
useDocker,
|
|
32
|
+
fileHashes,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readWorkflowMeta(projectRoot) {
|
|
37
|
+
const metaPath = path.join(projectRoot, '.claude', 'workflow-meta.json');
|
|
38
|
+
try {
|
|
39
|
+
const content = await readFile(metaPath);
|
|
40
|
+
return JSON.parse(content);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function workflowMetaExists(projectRoot) {
|
|
47
|
+
const metaPath = path.join(projectRoot, '.claude', 'workflow-meta.json');
|
|
48
|
+
return fileExists(metaPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeWorkflowMeta(projectRoot, meta) {
|
|
52
|
+
const metaPath = path.join(projectRoot, '.claude', 'workflow-meta.json');
|
|
53
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileExists, dirExists, readFile, listFiles } from '../utils/file.js';
|
|
3
|
+
import { workflowMetaExists } from './config.js';
|
|
4
|
+
|
|
5
|
+
export async function detectScenario(projectRoot) {
|
|
6
|
+
if (await workflowMetaExists(projectRoot)) {
|
|
7
|
+
return 'upgrade';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const hasClaudeDir = await dirExists(path.join(projectRoot, '.claude'));
|
|
11
|
+
const hasClaudeMd = await fileExists(path.join(projectRoot, 'CLAUDE.md'));
|
|
12
|
+
const hasMcpJson = await fileExists(path.join(projectRoot, '.mcp.json'));
|
|
13
|
+
|
|
14
|
+
if (hasClaudeDir || hasClaudeMd || hasMcpJson) {
|
|
15
|
+
return 'existing';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return 'fresh';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function scanExistingSetup(projectRoot) {
|
|
22
|
+
const hasClaudeDir = await dirExists(path.join(projectRoot, '.claude'));
|
|
23
|
+
const hasClaudeMd = await fileExists(path.join(projectRoot, 'CLAUDE.md'));
|
|
24
|
+
|
|
25
|
+
let claudeMdLineCount = 0;
|
|
26
|
+
if (hasClaudeMd) {
|
|
27
|
+
const content = await readFile(path.join(projectRoot, 'CLAUDE.md'));
|
|
28
|
+
claudeMdLineCount = content.split(/\r?\n/).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
hasClaudeDir,
|
|
33
|
+
hasClaudeMd,
|
|
34
|
+
claudeMdLineCount,
|
|
35
|
+
hasSettingsJson: await fileExists(path.join(projectRoot, '.claude', 'settings.json')),
|
|
36
|
+
hasMcpJson: await fileExists(path.join(projectRoot, '.mcp.json')),
|
|
37
|
+
existingSkills: await listFiles(path.join(projectRoot, '.claude', 'skills')),
|
|
38
|
+
existingAgents: await listFiles(path.join(projectRoot, '.claude', 'agents')),
|
|
39
|
+
existingCommands: await listFiles(path.join(projectRoot, '.claude', 'commands')),
|
|
40
|
+
hasProgressMd: await fileExists(path.join(projectRoot, 'docs', 'spec', 'PROGRESS.md')),
|
|
41
|
+
hasSpecMd: await fileExists(path.join(projectRoot, 'docs', 'spec', 'SPEC.md')),
|
|
42
|
+
};
|
|
43
|
+
}
|