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