worclaude 1.4.0 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worclaude",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI tool that scaffolds a comprehensive Claude Code workflow into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,234 @@
1
+ import path from 'node:path';
2
+ import inquirer from 'inquirer';
3
+ import ora from 'ora';
4
+ import { workflowMetaExists, readWorkflowMeta } from '../core/config.js';
5
+ import { createBackup } from '../core/backup.js';
6
+ import {
7
+ classifyClaudeFiles,
8
+ detectRootFiles,
9
+ removeTrackedFiles,
10
+ removeRootFiles,
11
+ cleanGitignore,
12
+ } from '../core/remover.js';
13
+ import * as display from '../utils/display.js';
14
+
15
+ export async function deleteCommand() {
16
+ const projectRoot = process.cwd();
17
+
18
+ // Pre-flight: ensure worclaude is installed
19
+ if (!(await workflowMetaExists(projectRoot))) {
20
+ display.error('No worclaude workflow found in this project.');
21
+ display.info('Run `worclaude init` to set up a workflow first.');
22
+ return;
23
+ }
24
+
25
+ const meta = await readWorkflowMeta(projectRoot);
26
+ if (!meta) {
27
+ display.error('workflow-meta.json is corrupted or unreadable.');
28
+ display.info('You may need to manually remove the .claude/ directory.');
29
+ return;
30
+ }
31
+
32
+ display.sectionHeader('DELETE WORKFLOW');
33
+ display.newline();
34
+
35
+ // Step 1: Mode selection
36
+ const { mode } = await inquirer.prompt([
37
+ {
38
+ type: 'list',
39
+ name: 'mode',
40
+ message: 'What would you like to do?',
41
+ choices: [
42
+ { name: 'Remove workflow from this project', value: 'project' },
43
+ { name: 'Remove workflow and uninstall worclaude globally', value: 'global' },
44
+ new inquirer.Separator(),
45
+ { name: '← Cancel', value: 'cancel' },
46
+ ],
47
+ },
48
+ ]);
49
+
50
+ if (mode === 'cancel') {
51
+ display.info('Delete cancelled.');
52
+ return;
53
+ }
54
+
55
+ // Step 2: Classify files
56
+ const classification = await classifyClaudeFiles(projectRoot, meta);
57
+ const rootFiles = await detectRootFiles(projectRoot);
58
+
59
+ // Step 3: Show preview
60
+ display.newline();
61
+ display.barLine('Workflow files in .claude/:');
62
+ if (classification.safeToDelete.length > 0) {
63
+ display.barLine(
64
+ ` ${display.green('✓')} ${classification.safeToDelete.length} unmodified files (safe to remove)`
65
+ );
66
+ }
67
+ if (classification.modified.length > 0) {
68
+ display.barLine(
69
+ ` ${display.yellow('~')} ${classification.modified.length} files you've customized`
70
+ );
71
+ for (const key of classification.modified) {
72
+ display.barLine(` ${display.yellow('~')} .claude/${key}`);
73
+ }
74
+ }
75
+ if (classification.userOwned.length > 0) {
76
+ display.barLine(
77
+ ` ${display.blue('●')} ${classification.userOwned.length} user-added files (will NOT be touched)`
78
+ );
79
+ }
80
+
81
+ // Step 4: Handle modified files
82
+ let filesToDelete = [...classification.safeToDelete];
83
+
84
+ if (classification.modified.length > 0) {
85
+ display.newline();
86
+ const { modifiedAction } = await inquirer.prompt([
87
+ {
88
+ type: 'list',
89
+ name: 'modifiedAction',
90
+ message: `${classification.modified.length} file(s) have been customized. What should we do?`,
91
+ choices: [
92
+ { name: "Delete them too (they'll be in the backup)", value: 'delete' },
93
+ { name: 'Keep them in .claude/', value: 'keep' },
94
+ ],
95
+ },
96
+ ]);
97
+
98
+ if (modifiedAction === 'delete') {
99
+ filesToDelete.push(...classification.modified);
100
+ }
101
+ }
102
+
103
+ // Step 5: Handle root-level files (settings.json + project root files)
104
+ let rootFilesToDelete = [];
105
+
106
+ if (rootFiles.length > 0) {
107
+ display.newline();
108
+ display.info('These files were created or modified by worclaude but may contain your work:');
109
+ for (const f of rootFiles) {
110
+ display.dim(` ${f.label}`);
111
+ }
112
+ display.newline();
113
+
114
+ const { rootAction } = await inquirer.prompt([
115
+ {
116
+ type: 'list',
117
+ name: 'rootAction',
118
+ message: 'What would you like to do with these files?',
119
+ choices: [
120
+ { name: 'Keep all (recommended)', value: 'keep' },
121
+ { name: 'Let me choose which to remove', value: 'choose' },
122
+ { name: 'Remove all', value: 'remove' },
123
+ ],
124
+ default: 0,
125
+ },
126
+ ]);
127
+
128
+ if (rootAction === 'remove') {
129
+ rootFilesToDelete = rootFiles.map((f) => f.path);
130
+ } else if (rootAction === 'choose') {
131
+ const { selected } = await inquirer.prompt([
132
+ {
133
+ type: 'checkbox',
134
+ name: 'selected',
135
+ message: 'Select files to remove:',
136
+ choices: rootFiles.map((f) => ({ name: f.label, value: f.path })),
137
+ },
138
+ ]);
139
+ rootFilesToDelete = selected;
140
+ }
141
+ }
142
+
143
+ // Step 6: Final confirmation
144
+ const totalDeletions = filesToDelete.length + rootFilesToDelete.length;
145
+
146
+ if (totalDeletions === 0) {
147
+ display.newline();
148
+ display.info('No files selected for removal.');
149
+ return;
150
+ }
151
+
152
+ display.newline();
153
+ display.warn(`This will permanently delete ${totalDeletions} file(s).`);
154
+ display.dim(' A backup will be created first.');
155
+ display.newline();
156
+
157
+ const { confirm } = await inquirer.prompt([
158
+ {
159
+ type: 'list',
160
+ name: 'confirm',
161
+ message: 'Confirm deletion?',
162
+ choices: [
163
+ { name: 'Yes, delete', value: true },
164
+ { name: 'No, cancel', value: false },
165
+ ],
166
+ default: 1,
167
+ },
168
+ ]);
169
+
170
+ if (!confirm) {
171
+ display.info('Delete cancelled.');
172
+ return;
173
+ }
174
+
175
+ // Step 7: Execute
176
+ const spinner = ora('Creating backup...').start();
177
+
178
+ try {
179
+ const backupDir = await createBackup(projectRoot);
180
+ spinner.text = 'Removing workflow files...';
181
+
182
+ // Remove .claude/ tracked files
183
+ const claudeRemoved = await removeTrackedFiles(projectRoot, filesToDelete);
184
+
185
+ // Remove root files
186
+ let rootRemoved = 0;
187
+ if (rootFilesToDelete.length > 0) {
188
+ rootRemoved = await removeRootFiles(projectRoot, rootFilesToDelete);
189
+ }
190
+
191
+ // Clean .gitignore
192
+ const gitignoreCleaned = await cleanGitignore(projectRoot);
193
+
194
+ spinner.succeed('Workflow removed!');
195
+
196
+ // Step 8: Report
197
+ display.newline();
198
+ if (claudeRemoved > 0) {
199
+ display.success(`Removed ${claudeRemoved} workflow files from .claude/`);
200
+ }
201
+ if (rootRemoved > 0) {
202
+ display.success(`Removed ${rootRemoved} root-level file(s)`);
203
+ }
204
+ if (gitignoreCleaned) {
205
+ display.success('Cleaned up .gitignore');
206
+ }
207
+
208
+ const keptModified = classification.modified.filter((f) => !filesToDelete.includes(f));
209
+ if (keptModified.length > 0) {
210
+ display.info(`Kept ${keptModified.length} customized file(s) in .claude/`);
211
+ }
212
+ if (classification.userOwned.length > 0) {
213
+ display.info(`Kept ${classification.userOwned.length} user-added file(s) in .claude/`);
214
+ }
215
+
216
+ const keptRootFiles = rootFiles.filter((f) => !rootFilesToDelete.includes(f.path));
217
+ if (keptRootFiles.length > 0) {
218
+ display.info(`Kept: ${keptRootFiles.map((f) => f.label).join(', ')}`);
219
+ }
220
+
221
+ display.newline();
222
+ display.dim(` Backup: ${path.basename(backupDir)}/`);
223
+
224
+ // Step 9: Global uninstall hint
225
+ if (mode === 'global') {
226
+ display.newline();
227
+ display.info('To uninstall worclaude CLI globally, run:');
228
+ display.dim(' npm uninstall -g worclaude');
229
+ }
230
+ } catch (err) {
231
+ spinner.fail('Delete failed.');
232
+ display.error(err.message);
233
+ }
234
+ }
@@ -676,6 +676,14 @@ export async function initCommand() {
676
676
  const version = await getPackageVersion();
677
677
  display.banner(version);
678
678
 
679
+ // Windows guidance: hooks require Git Bash
680
+ if (process.platform === 'win32') {
681
+ display.info(
682
+ 'Windows detected \u2014 hooks require Git for Windows (Git Bash). See: https://gitforwindows.org'
683
+ );
684
+ display.newline();
685
+ }
686
+
679
687
  // Step 3: If existing project, show detection report and confirm
680
688
  let existingScan = null;
681
689
  let backupPath = null;
@@ -37,6 +37,16 @@ export async function createBackup(projectRoot) {
37
37
  await copyFile(mcpPath, path.join(backupDir, '.mcp.json'));
38
38
  }
39
39
 
40
+ const progressPath = path.join(projectRoot, 'docs', 'spec', 'PROGRESS.md');
41
+ if (await fileExists(progressPath)) {
42
+ await copyFile(progressPath, path.join(backupDir, 'docs', 'spec', 'PROGRESS.md'));
43
+ }
44
+
45
+ const specPath = path.join(projectRoot, 'docs', 'spec', 'SPEC.md');
46
+ if (await fileExists(specPath)) {
47
+ await copyFile(specPath, path.join(backupDir, 'docs', 'spec', 'SPEC.md'));
48
+ }
49
+
40
50
  return backupDir;
41
51
  }
