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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai run - Execute a tool command
|
|
3
|
+
*/
|
|
4
|
+
import { outputError, log } from '../lib/cli.js';
|
|
5
|
+
import { executeTool, ExecuteToolError } from '../core/execute.js';
|
|
6
|
+
import { remoteRunTool, RemoteConnectionError, RemoteApiError } from '../core/remote-client.js';
|
|
7
|
+
import { getRemote } from '../core/remotes.js';
|
|
8
|
+
export async function runCommand(packageName, command, args, options) {
|
|
9
|
+
// Parse environment variables from options (-e KEY=value)
|
|
10
|
+
const extraEnv = {};
|
|
11
|
+
if (options.env) {
|
|
12
|
+
for (const envVar of options.env) {
|
|
13
|
+
const eqIndex = envVar.indexOf('=');
|
|
14
|
+
if (eqIndex > 0) {
|
|
15
|
+
extraEnv[envVar.slice(0, eqIndex)] = envVar.slice(eqIndex + 1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Validate scope option
|
|
20
|
+
let scope = 'full';
|
|
21
|
+
if (options.scope) {
|
|
22
|
+
const validScopes = ['read', 'write', 'full'];
|
|
23
|
+
if (!validScopes.includes(options.scope)) {
|
|
24
|
+
outputError('INVALID_INPUT', `Invalid scope: ${options.scope}`, {
|
|
25
|
+
validScopes,
|
|
26
|
+
hint: 'Use --scope read, --scope write, or --scope full'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
scope = options.scope;
|
|
30
|
+
}
|
|
31
|
+
// Parse timeout
|
|
32
|
+
let timeout;
|
|
33
|
+
if (options.timeout) {
|
|
34
|
+
timeout = parseInt(options.timeout, 10);
|
|
35
|
+
if (isNaN(timeout) || timeout < 0) {
|
|
36
|
+
outputError('INVALID_INPUT', 'Timeout must be a positive number (milliseconds)');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Remote execution
|
|
40
|
+
if (options.remote) {
|
|
41
|
+
const remote = getRemote(options.remote);
|
|
42
|
+
if (!remote) {
|
|
43
|
+
outputError('NOT_FOUND', `Remote "${options.remote}" not found`, {
|
|
44
|
+
hint: 'Use "cli4ai remotes add <name> <url>" to configure a remote'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
log(`Executing on remote: ${remote.name} (${remote.url})`);
|
|
48
|
+
try {
|
|
49
|
+
const result = await remoteRunTool(options.remote, {
|
|
50
|
+
package: packageName,
|
|
51
|
+
command,
|
|
52
|
+
args,
|
|
53
|
+
env: Object.keys(extraEnv).length > 0 ? extraEnv : undefined,
|
|
54
|
+
timeout,
|
|
55
|
+
scope
|
|
56
|
+
});
|
|
57
|
+
// Output stdout/stderr
|
|
58
|
+
if (result.stdout) {
|
|
59
|
+
process.stdout.write(result.stdout);
|
|
60
|
+
}
|
|
61
|
+
if (result.stderr) {
|
|
62
|
+
process.stderr.write(result.stderr);
|
|
63
|
+
}
|
|
64
|
+
process.exitCode = result.exitCode;
|
|
65
|
+
if (!result.success && result.error) {
|
|
66
|
+
log(`Remote error: ${result.error.message}`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof RemoteConnectionError) {
|
|
72
|
+
outputError('NETWORK_ERROR', err.message, { remote: options.remote, url: remote.url });
|
|
73
|
+
}
|
|
74
|
+
if (err instanceof RemoteApiError) {
|
|
75
|
+
outputError(err.code, err.message, err.details);
|
|
76
|
+
}
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
outputError('API_ERROR', message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Local execution
|
|
82
|
+
try {
|
|
83
|
+
const result = await executeTool({
|
|
84
|
+
packageName,
|
|
85
|
+
command,
|
|
86
|
+
args,
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
env: extraEnv,
|
|
89
|
+
capture: 'inherit',
|
|
90
|
+
timeoutMs: timeout,
|
|
91
|
+
scope,
|
|
92
|
+
sandbox: options.sandbox ?? false
|
|
93
|
+
});
|
|
94
|
+
process.exitCode = result.exitCode;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err instanceof ExecuteToolError) {
|
|
99
|
+
outputError(err.code, err.message, err.details);
|
|
100
|
+
}
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
outputError('API_ERROR', message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* cli4ai scheduler start [--foreground] # Start daemon
|
|
5
|
+
* cli4ai scheduler stop # Stop daemon
|
|
6
|
+
* cli4ai scheduler status # Show status + upcoming runs
|
|
7
|
+
* cli4ai scheduler logs [routine] # View logs
|
|
8
|
+
* cli4ai scheduler history [routine] # View execution history
|
|
9
|
+
* cli4ai scheduler run <routine> # Manual trigger
|
|
10
|
+
*/
|
|
11
|
+
interface SchedulerStartOptions {
|
|
12
|
+
foreground?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function schedulerStartCommand(options: SchedulerStartOptions): Promise<void>;
|
|
15
|
+
export declare function schedulerStopCommand(): Promise<void>;
|
|
16
|
+
export declare function schedulerStatusCommand(): Promise<void>;
|
|
17
|
+
interface SchedulerLogsOptions {
|
|
18
|
+
follow?: boolean;
|
|
19
|
+
lines?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function schedulerLogsCommand(options: SchedulerLogsOptions): Promise<void>;
|
|
22
|
+
interface SchedulerHistoryOptions {
|
|
23
|
+
limit?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function schedulerHistoryCommand(routineName?: string, options?: SchedulerHistoryOptions): Promise<void>;
|
|
26
|
+
export declare function schedulerRunCommand(routineName: string): Promise<void>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* cli4ai scheduler start [--foreground] # Start daemon
|
|
5
|
+
* cli4ai scheduler stop # Stop daemon
|
|
6
|
+
* cli4ai scheduler status # Show status + upcoming runs
|
|
7
|
+
* cli4ai scheduler logs [routine] # View logs
|
|
8
|
+
* cli4ai scheduler history [routine] # View execution history
|
|
9
|
+
* cli4ai scheduler run <routine> # Manual trigger
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { resolve, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
16
|
+
import { isDaemonRunning, getDaemonPid, removeDaemonPid, loadSchedulerState, getRunHistory, Scheduler, SCHEDULER_LOG_FILE, SCHEDULER_PID_FILE } from '../core/scheduler.js';
|
|
17
|
+
import { getScheduledRoutines } from '../core/routines.js';
|
|
18
|
+
export async function schedulerStartCommand(options) {
|
|
19
|
+
// Check if already running
|
|
20
|
+
if (isDaemonRunning()) {
|
|
21
|
+
const pid = getDaemonPid();
|
|
22
|
+
outputError('INVALID_INPUT', `Scheduler is already running (PID ${pid})`, {
|
|
23
|
+
hint: 'Use "cli4ai scheduler stop" to stop it first'
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Clean up stale PID file
|
|
28
|
+
if (existsSync(SCHEDULER_PID_FILE)) {
|
|
29
|
+
removeDaemonPid();
|
|
30
|
+
}
|
|
31
|
+
if (options.foreground) {
|
|
32
|
+
// Run in foreground (blocking)
|
|
33
|
+
log('Starting scheduler in foreground mode...');
|
|
34
|
+
log('Press Ctrl+C to stop');
|
|
35
|
+
const scheduler = new Scheduler({ projectDir: process.cwd() });
|
|
36
|
+
await scheduler.start();
|
|
37
|
+
// Keep running until signal
|
|
38
|
+
await new Promise((resolve) => {
|
|
39
|
+
process.on('SIGINT', async () => {
|
|
40
|
+
log('\nStopping scheduler...');
|
|
41
|
+
await scheduler.stop();
|
|
42
|
+
resolve();
|
|
43
|
+
});
|
|
44
|
+
process.on('SIGTERM', async () => {
|
|
45
|
+
await scheduler.stop();
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
output({ status: 'stopped' });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Run as daemon (background)
|
|
53
|
+
const daemonScript = resolve(dirname(fileURLToPath(import.meta.url)), '../core/scheduler-daemon.ts');
|
|
54
|
+
// Spawn with detached mode
|
|
55
|
+
const child = spawn('npx', ['tsx', daemonScript, '--project-dir', process.cwd()], {
|
|
56
|
+
detached: true,
|
|
57
|
+
stdio: 'ignore',
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
CLI4AI_DAEMON: 'true'
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// Unref to allow parent to exit
|
|
64
|
+
child.unref();
|
|
65
|
+
// Wait a moment for the daemon to write its PID
|
|
66
|
+
await new Promise(r => setTimeout(r, 500));
|
|
67
|
+
const pid = getDaemonPid();
|
|
68
|
+
if (pid && isDaemonRunning()) {
|
|
69
|
+
log(`Scheduler daemon started (PID ${pid})`);
|
|
70
|
+
output({ status: 'started', pid });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
outputError('API_ERROR', 'Failed to start scheduler daemon', {
|
|
74
|
+
hint: 'Check logs with "cli4ai scheduler logs"'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
// SCHEDULER STOP
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81
|
+
export async function schedulerStopCommand() {
|
|
82
|
+
const pid = getDaemonPid();
|
|
83
|
+
if (!pid) {
|
|
84
|
+
outputError('NOT_FOUND', 'Scheduler daemon is not running');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!isDaemonRunning()) {
|
|
88
|
+
// Clean up stale PID file
|
|
89
|
+
removeDaemonPid();
|
|
90
|
+
log('Scheduler was not running (cleaned up stale PID file)');
|
|
91
|
+
output({ status: 'stopped', pid: null });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
log(`Stopping scheduler daemon (PID ${pid})...`);
|
|
95
|
+
try {
|
|
96
|
+
// Send SIGTERM for graceful shutdown
|
|
97
|
+
process.kill(pid, 'SIGTERM');
|
|
98
|
+
// Wait for process to exit (up to 10 seconds)
|
|
99
|
+
const maxWait = 10000;
|
|
100
|
+
const checkInterval = 100;
|
|
101
|
+
let waited = 0;
|
|
102
|
+
while (waited < maxWait) {
|
|
103
|
+
await new Promise(r => setTimeout(r, checkInterval));
|
|
104
|
+
waited += checkInterval;
|
|
105
|
+
if (!isDaemonRunning()) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (isDaemonRunning()) {
|
|
110
|
+
// Force kill if still running
|
|
111
|
+
log('Scheduler did not stop gracefully, sending SIGKILL...');
|
|
112
|
+
process.kill(pid, 'SIGKILL');
|
|
113
|
+
await new Promise(r => setTimeout(r, 500));
|
|
114
|
+
}
|
|
115
|
+
removeDaemonPid();
|
|
116
|
+
log('Scheduler daemon stopped');
|
|
117
|
+
output({ status: 'stopped', pid });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err.code === 'ESRCH') {
|
|
121
|
+
// Process doesn't exist
|
|
122
|
+
removeDaemonPid();
|
|
123
|
+
log('Scheduler was not running (cleaned up stale PID file)');
|
|
124
|
+
output({ status: 'stopped', pid: null });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
outputError('API_ERROR', `Failed to stop scheduler: ${err instanceof Error ? err.message : String(err)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
132
|
+
// SCHEDULER STATUS
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
134
|
+
export async function schedulerStatusCommand() {
|
|
135
|
+
const pid = getDaemonPid();
|
|
136
|
+
const running = isDaemonRunning();
|
|
137
|
+
// Get scheduled routines
|
|
138
|
+
const scheduledRoutines = getScheduledRoutines(process.cwd());
|
|
139
|
+
const state = loadSchedulerState();
|
|
140
|
+
if (!running) {
|
|
141
|
+
log('Scheduler: not running');
|
|
142
|
+
if (scheduledRoutines.length === 0) {
|
|
143
|
+
log('\nNo scheduled routines found.');
|
|
144
|
+
log('Add a "schedule" field to your .routine.json files to enable scheduling.');
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
log(`\nFound ${scheduledRoutines.length} scheduled routine(s):`);
|
|
148
|
+
for (const routine of scheduledRoutines) {
|
|
149
|
+
const scheduleStr = routine.schedule.cron
|
|
150
|
+
? `cron: ${routine.schedule.cron}`
|
|
151
|
+
: `interval: ${routine.schedule.interval}`;
|
|
152
|
+
log(` - ${routine.name} (${scheduleStr})`);
|
|
153
|
+
}
|
|
154
|
+
log('\nStart the scheduler with: cli4ai scheduler start');
|
|
155
|
+
}
|
|
156
|
+
output({
|
|
157
|
+
running: false,
|
|
158
|
+
pid: null,
|
|
159
|
+
routines: scheduledRoutines.map(r => ({
|
|
160
|
+
name: r.name,
|
|
161
|
+
schedule: r.schedule,
|
|
162
|
+
path: r.path
|
|
163
|
+
}))
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
log(`Scheduler: running (PID ${pid})`);
|
|
168
|
+
if (state) {
|
|
169
|
+
log(`Started: ${state.startedAt}`);
|
|
170
|
+
const routineStates = Object.values(state.routines);
|
|
171
|
+
if (routineStates.length === 0) {
|
|
172
|
+
log('\nNo scheduled routines.');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
log(`\nScheduled routines (${routineStates.length}):`);
|
|
176
|
+
log('');
|
|
177
|
+
for (const routine of routineStates) {
|
|
178
|
+
const scheduleStr = routine.schedule.cron
|
|
179
|
+
? `cron: ${routine.schedule.cron}`
|
|
180
|
+
: `interval: ${routine.schedule.interval}`;
|
|
181
|
+
const statusIcon = routine.running ? '⏳' :
|
|
182
|
+
routine.lastStatus === 'success' ? '✓' :
|
|
183
|
+
routine.lastStatus === 'failed' ? '✗' : '○';
|
|
184
|
+
log(` ${statusIcon} ${routine.name}`);
|
|
185
|
+
log(` Schedule: ${scheduleStr}`);
|
|
186
|
+
if (routine.nextRunAt) {
|
|
187
|
+
const nextRun = new Date(routine.nextRunAt);
|
|
188
|
+
const now = new Date();
|
|
189
|
+
const diffMs = nextRun.getTime() - now.getTime();
|
|
190
|
+
const diffMins = Math.round(diffMs / 60000);
|
|
191
|
+
if (diffMins < 1) {
|
|
192
|
+
log(` Next run: in < 1 minute`);
|
|
193
|
+
}
|
|
194
|
+
else if (diffMins < 60) {
|
|
195
|
+
log(` Next run: in ${diffMins} minute${diffMins === 1 ? '' : 's'}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const diffHours = Math.round(diffMins / 60);
|
|
199
|
+
log(` Next run: in ${diffHours} hour${diffHours === 1 ? '' : 's'}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (routine.lastRunAt) {
|
|
203
|
+
log(` Last run: ${routine.lastRunAt} (${routine.lastStatus})`);
|
|
204
|
+
}
|
|
205
|
+
log('');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
output({
|
|
210
|
+
running: true,
|
|
211
|
+
pid,
|
|
212
|
+
startedAt: state?.startedAt,
|
|
213
|
+
routines: state ? Object.values(state.routines) : []
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
export async function schedulerLogsCommand(options) {
|
|
217
|
+
if (!existsSync(SCHEDULER_LOG_FILE)) {
|
|
218
|
+
log('No scheduler logs found.');
|
|
219
|
+
output({ logs: [] });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const lines = options.lines ?? 50;
|
|
223
|
+
if (options.follow) {
|
|
224
|
+
// Tail -f mode
|
|
225
|
+
log(`Tailing scheduler logs (${SCHEDULER_LOG_FILE})...`);
|
|
226
|
+
log('Press Ctrl+C to stop\n');
|
|
227
|
+
// Read initial content
|
|
228
|
+
const content = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
|
|
229
|
+
const allLines = content.split('\n');
|
|
230
|
+
const lastLines = allLines.slice(-lines);
|
|
231
|
+
process.stdout.write(lastLines.join('\n') + '\n');
|
|
232
|
+
// Watch for new content
|
|
233
|
+
const { watch } = await import('fs');
|
|
234
|
+
let lastSize = content.length;
|
|
235
|
+
const watcher = watch(SCHEDULER_LOG_FILE, (eventType) => {
|
|
236
|
+
if (eventType === 'change') {
|
|
237
|
+
const newContent = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
|
|
238
|
+
if (newContent.length > lastSize) {
|
|
239
|
+
process.stdout.write(newContent.slice(lastSize));
|
|
240
|
+
lastSize = newContent.length;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
// Keep running until interrupted
|
|
245
|
+
await new Promise((resolve) => {
|
|
246
|
+
let closed = false;
|
|
247
|
+
const closeWatcher = () => {
|
|
248
|
+
if (closed)
|
|
249
|
+
return;
|
|
250
|
+
closed = true;
|
|
251
|
+
try {
|
|
252
|
+
watcher.close();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const stop = () => {
|
|
259
|
+
closeWatcher();
|
|
260
|
+
resolve();
|
|
261
|
+
};
|
|
262
|
+
process.once('exit', closeWatcher);
|
|
263
|
+
process.once('SIGINT', stop);
|
|
264
|
+
process.once('SIGTERM', stop);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Read last N lines
|
|
269
|
+
const content = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
|
|
270
|
+
const allLines = content.split('\n').filter(l => l.trim());
|
|
271
|
+
const lastLines = allLines.slice(-lines);
|
|
272
|
+
for (const line of lastLines) {
|
|
273
|
+
log(line);
|
|
274
|
+
}
|
|
275
|
+
output({ logs: lastLines });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export async function schedulerHistoryCommand(routineName, options = {}) {
|
|
279
|
+
const limit = options.limit ?? 20;
|
|
280
|
+
const history = getRunHistory(routineName, limit);
|
|
281
|
+
if (history.length === 0) {
|
|
282
|
+
log(routineName
|
|
283
|
+
? `No execution history found for routine: ${routineName}`
|
|
284
|
+
: 'No execution history found.');
|
|
285
|
+
output({ history: [] });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
log(routineName
|
|
289
|
+
? `Execution history for ${routineName} (last ${history.length}):`
|
|
290
|
+
: `Execution history (last ${history.length}):`);
|
|
291
|
+
log('');
|
|
292
|
+
for (const record of history) {
|
|
293
|
+
const statusIcon = record.status === 'success' ? '✓' : '✗';
|
|
294
|
+
const duration = record.durationMs < 1000
|
|
295
|
+
? `${record.durationMs}ms`
|
|
296
|
+
: `${(record.durationMs / 1000).toFixed(1)}s`;
|
|
297
|
+
log(` ${statusIcon} ${record.routine}`);
|
|
298
|
+
log(` Started: ${record.startedAt}`);
|
|
299
|
+
log(` Duration: ${duration}`);
|
|
300
|
+
if (record.retryAttempt > 0) {
|
|
301
|
+
log(` Retry: #${record.retryAttempt}`);
|
|
302
|
+
}
|
|
303
|
+
if (record.error) {
|
|
304
|
+
log(` Error: ${record.error}`);
|
|
305
|
+
}
|
|
306
|
+
log('');
|
|
307
|
+
}
|
|
308
|
+
output({ history });
|
|
309
|
+
}
|
|
310
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
311
|
+
// SCHEDULER RUN
|
|
312
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
313
|
+
export async function schedulerRunCommand(routineName) {
|
|
314
|
+
// Check if routine exists and is scheduled
|
|
315
|
+
const scheduledRoutines = getScheduledRoutines(process.cwd());
|
|
316
|
+
const routine = scheduledRoutines.find(r => r.name === routineName);
|
|
317
|
+
if (!routine) {
|
|
318
|
+
// Check if it exists but isn't scheduled
|
|
319
|
+
const { resolveRoutine } = await import('../core/routines.js');
|
|
320
|
+
const exists = resolveRoutine(routineName, process.cwd());
|
|
321
|
+
if (exists) {
|
|
322
|
+
outputError('NOT_FOUND', `Routine "${routineName}" exists but has no schedule configured`, {
|
|
323
|
+
hint: 'Add a "schedule" field to enable scheduling, or use "cli4ai routines run" for one-time execution'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
outputError('NOT_FOUND', `Routine not found: ${routineName}`);
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
log(`Running routine: ${routineName}`);
|
|
332
|
+
const scheduler = new Scheduler({ projectDir: process.cwd() });
|
|
333
|
+
scheduler.refreshRoutines();
|
|
334
|
+
try {
|
|
335
|
+
const record = await scheduler.runNow(routineName);
|
|
336
|
+
if (record.status === 'success') {
|
|
337
|
+
log(`Routine completed successfully in ${record.durationMs}ms`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
log(`Routine failed (exit code ${record.exitCode})`);
|
|
341
|
+
if (record.error) {
|
|
342
|
+
log(`Error: ${record.error}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
output(record);
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
outputError('API_ERROR', `Failed to run routine: ${err instanceof Error ? err.message : String(err)}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai search - Search for packages
|
|
3
|
+
*/
|
|
4
|
+
import { readdirSync, existsSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
import { platform } from 'os';
|
|
8
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
9
|
+
// Windows-safe npm command
|
|
10
|
+
const npmCmd = platform() === 'win32' ? 'npm.cmd' : 'npm';
|
|
11
|
+
import { loadConfig, getGlobalPackages, getLocalPackages } from '../core/config.js';
|
|
12
|
+
import { tryLoadManifest } from '../core/manifest.js';
|
|
13
|
+
import { remoteListPackages, RemoteConnectionError, RemoteApiError } from '../core/remote-client.js';
|
|
14
|
+
export async function searchCommand(query, options) {
|
|
15
|
+
const limit = parseInt(options.limit || '20', 10);
|
|
16
|
+
const queryLower = query.toLowerCase();
|
|
17
|
+
// Handle remote search
|
|
18
|
+
if (options.remote) {
|
|
19
|
+
try {
|
|
20
|
+
const packageList = await remoteListPackages(options.remote);
|
|
21
|
+
const results = packageList.packages
|
|
22
|
+
.filter(pkg => pkg.name.toLowerCase().includes(queryLower) ||
|
|
23
|
+
pkg.path.toLowerCase().includes(queryLower))
|
|
24
|
+
.slice(0, limit)
|
|
25
|
+
.map(pkg => ({
|
|
26
|
+
name: pkg.name,
|
|
27
|
+
version: pkg.version,
|
|
28
|
+
source: 'remote',
|
|
29
|
+
installed: true
|
|
30
|
+
}));
|
|
31
|
+
output({
|
|
32
|
+
remote: options.remote,
|
|
33
|
+
query,
|
|
34
|
+
results,
|
|
35
|
+
count: results.length
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err instanceof RemoteConnectionError) {
|
|
40
|
+
outputError('NETWORK_ERROR', err.message, { remote: err.remoteName, url: err.url });
|
|
41
|
+
}
|
|
42
|
+
else if (err instanceof RemoteApiError) {
|
|
43
|
+
outputError(err.code, err.message, { remote: err.remoteName, details: err.details });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const results = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
// Get installed packages
|
|
54
|
+
const installedNames = new Set([
|
|
55
|
+
...getLocalPackages(process.cwd()).map(p => p.name),
|
|
56
|
+
...getGlobalPackages().map(p => p.name)
|
|
57
|
+
]);
|
|
58
|
+
// Search local registries
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
for (const registryPath of config.localRegistries) {
|
|
61
|
+
if (!existsSync(registryPath))
|
|
62
|
+
continue;
|
|
63
|
+
try {
|
|
64
|
+
for (const entry of readdirSync(registryPath, { withFileTypes: true })) {
|
|
65
|
+
if (!entry.isDirectory())
|
|
66
|
+
continue;
|
|
67
|
+
if (seen.has(entry.name))
|
|
68
|
+
continue;
|
|
69
|
+
const pkgPath = resolve(registryPath, entry.name);
|
|
70
|
+
const manifest = tryLoadManifest(pkgPath);
|
|
71
|
+
if (!manifest)
|
|
72
|
+
continue;
|
|
73
|
+
// Check if matches query
|
|
74
|
+
if (matches(manifest, queryLower)) {
|
|
75
|
+
seen.add(manifest.name);
|
|
76
|
+
results.push({
|
|
77
|
+
name: manifest.name,
|
|
78
|
+
version: manifest.version,
|
|
79
|
+
description: manifest.description,
|
|
80
|
+
path: pkgPath,
|
|
81
|
+
source: 'local-registry',
|
|
82
|
+
installed: installedNames.has(manifest.name)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (results.length >= limit)
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Skip inaccessible registries
|
|
91
|
+
}
|
|
92
|
+
if (results.length >= limit)
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
// Search npm for @cli4ai packages
|
|
96
|
+
if (results.length < limit) {
|
|
97
|
+
try {
|
|
98
|
+
log(`Searching npm for @cli4ai packages...`);
|
|
99
|
+
// Use spawnSync with argument array to prevent command injection
|
|
100
|
+
const searchResult = spawnSync(npmCmd, ['search', `@cli4ai/${query}`, '--json'], {
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
timeout: 10000,
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
104
|
+
});
|
|
105
|
+
let npmResults = searchResult.stdout || '';
|
|
106
|
+
// Fallback to searching @cli4ai if specific query fails
|
|
107
|
+
if (!npmResults || npmResults === '[]') {
|
|
108
|
+
const fallbackResult = spawnSync(npmCmd, ['search', '@cli4ai', '--json'], {
|
|
109
|
+
encoding: 'utf-8',
|
|
110
|
+
timeout: 10000,
|
|
111
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
112
|
+
});
|
|
113
|
+
npmResults = fallbackResult.stdout || '[]';
|
|
114
|
+
}
|
|
115
|
+
const packages = JSON.parse(npmResults || '[]');
|
|
116
|
+
for (const pkg of packages) {
|
|
117
|
+
if (seen.has(pkg.name))
|
|
118
|
+
continue;
|
|
119
|
+
// Filter to only @cli4ai scoped packages
|
|
120
|
+
if (!pkg.name.startsWith('@cli4ai/'))
|
|
121
|
+
continue;
|
|
122
|
+
// Check if matches query
|
|
123
|
+
const shortName = pkg.name.replace('@cli4ai/', '');
|
|
124
|
+
if (shortName.toLowerCase().includes(queryLower) ||
|
|
125
|
+
pkg.description?.toLowerCase().includes(queryLower) ||
|
|
126
|
+
pkg.keywords?.some((k) => k.toLowerCase().includes(queryLower))) {
|
|
127
|
+
seen.add(pkg.name);
|
|
128
|
+
results.push({
|
|
129
|
+
name: shortName,
|
|
130
|
+
version: pkg.version,
|
|
131
|
+
description: pkg.description,
|
|
132
|
+
source: 'npm',
|
|
133
|
+
installed: installedNames.has(shortName) || installedNames.has(pkg.name)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (results.length >= limit)
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// npm search failed, continue with local results
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
output({
|
|
145
|
+
query,
|
|
146
|
+
results: results.slice(0, limit),
|
|
147
|
+
count: results.length,
|
|
148
|
+
registries: config.localRegistries
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function matches(manifest, query) {
|
|
152
|
+
// Match against name
|
|
153
|
+
if (manifest.name.toLowerCase().includes(query))
|
|
154
|
+
return true;
|
|
155
|
+
// Match against description
|
|
156
|
+
if (manifest.description?.toLowerCase().includes(query))
|
|
157
|
+
return true;
|
|
158
|
+
// Match against keywords
|
|
159
|
+
if (manifest.keywords?.some(k => k.toLowerCase().includes(query)))
|
|
160
|
+
return true;
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai secrets - Manage secrets for CLI tools
|
|
3
|
+
*/
|
|
4
|
+
interface SecretsOptions {
|
|
5
|
+
package?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* cli4ai secrets set <key> [value]
|
|
9
|
+
*/
|
|
10
|
+
export declare function secretsSetCommand(key: string, value?: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* cli4ai secrets get <key>
|
|
13
|
+
*/
|
|
14
|
+
export declare function secretsGetCommand(key: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* cli4ai secrets list
|
|
17
|
+
*/
|
|
18
|
+
export declare function secretsListCommand(options: SecretsOptions): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* cli4ai secrets delete <key>
|
|
21
|
+
*/
|
|
22
|
+
export declare function secretsDeleteCommand(key: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* cli4ai secrets init [package]
|
|
25
|
+
* Interactive setup for a package's required secrets
|
|
26
|
+
*/
|
|
27
|
+
export declare function secretsInitCommand(packageName?: string, options?: SecretsOptions): Promise<void>;
|
|
28
|
+
export {};
|