@mthanhlm/autodev 0.1.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/PUBLISH.md +75 -0
- package/README.md +53 -0
- package/autodev/bin/autodev-tools.cjs +346 -0
- package/autodev/templates/config.json +20 -0
- package/autodev/templates/plan.md +25 -0
- package/autodev/templates/project.md +21 -0
- package/autodev/templates/requirements.md +12 -0
- package/autodev/templates/roadmap.md +17 -0
- package/autodev/templates/state.md +10 -0
- package/autodev/templates/summary.md +16 -0
- package/autodev/templates/uat.md +15 -0
- package/autodev/workflows/execute-phase.md +50 -0
- package/autodev/workflows/help.md +57 -0
- package/autodev/workflows/new-project.md +62 -0
- package/autodev/workflows/plan-phase.md +54 -0
- package/autodev/workflows/progress.md +15 -0
- package/autodev/workflows/verify-work.md +39 -0
- package/bin/install.js +565 -0
- package/commands/autodev/execute-phase.md +26 -0
- package/commands/autodev/help.md +18 -0
- package/commands/autodev/new-project.md +28 -0
- package/commands/autodev/plan-phase.md +25 -0
- package/commands/autodev/progress.md +18 -0
- package/commands/autodev/verify-work.md +24 -0
- package/hooks/autodev-context-monitor.js +66 -0
- package/hooks/autodev-git-guard.js +55 -0
- package/hooks/autodev-phase-boundary.sh +20 -0
- package/hooks/autodev-prompt-guard.js +55 -0
- package/hooks/autodev-read-guard.js +49 -0
- package/hooks/autodev-session-state.sh +22 -0
- package/hooks/autodev-statusline.js +45 -0
- package/hooks/autodev-workflow-guard.js +51 -0
- package/package.json +38 -0
- package/scripts/run-tests.cjs +23 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
const green = '\x1b[32m';
|
|
9
|
+
const yellow = '\x1b[33m';
|
|
10
|
+
const cyan = '\x1b[36m';
|
|
11
|
+
const reset = '\x1b[0m';
|
|
12
|
+
|
|
13
|
+
const MANAGED_PREFIX = 'autodev-';
|
|
14
|
+
const HOOK_FILES = [
|
|
15
|
+
'autodev-context-monitor.js',
|
|
16
|
+
'autodev-git-guard.js',
|
|
17
|
+
'autodev-phase-boundary.sh',
|
|
18
|
+
'autodev-prompt-guard.js',
|
|
19
|
+
'autodev-read-guard.js',
|
|
20
|
+
'autodev-session-state.sh',
|
|
21
|
+
'autodev-statusline.js',
|
|
22
|
+
'autodev-workflow-guard.js'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function stripJsonComments(input) {
|
|
26
|
+
let output = '';
|
|
27
|
+
let inString = false;
|
|
28
|
+
let stringChar = '';
|
|
29
|
+
let escaped = false;
|
|
30
|
+
let inLineComment = false;
|
|
31
|
+
let inBlockComment = false;
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
34
|
+
const char = input[index];
|
|
35
|
+
const next = input[index + 1];
|
|
36
|
+
|
|
37
|
+
if (inLineComment) {
|
|
38
|
+
if (char === '\n') {
|
|
39
|
+
inLineComment = false;
|
|
40
|
+
output += char;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (inBlockComment) {
|
|
46
|
+
if (char === '*' && next === '/') {
|
|
47
|
+
inBlockComment = false;
|
|
48
|
+
index += 1;
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (inString) {
|
|
54
|
+
output += char;
|
|
55
|
+
if (escaped) {
|
|
56
|
+
escaped = false;
|
|
57
|
+
} else if (char === '\\') {
|
|
58
|
+
escaped = true;
|
|
59
|
+
} else if (char === stringChar) {
|
|
60
|
+
inString = false;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if ((char === '"' || char === "'") && !inString) {
|
|
66
|
+
inString = true;
|
|
67
|
+
stringChar = char;
|
|
68
|
+
output += char;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (char === '/' && next === '/') {
|
|
73
|
+
inLineComment = true;
|
|
74
|
+
index += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (char === '/' && next === '*') {
|
|
79
|
+
inBlockComment = true;
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
output += char;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return output;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readSettings(settingsPath) {
|
|
91
|
+
if (!fs.existsSync(settingsPath)) {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(raw);
|
|
99
|
+
} catch {
|
|
100
|
+
return JSON.parse(stripJsonComments(raw));
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn(` ${yellow}Warning:${reset} could not parse ${settingsPath}: ${error.message}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeSettings(settingsPath, settings) {
|
|
109
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
110
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractFrontmatterAndBody(content) {
|
|
114
|
+
if (!content.startsWith('---\n')) {
|
|
115
|
+
return { frontmatter: null, body: content };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const endIndex = content.indexOf('\n---\n', 4);
|
|
119
|
+
if (endIndex === -1) {
|
|
120
|
+
return { frontmatter: null, body: content };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
frontmatter: content.slice(4, endIndex),
|
|
125
|
+
body: content.slice(endIndex + 5).trim()
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractFrontmatterField(frontmatter, name) {
|
|
130
|
+
const match = frontmatter.match(new RegExp(`^${name}:\\s*(.+)$`, 'm'));
|
|
131
|
+
return match ? match[1].trim() : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function yamlQuote(value) {
|
|
135
|
+
return JSON.stringify(String(value));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function convertCommandToClaudeSkill(content, skillName) {
|
|
139
|
+
const { frontmatter, body } = extractFrontmatterAndBody(content);
|
|
140
|
+
if (!frontmatter) {
|
|
141
|
+
return content;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
145
|
+
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
|
|
146
|
+
const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
147
|
+
const toolsBlock = toolsMatch ? `allowed-tools:\n${toolsMatch[1].endsWith('\n') ? toolsMatch[1] : `${toolsMatch[1]}\n`}` : '';
|
|
148
|
+
|
|
149
|
+
let rebuilt = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`;
|
|
150
|
+
if (argumentHint) {
|
|
151
|
+
rebuilt += `argument-hint: ${yamlQuote(argumentHint)}\n`;
|
|
152
|
+
}
|
|
153
|
+
if (toolsBlock) {
|
|
154
|
+
rebuilt += toolsBlock;
|
|
155
|
+
}
|
|
156
|
+
rebuilt += '---';
|
|
157
|
+
return `${rebuilt}\n${body}\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function transformInstalledContent(content, pathPrefix) {
|
|
161
|
+
const normalized = pathPrefix.endsWith('/') ? pathPrefix : `${pathPrefix}/`;
|
|
162
|
+
const bare = normalized.replace(/\/$/, '');
|
|
163
|
+
return content
|
|
164
|
+
.replace(/~\/\.claude\//g, normalized)
|
|
165
|
+
.replace(/\$HOME\/\.claude\//g, normalized)
|
|
166
|
+
.replace(/~\/\.claude\b/g, bare)
|
|
167
|
+
.replace(/\$HOME\/\.claude\b/g, bare)
|
|
168
|
+
.replace(/\.\/\.claude\//g, normalized)
|
|
169
|
+
.replace(/\.\/\.claude\b/g, bare);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function copyTextTree(srcDir, destDir, transform) {
|
|
173
|
+
if (!fs.existsSync(srcDir)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
178
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
179
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
180
|
+
const destPath = path.join(destDir, entry.name);
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
copyTextTree(srcPath, destPath, transform);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
187
|
+
if (transform) {
|
|
188
|
+
content = transform(content);
|
|
189
|
+
}
|
|
190
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function copyCommandsAsLocal(srcDir, destDir, transform) {
|
|
195
|
+
if (fs.existsSync(destDir)) {
|
|
196
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
199
|
+
|
|
200
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
201
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
202
|
+
const destPath = path.join(destDir, entry.name);
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
copyCommandsAsLocal(srcPath, destPath, transform);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
208
|
+
content = transform(content);
|
|
209
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function copyCommandsAsGlobalSkills(srcDir, skillsDir, prefix, transform) {
|
|
214
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
217
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
218
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
223
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const baseName = entry.name.replace(/\.md$/, '');
|
|
228
|
+
const skillName = `${prefix}-${baseName}`;
|
|
229
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
230
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
231
|
+
|
|
232
|
+
let content = fs.readFileSync(path.join(srcDir, entry.name), 'utf8');
|
|
233
|
+
content = transform(content);
|
|
234
|
+
content = convertCommandToClaudeSkill(content, skillName);
|
|
235
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf8');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function removeManagedSettings(settings) {
|
|
240
|
+
if (settings.statusLine?.command && settings.statusLine.command.includes('autodev-statusline')) {
|
|
241
|
+
delete settings.statusLine;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
|
245
|
+
return settings;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const eventName of Object.keys(settings.hooks)) {
|
|
249
|
+
const entries = Array.isArray(settings.hooks[eventName]) ? settings.hooks[eventName] : [];
|
|
250
|
+
const filtered = entries.filter(entry => {
|
|
251
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
252
|
+
return !hooks.some(hook => typeof hook.command === 'string' && hook.command.includes('autodev-'));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (filtered.length > 0) {
|
|
256
|
+
settings.hooks[eventName] = filtered;
|
|
257
|
+
} else {
|
|
258
|
+
delete settings.hooks[eventName];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
263
|
+
delete settings.hooks;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return settings;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function ensureHook(settings, eventName, matcher, command, timeout) {
|
|
270
|
+
if (!settings.hooks) {
|
|
271
|
+
settings.hooks = {};
|
|
272
|
+
}
|
|
273
|
+
if (!settings.hooks[eventName]) {
|
|
274
|
+
settings.hooks[eventName] = [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const exists = settings.hooks[eventName].some(entry =>
|
|
278
|
+
Array.isArray(entry.hooks) && entry.hooks.some(hook => hook.command === command)
|
|
279
|
+
);
|
|
280
|
+
if (exists) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const hookEntry = { type: 'command', command };
|
|
285
|
+
if (timeout) {
|
|
286
|
+
hookEntry.timeout = timeout;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const entry = { hooks: [hookEntry] };
|
|
290
|
+
if (matcher) {
|
|
291
|
+
entry.matcher = matcher;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
settings.hooks[eventName].push(entry);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function setExecutableBits(targetDir) {
|
|
298
|
+
const candidates = [
|
|
299
|
+
path.join(targetDir, 'hooks', 'autodev-phase-boundary.sh'),
|
|
300
|
+
path.join(targetDir, 'hooks', 'autodev-session-state.sh'),
|
|
301
|
+
path.join(targetDir, 'autodev', 'bin', 'autodev-tools.cjs')
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
for (const file of candidates) {
|
|
305
|
+
if (fs.existsSync(file)) {
|
|
306
|
+
fs.chmodSync(file, 0o755);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildGlobalCommand(targetDir, fileName, shell = 'node') {
|
|
312
|
+
const filePath = path.join(targetDir, 'hooks', fileName).replace(/\\/g, '/');
|
|
313
|
+
return `${shell} "${filePath}"`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function buildLocalCommand(fileName, shell = 'node') {
|
|
317
|
+
return `${shell} "$CLAUDE_PROJECT_DIR"/.claude/hooks/${fileName}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function configureSettings(targetDir, isGlobal, options = {}) {
|
|
321
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
322
|
+
const settings = readSettings(settingsPath);
|
|
323
|
+
if (settings === null) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
removeManagedSettings(settings);
|
|
328
|
+
|
|
329
|
+
const preToolEvent = 'PreToolUse';
|
|
330
|
+
const postToolEvent = 'PostToolUse';
|
|
331
|
+
const sessionStartEvent = 'SessionStart';
|
|
332
|
+
|
|
333
|
+
const contextMonitorCommand = isGlobal
|
|
334
|
+
? buildGlobalCommand(targetDir, 'autodev-context-monitor.js')
|
|
335
|
+
: buildLocalCommand('autodev-context-monitor.js');
|
|
336
|
+
const promptGuardCommand = isGlobal
|
|
337
|
+
? buildGlobalCommand(targetDir, 'autodev-prompt-guard.js')
|
|
338
|
+
: buildLocalCommand('autodev-prompt-guard.js');
|
|
339
|
+
const readGuardCommand = isGlobal
|
|
340
|
+
? buildGlobalCommand(targetDir, 'autodev-read-guard.js')
|
|
341
|
+
: buildLocalCommand('autodev-read-guard.js');
|
|
342
|
+
const workflowGuardCommand = isGlobal
|
|
343
|
+
? buildGlobalCommand(targetDir, 'autodev-workflow-guard.js')
|
|
344
|
+
: buildLocalCommand('autodev-workflow-guard.js');
|
|
345
|
+
const gitGuardCommand = isGlobal
|
|
346
|
+
? buildGlobalCommand(targetDir, 'autodev-git-guard.js')
|
|
347
|
+
: buildLocalCommand('autodev-git-guard.js');
|
|
348
|
+
const sessionStateCommand = isGlobal
|
|
349
|
+
? buildGlobalCommand(targetDir, 'autodev-session-state.sh', 'bash')
|
|
350
|
+
: buildLocalCommand('autodev-session-state.sh', 'bash');
|
|
351
|
+
const phaseBoundaryCommand = isGlobal
|
|
352
|
+
? buildGlobalCommand(targetDir, 'autodev-phase-boundary.sh', 'bash')
|
|
353
|
+
: buildLocalCommand('autodev-phase-boundary.sh', 'bash');
|
|
354
|
+
const statusLineCommand = isGlobal
|
|
355
|
+
? buildGlobalCommand(targetDir, 'autodev-statusline.js')
|
|
356
|
+
: buildLocalCommand('autodev-statusline.js');
|
|
357
|
+
|
|
358
|
+
ensureHook(settings, sessionStartEvent, null, sessionStateCommand);
|
|
359
|
+
ensureHook(settings, preToolEvent, 'Write|Edit', promptGuardCommand, 5);
|
|
360
|
+
ensureHook(settings, preToolEvent, 'Write|Edit', readGuardCommand, 5);
|
|
361
|
+
ensureHook(settings, preToolEvent, 'Write|Edit', workflowGuardCommand, 5);
|
|
362
|
+
ensureHook(settings, preToolEvent, 'Bash', gitGuardCommand, 5);
|
|
363
|
+
ensureHook(settings, postToolEvent, 'Bash|Edit|Write|MultiEdit|Agent|Task', contextMonitorCommand, 10);
|
|
364
|
+
ensureHook(settings, postToolEvent, 'Write|Edit', phaseBoundaryCommand, 5);
|
|
365
|
+
|
|
366
|
+
if (options.installStatusLine !== false) {
|
|
367
|
+
settings.statusLine = {
|
|
368
|
+
type: 'command',
|
|
369
|
+
command: statusLineCommand
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
writeSettings(settingsPath, settings);
|
|
374
|
+
return settingsPath;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function removeInstalledFiles(targetDir, isGlobal) {
|
|
378
|
+
const supportDir = path.join(targetDir, 'autodev');
|
|
379
|
+
if (fs.existsSync(supportDir)) {
|
|
380
|
+
fs.rmSync(supportDir, { recursive: true, force: true });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
384
|
+
for (const hookName of HOOK_FILES) {
|
|
385
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
386
|
+
if (fs.existsSync(hookPath)) {
|
|
387
|
+
fs.rmSync(hookPath, { force: true });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (isGlobal) {
|
|
392
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
393
|
+
if (fs.existsSync(skillsDir)) {
|
|
394
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
395
|
+
if (entry.isDirectory() && entry.name.startsWith(MANAGED_PREFIX)) {
|
|
396
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
const commandsDir = path.join(targetDir, 'commands', 'autodev');
|
|
402
|
+
if (fs.existsSync(commandsDir)) {
|
|
403
|
+
fs.rmSync(commandsDir, { recursive: true, force: true });
|
|
404
|
+
}
|
|
405
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
406
|
+
if (fs.existsSync(skillsDir)) {
|
|
407
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
408
|
+
if (entry.isDirectory() && entry.name.startsWith(MANAGED_PREFIX)) {
|
|
409
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function resolveTargetDir({ scope, cwd, targetDir }) {
|
|
417
|
+
if (targetDir) {
|
|
418
|
+
return path.resolve(targetDir);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (scope === 'global') {
|
|
422
|
+
return path.resolve(process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return path.join(path.resolve(cwd), '.claude');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function install(options = {}) {
|
|
429
|
+
const scope = options.scope === 'local' ? 'local' : 'global';
|
|
430
|
+
const cwd = options.cwd || process.cwd();
|
|
431
|
+
const targetDir = resolveTargetDir({ scope, cwd, targetDir: options.targetDir });
|
|
432
|
+
const isGlobal = scope === 'global';
|
|
433
|
+
const silent = Boolean(options.silent);
|
|
434
|
+
const srcRoot = path.resolve(__dirname, '..');
|
|
435
|
+
const pathPrefix = isGlobal
|
|
436
|
+
? `${targetDir.replace(/\\/g, '/')}/`
|
|
437
|
+
: './.claude/';
|
|
438
|
+
const transform = content => transformInstalledContent(content, pathPrefix);
|
|
439
|
+
|
|
440
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
441
|
+
removeInstalledFiles(targetDir, isGlobal);
|
|
442
|
+
|
|
443
|
+
if (isGlobal) {
|
|
444
|
+
copyCommandsAsGlobalSkills(
|
|
445
|
+
path.join(srcRoot, 'commands', 'autodev'),
|
|
446
|
+
path.join(targetDir, 'skills'),
|
|
447
|
+
'autodev',
|
|
448
|
+
transform
|
|
449
|
+
);
|
|
450
|
+
} else {
|
|
451
|
+
copyCommandsAsLocal(
|
|
452
|
+
path.join(srcRoot, 'commands', 'autodev'),
|
|
453
|
+
path.join(targetDir, 'commands', 'autodev'),
|
|
454
|
+
transform
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
copyTextTree(path.join(srcRoot, 'autodev'), path.join(targetDir, 'autodev'), transform);
|
|
459
|
+
copyTextTree(path.join(srcRoot, 'hooks'), path.join(targetDir, 'hooks'), transform);
|
|
460
|
+
setExecutableBits(targetDir);
|
|
461
|
+
const settingsPath = configureSettings(targetDir, isGlobal, options);
|
|
462
|
+
|
|
463
|
+
if (!silent) {
|
|
464
|
+
const locationLabel = isGlobal ? 'global' : 'local';
|
|
465
|
+
console.log(` ${green}Installed${reset} autodev (${locationLabel}) at ${cyan}${targetDir}${reset}`);
|
|
466
|
+
if (settingsPath) {
|
|
467
|
+
console.log(` ${green}Updated${reset} ${cyan}${settingsPath}${reset}`);
|
|
468
|
+
}
|
|
469
|
+
console.log(` Run ${cyan}/autodev-help${reset} in Claude Code.`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return { targetDir, settingsPath };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function uninstall(options = {}) {
|
|
476
|
+
const scope = options.scope === 'local' ? 'local' : 'global';
|
|
477
|
+
const cwd = options.cwd || process.cwd();
|
|
478
|
+
const targetDir = resolveTargetDir({ scope, cwd, targetDir: options.targetDir });
|
|
479
|
+
const isGlobal = scope === 'global';
|
|
480
|
+
const silent = Boolean(options.silent);
|
|
481
|
+
|
|
482
|
+
removeInstalledFiles(targetDir, isGlobal);
|
|
483
|
+
|
|
484
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
485
|
+
const settings = readSettings(settingsPath);
|
|
486
|
+
if (settings) {
|
|
487
|
+
removeManagedSettings(settings);
|
|
488
|
+
writeSettings(settingsPath, settings);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!silent) {
|
|
492
|
+
console.log(` ${green}Removed${reset} autodev from ${cyan}${targetDir}${reset}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { targetDir, settingsPath };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function printHelp() {
|
|
499
|
+
console.log(`Usage: npx @mthanhlm/autodev [--global|--local] [--uninstall]
|
|
500
|
+
|
|
501
|
+
Options:
|
|
502
|
+
--global Install to Claude Code config directory
|
|
503
|
+
--local Install to the current project (.claude/)
|
|
504
|
+
--uninstall Remove autodev from the selected location
|
|
505
|
+
--help Show this help
|
|
506
|
+
|
|
507
|
+
Examples:
|
|
508
|
+
npx @mthanhlm/autodev@latest --global
|
|
509
|
+
npx @mthanhlm/autodev@latest --local
|
|
510
|
+
npx @mthanhlm/autodev@latest --global --uninstall
|
|
511
|
+
`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function askScope() {
|
|
515
|
+
return new Promise(resolve => {
|
|
516
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
517
|
+
rl.question('Install autodev globally or locally? [g/l] ', answer => {
|
|
518
|
+
rl.close();
|
|
519
|
+
resolve(answer.trim().toLowerCase().startsWith('l') ? 'local' : 'global');
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function main() {
|
|
525
|
+
const args = process.argv.slice(2);
|
|
526
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
527
|
+
printHelp();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
let scope = null;
|
|
532
|
+
if (args.includes('--global') || args.includes('-g')) {
|
|
533
|
+
scope = 'global';
|
|
534
|
+
} else if (args.includes('--local') || args.includes('-l')) {
|
|
535
|
+
scope = 'local';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!scope) {
|
|
539
|
+
scope = await askScope();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (args.includes('--uninstall') || args.includes('-u')) {
|
|
543
|
+
uninstall({ scope });
|
|
544
|
+
} else {
|
|
545
|
+
install({ scope });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (require.main === module) {
|
|
550
|
+
main().catch(error => {
|
|
551
|
+
console.error(error);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
module.exports = {
|
|
557
|
+
configureSettings,
|
|
558
|
+
convertCommandToClaudeSkill,
|
|
559
|
+
install,
|
|
560
|
+
readSettings,
|
|
561
|
+
removeManagedSettings,
|
|
562
|
+
stripJsonComments,
|
|
563
|
+
transformInstalledContent,
|
|
564
|
+
uninstall
|
|
565
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autodev:execute-phase
|
|
3
|
+
description: Execute a phase plan sequentially without git writes
|
|
4
|
+
argument-hint: "[phase-number]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Edit
|
|
9
|
+
- Bash
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
- TodoWrite
|
|
13
|
+
- AskUserQuestion
|
|
14
|
+
---
|
|
15
|
+
<objective>
|
|
16
|
+
Execute one phase plan from `.autodev/phases/` and record the result in a summary.
|
|
17
|
+
</objective>
|
|
18
|
+
|
|
19
|
+
<execution_context>
|
|
20
|
+
@~/.claude/autodev/workflows/execute-phase.md
|
|
21
|
+
@~/.claude/autodev/templates/summary.md
|
|
22
|
+
</execution_context>
|
|
23
|
+
|
|
24
|
+
<process>
|
|
25
|
+
Execute the workflow in @~/.claude/autodev/workflows/execute-phase.md end-to-end.
|
|
26
|
+
</process>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autodev:help
|
|
3
|
+
description: Show the compact autodev command reference
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Bash
|
|
7
|
+
---
|
|
8
|
+
<objective>
|
|
9
|
+
Show the autodev command reference and the default workflow.
|
|
10
|
+
</objective>
|
|
11
|
+
|
|
12
|
+
<execution_context>
|
|
13
|
+
@~/.claude/autodev/workflows/help.md
|
|
14
|
+
</execution_context>
|
|
15
|
+
|
|
16
|
+
<process>
|
|
17
|
+
Output the reference from @~/.claude/autodev/workflows/help.md directly.
|
|
18
|
+
</process>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autodev:new-project
|
|
3
|
+
description: Initialize a new autodev project with minimal ceremony
|
|
4
|
+
argument-hint: "[idea or goals]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Bash
|
|
9
|
+
- AskUserQuestion
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
---
|
|
13
|
+
<objective>
|
|
14
|
+
Create the `.autodev/` project state for a new or existing codebase without over-planning.
|
|
15
|
+
</objective>
|
|
16
|
+
|
|
17
|
+
<execution_context>
|
|
18
|
+
@~/.claude/autodev/workflows/new-project.md
|
|
19
|
+
@~/.claude/autodev/templates/config.json
|
|
20
|
+
@~/.claude/autodev/templates/project.md
|
|
21
|
+
@~/.claude/autodev/templates/requirements.md
|
|
22
|
+
@~/.claude/autodev/templates/roadmap.md
|
|
23
|
+
@~/.claude/autodev/templates/state.md
|
|
24
|
+
</execution_context>
|
|
25
|
+
|
|
26
|
+
<process>
|
|
27
|
+
Execute the workflow in @~/.claude/autodev/workflows/new-project.md end-to-end.
|
|
28
|
+
</process>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autodev:plan-phase
|
|
3
|
+
description: Create a practical plan for one roadmap phase
|
|
4
|
+
argument-hint: "[phase-number]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Bash
|
|
9
|
+
- AskUserQuestion
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
- WebFetch
|
|
13
|
+
---
|
|
14
|
+
<objective>
|
|
15
|
+
Create or update a single executable phase plan under `.autodev/phases/`.
|
|
16
|
+
</objective>
|
|
17
|
+
|
|
18
|
+
<execution_context>
|
|
19
|
+
@~/.claude/autodev/workflows/plan-phase.md
|
|
20
|
+
@~/.claude/autodev/templates/plan.md
|
|
21
|
+
</execution_context>
|
|
22
|
+
|
|
23
|
+
<process>
|
|
24
|
+
Execute the workflow in @~/.claude/autodev/workflows/plan-phase.md end-to-end.
|
|
25
|
+
</process>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autodev:progress
|
|
3
|
+
description: Show the current autodev status for the project
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Bash
|
|
7
|
+
---
|
|
8
|
+
<objective>
|
|
9
|
+
Render the current `.autodev/` progress and next recommended command.
|
|
10
|
+
</objective>
|
|
11
|
+
|
|
12
|
+
<execution_context>
|
|
13
|
+
@~/.claude/autodev/workflows/progress.md
|
|
14
|
+
</execution_context>
|
|
15
|
+
|
|
16
|
+
<process>
|
|
17
|
+
Execute the workflow in @~/.claude/autodev/workflows/progress.md end-to-end.
|
|
18
|
+
</process>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autodev:verify-work
|
|
3
|
+
description: Run lightweight user acceptance testing for a phase
|
|
4
|
+
argument-hint: "[phase-number]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Bash
|
|
9
|
+
- AskUserQuestion
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
---
|
|
13
|
+
<objective>
|
|
14
|
+
Record manual verification for one phase and keep the next step clear.
|
|
15
|
+
</objective>
|
|
16
|
+
|
|
17
|
+
<execution_context>
|
|
18
|
+
@~/.claude/autodev/workflows/verify-work.md
|
|
19
|
+
@~/.claude/autodev/templates/uat.md
|
|
20
|
+
</execution_context>
|
|
21
|
+
|
|
22
|
+
<process>
|
|
23
|
+
Execute the workflow in @~/.claude/autodev/workflows/verify-work.md end-to-end.
|
|
24
|
+
</process>
|