42
52
 
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import { hashFile } from '../utils/hash.js';
3
+ import {
4
+ fileExists,
5
+ dirExists,
6
+ readFile,
7
+ writeFile,
8
+ listFiles,
9
+ listFilesRecursive,
10
+ removeDirectory,
11
+ } from '../utils/file.js';
12
+
13
+ /**
14
+ * Classify .claude/ files into safe-to-delete, modified, missing, and user-owned.
15
+ * Uses only on-disk hash vs. stored hash (no template hash comparison).
16
+ */
17
+ export async function classifyClaudeFiles(projectRoot, meta) {
18
+ const claudeDir = path.join(projectRoot, '.claude');
19
+ const fileHashes = meta.fileHashes || {};
20
+
21
+ const safeToDelete = [];
22
+ const modified = [];
23
+ const missing = [];
24
+ const userOwned = [];
25
+
26
+ // Classify tracked files by comparing on-disk hash to stored hash
27
+ for (const [key, storedHash] of Object.entries(fileHashes)) {
28
+ const filePath = path.join(claudeDir, ...key.split('/'));
29
+ if (!(await fileExists(filePath))) {
30
+ missing.push(key);
31
+ continue;
32
+ }
33
+ const currentHash = await hashFile(filePath);
34
+ if (currentHash === storedHash) {
35
+ safeToDelete.push(key);
36
+ } else {
37
+ modified.push(key);
38
+ }
39
+ }
40
+
41
+ // workflow-meta.json is always worclaude's — safe to delete
42
+ if (await fileExists(path.join(claudeDir, 'workflow-meta.json'))) {
43
+ safeToDelete.push('workflow-meta.json');
44
+ }
45
+
46
+ // Scan disk for files not in fileHashes
47
+ const allTrackedKeys = new Set([
48
+ ...Object.keys(fileHashes),
49
+ 'workflow-meta.json',
50
+ 'settings.json',
51
+ ]);
52
+ const allDiskFiles = await listFilesRecursive(claudeDir);
53
+
54
+ for (const fp of allDiskFiles) {
55
+ const relKey = path.relative(claudeDir, fp).split(path.sep).join('/');
56
+ if (allTrackedKeys.has(relKey)) continue;
57
+
58
+ // .workflow-ref.md files are upgrade artifacts — safe to delete
59
+ if (relKey.endsWith('.workflow-ref.md')) {
60
+ safeToDelete.push(relKey);
61
+ } else {
62
+ userOwned.push(relKey);
63
+ }
64
+ }
65
+
66
+ return { safeToDelete, modified, missing, userOwned };
67
+ }
68
+
69
+ /**
70
+ * Detect root-level files that worclaude creates or modifies.
71
+ * settings.json is included here since it may contain user customizations.
72
+ */
73
+ export async function detectRootFiles(projectRoot) {
74
+ const candidates = [
75
+ {
76
+ path: path.join('.claude', 'settings.json'),
77
+ label: '.claude/settings.json (permissions & hooks)',
78
+ },
79
+ { path: 'CLAUDE.md', label: 'CLAUDE.md' },
80
+ { path: '.mcp.json', label: '.mcp.json' },
81
+ { path: path.join('docs', 'spec', 'PROGRESS.md'), label: 'docs/spec/PROGRESS.md' },
82
+ { path: path.join('docs', 'spec', 'SPEC.md'), label: 'docs/spec/SPEC.md' },
83
+ ];
84
+
85
+ const found = [];
86
+ for (const c of candidates) {
87
+ if (await fileExists(path.join(projectRoot, c.path))) {
88
+ found.push(c);
89
+ }
90
+ }
91
+
92
+ // CLAUDE.md.workflow-suggestions is an upgrade artifact
93
+ const suggestionsPath = 'CLAUDE.md.workflow-suggestions';
94
+ if (await fileExists(path.join(projectRoot, suggestionsPath))) {
95
+ found.push({ path: suggestionsPath, label: suggestionsPath });
96
+ }
97
+
98
+ return found;
99
+ }
100
+
101
+ /**
102
+ * Delete specified files from .claude/ and clean up empty directories.
103
+ * Uses fs.remove (via removeDirectory) which handles both files and directories.
104
+ */
105
+ export async function removeTrackedFiles(projectRoot, fileKeys) {
106
+ const claudeDir = path.join(projectRoot, '.claude');
107
+ let removedCount = 0;
108
+
109
+ for (const key of fileKeys) {
110
+ const filePath = path.join(claudeDir, ...key.split('/'));
111
+ if (await fileExists(filePath)) {
112
+ await removeDirectory(filePath);
113
+ removedCount++;
114
+ }
115
+ }
116
+
117
+ // Clean up empty subdirectories
118
+ for (const subdir of ['agents', 'commands', 'skills']) {
119
+ const dirPath = path.join(claudeDir, subdir);
120
+ if (await dirExists(dirPath)) {
121
+ const remaining = await listFiles(dirPath);
122
+ if (remaining.length === 0) {
123
+ await removeDirectory(dirPath);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Remove .claude/ itself only if completely empty
129
+ if (await dirExists(claudeDir)) {
130
+ const remaining = await listFilesRecursive(claudeDir);
131
+ if (remaining.length === 0) {
132
+ await removeDirectory(claudeDir);
133
+ }
134
+ }
135
+
136
+ return removedCount;
137
+ }
138
+
139
+ /**
140
+ * Delete specified root-level files and clean up empty docs directories.
141
+ */
142
+ export async function removeRootFiles(projectRoot, filePaths) {
143
+ let removedCount = 0;
144
+
145
+ for (const relPath of filePaths) {
146
+ const fullPath = path.join(projectRoot, relPath);
147
+ if (await fileExists(fullPath)) {
148
+ await removeDirectory(fullPath);
149
+ removedCount++;
150
+ }
151
+ }
152
+
153
+ // Clean up empty docs/spec/ then docs/
154
+ const specDir = path.join(projectRoot, 'docs', 'spec');
155
+ if (await dirExists(specDir)) {
156
+ const remaining = await listFiles(specDir);
157
+ if (remaining.length === 0) {
158
+ await removeDirectory(specDir);
159
+
160
+ const docsDir = path.join(projectRoot, 'docs');
161
+ if (await dirExists(docsDir)) {
162
+ const docsRemaining = await listFilesRecursive(docsDir);
163
+ if (docsRemaining.length === 0) {
164
+ await removeDirectory(docsDir);
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ return removedCount;
171
+ }
172
+
173
+ /**
174
+ * Remove worclaude entries from .gitignore.
175
+ * Matches lines individually. Keeps .claude-backup-* / so backups stay git-ignored.
176
+ */
177
+ export async function cleanGitignore(projectRoot) {
178
+ const gitignorePath = path.join(projectRoot, '.gitignore');
179
+ if (!(await fileExists(gitignorePath))) return false;
180
+
181
+ const content = await readFile(gitignorePath);
182
+ const lines = content.split('\n');
183
+
184
+ const REMOVE_LINES = new Set(['# Worclaude (generated workflow files)', '.claude/']);
185
+
186
+ const filtered = lines.filter((line) => !REMOVE_LINES.has(line.trim()));
187
+
188
+ // Collapse consecutive blank lines (max 2 in a row)
189
+ const cleaned = [];
190
+ let blankCount = 0;
191
+ for (const line of filtered) {
192
+ if (line.trim() === '') {
193
+ blankCount++;
194
+ if (blankCount <= 2) cleaned.push(line);
195
+ } else {
196
+ blankCount = 0;
197
+ cleaned.push(line);
198
+ }
199
+ }
200
+
201
+ // Trim trailing blank lines
202
+ while (cleaned.length > 0 && cleaned[cleaned.length - 1].trim() === '') {
203
+ cleaned.pop();
204
+ }
205
+ // Ensure file ends with newline if non-empty
206
+ const newContent = cleaned.length > 0 ? cleaned.join('\n') + '\n' : '';
207
+
208
+ if (newContent !== content) {
209
+ await writeFile(gitignorePath, newContent);
210
+ return true;
211
+ }
212
+ return false;
213
+ }
@@ -217,24 +217,6 @@ export const TECH_STACKS = [
217
217
  { name: 'Other / None', value: 'other' },
218
218
  ];
219
219
 
220
- export const FORMATTER_COMMANDS = {
221
- python: 'ruff format . || true',
222
- node: 'npx prettier --write . || true',
223
- java: "google-java-format -i $(find . -name '*.java' 2>/dev/null) || true",
224
- csharp: 'dotnet format || true',
225
- cpp: "find . -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' | xargs clang-format -i || true",
226
- go: 'gofmt -w . || true',
227
- php: 'php-cs-fixer fix . || true',
228
- ruby: 'rubocop -A || true',
229
- kotlin: 'ktlint -F || true',
230
- swift: 'swift-format format -r . -i || true',
231
- rust: 'cargo fmt || true',
232
- dart: 'dart format . || true',
233
- scala: 'scalafmt || true',
234
- elixir: 'mix format || true',
235
- zig: 'zig fmt . || true',
236
- };
237
-
238
220
  export const PROJECT_TYPE_DESCRIPTIONS = {
239
221
  'Full-stack web application': 'Frontend + backend in one repo',
240
222
  'Backend / API': 'Server, REST/GraphQL, no frontend',
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ import { statusCommand } from './commands/status.js';
8
8
  import { backupCommand } from './commands/backup.js';
9
9
  import { restoreCommand } from './commands/restore.js';
10
10
  import { diffCommand } from './commands/diff.js';
11
+ import { deleteCommand } from './commands/delete.js';
11
12
 
12
13
  const program = new Command();
13
14
 
@@ -46,4 +47,9 @@ program
46
47
  .description('Compare current setup against installed workflow version')
47
48
  .action(diffCommand);
48
49
 
50
+ program
51
+ .command('delete')
52
+ .description('Remove worclaude workflow from project')
53
+ .action(deleteCommand);
54
+
49
55
  program.parse();
@@ -56,6 +56,8 @@ The workflow installs a PostCompact hook that runs:
56
56
  cat CLAUDE.md && cat docs/spec/PROGRESS.md 2>/dev/null || true
57
57
  ```
58
58
 
59
+ > On Windows, this command runs in Git Bash (installed with [Git for Windows](https://gitforwindows.org)).
60
+
59
61
  This ensures you never lose your bearings after compaction. The hook fires
60
62
  automatically — you don't need to re-read these files manually.
61
63