cli4ai 1.2.0 → 1.2.2
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 +39 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.js +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +335 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +464 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +382 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +121 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +125 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +458 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +76 -0
- package/dist/commands/mcp-config.d.ts +10 -0
- package/dist/commands/mcp-config.js +49 -0
- package/dist/commands/remotes.d.ts +22 -0
- package/dist/commands/remotes.js +196 -0
- package/dist/commands/remove.d.ts +8 -0
- package/dist/commands/remove.js +61 -0
- package/dist/commands/routines.d.ts +29 -0
- package/dist/commands/routines.js +363 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/scheduler.d.ts +27 -0
- package/dist/commands/scheduler.js +350 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.js +162 -0
- package/dist/commands/secrets.d.ts +28 -0
- package/dist/commands/secrets.js +236 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +210 -0
- package/dist/core/config.d.ts +91 -0
- package/dist/core/config.js +738 -0
- package/dist/core/execute.d.ts +51 -0
- package/dist/core/execute.js +475 -0
- package/dist/core/link.d.ts +39 -0
- package/dist/core/link.js +214 -0
- package/dist/core/lockfile.d.ts +63 -0
- package/dist/core/lockfile.js +140 -0
- package/dist/core/manifest.d.ts +96 -0
- package/dist/core/manifest.js +224 -0
- package/dist/core/registry.d.ts +74 -0
- package/dist/core/registry.js +116 -0
- package/dist/core/remote-client.d.ts +98 -0
- package/dist/core/remote-client.js +252 -0
- package/dist/core/remotes.d.ts +88 -0
- package/dist/core/remotes.js +206 -0
- package/dist/core/routine-engine.d.ts +124 -0
- package/dist/core/routine-engine.js +699 -0
- package/dist/core/routines.d.ts +36 -0
- package/dist/core/routines.js +132 -0
- package/dist/core/scheduler-daemon.d.ts +10 -0
- package/dist/core/scheduler-daemon.js +77 -0
- package/dist/core/scheduler.d.ts +131 -0
- package/dist/core/scheduler.js +492 -0
- package/dist/core/secrets.d.ts +48 -0
- package/dist/core/secrets.js +384 -0
- package/dist/lib/cli.d.ts +84 -0
- package/dist/lib/cli.js +216 -0
- package/dist/mcp/adapter.d.ts +35 -0
- package/dist/mcp/adapter.js +94 -0
- package/dist/mcp/config-gen.d.ts +31 -0
- package/dist/mcp/config-gen.js +75 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +296 -0
- package/dist/server/service.d.ts +85 -0
- package/dist/server/service.js +304 -0
- package/package.json +6 -3
- package/src/bin.ts +0 -118
- package/src/cli.ts +0 -412
- package/src/commands/add.ts +0 -562
- package/src/commands/browse.ts +0 -449
- package/src/commands/config.ts +0 -154
- package/src/commands/info.ts +0 -133
- package/src/commands/init.ts +0 -514
- package/src/commands/list.ts +0 -95
- package/src/commands/mcp-config.ts +0 -69
- package/src/commands/remotes.ts +0 -253
- package/src/commands/remove.ts +0 -78
- package/src/commands/routines.ts +0 -427
- package/src/commands/run.ts +0 -127
- package/src/commands/scheduler.ts +0 -438
- package/src/commands/search.ts +0 -185
- package/src/commands/secrets.ts +0 -292
- package/src/commands/serve.ts +0 -66
- package/src/commands/start.ts +0 -40
- package/src/commands/update.ts +0 -252
- package/src/core/config.ts +0 -845
- package/src/core/execute.ts +0 -569
- package/src/core/link.ts +0 -246
- package/src/core/lockfile.ts +0 -187
- package/src/core/manifest.ts +0 -327
- package/src/core/registry.ts +0 -165
- package/src/core/remote-client.ts +0 -419
- package/src/core/remotes.ts +0 -268
- package/src/core/routine-engine.ts +0 -895
- package/src/core/routines.ts +0 -171
- package/src/core/scheduler-daemon.ts +0 -94
- package/src/core/scheduler.ts +0 -606
- package/src/core/secrets.ts +0 -430
- package/src/lib/cli.ts +0 -261
- package/src/mcp/adapter.ts +0 -131
- package/src/mcp/config-gen.ts +0 -106
- package/src/mcp/server.ts +0 -365
- package/src/server/service.ts +0 -434
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote hosts management command.
|
|
3
|
+
*
|
|
4
|
+
* Manage connections to remote cli4ai instances for distributed execution.
|
|
5
|
+
*/
|
|
6
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
7
|
+
import { getRemotes, getRemote, addRemote, updateRemote, removeRemote, RemoteNotFoundError, RemoteAlreadyExistsError, InvalidRemoteUrlError } from '../core/remotes.js';
|
|
8
|
+
import { testRemoteConnection, remoteListPackages } from '../core/remote-client.js';
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
// LIST REMOTES
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
export async function remotesListCommand() {
|
|
13
|
+
const remotes = getRemotes();
|
|
14
|
+
if (remotes.length === 0) {
|
|
15
|
+
output({ remotes: [], message: 'No remotes configured. Use "cli4ai remotes add <name> <url>" to add one.' });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
output({
|
|
19
|
+
remotes: remotes.map(r => ({
|
|
20
|
+
name: r.name,
|
|
21
|
+
url: r.url,
|
|
22
|
+
description: r.description,
|
|
23
|
+
hasApiKey: !!r.apiKey,
|
|
24
|
+
addedAt: r.addedAt,
|
|
25
|
+
lastConnected: r.lastConnected
|
|
26
|
+
}))
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export async function remotesAddCommand(name, url, options) {
|
|
30
|
+
try {
|
|
31
|
+
const remote = addRemote(name, url, {
|
|
32
|
+
apiKey: options.apiKey,
|
|
33
|
+
description: options.description
|
|
34
|
+
});
|
|
35
|
+
log(`Added remote: ${remote.name} -> ${remote.url}`);
|
|
36
|
+
// Test connection if requested
|
|
37
|
+
if (options.test !== false) {
|
|
38
|
+
log('Testing connection...');
|
|
39
|
+
const result = await testRemoteConnection(name);
|
|
40
|
+
if (result.success) {
|
|
41
|
+
log(`Connection successful: ${result.message}`);
|
|
42
|
+
output({
|
|
43
|
+
remote: {
|
|
44
|
+
name: remote.name,
|
|
45
|
+
url: remote.url,
|
|
46
|
+
description: remote.description,
|
|
47
|
+
hasApiKey: !!remote.apiKey
|
|
48
|
+
},
|
|
49
|
+
connectionTest: result
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
log(`Warning: Connection failed - ${result.message}`);
|
|
54
|
+
log('The remote was added but could not be reached. Check the URL and try again.');
|
|
55
|
+
output({
|
|
56
|
+
remote: {
|
|
57
|
+
name: remote.name,
|
|
58
|
+
url: remote.url,
|
|
59
|
+
description: remote.description,
|
|
60
|
+
hasApiKey: !!remote.apiKey
|
|
61
|
+
},
|
|
62
|
+
connectionTest: result
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
output({
|
|
68
|
+
remote: {
|
|
69
|
+
name: remote.name,
|
|
70
|
+
url: remote.url,
|
|
71
|
+
description: remote.description,
|
|
72
|
+
hasApiKey: !!remote.apiKey
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err instanceof RemoteAlreadyExistsError) {
|
|
79
|
+
outputError('ALREADY_EXISTS', `Remote "${err.remoteName}" already exists. Use "cli4ai remotes update" to modify it.`);
|
|
80
|
+
}
|
|
81
|
+
if (err instanceof InvalidRemoteUrlError) {
|
|
82
|
+
outputError('INVALID_INPUT', `Invalid URL "${err.url}": ${err.reason}`);
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export async function remotesUpdateCommand(name, options) {
|
|
88
|
+
try {
|
|
89
|
+
// Check if any update provided
|
|
90
|
+
if (!options.url && !options.apiKey && !options.description) {
|
|
91
|
+
outputError('INVALID_INPUT', 'No updates provided. Use --url, --api-key, or --description.');
|
|
92
|
+
}
|
|
93
|
+
const remote = updateRemote(name, options);
|
|
94
|
+
output({
|
|
95
|
+
updated: true,
|
|
96
|
+
remote: {
|
|
97
|
+
name: remote.name,
|
|
98
|
+
url: remote.url,
|
|
99
|
+
description: remote.description,
|
|
100
|
+
hasApiKey: !!remote.apiKey
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err instanceof RemoteNotFoundError) {
|
|
106
|
+
outputError('NOT_FOUND', `Remote "${err.remoteName}" not found`);
|
|
107
|
+
}
|
|
108
|
+
if (err instanceof InvalidRemoteUrlError) {
|
|
109
|
+
outputError('INVALID_INPUT', `Invalid URL "${err.url}": ${err.reason}`);
|
|
110
|
+
}
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
115
|
+
// REMOVE REMOTE
|
|
116
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
117
|
+
export async function remotesRemoveCommand(name) {
|
|
118
|
+
try {
|
|
119
|
+
removeRemote(name);
|
|
120
|
+
output({ removed: true, name });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (err instanceof RemoteNotFoundError) {
|
|
124
|
+
outputError('NOT_FOUND', `Remote "${err.remoteName}" not found`);
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
130
|
+
// SHOW REMOTE
|
|
131
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
132
|
+
export async function remotesShowCommand(name) {
|
|
133
|
+
const remote = getRemote(name);
|
|
134
|
+
if (!remote) {
|
|
135
|
+
outputError('NOT_FOUND', `Remote "${name}" not found`);
|
|
136
|
+
}
|
|
137
|
+
output({
|
|
138
|
+
name: remote.name,
|
|
139
|
+
url: remote.url,
|
|
140
|
+
description: remote.description,
|
|
141
|
+
hasApiKey: !!remote.apiKey,
|
|
142
|
+
addedAt: remote.addedAt,
|
|
143
|
+
lastConnected: remote.lastConnected
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
147
|
+
// TEST REMOTE
|
|
148
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
149
|
+
export async function remotesTestCommand(name) {
|
|
150
|
+
const remote = getRemote(name);
|
|
151
|
+
if (!remote) {
|
|
152
|
+
outputError('NOT_FOUND', `Remote "${name}" not found`);
|
|
153
|
+
}
|
|
154
|
+
log(`Testing connection to ${remote.name} (${remote.url})...`);
|
|
155
|
+
const result = await testRemoteConnection(name);
|
|
156
|
+
if (result.success) {
|
|
157
|
+
log(`Connection successful!`);
|
|
158
|
+
output({
|
|
159
|
+
name,
|
|
160
|
+
url: remote.url,
|
|
161
|
+
connected: true,
|
|
162
|
+
...result.details
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
log(`Connection failed: ${result.message}`);
|
|
167
|
+
output({
|
|
168
|
+
name,
|
|
169
|
+
url: remote.url,
|
|
170
|
+
connected: false,
|
|
171
|
+
error: result.message
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
176
|
+
// LIST PACKAGES ON REMOTE
|
|
177
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
178
|
+
export async function remotesPackagesCommand(name) {
|
|
179
|
+
const remote = getRemote(name);
|
|
180
|
+
if (!remote) {
|
|
181
|
+
outputError('NOT_FOUND', `Remote "${name}" not found`);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const result = await remoteListPackages(name);
|
|
185
|
+
output({
|
|
186
|
+
remote: name,
|
|
187
|
+
packages: result.packages
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof Error) {
|
|
192
|
+
outputError('API_ERROR', `Failed to list packages on remote "${name}": ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai remove - Uninstall packages
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, rmSync, lstatSync, unlinkSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
7
|
+
import { PACKAGES_DIR, LOCAL_PACKAGES_DIR } from '../core/config.js';
|
|
8
|
+
import { unlockPackage } from '../core/lockfile.js';
|
|
9
|
+
import { unlinkPackage as unlinkFromPath } from '../core/link.js';
|
|
10
|
+
const PKG_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
11
|
+
export async function removeCommand(packages, options) {
|
|
12
|
+
const results = [];
|
|
13
|
+
const errors = [];
|
|
14
|
+
const targetDir = options.global ? PACKAGES_DIR : resolve(process.cwd(), LOCAL_PACKAGES_DIR);
|
|
15
|
+
const projectDir = process.cwd();
|
|
16
|
+
for (const pkg of packages) {
|
|
17
|
+
if (!PKG_NAME_PATTERN.test(pkg)) {
|
|
18
|
+
errors.push({ package: pkg, error: 'Invalid package name' });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const pkgPath = resolve(targetDir, pkg);
|
|
22
|
+
if (!existsSync(pkgPath)) {
|
|
23
|
+
errors.push({ package: pkg, error: 'Not installed' });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const stat = lstatSync(pkgPath);
|
|
28
|
+
if (stat.isSymbolicLink()) {
|
|
29
|
+
unlinkSync(pkgPath);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
rmSync(pkgPath, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
results.push({ name: pkg, path: pkgPath });
|
|
35
|
+
if (options.global) {
|
|
36
|
+
// Remove from PATH
|
|
37
|
+
unlinkFromPath(pkg);
|
|
38
|
+
log(`- ${pkg} (unlinked from PATH)`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
log(`- ${pkg}`);
|
|
42
|
+
// Update lockfile (only for local/project installs)
|
|
43
|
+
unlockPackage(projectDir, pkg);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
errors.push({
|
|
48
|
+
package: pkg,
|
|
49
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (errors.length > 0 && results.length === 0) {
|
|
54
|
+
outputError('INSTALL_ERROR', 'Failed to remove packages', { errors });
|
|
55
|
+
}
|
|
56
|
+
output({
|
|
57
|
+
removed: results,
|
|
58
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
59
|
+
location: options.global ? 'global' : 'local'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai routines - Manage and run routines
|
|
3
|
+
*/
|
|
4
|
+
interface RoutinesListOptions {
|
|
5
|
+
global?: boolean;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface RoutinesRunOptions {
|
|
9
|
+
global?: boolean;
|
|
10
|
+
var?: string[];
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface RoutinesCreateOptions {
|
|
14
|
+
global?: boolean;
|
|
15
|
+
type?: 'yaml' | 'json' | 'bash';
|
|
16
|
+
}
|
|
17
|
+
export declare function routinesListCommand(options: RoutinesListOptions): Promise<void>;
|
|
18
|
+
export declare function routinesCreateCommand(name: string, options: RoutinesCreateOptions): Promise<void>;
|
|
19
|
+
export declare function routinesRunCommand(name: string, args: string[], options: RoutinesRunOptions): Promise<void>;
|
|
20
|
+
export declare function routinesShowCommand(name: string, options: {
|
|
21
|
+
global?: boolean;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
export declare function routinesRemoveCommand(name: string, options: {
|
|
24
|
+
global?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
export declare function routinesEditCommand(name: string, options: {
|
|
27
|
+
global?: boolean;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai routines - Manage and run routines
|
|
3
|
+
*/
|
|
4
|
+
import { spawn, spawnSync } from 'child_process';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync, rmSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
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 } from '../core/routines.js';
|
|
11
|
+
import { loadRoutineDefinition, dryRunRoutine, runRoutine, RoutineParseError, RoutineValidationError, RoutineTemplateError } from '../core/routine-engine.js';
|
|
12
|
+
function ensureValidRoutineName(name) {
|
|
13
|
+
try {
|
|
14
|
+
validateRoutineName(name);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
outputError('INVALID_INPUT', err instanceof Error ? err.message : String(err), { name });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function normalizeEnvSegment(value) {
|
|
21
|
+
return value
|
|
22
|
+
.trim()
|
|
23
|
+
.toUpperCase()
|
|
24
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
25
|
+
.replace(/^_+|_+$/g, '');
|
|
26
|
+
}
|
|
27
|
+
function parseVars(vars) {
|
|
28
|
+
const result = {};
|
|
29
|
+
if (!vars)
|
|
30
|
+
return result;
|
|
31
|
+
for (const entry of vars) {
|
|
32
|
+
const eq = entry.indexOf('=');
|
|
33
|
+
if (eq <= 0) {
|
|
34
|
+
outputError('INVALID_INPUT', `Invalid --var (expected KEY=value): ${entry}`);
|
|
35
|
+
}
|
|
36
|
+
const key = entry.slice(0, eq).trim();
|
|
37
|
+
const value = entry.slice(eq + 1);
|
|
38
|
+
if (!key) {
|
|
39
|
+
outputError('INVALID_INPUT', `Invalid --var (empty key): ${entry}`);
|
|
40
|
+
}
|
|
41
|
+
result[key] = value;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function varsToEnv(vars) {
|
|
46
|
+
const env = {};
|
|
47
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
48
|
+
const envKey = `CLI4AI_VAR_${normalizeEnvSegment(key)}`;
|
|
49
|
+
env[envKey] = value;
|
|
50
|
+
}
|
|
51
|
+
return env;
|
|
52
|
+
}
|
|
53
|
+
function collectStream(stream) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const chunks = [];
|
|
56
|
+
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
57
|
+
stream.on('error', reject);
|
|
58
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export async function routinesListCommand(options) {
|
|
62
|
+
const local = options.global ? [] : getLocalRoutines(process.cwd());
|
|
63
|
+
const global = getGlobalRoutines();
|
|
64
|
+
const routines = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const r of local) {
|
|
67
|
+
routines.push(r);
|
|
68
|
+
seen.add(r.name);
|
|
69
|
+
}
|
|
70
|
+
for (const r of global) {
|
|
71
|
+
if (!seen.has(r.name)) {
|
|
72
|
+
routines.push(r);
|
|
73
|
+
seen.add(r.name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (options.json || !process.stdout.isTTY) {
|
|
77
|
+
output({
|
|
78
|
+
routines: routines.map(r => ({
|
|
79
|
+
name: r.name,
|
|
80
|
+
kind: r.kind,
|
|
81
|
+
scope: r.scope,
|
|
82
|
+
path: r.path
|
|
83
|
+
})),
|
|
84
|
+
count: routines.length
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (routines.length === 0) {
|
|
89
|
+
console.log('No routines found');
|
|
90
|
+
console.log('\nCreate one: cli4ai routines create <name>\n');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log(`\nRoutines (${routines.length}):\n`);
|
|
94
|
+
for (const r of routines) {
|
|
95
|
+
const scopeTag = r.scope === 'local' ? '' : ' (global)';
|
|
96
|
+
console.log(` ${r.name}${scopeTag} [${r.kind}]`);
|
|
97
|
+
console.log(` ${r.path}`);
|
|
98
|
+
}
|
|
99
|
+
console.log('');
|
|
100
|
+
}
|
|
101
|
+
function getTargetDir(global, projectDir) {
|
|
102
|
+
if (global) {
|
|
103
|
+
ensureCli4aiHome();
|
|
104
|
+
return ROUTINES_DIR;
|
|
105
|
+
}
|
|
106
|
+
ensureLocalDir(projectDir);
|
|
107
|
+
return resolve(projectDir, LOCAL_ROUTINES_DIR);
|
|
108
|
+
}
|
|
109
|
+
export async function routinesCreateCommand(name, options) {
|
|
110
|
+
ensureValidRoutineName(name);
|
|
111
|
+
const projectDir = process.cwd();
|
|
112
|
+
const type = options.type ?? 'yaml';
|
|
113
|
+
if (type !== 'yaml' && type !== 'bash' && type !== 'json') {
|
|
114
|
+
outputError('INVALID_INPUT', `Invalid routine type: ${type}`, {
|
|
115
|
+
allowed: ['yaml', 'json', 'bash']
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const dir = getTargetDir(!!options.global, projectDir);
|
|
119
|
+
const suffixMap = {
|
|
120
|
+
yaml: '.routine.yaml',
|
|
121
|
+
json: '.routine.json',
|
|
122
|
+
bash: '.routine.sh'
|
|
123
|
+
};
|
|
124
|
+
const suffix = suffixMap[type];
|
|
125
|
+
const filePath = resolve(dir, `${name}${suffix}`);
|
|
126
|
+
if (existsSync(filePath)) {
|
|
127
|
+
outputError('INVALID_INPUT', `Routine already exists: ${name}`, { path: filePath });
|
|
128
|
+
}
|
|
129
|
+
if (type === 'yaml') {
|
|
130
|
+
const template = {
|
|
131
|
+
version: 1,
|
|
132
|
+
name,
|
|
133
|
+
description: 'TODO: describe this routine',
|
|
134
|
+
vars: {},
|
|
135
|
+
steps: [],
|
|
136
|
+
result: '{{steps}}'
|
|
137
|
+
};
|
|
138
|
+
writeFileSync(filePath, stringifyYaml(template));
|
|
139
|
+
}
|
|
140
|
+
else if (type === 'json') {
|
|
141
|
+
const template = {
|
|
142
|
+
version: 1,
|
|
143
|
+
name,
|
|
144
|
+
description: 'TODO: describe this routine',
|
|
145
|
+
vars: {},
|
|
146
|
+
steps: [],
|
|
147
|
+
result: '{{steps}}'
|
|
148
|
+
};
|
|
149
|
+
writeFileSync(filePath, JSON.stringify(template, null, 2) + '\n');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const template = `#!/usr/bin/env bash
|
|
153
|
+
set -euo pipefail
|
|
154
|
+
|
|
155
|
+
# Example:
|
|
156
|
+
# cli4ai run github trending
|
|
157
|
+
#
|
|
158
|
+
# Vars passed via --var KEY=value are available as env vars:
|
|
159
|
+
# CLI4AI_VAR_KEY=value
|
|
160
|
+
|
|
161
|
+
echo "TODO: implement ${name}" 1>&2
|
|
162
|
+
`;
|
|
163
|
+
writeFileSync(filePath, template);
|
|
164
|
+
try {
|
|
165
|
+
chmodSync(filePath, 0o755);
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
}
|
|
169
|
+
output({
|
|
170
|
+
action: 'create',
|
|
171
|
+
name,
|
|
172
|
+
type,
|
|
173
|
+
scope: options.global ? 'global' : 'local',
|
|
174
|
+
path: filePath
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export async function routinesRunCommand(name, args, options) {
|
|
178
|
+
ensureValidRoutineName(name);
|
|
179
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
180
|
+
if (!routine) {
|
|
181
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`, {
|
|
182
|
+
hint: options.global
|
|
183
|
+
? 'Create a global routine in ~/.cli4ai/routines'
|
|
184
|
+
: 'Create a routine in ./.cli4ai/routines or ~/.cli4ai/routines'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const vars = parseVars(options.var);
|
|
188
|
+
// Structured routines (YAML or JSON) use the routine engine
|
|
189
|
+
const isStructured = routine.kind === 'yaml' || routine.kind === 'json';
|
|
190
|
+
if (options.dryRun) {
|
|
191
|
+
if (isStructured) {
|
|
192
|
+
try {
|
|
193
|
+
const def = loadRoutineDefinition(routine.path);
|
|
194
|
+
const plan = await dryRunRoutine(def, vars, process.cwd());
|
|
195
|
+
output({
|
|
196
|
+
kind: routine.kind,
|
|
197
|
+
scope: routine.scope,
|
|
198
|
+
path: routine.path,
|
|
199
|
+
...plan
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
if (err instanceof RoutineParseError) {
|
|
205
|
+
outputError('PARSE_ERROR', err.message, { path: err.path });
|
|
206
|
+
}
|
|
207
|
+
if (err instanceof RoutineValidationError || err instanceof RoutineTemplateError) {
|
|
208
|
+
outputError('INVALID_INPUT', err.message, err.details);
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const plan = {
|
|
214
|
+
routine: routine.name,
|
|
215
|
+
kind: routine.kind,
|
|
216
|
+
scope: routine.scope,
|
|
217
|
+
path: routine.path,
|
|
218
|
+
vars,
|
|
219
|
+
exec: { cmd: 'bash', args: [routine.path, ...args] }
|
|
220
|
+
};
|
|
221
|
+
if (!process.stdout.isTTY) {
|
|
222
|
+
output(plan);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
console.log(`\nRoutine: ${routine.name} [${routine.kind}]`);
|
|
226
|
+
console.log(`Path: ${routine.path}`);
|
|
227
|
+
console.log(`Vars: ${Object.keys(vars).length ? JSON.stringify(vars) : '(none)'}`);
|
|
228
|
+
console.log(`\nWould run:\n bash ${routine.path}${args.length ? ' ' + args.join(' ') : ''}\n`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (isStructured) {
|
|
232
|
+
try {
|
|
233
|
+
const def = loadRoutineDefinition(routine.path);
|
|
234
|
+
const summary = await runRoutine(def, vars, process.cwd());
|
|
235
|
+
output({
|
|
236
|
+
kind: routine.kind,
|
|
237
|
+
scope: routine.scope,
|
|
238
|
+
path: routine.path,
|
|
239
|
+
...summary
|
|
240
|
+
});
|
|
241
|
+
process.exit(summary.exitCode);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
if (err instanceof RoutineParseError) {
|
|
245
|
+
outputError('PARSE_ERROR', err.message, { path: err.path });
|
|
246
|
+
}
|
|
247
|
+
if (err instanceof RoutineValidationError || err instanceof RoutineTemplateError) {
|
|
248
|
+
outputError('INVALID_INPUT', err.message, err.details);
|
|
249
|
+
}
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const startTime = Date.now();
|
|
254
|
+
const isTTY = process.stdout.isTTY;
|
|
255
|
+
const varEnv = varsToEnv(vars);
|
|
256
|
+
const child = spawn('bash', [routine.path, ...args], {
|
|
257
|
+
cwd: process.cwd(),
|
|
258
|
+
stdio: isTTY ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
259
|
+
env: {
|
|
260
|
+
...process.env,
|
|
261
|
+
...varEnv,
|
|
262
|
+
C4AI_ROUTINE_NAME: routine.name,
|
|
263
|
+
C4AI_ROUTINE_PATH: routine.path
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
if (isTTY) {
|
|
267
|
+
const exitCode = await new Promise((resolve) => {
|
|
268
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
269
|
+
child.on('error', (err) => outputError('API_ERROR', `Failed to execute routine: ${err.message}`));
|
|
270
|
+
});
|
|
271
|
+
process.exit(exitCode);
|
|
272
|
+
}
|
|
273
|
+
// Non-TTY: capture and emit single JSON summary.
|
|
274
|
+
const stdoutPromise = child.stdout ? collectStream(child.stdout) : Promise.resolve('');
|
|
275
|
+
const stderrPromise = child.stderr ? collectStream(child.stderr) : Promise.resolve('');
|
|
276
|
+
if (child.stderr) {
|
|
277
|
+
child.stderr.on('data', (chunk) => {
|
|
278
|
+
process.stderr.write(chunk);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const exitCode = await new Promise((resolve) => {
|
|
282
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
283
|
+
child.on('error', (err) => outputError('API_ERROR', `Failed to execute routine: ${err.message}`));
|
|
284
|
+
});
|
|
285
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
286
|
+
output({
|
|
287
|
+
routine: routine.name,
|
|
288
|
+
kind: routine.kind,
|
|
289
|
+
scope: routine.scope,
|
|
290
|
+
status: exitCode === 0 ? 'success' : 'failed',
|
|
291
|
+
exitCode,
|
|
292
|
+
durationMs: Date.now() - startTime,
|
|
293
|
+
stdout,
|
|
294
|
+
stderr
|
|
295
|
+
});
|
|
296
|
+
process.exit(exitCode);
|
|
297
|
+
}
|
|
298
|
+
export async function routinesShowCommand(name, options) {
|
|
299
|
+
ensureValidRoutineName(name);
|
|
300
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
301
|
+
if (!routine) {
|
|
302
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`);
|
|
303
|
+
}
|
|
304
|
+
const content = readFileSync(routine.path, 'utf-8');
|
|
305
|
+
if (!process.stdout.isTTY) {
|
|
306
|
+
output({ routine: routine.name, kind: routine.kind, scope: routine.scope, path: routine.path, content });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
log(`\n${routine.path}\n`);
|
|
310
|
+
process.stdout.write(content);
|
|
311
|
+
if (!content.endsWith('\n'))
|
|
312
|
+
process.stdout.write('\n');
|
|
313
|
+
}
|
|
314
|
+
export async function routinesRemoveCommand(name, options) {
|
|
315
|
+
ensureValidRoutineName(name);
|
|
316
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
317
|
+
if (!routine) {
|
|
318
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`);
|
|
319
|
+
}
|
|
320
|
+
rmSync(routine.path, { force: true });
|
|
321
|
+
output({
|
|
322
|
+
action: 'remove',
|
|
323
|
+
name: routine.name,
|
|
324
|
+
scope: routine.scope,
|
|
325
|
+
kind: routine.kind,
|
|
326
|
+
path: routine.path
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
export async function routinesEditCommand(name, options) {
|
|
330
|
+
ensureValidRoutineName(name);
|
|
331
|
+
const routine = resolveRoutine(name, process.cwd(), { globalOnly: options.global });
|
|
332
|
+
if (!routine) {
|
|
333
|
+
outputError('NOT_FOUND', `Routine not found: ${name}`);
|
|
334
|
+
}
|
|
335
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
336
|
+
if (!editor || !process.stdout.isTTY || !process.stdin.isTTY) {
|
|
337
|
+
output({
|
|
338
|
+
action: 'edit',
|
|
339
|
+
name: routine.name,
|
|
340
|
+
path: routine.path,
|
|
341
|
+
hint: editor ? 'Run your editor with the path above' : 'Set $EDITOR (or $VISUAL) to enable `cli4ai routines edit`'
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// SECURITY: Parse editor command to prevent shell injection
|
|
346
|
+
// Common editors may have arguments like "code --wait" or "vim -c 'set nu'"
|
|
347
|
+
// We split on spaces but this is a simple approach; complex cases should use exec wrapper
|
|
348
|
+
const editorParts = editor.trim().split(/\s+/);
|
|
349
|
+
const editorCmd = editorParts[0];
|
|
350
|
+
const editorArgs = [...editorParts.slice(1), routine.path];
|
|
351
|
+
// Validate editor command doesn't contain shell metacharacters in the base command
|
|
352
|
+
if (!/^[a-zA-Z0-9_.\-\/]+$/.test(editorCmd)) {
|
|
353
|
+
outputError('INVALID_INPUT', 'Invalid $EDITOR value - contains unsafe characters', {
|
|
354
|
+
editor: editorCmd,
|
|
355
|
+
hint: 'Set $EDITOR to a simple command like "vim", "nano", or "code --wait"'
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const result = spawnSync(editorCmd, editorArgs, {
|
|
359
|
+
stdio: 'inherit'
|
|
360
|
+
// SECURITY: shell: false (default) prevents command injection
|
|
361
|
+
});
|
|
362
|
+
process.exit(result.status ?? 1);
|
|
363
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai run - Execute a tool command
|
|
3
|
+
*/
|
|
4
|
+
interface RunOptions {
|
|
5
|
+
env?: string[];
|
|
6
|
+
scope?: string;
|
|
7
|
+
sandbox?: boolean;
|
|
8
|
+
remote?: string;
|
|
9
|
+
timeout?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runCommand(packageName: string, command: string | undefined, args: string[], options: RunOptions): Promise<void>;
|
|
12
|
+
export {};
|