@mthanhlm/autodev 0.4.3 → 0.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.
Files changed (73) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/PUBLISH.md +9 -40
  3. package/README.md +70 -91
  4. package/autodev/bin/autodev-tools.cjs +587 -811
  5. package/autodev/templates/brief.md +19 -0
  6. package/autodev/templates/context.md +16 -0
  7. package/autodev/templates/plan.md +26 -46
  8. package/autodev/templates/run.md +20 -0
  9. package/bin/install.js +229 -342
  10. package/commands/autodev/index.md +117 -9
  11. package/commands/autodev/status.md +22 -0
  12. package/hooks/autodev-auto-format.js +3 -3
  13. package/hooks/autodev-git-guard.js +5 -7
  14. package/hooks/autodev-paths.js +3 -3
  15. package/package.json +4 -5
  16. package/scripts/run-tests.cjs +10 -0
  17. package/agents/autodev-codebase-domain.md +0 -25
  18. package/agents/autodev-codebase-quality.md +0 -25
  19. package/agents/autodev-codebase-runtime.md +0 -25
  20. package/agents/autodev-codebase-structure.md +0 -25
  21. package/agents/autodev-review-integration.md +0 -30
  22. package/agents/autodev-review-polish.md +0 -30
  23. package/agents/autodev-review-quality.md +0 -30
  24. package/agents/autodev-review-security.md +0 -30
  25. package/agents/autodev-task-worker.md +0 -39
  26. package/autodev/templates/codebase/domain.md +0 -13
  27. package/autodev/templates/codebase/quality.md +0 -13
  28. package/autodev/templates/codebase/runtime.md +0 -13
  29. package/autodev/templates/codebase/structure.md +0 -13
  30. package/autodev/templates/codebase/summary.md +0 -13
  31. package/autodev/templates/config.json +0 -22
  32. package/autodev/templates/project-state.md +0 -13
  33. package/autodev/templates/project.md +0 -24
  34. package/autodev/templates/requirements.md +0 -14
  35. package/autodev/templates/review.md +0 -27
  36. package/autodev/templates/roadmap.md +0 -17
  37. package/autodev/templates/state.md +0 -13
  38. package/autodev/templates/summary.md +0 -22
  39. package/autodev/templates/task-summary.md +0 -18
  40. package/autodev/templates/task.md +0 -23
  41. package/autodev/templates/track-state.md +0 -14
  42. package/autodev/templates/track.md +0 -24
  43. package/autodev/templates/uat.md +0 -18
  44. package/autodev/workflows/autodev.md +0 -79
  45. package/autodev/workflows/cleanup.md +0 -51
  46. package/autodev/workflows/execute-phase.md +0 -127
  47. package/autodev/workflows/explore-codebase.md +0 -66
  48. package/autodev/workflows/help.md +0 -110
  49. package/autodev/workflows/new-project.md +0 -101
  50. package/autodev/workflows/plan-phase.md +0 -126
  51. package/autodev/workflows/progress.md +0 -18
  52. package/autodev/workflows/review-phase.md +0 -73
  53. package/autodev/workflows/review-plan.md +0 -55
  54. package/autodev/workflows/review-task.md +0 -70
  55. package/autodev/workflows/verify-work.md +0 -57
  56. package/commands/autodev/cleanup.md +0 -23
  57. package/commands/autodev/execute-phase.md +0 -29
  58. package/commands/autodev/explore-codebase.md +0 -33
  59. package/commands/autodev/help.md +0 -18
  60. package/commands/autodev/new-project.md +0 -30
  61. package/commands/autodev/plan-phase.md +0 -26
  62. package/commands/autodev/progress.md +0 -18
  63. package/commands/autodev/review-phase.md +0 -29
  64. package/commands/autodev/review-task.md +0 -25
  65. package/commands/autodev/verify-work.md +0 -24
  66. package/hooks/autodev-context-monitor.js +0 -59
  67. package/hooks/autodev-phase-boundary.sh +0 -49
  68. package/hooks/autodev-prompt-guard.js +0 -55
  69. package/hooks/autodev-read-guard.js +0 -42
  70. package/hooks/autodev-session-state.sh +0 -51
  71. package/hooks/autodev-statusline.js +0 -78
  72. package/hooks/autodev-workflow-guard.js +0 -43
  73. package/hooks/hooks.json +0 -89
package/bin/install.js CHANGED
@@ -1,29 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
- const path = require('path');
5
4
  const os = require('os');
6
- const readline = require('readline');
5
+ const path = require('path');
7
6
 
8
7
  const green = '\x1b[32m';
9
8
  const yellow = '\x1b[33m';
10
9
  const cyan = '\x1b[36m';
11
10
  const reset = '\x1b[0m';
12
11
 
