@fredlackey/devutils 0.0.19 → 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/README.md +223 -32
- package/package.json +7 -5
- package/src/api/loader.js +229 -0
- package/src/api/registry.json +62 -0
- package/src/cli.js +305 -0
- package/src/commands/ai/index.js +16 -0
- package/src/commands/ai/launch.js +112 -0
- package/src/commands/ai/list.js +54 -0
- package/src/commands/ai/resume.js +70 -0
- package/src/commands/ai/sessions.js +121 -0
- package/src/commands/ai/set.js +131 -0
- package/src/commands/ai/show.js +74 -0
- package/src/commands/ai/tools.js +46 -0
- package/src/commands/alias/add.js +93 -0
- package/src/commands/alias/helpers.js +107 -0
- package/src/commands/alias/index.js +14 -0
- package/src/commands/alias/list.js +55 -0
- package/src/commands/alias/remove.js +62 -0
- package/src/commands/alias/sync.js +109 -0
- package/src/commands/api/disable.js +73 -0
- package/src/commands/api/enable.js +148 -0
- package/src/commands/api/index.js +15 -0
- package/src/commands/api/list.js +66 -0
- package/src/commands/api/update.js +87 -0
- package/src/commands/auth/index.js +15 -0
- package/src/commands/auth/list.js +49 -0
- package/src/commands/auth/login.js +384 -0
- package/src/commands/auth/logout.js +111 -0
- package/src/commands/auth/refresh.js +184 -0
- package/src/commands/auth/services.js +169 -0
- package/src/commands/auth/status.js +104 -0
- package/src/commands/config/export.js +224 -0
- package/src/commands/config/get.js +52 -0
- package/src/commands/config/import.js +308 -0
- package/src/commands/config/index.js +17 -0
- package/src/commands/config/init.js +143 -0
- package/src/commands/config/reset.js +57 -0
- package/src/commands/config/set.js +93 -0
- package/src/commands/config/show.js +35 -0
- package/src/commands/help.js +338 -0
- package/src/commands/identity/add.js +133 -0
- package/src/commands/identity/index.js +17 -0
- package/src/commands/identity/link.js +76 -0
- package/src/commands/identity/list.js +48 -0
- package/src/commands/identity/remove.js +72 -0
- package/src/commands/identity/show.js +65 -0
- package/src/commands/identity/sync.js +172 -0
- package/src/commands/identity/unlink.js +57 -0
- package/src/commands/ignore/add.js +165 -0
- package/src/commands/ignore/index.js +14 -0
- package/src/commands/ignore/list.js +89 -0
- package/src/commands/ignore/markers.js +43 -0
- package/src/commands/ignore/remove.js +164 -0
- package/src/commands/ignore/show.js +169 -0
- package/src/commands/machine/detect.js +122 -0
- package/src/commands/machine/index.js +14 -0
- package/src/commands/machine/list.js +74 -0
- package/src/commands/machine/set.js +106 -0
- package/src/commands/machine/show.js +35 -0
- package/src/commands/schema.js +152 -0
- package/src/commands/search/collections.js +134 -0
- package/src/commands/search/get.js +71 -0
- package/src/commands/search/index-cmd.js +54 -0
- package/src/commands/search/index.js +21 -0
- package/src/commands/search/keyword.js +60 -0
- package/src/commands/search/qmd.js +70 -0
- package/src/commands/search/query.js +64 -0
- package/src/commands/search/semantic.js +62 -0
- package/src/commands/search/status.js +46 -0
- package/src/commands/status.js +276 -0
- package/src/commands/tools/check.js +79 -0
- package/src/commands/tools/index.js +14 -0
- package/src/commands/tools/install.js +110 -0
- package/src/commands/tools/list.js +91 -0
- package/src/commands/tools/search.js +60 -0
- package/src/commands/update.js +113 -0
- package/src/commands/util/add.js +151 -0
- package/src/commands/util/index.js +15 -0
- package/src/commands/util/list.js +97 -0
- package/src/commands/util/remove.js +76 -0
- package/src/commands/util/run.js +79 -0
- package/src/commands/util/show.js +67 -0
- package/src/commands/version.js +33 -0
- package/src/installers/_template.js +104 -0
- package/src/installers/git.js +150 -0
- package/src/installers/homebrew.js +190 -0
- package/src/installers/node.js +223 -0
- package/src/installers/registry.json +29 -0
- package/src/lib/config.js +125 -0
- package/src/lib/detect.js +74 -0
- package/src/lib/errors.js +114 -0
- package/src/lib/github.js +315 -0
- package/src/lib/installer.js +225 -0
- package/src/lib/output.js +239 -0
- package/src/lib/platform.js +112 -0
- package/src/lib/platforms/amazon-linux.js +41 -0
- package/src/lib/platforms/gitbash.js +46 -0
- package/src/lib/platforms/macos.js +45 -0
- package/src/lib/platforms/raspbian.js +41 -0
- package/src/lib/platforms/ubuntu.js +39 -0
- package/src/lib/platforms/windows.js +45 -0
- package/src/lib/prompt.js +161 -0
- package/src/lib/schema.js +211 -0
- package/src/lib/shell.js +75 -0
- package/src/patterns/gitignore/claude-code.txt +25 -0
- package/src/patterns/gitignore/docker.txt +15 -0
- package/src/patterns/gitignore/go.txt +24 -0
- package/src/patterns/gitignore/java.txt +38 -0
- package/src/patterns/gitignore/jetbrains.txt +26 -0
- package/src/patterns/gitignore/linux.txt +18 -0
- package/src/patterns/gitignore/macos.txt +27 -0
- package/src/patterns/gitignore/node.txt +51 -0
- package/src/patterns/gitignore/python.txt +55 -0
- package/src/patterns/gitignore/rust.txt +14 -0
- package/src/patterns/gitignore/terraform.txt +30 -0
- package/src/patterns/gitignore/vscode.txt +15 -0
- package/src/patterns/gitignore/windows.txt +25 -0
- package/src/utils/clone/index.js +165 -0
- package/src/utils/git-push/index.js +230 -0
- package/src/utils/git-status/index.js +116 -0
- package/src/utils/git-status/unix.sh +75 -0
- package/src/utils/registry.json +41 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Command router. Parses arguments, resolves the service and method, applies
|
|
6
|
+
* global flags, runs output format detection, and dispatches to the right
|
|
7
|
+
* command file.
|
|
8
|
+
*
|
|
9
|
+
* This file should not contain business logic. It's routing only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const TOP_LEVEL_COMMANDS = ['status', 'update', 'version', 'schema', 'help'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extracts global flags from the argument list and returns the remaining args.
|
|
16
|
+
* Global flags can appear anywhere in the argument list.
|
|
17
|
+
*
|
|
18
|
+
* @param {string[]} args - Raw arguments from process.argv.slice(2).
|
|
19
|
+
* @returns {{ flags: object, remaining: string[] }}
|
|
20
|
+
*/
|
|
21
|
+
function parseGlobalFlags(args) {
|
|
22
|
+
const flags = {
|
|
23
|
+
format: null,
|
|
24
|
+
dryRun: false,
|
|
25
|
+
verbose: false,
|
|
26
|
+
quiet: false,
|
|
27
|
+
help: false,
|
|
28
|
+
version: false,
|
|
29
|
+
jsonInput: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const remaining = [];
|
|
33
|
+
let i = 0;
|
|
34
|
+
|
|
35
|
+
while (i < args.length) {
|
|
36
|
+
const arg = args[i];
|
|
37
|
+
|
|
38
|
+
if (arg === '--format' && i + 1 < args.length) {
|
|
39
|
+
flags.format = args[i + 1];
|
|
40
|
+
i += 2;
|
|
41
|
+
} else if (arg === '--dry-run') {
|
|
42
|
+
flags.dryRun = true;
|
|
43
|
+
i++;
|
|
44
|
+
} else if (arg === '--verbose') {
|
|
45
|
+
flags.verbose = true;
|
|
46
|
+
i++;
|
|
47
|
+
} else if (arg === '--quiet') {
|
|
48
|
+
flags.quiet = true;
|
|
49
|
+
i++;
|
|
50
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
51
|
+
flags.help = true;
|
|
52
|
+
i++;
|
|
53
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
54
|
+
flags.version = true;
|
|
55
|
+
i++;
|
|
56
|
+
} else if (arg === '--json' && i + 1 < args.length) {
|
|
57
|
+
flags.jsonInput = args[i + 1];
|
|
58
|
+
i += 2;
|
|
59
|
+
} else {
|
|
60
|
+
remaining.push(arg);
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { flags, remaining };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parses remaining command arguments into a structured object.
|
|
70
|
+
* --key value pairs go into flags, everything else into positional.
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} rawArgs - Arguments after service/method have been stripped.
|
|
73
|
+
* @returns {{ positional: string[], flags: object }}
|
|
74
|
+
*/
|
|
75
|
+
function parseCommandArgs(rawArgs) {
|
|
76
|
+
const positional = [];
|
|
77
|
+
const flags = {};
|
|
78
|
+
let i = 0;
|
|
79
|
+
|
|
80
|
+
while (i < rawArgs.length) {
|
|
81
|
+
const arg = rawArgs[i];
|
|
82
|
+
|
|
83
|
+
if (arg.startsWith('--')) {
|
|
84
|
+
const key = arg.slice(2);
|
|
85
|
+
|
|
86
|
+
// Check if next arg exists and is not itself a flag
|
|
87
|
+
if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('--')) {
|
|
88
|
+
flags[key] = rawArgs[i + 1];
|
|
89
|
+
i += 2;
|
|
90
|
+
} else {
|
|
91
|
+
flags[key] = true;
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
positional.push(arg);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { positional, flags };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Displays the CLI help message listing all services, commands, and global flags.
|
|
105
|
+
*/
|
|
106
|
+
function showHelp() {
|
|
107
|
+
const pkg = require('../package.json');
|
|
108
|
+
console.log(`DevUtils CLI v${pkg.version}`);
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log('Usage: dev <service> <method> [arguments] [flags]');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log('Services:');
|
|
113
|
+
console.log(' config User configuration and onboarding');
|
|
114
|
+
console.log(' machine Machine profiles and detection');
|
|
115
|
+
console.log(' identity Git identities, SSH keys, GPG signing');
|
|
116
|
+
console.log(' tools Tool installation and management');
|
|
117
|
+
console.log(' ignore .gitignore pattern management');
|
|
118
|
+
console.log(' util Utility functions');
|
|
119
|
+
console.log(' alias Shorthand bin entries');
|
|
120
|
+
console.log(' auth OAuth and credential management');
|
|
121
|
+
console.log(' api API plugin system');
|
|
122
|
+
console.log(' ai AI coding assistant launcher');
|
|
123
|
+
console.log(' search Markdown search');
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log('Commands:');
|
|
126
|
+
console.log(' status Overall health check');
|
|
127
|
+
console.log(' version Show current version');
|
|
128
|
+
console.log(' help Show this help message');
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log('Global Flags:');
|
|
131
|
+
console.log(' --format <json|table|yaml|csv> Output format');
|
|
132
|
+
console.log(' --dry-run Show what would happen');
|
|
133
|
+
console.log(' --verbose Increase output detail');
|
|
134
|
+
console.log(' --quiet Suppress non-essential output');
|
|
135
|
+
console.log(' --json <data> Pass structured input as JSON');
|
|
136
|
+
console.log(' --help, -h Show help');
|
|
137
|
+
console.log(' --version, -v Show version');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolves a command from the remaining arguments.
|
|
142
|
+
* Checks top-level commands first, then service/method routing.
|
|
143
|
+
*
|
|
144
|
+
* @param {string[]} remaining - Arguments with global flags stripped.
|
|
145
|
+
* @returns {object|null} The resolved command, args, and optional service.
|
|
146
|
+
*/
|
|
147
|
+
function resolveCommand(remaining) {
|
|
148
|
+
const serviceName = remaining[0];
|
|
149
|
+
const methodName = remaining[1];
|
|
150
|
+
const commandArgs = remaining.slice(2);
|
|
151
|
+
|
|
152
|
+
// Check for top-level command first
|
|
153
|
+
if (TOP_LEVEL_COMMANDS.includes(serviceName)) {
|
|
154
|
+
try {
|
|
155
|
+
const cmd = require(`./commands/${serviceName}`);
|
|
156
|
+
return { command: cmd, args: parseCommandArgs(remaining.slice(1)) };
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Try to load the service index
|
|
163
|
+
let service;
|
|
164
|
+
try {
|
|
165
|
+
service = require(`./commands/${serviceName}/index`);
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// If no method specified, show service help
|
|
171
|
+
if (!methodName) {
|
|
172
|
+
return { service, command: null, args: parseCommandArgs([]) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Look up the method in the service's commands
|
|
176
|
+
if (!service.commands || !service.commands[methodName]) {
|
|
177
|
+
return { service, command: null, unknownMethod: methodName, args: parseCommandArgs(commandArgs) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Lazy-load the command
|
|
181
|
+
const command = service.commands[methodName]();
|
|
182
|
+
return { command, args: parseCommandArgs(commandArgs), service };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Builds the context object that every command receives.
|
|
187
|
+
* Contains all foundation modules pre-configured for the current invocation.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} flags - The parsed global flags.
|
|
190
|
+
* @returns {object} The context object.
|
|
191
|
+
*/
|
|
192
|
+
function buildContext(flags) {
|
|
193
|
+
const detect = require('./lib/detect');
|
|
194
|
+
const output = require('./lib/output');
|
|
195
|
+
const errors = require('./lib/errors');
|
|
196
|
+
const platform = require('./lib/platform');
|
|
197
|
+
const shell = require('./lib/shell');
|
|
198
|
+
const config = require('./lib/config');
|
|
199
|
+
const prompt = require('./lib/prompt');
|
|
200
|
+
|
|
201
|
+
// Determine output format: flag override > detection
|
|
202
|
+
const detected = detect.detectOutputMode();
|
|
203
|
+
const format = flags.format || detected.format;
|
|
204
|
+
const caller = detected.caller;
|
|
205
|
+
|
|
206
|
+
// Create pre-configured formatter
|
|
207
|
+
const formatter = output.createFormatter({ format, caller });
|
|
208
|
+
|
|
209
|
+
let jsonInput = null;
|
|
210
|
+
if (flags.jsonInput) {
|
|
211
|
+
try {
|
|
212
|
+
jsonInput = JSON.parse(flags.jsonInput);
|
|
213
|
+
} catch {
|
|
214
|
+
jsonInput = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
platform,
|
|
220
|
+
shell,
|
|
221
|
+
config,
|
|
222
|
+
prompt,
|
|
223
|
+
errors,
|
|
224
|
+
output: formatter,
|
|
225
|
+
flags: {
|
|
226
|
+
format,
|
|
227
|
+
caller,
|
|
228
|
+
dryRun: flags.dryRun,
|
|
229
|
+
verbose: flags.verbose,
|
|
230
|
+
quiet: flags.quiet,
|
|
231
|
+
jsonInput,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Main entry point. Parses args, resolves the command, builds context, and runs.
|
|
238
|
+
*/
|
|
239
|
+
async function main() {
|
|
240
|
+
const { flags, remaining } = parseGlobalFlags(process.argv.slice(2));
|
|
241
|
+
|
|
242
|
+
// Handle --version
|
|
243
|
+
if (flags.version) {
|
|
244
|
+
const pkg = require('../package.json');
|
|
245
|
+
console.log(pkg.version);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle --help or no arguments
|
|
250
|
+
if (flags.help || remaining.length === 0) {
|
|
251
|
+
showHelp();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Resolve the command
|
|
256
|
+
const resolved = resolveCommand(remaining);
|
|
257
|
+
|
|
258
|
+
if (!resolved || !resolved.command) {
|
|
259
|
+
const errors = require('./lib/errors');
|
|
260
|
+
if (resolved && resolved.unknownMethod) {
|
|
261
|
+
const methods = Object.keys(resolved.service.commands || {}).join(', ');
|
|
262
|
+
errors.throwError(
|
|
263
|
+
404,
|
|
264
|
+
`Unknown method "${resolved.unknownMethod}" for service "${resolved.service.name}". Available methods: ${methods}`,
|
|
265
|
+
resolved.service.name
|
|
266
|
+
);
|
|
267
|
+
} else if (resolved && resolved.service && !resolved.command) {
|
|
268
|
+
// Service exists but no method given -- show service commands
|
|
269
|
+
const methods = Object.keys(resolved.service.commands || {});
|
|
270
|
+
console.log(`${resolved.service.name}: ${resolved.service.description}`);
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log('Methods:');
|
|
273
|
+
for (const method of methods) {
|
|
274
|
+
console.log(` dev ${resolved.service.name} ${method}`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
} else {
|
|
278
|
+
const serviceName = remaining[0];
|
|
279
|
+
errors.throwError(
|
|
280
|
+
404,
|
|
281
|
+
`Unknown command "${serviceName}". Run "dev help" to see available commands.`,
|
|
282
|
+
'cli'
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Build context and run
|
|
289
|
+
const context = buildContext(flags);
|
|
290
|
+
try {
|
|
291
|
+
await resolved.command.run(resolved.args, context);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
const errors = require('./lib/errors');
|
|
294
|
+
if (errors.isDevUtilsError(err)) {
|
|
295
|
+
context.output.err(err);
|
|
296
|
+
} else {
|
|
297
|
+
errors.throwError(500, err.message || 'An unexpected error occurred', 'cli');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
main().catch(err => {
|
|
303
|
+
console.error(err);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI service registration.
|
|
3
|
+
* AI coding assistant launcher and session management.
|
|
4
|
+
*/
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'ai',
|
|
7
|
+
description: 'AI coding assistant launcher and session management',
|
|
8
|
+
commands: {
|
|
9
|
+
launch: () => require('./launch'),
|
|
10
|
+
resume: () => require('./resume'),
|
|
11
|
+
list: () => require('./list'),
|
|
12
|
+
sessions: () => require('./sessions'),
|
|
13
|
+
show: () => require('./show'),
|
|
14
|
+
set: () => require('./set'),
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const shell = require('../../lib/shell');
|
|
7
|
+
const { AI_TOOLS } = require('./tools');
|
|
8
|
+
|
|
9
|
+
const AI_CONFIG_FILE = path.join(os.homedir(), '.devutils', 'ai.json');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reads ~/.devutils/ai.json and returns its contents.
|
|
13
|
+
* Returns an empty object if the file does not exist or is unreadable.
|
|
14
|
+
*
|
|
15
|
+
* @returns {object} The parsed AI config, or {}.
|
|
16
|
+
*/
|
|
17
|
+
function readAiConfig() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(AI_CONFIG_FILE, 'utf8'));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
description: 'Start an AI coding tool with configured defaults',
|
|
27
|
+
arguments: [
|
|
28
|
+
{ name: 'tool', description: 'AI tool to launch (e.g., claude, gemini)', required: true }
|
|
29
|
+
],
|
|
30
|
+
flags: [
|
|
31
|
+
{ name: 'mode', type: 'string', description: 'Override the launch mode (e.g., danger, yolo, default)' },
|
|
32
|
+
{ name: 'model', type: 'string', description: 'Override the model selection' },
|
|
33
|
+
{ name: 'prompt', type: 'string', description: 'Pass an initial prompt to the AI tool' }
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Launches an AI coding tool with configured defaults.
|
|
39
|
+
* Reads user preferences from ai.json, builds the command with
|
|
40
|
+
* mode/model/flags, and spawns the tool with stdio inherit.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} args - Parsed CLI arguments { positional, flags, extra }.
|
|
43
|
+
* @param {object} context - CLI context { output, errors }.
|
|
44
|
+
*/
|
|
45
|
+
async function run(args, context) {
|
|
46
|
+
const toolName = args.positional[0];
|
|
47
|
+
|
|
48
|
+
if (!toolName) {
|
|
49
|
+
context.errors.throwError(400, 'Missing required argument: <tool>. Example: dev ai launch claude', 'ai');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate the tool name
|
|
54
|
+
const toolConfig = AI_TOOLS[toolName];
|
|
55
|
+
if (!toolConfig) {
|
|
56
|
+
const available = Object.keys(AI_TOOLS).join(', ');
|
|
57
|
+
context.output.info(`Unknown AI tool "${toolName}". Available: ${available}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check that the tool is installed
|
|
62
|
+
const isInstalled = shell.commandExists(toolConfig.binary);
|
|
63
|
+
if (!isInstalled) {
|
|
64
|
+
context.output.info(`${toolConfig.displayName} is not installed.`);
|
|
65
|
+
context.output.info(`Install it and make sure "${toolConfig.binary}" is on your PATH.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Read user configuration
|
|
70
|
+
const aiConfig = readAiConfig();
|
|
71
|
+
const toolUserConfig = aiConfig[toolName] || {};
|
|
72
|
+
|
|
73
|
+
// Build the command
|
|
74
|
+
const parts = [toolConfig.binary];
|
|
75
|
+
|
|
76
|
+
// Determine mode: command-line flag overrides config, config overrides default
|
|
77
|
+
const mode = args.flags.mode || toolUserConfig.mode || 'default';
|
|
78
|
+
const modeFlags = toolConfig.modes[mode];
|
|
79
|
+
if (!modeFlags) {
|
|
80
|
+
const available = Object.keys(toolConfig.modes).join(', ');
|
|
81
|
+
context.output.info(`Unknown mode "${mode}" for ${toolConfig.displayName}. Available: ${available}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
parts.push(...modeFlags);
|
|
85
|
+
|
|
86
|
+
// Model flag
|
|
87
|
+
const model = args.flags.model || toolUserConfig.model;
|
|
88
|
+
if (model) {
|
|
89
|
+
parts.push(toolConfig.modelFlag, model);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Prompt flag
|
|
93
|
+
if (args.flags.prompt) {
|
|
94
|
+
parts.push(toolConfig.promptFlag, JSON.stringify(args.flags.prompt));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// User's default flags from config
|
|
98
|
+
if (toolUserConfig.flags && toolUserConfig.flags.length > 0) {
|
|
99
|
+
parts.push(...toolUserConfig.flags);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Pass through any remaining unrecognized flags from the command line
|
|
103
|
+
if (args.extra && args.extra.length > 0) {
|
|
104
|
+
parts.push(...args.extra);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const command = parts.join(' ');
|
|
108
|
+
context.output.info(`Launching ${toolConfig.displayName}...`);
|
|
109
|
+
await shell.exec(command, { stdio: 'inherit' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const shell = require('../../lib/shell');
|
|
7
|
+
const { AI_TOOLS } = require('./tools');
|
|
8
|
+
|
|
9
|
+
const AI_CONFIG_FILE = path.join(os.homedir(), '.devutils', 'ai.json');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reads ~/.devutils/ai.json and returns its contents.
|
|
13
|
+
* Returns an empty object if the file does not exist or is unreadable.
|
|
14
|
+
*
|
|
15
|
+
* @returns {object} The parsed AI config, or {}.
|
|
16
|
+
*/
|
|
17
|
+
function readAiConfig() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(AI_CONFIG_FILE, 'utf8'));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
description: 'List available AI coding tools and their install status',
|
|
27
|
+
arguments: [],
|
|
28
|
+
flags: []
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lists all known AI coding tools with their install status,
|
|
33
|
+
* current mode, and available modes.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
36
|
+
* @param {object} context - CLI context { output }.
|
|
37
|
+
*/
|
|
38
|
+
async function run(args, context) {
|
|
39
|
+
const aiConfig = readAiConfig();
|
|
40
|
+
|
|
41
|
+
const results = Object.entries(AI_TOOLS).map(([name, tool]) => ({
|
|
42
|
+
name: name,
|
|
43
|
+
displayName: tool.displayName,
|
|
44
|
+
binary: tool.binary,
|
|
45
|
+
installed: shell.commandExists(tool.binary),
|
|
46
|
+
configured: !!aiConfig[name],
|
|
47
|
+
mode: (aiConfig[name] && aiConfig[name].mode) || 'default',
|
|
48
|
+
availableModes: Object.keys(tool.modes)
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
context.output.out(results);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const shell = require('../../lib/shell');
|
|
4
|
+
const { AI_TOOLS } = require('./tools');
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
description: 'Resume a previous AI coding session by ID',
|
|
8
|
+
arguments: [
|
|
9
|
+
{ name: 'tool', description: 'AI tool to resume (e.g., claude, gemini)', required: true },
|
|
10
|
+
{ name: 'session', description: 'Session ID to resume', required: true }
|
|
11
|
+
],
|
|
12
|
+
flags: []
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resumes a previous AI coding session by session ID.
|
|
17
|
+
* Validates the tool name, checks the binary is installed,
|
|
18
|
+
* and spawns the tool with the resume flag and session ID.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} args - Parsed CLI arguments { positional, flags, extra }.
|
|
21
|
+
* @param {object} context - CLI context { output, errors }.
|
|
22
|
+
*/
|
|
23
|
+
async function run(args, context) {
|
|
24
|
+
const toolName = args.positional[0];
|
|
25
|
+
const sessionId = args.positional[1];
|
|
26
|
+
|
|
27
|
+
if (!toolName) {
|
|
28
|
+
context.errors.throwError(400, 'Missing required argument: <tool>. Example: dev ai resume claude abc123', 'ai');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!sessionId) {
|
|
33
|
+
context.errors.throwError(400, 'Missing required argument: <session>. Example: dev ai resume claude abc123', 'ai');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate the tool name
|
|
38
|
+
const toolConfig = AI_TOOLS[toolName];
|
|
39
|
+
if (!toolConfig) {
|
|
40
|
+
const available = Object.keys(AI_TOOLS).join(', ');
|
|
41
|
+
context.output.info(`Unknown AI tool "${toolName}". Available: ${available}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check that the tool is installed
|
|
46
|
+
const isInstalled = shell.commandExists(toolConfig.binary);
|
|
47
|
+
if (!isInstalled) {
|
|
48
|
+
context.output.info(`${toolConfig.displayName} is not installed.`);
|
|
49
|
+
context.output.info(`Install it and make sure "${toolConfig.binary}" is on your PATH.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Build the command
|
|
54
|
+
const parts = [
|
|
55
|
+
toolConfig.binary,
|
|
56
|
+
toolConfig.resumeFlag,
|
|
57
|
+
sessionId
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Pass through extra flags
|
|
61
|
+
if (args.extra && args.extra.length > 0) {
|
|
62
|
+
parts.push(...args.extra);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const command = parts.join(' ');
|
|
66
|
+
context.output.info(`Resuming ${toolConfig.displayName} session ${sessionId}...`);
|
|
67
|
+
await shell.exec(command, { stdio: 'inherit' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const shell = require('../../lib/shell');
|
|
6
|
+
const { AI_TOOLS } = require('./tools');
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
description: 'List recent sessions for an AI tool',
|
|
10
|
+
arguments: [
|
|
11
|
+
{ name: 'tool', description: 'AI tool name (e.g., claude, gemini)', required: true }
|
|
12
|
+
],
|
|
13
|
+
flags: [
|
|
14
|
+
{ name: 'limit', type: 'number', description: 'Maximum number of sessions to show (default: 10)' }
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Scans a directory recursively for session files and returns metadata.
|
|
20
|
+
* Wraps all file operations in try/catch to handle unreadable files gracefully.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} dirPath - The directory to scan.
|
|
23
|
+
* @param {string} toolName - The tool name (for building resume commands).
|
|
24
|
+
* @returns {Array<object>} An array of session objects sorted by last-modified date.
|
|
25
|
+
*/
|
|
26
|
+
function findSessions(dirPath, toolName) {
|
|
27
|
+
const sessions = [];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
31
|
+
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
// Recurse into subdirectories (e.g., project-specific directories)
|
|
38
|
+
const nested = findSessions(fullPath, toolName);
|
|
39
|
+
sessions.push(...nested);
|
|
40
|
+
} else if (entry.isFile()) {
|
|
41
|
+
// Extract session ID from filename (strip extension)
|
|
42
|
+
const sessionId = path.basename(entry.name, path.extname(entry.name));
|
|
43
|
+
const stats = fs.statSync(fullPath);
|
|
44
|
+
|
|
45
|
+
sessions.push({
|
|
46
|
+
id: sessionId,
|
|
47
|
+
tool: toolName,
|
|
48
|
+
path: fullPath,
|
|
49
|
+
lastActive: stats.mtime.toISOString(),
|
|
50
|
+
resumeCommand: `dev ai resume ${toolName} ${sessionId}`
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Skip unreadable entries
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Directory not readable, return empty
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return sessions;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lists recent sessions for a specified AI tool by reading
|
|
66
|
+
* the tool's session storage directory.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
69
|
+
* @param {object} context - CLI context { output, errors }.
|
|
70
|
+
*/
|
|
71
|
+
async function run(args, context) {
|
|
72
|
+
const toolName = args.positional[0];
|
|
73
|
+
|
|
74
|
+
if (!toolName) {
|
|
75
|
+
context.errors.throwError(400, 'Missing required argument: <tool>. Example: dev ai sessions claude', 'ai');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate the tool name
|
|
80
|
+
const toolConfig = AI_TOOLS[toolName];
|
|
81
|
+
if (!toolConfig) {
|
|
82
|
+
const available = Object.keys(AI_TOOLS).join(', ');
|
|
83
|
+
context.output.info(`Unknown AI tool "${toolName}". Available: ${available}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Find a session directory that exists
|
|
88
|
+
let sessionDir = null;
|
|
89
|
+
for (const candidate of toolConfig.sessionPaths) {
|
|
90
|
+
if (fs.existsSync(candidate)) {
|
|
91
|
+
sessionDir = candidate;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!sessionDir) {
|
|
97
|
+
context.output.info(
|
|
98
|
+
`No session data found for ${toolConfig.displayName}. Sessions may be stored in a location DevUtils doesn't know about yet.`
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Scan for sessions
|
|
104
|
+
const sessions = findSessions(sessionDir, toolName);
|
|
105
|
+
|
|
106
|
+
if (sessions.length === 0) {
|
|
107
|
+
context.output.info(`No sessions found for ${toolConfig.displayName}.`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Sort by last-modified date, most recent first
|
|
112
|
+
sessions.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
|
|
113
|
+
|
|
114
|
+
// Apply the limit
|
|
115
|
+
const limit = args.flags.limit || 10;
|
|
116
|
+
const limited = sessions.slice(0, limit);
|
|
117
|
+
|
|
118
|
+
context.output.out(limited);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { meta, run };
|