cli4ai 0.8.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 +275 -0
- package/package.json +49 -0
- package/src/bin.ts +120 -0
- package/src/cli.ts +256 -0
- package/src/commands/add.ts +530 -0
- package/src/commands/browse.ts +449 -0
- package/src/commands/config.ts +126 -0
- package/src/commands/info.ts +102 -0
- package/src/commands/init.test.ts +163 -0
- package/src/commands/init.ts +560 -0
- package/src/commands/list.ts +89 -0
- package/src/commands/mcp-config.ts +59 -0
- package/src/commands/remove.ts +72 -0
- package/src/commands/routines.ts +393 -0
- package/src/commands/run.ts +45 -0
- package/src/commands/search.ts +148 -0
- package/src/commands/secrets.ts +273 -0
- package/src/commands/start.ts +40 -0
- package/src/commands/update.ts +218 -0
- package/src/core/config.test.ts +188 -0
- package/src/core/config.ts +649 -0
- package/src/core/execute.ts +507 -0
- package/src/core/link.test.ts +238 -0
- package/src/core/link.ts +190 -0
- package/src/core/lockfile.test.ts +337 -0
- package/src/core/lockfile.ts +308 -0
- package/src/core/manifest.test.ts +327 -0
- package/src/core/manifest.ts +319 -0
- package/src/core/routine-engine.test.ts +139 -0
- package/src/core/routine-engine.ts +725 -0
- package/src/core/routines.ts +111 -0
- package/src/core/secrets.test.ts +79 -0
- package/src/core/secrets.ts +430 -0
- package/src/lib/cli.ts +234 -0
- package/src/mcp/adapter.test.ts +132 -0
- package/src/mcp/adapter.ts +123 -0
- package/src/mcp/config-gen.test.ts +214 -0
- package/src/mcp/config-gen.ts +106 -0
- package/src/mcp/server.ts +363 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai remove - Uninstall packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, rmSync, lstatSync, unlinkSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
8
|
+
import { PACKAGES_DIR, LOCAL_PACKAGES_DIR } from '../core/config.js';
|
|
9
|
+
import { unlockPackage } from '../core/lockfile.js';
|
|
10
|
+
import { unlinkPackage as unlinkFromPath } from '../core/link.js';
|
|
11
|
+
|
|
12
|
+
interface RemoveOptions {
|
|
13
|
+
global?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RemoveResult {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function removeCommand(packages: string[], options: RemoveOptions): Promise<void> {
|
|
22
|
+
const results: RemoveResult[] = [];
|
|
23
|
+
const errors: { package: string; error: string }[] = [];
|
|
24
|
+
|
|
25
|
+
const targetDir = options.global ? PACKAGES_DIR : resolve(process.cwd(), LOCAL_PACKAGES_DIR);
|
|
26
|
+
const projectDir = process.cwd();
|
|
27
|
+
|
|
28
|
+
for (const pkg of packages) {
|
|
29
|
+
const pkgPath = resolve(targetDir, pkg);
|
|
30
|
+
|
|
31
|
+
if (!existsSync(pkgPath)) {
|
|
32
|
+
errors.push({ package: pkg, error: 'Not installed' });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const stat = lstatSync(pkgPath);
|
|
38
|
+
if (stat.isSymbolicLink()) {
|
|
39
|
+
unlinkSync(pkgPath);
|
|
40
|
+
} else {
|
|
41
|
+
rmSync(pkgPath, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
results.push({ name: pkg, path: pkgPath });
|
|
45
|
+
|
|
46
|
+
if (options.global) {
|
|
47
|
+
// Remove from PATH
|
|
48
|
+
unlinkFromPath(pkg);
|
|
49
|
+
log(`- ${pkg} (unlinked from PATH)`);
|
|
50
|
+
} else {
|
|
51
|
+
log(`- ${pkg}`);
|
|
52
|
+
// Update lockfile (only for local/project installs)
|
|
53
|
+
unlockPackage(projectDir, pkg);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
errors.push({
|
|
57
|
+
package: pkg,
|
|
58
|
+
error: err instanceof Error ? err.message : String(err)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (errors.length > 0 && results.length === 0) {
|
|
64
|
+
outputError('INSTALL_ERROR', 'Failed to remove packages', { errors });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
output({
|
|
68
|
+
removed: results,
|
|
69
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
70
|
+
location: options.global ? 'global' : 'local'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai routines - Manage and run routines
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn, spawnSync } from 'child_process';
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync, rmSync } from 'fs';
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
9
|
+
import { ensureCli4aiHome, ensureLocalDir, ROUTINES_DIR, LOCAL_ROUTINES_DIR } from '../core/config.js';
|
|
10
|
+
import { getGlobalRoutines, getLocalRoutines, resolveRoutine, validateRoutineName, type RoutineInfo } from '../core/routines.js';
|
|
11
|
+
import { loadRoutineDefinition, dryRunRoutine, runRoutine, RoutineParseError, RoutineValidationError, RoutineTemplateError } from '../core/routine-engine.js';
|
|
12
|
+
|
|
13
|
+
interface RoutinesListOptions {
|
|
14
|
+
global?: boolean;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RoutinesRunOptions {
|
|
19
|
+
global?: boolean;
|
|
20
|
+
var?: string[];
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RoutinesCreateOptions {
|
|
25
|
+
global?: boolean;
|
|
26
|
+
type?: 'json' | 'bash';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureValidRoutineName(name: string): void {
|
|
30
|
+
try {
|
|
31
|
+
validateRoutineName(name);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
outputError('INVALID_INPUT', err instanceof Error ? err.message : String(err), { name });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeEnvSegment(value: string): string {
|
|
38
|
+
return value
|
|
39
|
+
.trim()
|
|
40
|
+
.toUpperCase()
|
|
41
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
42
|
+
.replace(/^_+|_+$/g, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseVars(vars: string[] | undefined): Record<string, string> {
|
|
46
|
+
const result: Record<string, string> = {};
|
|
47
|
+
if (!vars) return result;
|
|
48
|
+
|
|
49
|
+
for (const entry of vars) {
|
|
50
|
+
const eq = entry.indexOf('=');
|
|
51
|
+
if (eq <= 0) {
|
|
52
|
+
outputError('INVALID_INPUT', `Invalid --var (expected KEY=value): ${entry}`);
|
|
53
|
+
}
|
|
54
|
+
const key = entry.slice(0, eq).trim();
|
|
55
|
+
const value = entry.slice(eq + 1);
|
|
56
|
+
if (!key) {
|
|
57
|
+
outputError('INVALID_INPUT', `Invalid --var (empty key): ${entry}`);
|
|
58
|
+
}
|
|
59
|
+
result[key] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function varsToEnv(vars: Record<string, string>): Record<string, string> {
|
|
66
|
+
const env: Record<string, string> = {};
|
|
67
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
68
|
+
const envKey = `CLI4AI_VAR_${normalizeEnvSegment(key)}`;
|
|
69
|
+
env[envKey] = value;
|
|
70
|
+
}
|
|
71
|
+
return env;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectStream(stream: NodeJS.ReadableStream): Promise<string> {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const chunks: Buffer[] = [];
|
|
77
|
+
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
78
|
+
stream.on('error', reject);
|
|
79
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function routinesListCommand(options: RoutinesListOptions): Promise<void> {
|
|
84
|
+
const local = options.global ? [] : getLocalRoutines(process.cwd());
|
|
85
|
+
const global = getGlobalRoutines();
|
|
86
|
+
|
|
87
|
+
const routines: RoutineInfo[] = [];
|
|
88
|
+
const seen = new Set<string>();
|
|
89
|
+
|
|
90
|
+
for (const r of local) {
|
|
91
|
+
routines.push(r);
|
|
92
|
+
seen.add(r.name);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const r of global) {
|
|
96
|
+
if (!seen.has(r.name)) {
|
|
97
|
+
routines.push(r);
|
|
98
|
+
seen.add(r.name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.json || !process.stdout.isTTY) {
|
|
103
|
+
output({
|
|
104
|
+
routines: routines.map(r => ({
|
|
105
|
+
name: r.name,
|
|
106
|
+
kind: r.kind,
|
|
107
|
+
scope: r.scope,
|
|
108
|
+
path: r.path
|
|
109
|
+
})),
|
|
110
|
+
count: routines.length
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (routines.length === 0) {
|
|
116
|
+
console.log('No routines found');
|
|
117
|
+
console.log('\nCreate one: cli4ai routines create <name>\n');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`\nRoutines (${routines.length}):\n`);
|
|
122
|
+
for (const r of routines) {
|
|
123
|
+
const scopeTag = r.scope === 'local' ? '' : ' (global)';
|
|
124
|
+
console.log(` ${r.name}${scopeTag} [${r.kind}]`);
|
|
125
|
+
console.log(` ${r.path}`);
|
|
126
|
+
}
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getTargetDir(global: boolean, projectDir: string): string {
|
|
131
|
+
if (global) {
|
|
132
|
+
ensureCli4aiHome();
|
|
133
|
+
return ROUTINES_DIR;
|
|
134
|
+
}
|
|
135
|
+
ensureLocalDir(projectDir);
|
|
136
|
+
return resolve(projectDir, LOCAL_ROUTINES_DIR);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function routinesCreateCommand(name: string, options: RoutinesCreateOptions): Promise<void> {
|
|
140
|
+
ensureValidRoutineName(name);
|
|
141
|
+
const projectDir = process.cwd();
|
|
142
|
+
const type = options.type ?? 'bash';
|
|
143
|
+
if (type !== 'bash' && type !== 'json') {
|
|
144
|
+
outputError('INVALID_INPUT', `Invalid routine type: ${type}`, {
|
|
145
|
+
allowed: ['bash', 'json']
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const dir = getTargetDir(!!options.global, projectDir);
|
|
149
|
+
|
|
150
|
+
const suffix = type === 'json' ? '.routine.json' : '.routine.sh';
|
|
151
|
+
const filePath = resolve(dir, `${name}${suffix}`);
|
|
152
|
+
|
|
153
|
+
if (existsSync(filePath)) {
|
|
154
|
+
outputError('INVALID_INPUT', `Routine already exists: ${name}`, { path: filePath });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (type === 'json') {
|
|
158
|
+
const template = {
|
|
159
|
+
version: 1,
|
|
160
|
+
name,
|
|
161
|
+
description: 'TODO: describe this routine',
|
|
162
|
+
vars: {},
|
|
163
|
+
steps: [],
|
|
164
|
+
result: '{{steps}}'
|
|
165
|
+
};
|
|
166
|
+
writeFileSync(filePath, JSON.stringify(template, null, 2) + '\n');
|
|
167
|
+
} else {
|
|
168
|
+
const template = `#!/usr/bin/env bash
|
|
169
|
+
set -euo pipefail
|
|
170
|
+
|
|
171
|
+
# Example:
|
|
172
|
+
# cli4ai run github trending
|
|
173
|
+
#
|
|
174
|
+
# Vars passed via --var KEY=value are available as env vars:
|
|
175
|
+
# CLI4AI_VAR_KEY=value
|
|
176
|
+
|
|
177
|
+
echo "TODO: implement ${name}" 1>&2
|
|
178
|
+
`;
|
|
179
|
+
writeFileSync(filePath, template);
|
|
180
|
+
try {
|
|
181
|
+
chmodSync(filePath, 0o755);
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
output({
|
|
186
|
+
action: 'create',
|
|
187
|
+
name,
|
|
188
|
+
type,
|
|
189
|
+
scope: options.global ? 'global' : 'local',
|
|
190
|
+
path: filePath
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function routinesRunCommand(
|
|
195
|
+
name: string,
|
|
196
|
+
args: string[],
|
|
197
|
+
options: RoutinesRunOptions
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
ensureValidRoutineName(name);
|
|
200
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
201
|
+
if (!routine) {
|
|
202
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`, {
|
|
203
|
+
hint: options.global
|
|
204
|
+
? 'Create a global routine in ~/.cli4ai/routines'
|
|
205
|
+
: 'Create a routine in ./.cli4ai/routines or ~/.cli4ai/routines'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const vars = parseVars(options.var);
|
|
210
|
+
|
|
211
|
+
if (options.dryRun) {
|
|
212
|
+
if (routine.kind === 'json') {
|
|
213
|
+
try {
|
|
214
|
+
const def = loadRoutineDefinition(routine.path);
|
|
215
|
+
const plan = await dryRunRoutine(def, vars, process.cwd());
|
|
216
|
+
output({
|
|
217
|
+
kind: routine.kind,
|
|
218
|
+
scope: routine.scope,
|
|
219
|
+
path: routine.path,
|
|
220
|
+
...plan
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (err instanceof RoutineParseError) {
|
|
225
|
+
outputError('PARSE_ERROR', err.message, { path: err.path });
|
|
226
|
+
}
|
|
227
|
+
if (err instanceof RoutineValidationError || err instanceof RoutineTemplateError) {
|
|
228
|
+
outputError('INVALID_INPUT', err.message, err.details);
|
|
229
|
+
}
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const plan = {
|
|
235
|
+
routine: routine.name,
|
|
236
|
+
kind: routine.kind,
|
|
237
|
+
scope: routine.scope,
|
|
238
|
+
path: routine.path,
|
|
239
|
+
vars,
|
|
240
|
+
exec: { cmd: 'bash', args: [routine.path, ...args] }
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (!process.stdout.isTTY) {
|
|
244
|
+
output(plan);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(`\nRoutine: ${routine.name} [${routine.kind}]`);
|
|
249
|
+
console.log(`Path: ${routine.path}`);
|
|
250
|
+
console.log(`Vars: ${Object.keys(vars).length ? JSON.stringify(vars) : '(none)'}`);
|
|
251
|
+
console.log(`\nWould run:\n bash ${routine.path}${args.length ? ' ' + args.join(' ') : ''}\n`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (routine.kind === 'json') {
|
|
256
|
+
try {
|
|
257
|
+
const def = loadRoutineDefinition(routine.path);
|
|
258
|
+
const summary = await runRoutine(def, vars, process.cwd());
|
|
259
|
+
output({
|
|
260
|
+
kind: routine.kind,
|
|
261
|
+
scope: routine.scope,
|
|
262
|
+
path: routine.path,
|
|
263
|
+
...summary
|
|
264
|
+
});
|
|
265
|
+
process.exit(summary.exitCode);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof RoutineParseError) {
|
|
268
|
+
outputError('PARSE_ERROR', err.message, { path: err.path });
|
|
269
|
+
}
|
|
270
|
+
if (err instanceof RoutineValidationError || err instanceof RoutineTemplateError) {
|
|
271
|
+
outputError('INVALID_INPUT', err.message, err.details);
|
|
272
|
+
}
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const startTime = Date.now();
|
|
278
|
+
const isTTY = process.stdout.isTTY;
|
|
279
|
+
const varEnv = varsToEnv(vars);
|
|
280
|
+
|
|
281
|
+
const child = spawn('bash', [routine.path, ...args], {
|
|
282
|
+
cwd: process.cwd(),
|
|
283
|
+
stdio: isTTY ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
284
|
+
env: {
|
|
285
|
+
...process.env,
|
|
286
|
+
...varEnv,
|
|
287
|
+
C4AI_ROUTINE_NAME: routine.name,
|
|
288
|
+
C4AI_ROUTINE_PATH: routine.path
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (isTTY) {
|
|
293
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
294
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
295
|
+
child.on('error', (err) => outputError('API_ERROR', `Failed to execute routine: ${err.message}`));
|
|
296
|
+
});
|
|
297
|
+
process.exit(exitCode);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Non-TTY: capture and emit single JSON summary.
|
|
301
|
+
const stdoutPromise = child.stdout ? collectStream(child.stdout) : Promise.resolve('');
|
|
302
|
+
const stderrPromise = child.stderr ? collectStream(child.stderr) : Promise.resolve('');
|
|
303
|
+
|
|
304
|
+
if (child.stderr) {
|
|
305
|
+
child.stderr.on('data', (chunk) => {
|
|
306
|
+
process.stderr.write(chunk);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
311
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
312
|
+
child.on('error', (err) => outputError('API_ERROR', `Failed to execute routine: ${err.message}`));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
316
|
+
|
|
317
|
+
output({
|
|
318
|
+
routine: routine.name,
|
|
319
|
+
kind: routine.kind,
|
|
320
|
+
scope: routine.scope,
|
|
321
|
+
status: exitCode === 0 ? 'success' : 'failed',
|
|
322
|
+
exitCode,
|
|
323
|
+
durationMs: Date.now() - startTime,
|
|
324
|
+
stdout,
|
|
325
|
+
stderr
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
process.exit(exitCode);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function routinesShowCommand(name: string, options: { global?: boolean }): Promise<void> {
|
|
332
|
+
ensureValidRoutineName(name);
|
|
333
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
334
|
+
if (!routine) {
|
|
335
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const content = readFileSync(routine.path, 'utf-8');
|
|
339
|
+
|
|
340
|
+
if (!process.stdout.isTTY) {
|
|
341
|
+
output({ routine: routine.name, kind: routine.kind, scope: routine.scope, path: routine.path, content });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
log(`\n${routine.path}\n`);
|
|
346
|
+
process.stdout.write(content);
|
|
347
|
+
if (!content.endsWith('\n')) process.stdout.write('\n');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function routinesRemoveCommand(name: string, options: { global?: boolean }): Promise<void> {
|
|
351
|
+
ensureValidRoutineName(name);
|
|
352
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
353
|
+
if (!routine) {
|
|
354
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
rmSync(routine.path, { force: true });
|
|
358
|
+
|
|
359
|
+
output({
|
|
360
|
+
action: 'remove',
|
|
361
|
+
name: routine.name,
|
|
362
|
+
scope: routine.scope,
|
|
363
|
+
kind: routine.kind,
|
|
364
|
+
path: routine.path
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function routinesEditCommand(name: string, options: { global?: boolean }): Promise<void> {
|
|
369
|
+
ensureValidRoutineName(name);
|
|
370
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
371
|
+
if (!routine) {
|
|
372
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
376
|
+
|
|
377
|
+
if (!editor || !process.stdout.isTTY || !process.stdin.isTTY) {
|
|
378
|
+
output({
|
|
379
|
+
action: 'edit',
|
|
380
|
+
name: routine.name,
|
|
381
|
+
path: routine.path,
|
|
382
|
+
hint: editor ? 'Run your editor with the path above' : 'Set $EDITOR (or $VISUAL) to enable `cli4ai routines edit`'
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = spawnSync(editor, [routine.path], {
|
|
388
|
+
stdio: 'inherit',
|
|
389
|
+
shell: true
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
process.exit(result.status ?? 0);
|
|
393
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai run - Execute a tool command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { outputError } from '../lib/cli.js';
|
|
6
|
+
import { executeTool, ExecuteToolError } from '../core/execute.js';
|
|
7
|
+
|
|
8
|
+
interface RunOptions {
|
|
9
|
+
env?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runCommand(
|
|
13
|
+
packageName: string,
|
|
14
|
+
command: string | undefined,
|
|
15
|
+
args: string[],
|
|
16
|
+
options: RunOptions
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
// Parse environment variables from options (-e KEY=value)
|
|
19
|
+
const extraEnv: Record<string, string> = {};
|
|
20
|
+
if (options.env) {
|
|
21
|
+
for (const envVar of options.env) {
|
|
22
|
+
const eqIndex = envVar.indexOf('=');
|
|
23
|
+
if (eqIndex > 0) {
|
|
24
|
+
extraEnv[envVar.slice(0, eqIndex)] = envVar.slice(eqIndex + 1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await executeTool({
|
|
31
|
+
packageName,
|
|
32
|
+
command,
|
|
33
|
+
args,
|
|
34
|
+
cwd: process.cwd(),
|
|
35
|
+
env: extraEnv,
|
|
36
|
+
capture: 'inherit'
|
|
37
|
+
});
|
|
38
|
+
process.exit(result.exitCode);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof ExecuteToolError) {
|
|
41
|
+
outputError(err.code, err.message, err.details);
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai search - Search for packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdirSync, existsSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
8
|
+
import { output, log } from '../lib/cli.js';
|
|
9
|
+
import { loadConfig, getGlobalPackages, getLocalPackages } from '../core/config.js';
|
|
10
|
+
import { tryLoadManifest, type Manifest } from '../core/manifest.js';
|
|
11
|
+
|
|
12
|
+
interface SearchOptions {
|
|
13
|
+
limit?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SearchResult {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
path?: string;
|
|
21
|
+
source: 'local-registry' | 'installed' | 'npm';
|
|
22
|
+
installed: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function searchCommand(query: string, options: SearchOptions): Promise<void> {
|
|
26
|
+
const limit = parseInt(options.limit || '20', 10);
|
|
27
|
+
const queryLower = query.toLowerCase();
|
|
28
|
+
|
|
29
|
+
const results: SearchResult[] = [];
|
|
30
|
+
const seen = new Set<string>();
|
|
31
|
+
|
|
32
|
+
// Get installed packages
|
|
33
|
+
const installedNames = new Set([
|
|
34
|
+
...getLocalPackages(process.cwd()).map(p => p.name),
|
|
35
|
+
...getGlobalPackages().map(p => p.name)
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Search local registries
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
for (const registryPath of config.localRegistries) {
|
|
41
|
+
if (!existsSync(registryPath)) continue;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
for (const entry of readdirSync(registryPath, { withFileTypes: true })) {
|
|
45
|
+
if (!entry.isDirectory()) continue;
|
|
46
|
+
if (seen.has(entry.name)) continue;
|
|
47
|
+
|
|
48
|
+
const pkgPath = resolve(registryPath, entry.name);
|
|
49
|
+
const manifest = tryLoadManifest(pkgPath);
|
|
50
|
+
|
|
51
|
+
if (!manifest) continue;
|
|
52
|
+
|
|
53
|
+
// Check if matches query
|
|
54
|
+
if (matches(manifest, queryLower)) {
|
|
55
|
+
seen.add(manifest.name);
|
|
56
|
+
results.push({
|
|
57
|
+
name: manifest.name,
|
|
58
|
+
version: manifest.version,
|
|
59
|
+
description: manifest.description,
|
|
60
|
+
path: pkgPath,
|
|
61
|
+
source: 'local-registry',
|
|
62
|
+
installed: installedNames.has(manifest.name)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (results.length >= limit) break;
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Skip inaccessible registries
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (results.length >= limit) break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Search npm for @cli4ai packages
|
|
76
|
+
if (results.length < limit) {
|
|
77
|
+
try {
|
|
78
|
+
log(`Searching npm for @cli4ai packages...`);
|
|
79
|
+
// Use spawnSync with argument array to prevent command injection
|
|
80
|
+
const searchResult = spawnSync('npm', ['search', `@cli4ai/${query}`, '--json'], {
|
|
81
|
+
encoding: 'utf-8',
|
|
82
|
+
timeout: 10000,
|
|
83
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let npmResults = searchResult.stdout || '';
|
|
87
|
+
|
|
88
|
+
// Fallback to searching @cli4ai if specific query fails
|
|
89
|
+
if (!npmResults || npmResults === '[]') {
|
|
90
|
+
const fallbackResult = spawnSync('npm', ['search', '@cli4ai', '--json'], {
|
|
91
|
+
encoding: 'utf-8',
|
|
92
|
+
timeout: 10000,
|
|
93
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
94
|
+
});
|
|
95
|
+
npmResults = fallbackResult.stdout || '[]';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const packages = JSON.parse(npmResults || '[]');
|
|
99
|
+
for (const pkg of packages) {
|
|
100
|
+
if (seen.has(pkg.name)) continue;
|
|
101
|
+
|
|
102
|
+
// Filter to only @cli4ai scoped packages
|
|
103
|
+
if (!pkg.name.startsWith('@cli4ai/')) continue;
|
|
104
|
+
|
|
105
|
+
// Check if matches query
|
|
106
|
+
const shortName = pkg.name.replace('@cli4ai/', '');
|
|
107
|
+
if (
|
|
108
|
+
shortName.toLowerCase().includes(queryLower) ||
|
|
109
|
+
pkg.description?.toLowerCase().includes(queryLower) ||
|
|
110
|
+
pkg.keywords?.some((k: string) => k.toLowerCase().includes(queryLower))
|
|
111
|
+
) {
|
|
112
|
+
seen.add(pkg.name);
|
|
113
|
+
results.push({
|
|
114
|
+
name: shortName,
|
|
115
|
+
version: pkg.version,
|
|
116
|
+
description: pkg.description,
|
|
117
|
+
source: 'npm',
|
|
118
|
+
installed: installedNames.has(shortName) || installedNames.has(pkg.name)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (results.length >= limit) break;
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// npm search failed, continue with local results
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
output({
|
|
130
|
+
query,
|
|
131
|
+
results: results.slice(0, limit),
|
|
132
|
+
count: results.length,
|
|
133
|
+
registries: config.localRegistries
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function matches(manifest: Manifest, query: string): boolean {
|
|
138
|
+
// Match against name
|
|
139
|
+
if (manifest.name.toLowerCase().includes(query)) return true;
|
|
140
|
+
|
|
141
|
+
// Match against description
|
|
142
|
+
if (manifest.description?.toLowerCase().includes(query)) return true;
|
|
143
|
+
|
|
144
|
+
// Match against keywords
|
|
145
|
+
if (manifest.keywords?.some(k => k.toLowerCase().includes(query))) return true;
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
}
|