13
- const MANAGED_PREFIX = 'autodev-';
14
12
  const ROOT_COMMAND = 'autodev';
15
- const HOOK_FILES = [
16
- 'hooks.json',
13
+ const SUPPORTED_SCOPE = 'global';
14
+ const LEGACY_SKILL_NAMES = [
15
+ 'autodev',
16
+ 'autodev-auto',
17
+ 'autodev-cleanup',
18
+ 'autodev-execute-phase',
19
+ 'autodev-explore-codebase',
20
+ 'autodev-help',
21
+ 'autodev-new-project',
22
+ 'autodev-plan-phase',
23
+ 'autodev-progress',
24
+ 'autodev-review-phase',
25
+ 'autodev-review-task',
26
+ 'autodev-status',
27
+ 'autodev-verify-work'
28
+ ];
29
+ const LEGACY_AGENT_FILES = [
30
+ 'autodev-codebase-domain.md',
31
+ 'autodev-codebase-quality.md',
32
+ 'autodev-codebase-runtime.md',
33
+ 'autodev-codebase-structure.md',
34
+ 'autodev-review-integration.md',
35
+ 'autodev-review-polish.md',
36
+ 'autodev-review-quality.md',
37
+ 'autodev-review-security.md',
38
+ 'autodev-task-worker.md'
39
+ ];
40
+ const MANAGED_HOOK_FILES = [
17
41
  'autodev-auto-format.js',
42
+ 'autodev-git-guard.js',
18
43
  'autodev-paths.js',
19
44
  'autodev-context-monitor.js',
20
- 'autodev-git-guard.js',
21
45
  'autodev-phase-boundary.sh',
22
46
  'autodev-prompt-guard.js',
23
47
  'autodev-read-guard.js',
24
48
  'autodev-session-state.sh',
25
49
  'autodev-statusline.js',
26
- 'autodev-workflow-guard.js'
50
+ 'autodev-workflow-guard.js',
51
+ 'hooks.json'
27
52
  ];
28
53
 
29
54
  function stripJsonComments(input) {
@@ -148,7 +173,9 @@ function convertCommandToClaudeSkill(content, skillName) {
148
173
  const description = extractFrontmatterField(frontmatter, 'description') || '';
149
174
  const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
150
175
  const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
151
- const toolsBlock = toolsMatch ? `allowed-tools:\n${toolsMatch[1].endsWith('\n') ? toolsMatch[1] : `${toolsMatch[1]}\n`}` : '';
176
+ const toolsBlock = toolsMatch
177
+ ? `allowed-tools:\n${toolsMatch[1].endsWith('\n') ? toolsMatch[1] : `${toolsMatch[1]}\n`}`
178
+ : '';
152
179
 
153
180
  let rebuilt = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`;
154
181
  if (argumentHint) {
@@ -173,6 +200,11 @@ function transformInstalledContent(content, pathPrefix) {
173
200
  .replace(/\.\/\.claude\b/g, bare);
174
201
  }
175
202
 
203
+ function commandNameFromSourceFile(fileName) {
204
+ const baseName = fileName.replace(/\.md$/, '');
205
+ return baseName === 'index' ? ROOT_COMMAND : `${ROOT_COMMAND}-${baseName}`;
206
+ }
207
+
176
208
  function copyTextTree(srcDir, destDir, transform) {
177
209
  if (!fs.existsSync(srcDir)) {
178
210
  return;
@@ -182,6 +214,7 @@ function copyTextTree(srcDir, destDir, transform) {
182
214
  for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
183
215
  const srcPath = path.join(srcDir, entry.name);
184
216
  const destPath = path.join(destDir, entry.name);
217
+
185
218
  if (entry.isDirectory()) {
186
219
  copyTextTree(srcPath, destPath, transform);
187
220
  continue;
@@ -195,111 +228,57 @@ function copyTextTree(srcDir, destDir, transform) {
195
228
  }
196
229
  }
197
230
 
198
- function commandNameFromSourceFile(fileName) {
199
- const baseName = fileName.replace(/\.md$/, '');
200
- return baseName === 'index' ? ROOT_COMMAND : `${ROOT_COMMAND}-${baseName}`;
201
- }
202
-
203
- function skillNameFromSourceFile(fileName, prefix) {
204
- const baseName = fileName.replace(/\.md$/, '');
205
- return baseName === 'index' ? prefix : `${prefix}-${baseName}`;
206
- }
207
-
208
- function copyCommandsAsLocal(srcDir, destDir, transform) {
209
- fs.mkdirSync(destDir, { recursive: true });
210
-
211
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
212
- if (!entry.isFile() || !entry.name.endsWith('.md')) {
213
- continue;
214
- }
215
-
216
- const srcPath = path.join(srcDir, entry.name);
217
- const destPath = path.join(destDir, `${commandNameFromSourceFile(entry.name)}.md`);
218
- let content = fs.readFileSync(srcPath, 'utf8');
219
- content = transform(content);
220
- fs.writeFileSync(destPath, content, 'utf8');
221
- }
222
- }
223
-
224
231
  function copyCommandsAsGlobalSkills(srcDir, skillsDir, prefix, transform) {
225
232
  fs.mkdirSync(skillsDir, { recursive: true });
226
233
 
227
- for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
228
- if (entry.isDirectory() && (entry.name === prefix || entry.name.startsWith(`${prefix}-`))) {
229
- fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
230
- }
231
- }
232
-
233
234
  for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
234
235
  if (!entry.isFile() || !entry.name.endsWith('.md')) {
235
236
  continue;
236
237
  }
237
238
 
238
- const skillName = skillNameFromSourceFile(entry.name, prefix);
239
+ const skillName = commandNameFromSourceFile(entry.name);
239
240
  const skillDir = path.join(skillsDir, skillName);
240
241
  fs.mkdirSync(skillDir, { recursive: true });
241
242
 
242
243
  let content = fs.readFileSync(path.join(srcDir, entry.name), 'utf8');
243
244
  content = transform(content);
244
- content = convertCommandToClaudeSkill(content, skillName);
245
+ content = convertCommandToClaudeSkill(content, skillName === prefix ? prefix : skillName);
245
246
  fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf8');
246
247
  }
247
248
  }
248
249
 
249
- function copyAgents(srcDir, destDir, transform) {
250
- if (!fs.existsSync(srcDir)) {
251
- return;
250
+ function currentManagedSkillNames(srcRoot) {
251
+ const commandsDir = path.join(srcRoot, 'commands', 'autodev');
252
+ if (!fs.existsSync(commandsDir)) {
253
+ return [];
252
254
  }
253
255
 
254
- fs.mkdirSync(destDir, { recursive: true });
255
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
256
- if (!entry.isFile()) {
257
- continue;
258
- }
259
-
260
- const srcPath = path.join(srcDir, entry.name);
261
- const destPath = path.join(destDir, entry.name);
262
- let content = fs.readFileSync(srcPath, 'utf8');
263
- content = transform(content);
264
- fs.writeFileSync(destPath, content, 'utf8');
265
- }
256
+ return fs.readdirSync(commandsDir, { withFileTypes: true })
257
+ .filter(entry => entry.isFile() && entry.name.endsWith('.md'))
258
+ .map(entry => commandNameFromSourceFile(entry.name));
266
259
  }
267
260
 
268
- function removeManagedSettings(settings) {
269
- if (settings.statusLine?.command && settings.statusLine.command.includes('autodev-statusline')) {
270
- delete settings.statusLine;
271
- }
272
-
273
- if (settings.env && typeof settings.env === 'object') {
274
- delete settings.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS;
275
- if (Object.keys(settings.env).length === 0) {
276
- delete settings.env;
277
- }
278
- }
279
-
280
- if (!settings.hooks || typeof settings.hooks !== 'object') {
281
- return settings;
282
- }
283
-
284
- for (const eventName of Object.keys(settings.hooks)) {
285
- const entries = Array.isArray(settings.hooks[eventName]) ? settings.hooks[eventName] : [];
286
- const filtered = entries.filter(entry => {
287
- const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
288
- return !hooks.some(hook => typeof hook.command === 'string' && hook.command.includes('autodev-'));
289
- });
261
+ function managedSkillNames(srcRoot) {
262
+ return [...new Set([...currentManagedSkillNames(srcRoot), ...LEGACY_SKILL_NAMES])];
263
+ }
290
264
 
291
- if (filtered.length > 0) {
292
- settings.hooks[eventName] = filtered;
293
- } else {
294
- delete settings.hooks[eventName];
295
- }
296
- }
265
+ function managedCommandStrings(targetDir) {
266
+ return [
267
+ `node "${path.join(targetDir, 'hooks', 'autodev-git-guard.js').replace(/\\/g, '/')}"`,
268
+ `node "${path.join(targetDir, 'hooks', 'autodev-auto-format.js').replace(/\\/g, '/')}"`,
269
+ ...MANAGED_HOOK_FILES
270
+ .filter(fileName => fileName.endsWith('.js') || fileName.endsWith('.sh'))
271
+ .map(fileName => path.join(targetDir, 'hooks', fileName).replace(/\\/g, '/'))
272
+ ];
273
+ }
297
274
 
298
- if (Object.keys(settings.hooks).length === 0) {
299
- delete settings.hooks;
275
+ function isManagedCommand(command, targetDir) {
276
+ if (typeof command !== 'string') {
277
+ return false;
300
278
  }
301
279
 
302
- return settings;
280
+ return managedCommandStrings(targetDir).some(pattern => command.includes(pattern))
281
+ || /autodev-(?:auto-format|context-monitor|git-guard|phase-boundary|prompt-guard|read-guard|session-state|statusline|workflow-guard)\.(?:js|sh)/.test(command);
303
282
  }
304
283
 
305
284
  function ensureHook(settings, eventName, matcher, command, timeout) {
@@ -330,355 +309,263 @@ function ensureHook(settings, eventName, matcher, command, timeout) {
330
309
  settings.hooks[eventName].push(entry);
331
310
  }
332
311
 
333
- function ensureEnvSetting(settings, name, value) {
334
- if (!settings.env || typeof settings.env !== 'object') {
335
- settings.env = {};
312
+ function removeManagedSettings(settings, targetDir) {
313
+ if (!settings || typeof settings !== 'object') {
314
+ return {};
336
315
  }
337
- settings.env[name] = String(value);
338
- }
339
316
 
340
- function setExecutableBits(targetDir) {
341
- const candidates = [
342
- path.join(targetDir, 'hooks', 'autodev-phase-boundary.sh'),
343
- path.join(targetDir, 'hooks', 'autodev-session-state.sh'),
344
- path.join(targetDir, 'autodev', 'bin', 'autodev-tools.cjs')
345
- ];
317
+ if (settings.statusLine?.command && isManagedCommand(settings.statusLine.command, targetDir)) {
318
+ delete settings.statusLine;
319
+ }
346
320
 
347
- for (const file of candidates) {
348
- if (fs.existsSync(file)) {
349
- fs.chmodSync(file, 0o755);
321
+ if (settings.env && typeof settings.env === 'object') {
322
+ delete settings.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS;
323
+ if (Object.keys(settings.env).length === 0) {
324
+ delete settings.env;
350
325
  }
351
326
  }
352
- }
353
327
 
354
- function buildGlobalCommand(targetDir, fileName, shell = 'node') {
355
- const filePath = path.join(targetDir, 'hooks', fileName).replace(/\\/g, '/');
356
- return `${shell} "${filePath}"`;
357
- }
328
+ if (!settings.hooks || typeof settings.hooks !== 'object') {
329
+ return settings;
330
+ }
358
331
 
359
- function buildLocalCommand(fileName, shell = 'node') {
360
- return `${shell} "$CLAUDE_PROJECT_DIR"/.claude/hooks/${fileName}`;
332
+ for (const eventName of Object.keys(settings.hooks)) {
333
+ const entries = Array.isArray(settings.hooks[eventName]) ? settings.hooks[eventName] : [];
334
+ const filtered = entries.filter(entry => {
335
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
336
+ return !hooks.some(hook => isManagedCommand(hook?.command, targetDir));
337
+ });
338
+
339
+ if (filtered.length > 0) {
340
+ settings.hooks[eventName] = filtered;
341
+ } else {
342
+ delete settings.hooks[eventName];
343
+ }
344
+ }
345
+
346
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
347
+ delete settings.hooks;
348
+ }
349
+
350
+ return settings;
361
351
  }
362
352
 
363
- function configureSettings(targetDir, isGlobal, options = {}) {
353
+ function configureSettings(targetDir) {
364
354
  const settingsPath = path.join(targetDir, 'settings.json');
365
355
  const settings = readSettings(settingsPath);
366
356
  if (settings === null) {
367
357
  return null;
368
358
  }
369
359
 
370
- removeManagedSettings(settings);
371
-
372
- const preToolEvent = 'PreToolUse';
373
- const postToolEvent = 'PostToolUse';
374
- const sessionStartEvent = 'SessionStart';
375
-
376
- const contextMonitorCommand = isGlobal
377
- ? buildGlobalCommand(targetDir, 'autodev-context-monitor.js')
378
- : buildLocalCommand('autodev-context-monitor.js');
379
- const autoFormatCommand = isGlobal
380
- ? buildGlobalCommand(targetDir, 'autodev-auto-format.js')
381
- : buildLocalCommand('autodev-auto-format.js');
382
- const promptGuardCommand = isGlobal
383
- ? buildGlobalCommand(targetDir, 'autodev-prompt-guard.js')
384
- : buildLocalCommand('autodev-prompt-guard.js');
385
- const readGuardCommand = isGlobal
386
- ? buildGlobalCommand(targetDir, 'autodev-read-guard.js')
387
- : buildLocalCommand('autodev-read-guard.js');
388
- const workflowGuardCommand = isGlobal
389
- ? buildGlobalCommand(targetDir, 'autodev-workflow-guard.js')
390
- : buildLocalCommand('autodev-workflow-guard.js');
391
- const gitGuardCommand = isGlobal
392
- ? buildGlobalCommand(targetDir, 'autodev-git-guard.js')
393
- : buildLocalCommand('autodev-git-guard.js');
394
- const sessionStateCommand = isGlobal
395
- ? buildGlobalCommand(targetDir, 'autodev-session-state.sh', 'bash')
396
- : buildLocalCommand('autodev-session-state.sh', 'bash');
397
- const phaseBoundaryCommand = isGlobal
398
- ? buildGlobalCommand(targetDir, 'autodev-phase-boundary.sh', 'bash')
399
- : buildLocalCommand('autodev-phase-boundary.sh', 'bash');
400
- const statusLineCommand = isGlobal
401
- ? buildGlobalCommand(targetDir, 'autodev-statusline.js')
402
- : buildLocalCommand('autodev-statusline.js');
403
-
404
- ensureHook(settings, sessionStartEvent, null, sessionStateCommand);
405
- ensureHook(settings, preToolEvent, 'Write|Edit', promptGuardCommand, 5);
406
- ensureHook(settings, preToolEvent, 'Write|Edit', readGuardCommand, 5);
407
- ensureHook(settings, preToolEvent, 'Write|Edit', workflowGuardCommand, 5);
408
- ensureHook(settings, preToolEvent, 'Bash', gitGuardCommand, 5);
409
- ensureHook(settings, postToolEvent, 'Edit|Write|MultiEdit', autoFormatCommand, 10);
410
- ensureHook(settings, postToolEvent, 'Bash|Edit|Write|MultiEdit|Agent|Task', contextMonitorCommand, 10);
411
- ensureHook(settings, postToolEvent, 'Write|Edit', phaseBoundaryCommand, 5);
412
-
413
- if (options.installStatusLine !== false) {
414
- settings.statusLine = {
415
- type: 'command',
416
- command: statusLineCommand
417
- };
418
- }
419
-
420
- if (options.disableBackgroundTasks) {
421
- ensureEnvSetting(settings, 'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS', '1');
360
+ removeManagedSettings(settings, targetDir);
361
+
362
+ ensureHook(
363
+ settings,
364
+ 'PreToolUse',
365
+ 'Bash',
366
+ `node "${path.join(targetDir, 'hooks', 'autodev-git-guard.js').replace(/\\/g, '/')}"`,
367
+ 5
368
+ );
369
+ ensureHook(
370
+ settings,
371
+ 'PostToolUse',
372
+ 'Edit|Write|MultiEdit',
373
+ `node "${path.join(targetDir, 'hooks', 'autodev-auto-format.js').replace(/\\/g, '/')}"`,
374
+ 10
375
+ );
376
+
377
+ if (!settings.env || typeof settings.env !== 'object') {
378
+ settings.env = {};
422
379
  }
380
+ settings.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = '1';
423
381
 
424
382
  writeSettings(settingsPath, settings);
425
383
  return settingsPath;
426
384
  }
427
385
 
428
- function removeInstalledFiles(targetDir, isGlobal) {
386
+ function removeManagedFiles(targetDir) {
429
387
  const supportDir = path.join(targetDir, 'autodev');
430
388
  if (fs.existsSync(supportDir)) {
431
389
  fs.rmSync(supportDir, { recursive: true, force: true });
432
390
  }
433
391
 
434
- const agentsDir = path.join(targetDir, 'agents');
435
- if (fs.existsSync(agentsDir)) {
436
- for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
437
- if (entry.isFile() && entry.name.startsWith(MANAGED_PREFIX) && entry.name.endsWith('.md')) {
438
- fs.rmSync(path.join(agentsDir, entry.name), { force: true });
439
- }
392
+ const hooksDir = path.join(targetDir, 'hooks');
393
+ for (const fileName of MANAGED_HOOK_FILES) {
394
+ const hookPath = path.join(hooksDir, fileName);
395
+ if (fs.existsSync(hookPath)) {
396
+ fs.rmSync(hookPath, { recursive: true, force: true });
440
397
  }
441
398
  }
442
399
 
443
- const hooksDir = path.join(targetDir, 'hooks');
444
- for (const hookName of HOOK_FILES) {
445
- const hookPath = path.join(hooksDir, hookName);
446
- if (fs.existsSync(hookPath)) {
447
- fs.rmSync(hookPath, { force: true });
400
+ const agentsDir = path.join(targetDir, 'agents');
401
+ for (const fileName of LEGACY_AGENT_FILES) {
402
+ const agentPath = path.join(agentsDir, fileName);
403
+ if (fs.existsSync(agentPath)) {
404
+ fs.rmSync(agentPath, { force: true });
448
405
  }
449
406
  }
450
407
 
451
- if (isGlobal) {
452
- const skillsDir = path.join(targetDir, 'skills');
453
- if (fs.existsSync(skillsDir)) {
454
- for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
455
- if (entry.isDirectory() && (entry.name === ROOT_COMMAND || entry.name.startsWith(MANAGED_PREFIX))) {
456
- fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
457
- }
458
- }
408
+ const skillsDir = path.join(targetDir, 'skills');
409
+ for (const skillName of managedSkillNames(path.resolve(__dirname, '..'))) {
410
+ const skillPath = path.join(skillsDir, skillName);
411
+ if (fs.existsSync(skillPath)) {
412
+ fs.rmSync(skillPath, { recursive: true, force: true });
459
413
  }
460
- } else {
461
- const commandsDir = path.join(targetDir, 'commands');
462
- if (fs.existsSync(commandsDir)) {
463
- for (const entry of fs.readdirSync(commandsDir, { withFileTypes: true })) {
464
- if (entry.isFile() && entry.name.startsWith(ROOT_COMMAND) && entry.name.endsWith('.md')) {
465
- fs.rmSync(path.join(commandsDir, entry.name), { force: true });
466
- }
414
+ }
415
+
416
+ const legacyCommandsDir = path.join(targetDir, 'commands');
417
+ if (fs.existsSync(legacyCommandsDir)) {
418
+ for (const skillName of managedSkillNames(path.resolve(__dirname, '..'))) {
419
+ const commandPath = path.join(legacyCommandsDir, `${skillName}.md`);
420
+ if (fs.existsSync(commandPath)) {
421
+ fs.rmSync(commandPath, { force: true });
467
422
  }
468
423
  }
469
- const legacyCommandsDir = path.join(commandsDir, ROOT_COMMAND);
470
- if (fs.existsSync(legacyCommandsDir)) {
471
- fs.rmSync(legacyCommandsDir, { recursive: true, force: true });
472
- }
473
- const skillsDir = path.join(targetDir, 'skills');
474
- if (fs.existsSync(skillsDir)) {
475
- for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
476
- if (entry.isDirectory() && (entry.name === ROOT_COMMAND || entry.name.startsWith(MANAGED_PREFIX))) {
477
- fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
478
- }
479
- }
424
+ const nestedLegacy = path.join(legacyCommandsDir, ROOT_COMMAND);
425
+ if (fs.existsSync(nestedLegacy)) {
426
+ fs.rmSync(nestedLegacy, { recursive: true, force: true });
480
427
  }
481
428
  }
482
429
  }
483
430
 
484
- function resolveTargetDir({ scope, cwd, targetDir }) {
485
- if (targetDir) {
486
- return path.resolve(targetDir);
431
+ function resolveTargetDir(options = {}) {
432
+ if (options.targetDir) {
433
+ return path.resolve(options.targetDir);
487
434
  }
488
435
 
489
- if (scope === 'global') {
490
- return path.resolve(process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'));
491
- }
436
+ return path.resolve(process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'));
437
+ }
492
438
 
493
- return path.join(path.resolve(cwd), '.claude');
439
+ function setExecutableBits(targetDir) {
440
+ const candidates = [
441
+ path.join(targetDir, 'autodev', 'bin', 'autodev-tools.cjs'),
442
+ path.join(targetDir, 'hooks', 'autodev-auto-format.js'),
443
+ path.join(targetDir, 'hooks', 'autodev-git-guard.js'),
444
+ path.join(targetDir, 'hooks', 'autodev-paths.js')
445
+ ];
446
+
447
+ for (const filePath of candidates) {
448
+ if (fs.existsSync(filePath)) {
449
+ fs.chmodSync(filePath, 0o755);
450
+ }
451
+ }
494
452
  }
495
453
 
496
454
  function install(options = {}) {
497
- const scope = options.scope === 'local' ? 'local' : 'global';
498
- const cwd = options.cwd || process.cwd();
499
- const targetDir = resolveTargetDir({ scope, cwd, targetDir: options.targetDir });
500
- const isGlobal = scope === 'global';
501
- const silent = Boolean(options.silent);
455
+ if ((options.scope || SUPPORTED_SCOPE) !== SUPPORTED_SCOPE) {
456
+ throw new Error('local install is no longer supported; install autodev globally');
457
+ }
458
+
502
459
  const srcRoot = path.resolve(__dirname, '..');
503
- const pathPrefix = isGlobal
504
- ? `${targetDir.replace(/\\/g, '/')}/`
505
- : './.claude/';
460
+ const targetDir = resolveTargetDir(options);
461
+ const pathPrefix = `${targetDir.replace(/\\/g, '/')}/`;
506
462
  const transform = content => transformInstalledContent(content, pathPrefix);
507
463
 
508
464
  fs.mkdirSync(targetDir, { recursive: true });
509
- removeInstalledFiles(targetDir, isGlobal);
510
-
511
- if (isGlobal) {
512
- copyCommandsAsGlobalSkills(
513
- path.join(srcRoot, 'commands', 'autodev'),
514
- path.join(targetDir, 'skills'),
515
- 'autodev',
516
- transform
517
- );
518
- } else {
519
- copyCommandsAsLocal(
520
- path.join(srcRoot, 'commands', 'autodev'),
521
- path.join(targetDir, 'commands'),
522
- transform
523
- );
524
- }
465
+ removeManagedFiles(targetDir);
525
466
 
467
+ copyCommandsAsGlobalSkills(
468
+ path.join(srcRoot, 'commands', 'autodev'),
469
+ path.join(targetDir, 'skills'),
470
+ ROOT_COMMAND,
471
+ transform
472
+ );
526
473
  copyTextTree(path.join(srcRoot, 'autodev'), path.join(targetDir, 'autodev'), transform);
527
- copyAgents(path.join(srcRoot, 'agents'), path.join(targetDir, 'agents'), transform);
528
- copyTextTree(path.join(srcRoot, 'hooks'), path.join(targetDir, 'hooks'), transform);
474
+
475
+ const hookDestRoot = path.join(targetDir, 'hooks');
476
+ fs.mkdirSync(hookDestRoot, { recursive: true });
477
+ for (const fileName of ['autodev-auto-format.js', 'autodev-git-guard.js', 'autodev-paths.js']) {
478
+ let content = fs.readFileSync(path.join(srcRoot, 'hooks', fileName), 'utf8');
479
+ content = transform(content);
480
+ fs.writeFileSync(path.join(hookDestRoot, fileName), content, 'utf8');
481
+ }
482
+
529
483
  setExecutableBits(targetDir);
530
- const settingsPath = configureSettings(targetDir, isGlobal, options);
484
+ const settingsPath = configureSettings(targetDir);
531
485
 
532
- if (!silent) {
533
- const locationLabel = isGlobal ? 'global' : 'local';
534
- console.log(` ${green}Installed${reset} autodev (${locationLabel}) at ${cyan}${targetDir}${reset}`);
486
+ if (!options.silent) {
487
+ console.log(`${cyan}autodev${reset} installed to ${green}${targetDir}${reset}`);
535
488
  if (settingsPath) {
536
489
  console.log(` ${green}Updated${reset} ${cyan}${settingsPath}${reset}`);
537
490
  }
538
- console.log(` Run ${cyan}/autodev${reset} in Claude Code.`);
491
+ console.log(` ${yellow}Background tasks disabled${reset} via CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1`);
539
492
  }
540
493
 
541
494
  return { targetDir, settingsPath };
542
495
  }
543
496
 
544
497
  function uninstall(options = {}) {
545
- const scope = options.scope === 'local' ? 'local' : 'global';
546
- const cwd = options.cwd || process.cwd();
547
- const targetDir = resolveTargetDir({ scope, cwd, targetDir: options.targetDir });
548
- const isGlobal = scope === 'global';
549
- const silent = Boolean(options.silent);
550
-
551
- removeInstalledFiles(targetDir, isGlobal);
498
+ if ((options.scope || SUPPORTED_SCOPE) !== SUPPORTED_SCOPE) {
499
+ throw new Error('local install is no longer supported; uninstall the global install instead');
500
+ }
552
501
 
502
+ const targetDir = resolveTargetDir(options);
553
503
  const settingsPath = path.join(targetDir, 'settings.json');
554
504
  const settings = readSettings(settingsPath);
555
- if (settings) {
556
- removeManagedSettings(settings);
505
+
506
+ removeManagedFiles(targetDir);
507
+
508
+ if (settings !== null) {
509
+ removeManagedSettings(settings, targetDir);
557
510
  writeSettings(settingsPath, settings);
558
511
  }
559
512
 
560
- if (!silent) {
561
- console.log(` ${green}Removed${reset} autodev from ${cyan}${targetDir}${reset}`);
513
+ if (!options.silent) {
514
+ console.log(`${cyan}autodev${reset} removed from ${green}${targetDir}${reset}`);
515
+ if (settings !== null) {
516
+ console.log(` ${green}Updated${reset} ${cyan}${settingsPath}${reset}`);
517
+ }
562
518
  }
563
519
 
564
520
  return { targetDir, settingsPath };
565
521
  }
566
522
 
567
523
  function printHelp() {
568
- console.log(`Usage: npx @mthanhlm/autodev [--global|--local] [--uninstall]
569
-
570
- Options:
571
- --global Install to Claude Code config directory
572
- --local Install to the current project (.claude/)
573
- --disable-background-tasks
574
- Set CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 in Claude Code settings.json
575
- --uninstall Remove autodev from the selected location
576
- --help Show this help
577
-
578
- Examples:
579
- npx @mthanhlm/autodev@latest
580
- npx @mthanhlm/autodev@latest --global
581
- npx @mthanhlm/autodev@latest --local
582
- npx @mthanhlm/autodev@latest --global --disable-background-tasks
583
- npx @mthanhlm/autodev@latest --global --uninstall
584
- `);
585
- }
586
-
587
- function askScope() {
588
- return new Promise(resolve => {
589
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
590
- console.log(`
591
- ${cyan}autodev install${reset}
592
-
593
- Runtime:
594
- Claude Code only
595
-
596
- Location:
597
- ${cyan}1${reset}) Global ${yellow}(Recommended)${reset} -> ~/.claude
598
- ${cyan}2${reset}) Local -> ./.claude
599
- `);
600
- rl.question(`Select install location ${yellow}[1]${reset}: `, answer => {
601
- rl.close();
602
- const trimmed = answer.trim().toLowerCase();
603
- if (trimmed === '2' || trimmed === 'l' || trimmed === 'local') {
604
- resolve('local');
605
- return;
606
- }
607
- resolve('global');
608
- });
609
- });
610
- }
611
-
612
- function askDisableBackgroundTasks() {
613
- return new Promise(resolve => {
614
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
615
- console.log(`
616
- Background tasks:
617
- ${cyan}1${reset}) Disable in Claude Code settings ${yellow}(Recommended for autodev)${reset}
618
- -> sets CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1
619
- ${cyan}2${reset}) Leave background-task settings unchanged
620
- `);
621
- rl.question(`Select background-task policy ${yellow}[1]${reset}: `, answer => {
622
- rl.close();
623
- const trimmed = answer.trim().toLowerCase();
624
- if (trimmed === '2' || trimmed === 'n' || trimmed === 'no' || trimmed === 'leave') {
625
- resolve(false);
626
- return;
627
- }
628
- resolve(true);
629
- });
630
- });
524
+ process.stdout.write(`Usage: npx @mthanhlm/autodev [--global] [--uninstall]\n\n`);
525
+ process.stdout.write(`Defaults:\n`);
526
+ process.stdout.write(` --global Install into ~/.claude (default)\n`);
527
+ process.stdout.write(` --uninstall Remove autodev from ~/.claude\n`);
528
+ process.stdout.write(`\nNotes:\n`);
529
+ process.stdout.write(` - Global install is the supported mode.\n`);
530
+ process.stdout.write(` - Install always sets CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1.\n`);
631
531
  }
632
532
 
633
- async function main() {
533
+ function main() {
634
534
  const args = process.argv.slice(2);
535
+
635
536
  if (args.includes('--help') || args.includes('-h')) {
636
537
  printHelp();
637
538
  return;
638
539
  }
639
540
 
640
- let scope = null;
641
- let scopeWasExplicit = false;
642
- if (args.includes('--global') || args.includes('-g')) {
643
- scope = 'global';
644
- scopeWasExplicit = true;
645
- } else if (args.includes('--local') || args.includes('-l')) {
646
- scope = 'local';
647
- scopeWasExplicit = true;
648
- }
649
-
650
- if (!scope) {
651
- scope = await askScope();
541
+ if (args.includes('--local')) {
542
+ process.stderr.write('autodev: local install is no longer supported\n');
543
+ process.exit(1);
652
544
  }
653
545
 
654
546
  const uninstallRequested = args.includes('--uninstall') || args.includes('-u');
655
- const disableBackgroundTasks = args.includes('--disable-background-tasks')
656
- ? true
657
- : uninstallRequested || scopeWasExplicit
658
- ? false
659
- : await askDisableBackgroundTasks();
660
547
 
661
- if (uninstallRequested) {
662
- uninstall({ scope });
663
- } else {
664
- install({ scope, disableBackgroundTasks });
548
+ try {
549
+ if (uninstallRequested) {
550
+ uninstall({ scope: SUPPORTED_SCOPE });
551
+ } else {
552
+ install({ scope: SUPPORTED_SCOPE });
553
+ }
554
+ } catch (error) {
555
+ process.stderr.write(`autodev: ${error.message}\n`);
556
+ process.exit(1);
665
557
  }
666
558
  }
667
559
 
668
560
  if (require.main === module) {
669
- main().catch(error => {
670
- console.error(error);
671
- process.exit(1);
672
- });
561
+ main();
673
562
  }
674
563
 
675
564
  module.exports = {
676
565
  configureSettings,
677
- convertCommandToClaudeSkill,
678
566
  install,
679
- readSettings,
567
+ managedSkillNames,
568
+ removeManagedFiles,
680
569
  removeManagedSettings,
681
- stripJsonComments,
682
- transformInstalledContent,
683
570
  uninstall
684
571
  };