cli4ai 0.8.3 → 0.9.1
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/package.json +8 -8
- package/src/cli.ts +1 -1
- package/src/commands/add.ts +4 -6
- package/src/commands/info.ts +1 -1
- package/src/commands/init.test.ts +61 -13
- package/src/commands/init.ts +177 -223
- package/src/commands/routines.ts +18 -3
- package/src/commands/scheduler.ts +1 -1
- package/src/commands/update.ts +22 -6
- package/src/core/config.test.ts +2 -2
- package/src/core/config.ts +13 -2
- package/src/core/execute.ts +33 -50
- package/src/core/link.test.ts +5 -5
- package/src/core/link.ts +19 -22
- package/src/core/lockfile.test.ts +1 -1
- package/src/core/manifest.test.ts +2 -2
- package/src/core/manifest.ts +12 -4
- package/src/core/routine-engine.test.ts +1 -1
- package/src/core/scheduler.test.ts +3 -3
- package/src/core/scheduler.ts +7 -2
- package/src/core/secrets.test.ts +1 -1
- package/src/lib/cli.ts +1 -1
- package/src/mcp/adapter.test.ts +1 -1
- package/src/mcp/config-gen.test.ts +1 -1
- package/src/mcp/server.ts +11 -17
package/src/commands/init.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { createManifest, MANIFEST_FILENAME, type Manifest } from '../core/manife
|
|
|
9
9
|
|
|
10
10
|
interface InitOptions {
|
|
11
11
|
template?: string;
|
|
12
|
-
runtime?: '
|
|
12
|
+
runtime?: 'node' | 'bun';
|
|
13
13
|
yes?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ export async function initCommand(name: string | undefined, options: InitOptions
|
|
|
18
18
|
const targetDir = name ? resolve(process.cwd(), name) : process.cwd();
|
|
19
19
|
const manifestPath = resolve(targetDir, MANIFEST_FILENAME);
|
|
20
20
|
const templateName = options.template || 'basic';
|
|
21
|
-
const runtime = options.runtime || '
|
|
21
|
+
const runtime = options.runtime || 'node';
|
|
22
22
|
|
|
23
23
|
// Check if cli4ai.json already exists
|
|
24
24
|
if (existsSync(manifestPath)) {
|
|
@@ -46,7 +46,7 @@ export async function initCommand(name: string | undefined, options: InitOptions
|
|
|
46
46
|
// Create entry file if it doesn't exist
|
|
47
47
|
const entryPath = resolve(targetDir, manifest.entry);
|
|
48
48
|
if (!existsSync(entryPath)) {
|
|
49
|
-
const template = getTemplate(templateName,
|
|
49
|
+
const template = getTemplate(templateName, manifest);
|
|
50
50
|
writeFileSync(entryPath, template);
|
|
51
51
|
log(`Created ${manifest.entry}`);
|
|
52
52
|
}
|
|
@@ -54,7 +54,7 @@ export async function initCommand(name: string | undefined, options: InitOptions
|
|
|
54
54
|
// Create helpful extras (non-destructive)
|
|
55
55
|
const readmePath = resolve(targetDir, 'README.md');
|
|
56
56
|
if (!existsSync(readmePath)) {
|
|
57
|
-
writeFileSync(readmePath, getReadmeTemplate(templateName,
|
|
57
|
+
writeFileSync(readmePath, getReadmeTemplate(templateName, manifest));
|
|
58
58
|
log('Created README.md');
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -66,11 +66,32 @@ export async function initCommand(name: string | undefined, options: InitOptions
|
|
|
66
66
|
|
|
67
67
|
const pkgJsonPath = resolve(targetDir, 'package.json');
|
|
68
68
|
if (!existsSync(pkgJsonPath)) {
|
|
69
|
-
const pkgJson = getDevPackageJson(templateName,
|
|
69
|
+
const pkgJson = getDevPackageJson(templateName, manifest);
|
|
70
70
|
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n');
|
|
71
71
|
log('Created package.json');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Create test file
|
|
75
|
+
const testPath = resolve(targetDir, manifest.entry.replace(/\.ts$/, '.test.ts'));
|
|
76
|
+
if (!existsSync(testPath)) {
|
|
77
|
+
writeFileSync(testPath, getTestTemplate(templateName, manifest));
|
|
78
|
+
log(`Created ${basename(testPath)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create tsconfig.json for TypeScript support
|
|
82
|
+
const tsconfigPath = resolve(targetDir, 'tsconfig.json');
|
|
83
|
+
if (!existsSync(tsconfigPath)) {
|
|
84
|
+
writeFileSync(tsconfigPath, getTsconfigTemplate());
|
|
85
|
+
log('Created tsconfig.json');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create vitest.config.ts
|
|
89
|
+
const vitestConfigPath = resolve(targetDir, 'vitest.config.ts');
|
|
90
|
+
if (!existsSync(vitestConfigPath)) {
|
|
91
|
+
writeFileSync(vitestConfigPath, getVitestConfigTemplate());
|
|
92
|
+
log('Created vitest.config.ts');
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
// Template-specific extras
|
|
75
96
|
if (templateName === 'api') {
|
|
76
97
|
const envExamplePath = resolve(targetDir, '.env.example');
|
|
@@ -80,14 +101,17 @@ export async function initCommand(name: string | undefined, options: InitOptions
|
|
|
80
101
|
}
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
const nextSteps = getNextSteps(name, targetDir, templateName,
|
|
104
|
+
const nextSteps = getNextSteps(name, targetDir, templateName, manifest);
|
|
84
105
|
|
|
85
106
|
output({
|
|
86
107
|
created: {
|
|
87
108
|
manifest: manifestPath,
|
|
88
109
|
entry: entryPath,
|
|
110
|
+
test: testPath,
|
|
89
111
|
readme: readmePath,
|
|
90
112
|
gitignore: gitignorePath,
|
|
113
|
+
tsconfig: tsconfigPath,
|
|
114
|
+
vitestConfig: vitestConfigPath,
|
|
91
115
|
packageJson: existsSync(resolve(targetDir, 'package.json')) ? resolve(targetDir, 'package.json') : undefined,
|
|
92
116
|
},
|
|
93
117
|
name: manifest.name,
|
|
@@ -109,7 +133,7 @@ function guessAuthor(): string | undefined {
|
|
|
109
133
|
return candidates[0];
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
function getBaseManifest(templateName: string, runtime: '
|
|
136
|
+
function getBaseManifest(templateName: string, runtime: 'node' | 'bun'): Partial<Manifest> {
|
|
113
137
|
const author = guessAuthor();
|
|
114
138
|
const keywords = Array.from(new Set([
|
|
115
139
|
'cli4ai',
|
|
@@ -118,7 +142,7 @@ function getBaseManifest(templateName: string, runtime: 'bun' | 'node'): Partial
|
|
|
118
142
|
]));
|
|
119
143
|
|
|
120
144
|
return {
|
|
121
|
-
entry:
|
|
145
|
+
entry: 'run.ts',
|
|
122
146
|
runtime,
|
|
123
147
|
description: `${templateName === 'basic' ? 'CLI' : templateName} tool`,
|
|
124
148
|
author,
|
|
@@ -128,7 +152,7 @@ function getBaseManifest(templateName: string, runtime: 'bun' | 'node'): Partial
|
|
|
128
152
|
};
|
|
129
153
|
}
|
|
130
154
|
|
|
131
|
-
function getTemplateManifest(templateName: string, runtime: '
|
|
155
|
+
function getTemplateManifest(templateName: string, runtime: 'node' | 'bun'): Partial<Manifest> {
|
|
132
156
|
switch (templateName) {
|
|
133
157
|
case 'api':
|
|
134
158
|
return {
|
|
@@ -144,7 +168,7 @@ function getTemplateManifest(templateName: string, runtime: 'bun' | 'node'): Par
|
|
|
144
168
|
description: 'API key for the service you are calling (stored via `cli4ai secrets`)',
|
|
145
169
|
}
|
|
146
170
|
},
|
|
147
|
-
dependencies: getDefaultDependencies(templateName
|
|
171
|
+
dependencies: getDefaultDependencies(templateName),
|
|
148
172
|
};
|
|
149
173
|
case 'browser':
|
|
150
174
|
return {
|
|
@@ -160,7 +184,7 @@ function getTemplateManifest(templateName: string, runtime: 'bun' | 'node'): Par
|
|
|
160
184
|
]
|
|
161
185
|
}
|
|
162
186
|
},
|
|
163
|
-
dependencies: getDefaultDependencies(templateName
|
|
187
|
+
dependencies: getDefaultDependencies(templateName),
|
|
164
188
|
};
|
|
165
189
|
default:
|
|
166
190
|
return {
|
|
@@ -170,23 +194,16 @@ function getTemplateManifest(templateName: string, runtime: 'bun' | 'node'): Par
|
|
|
170
194
|
args: [{ name: 'name', required: false }]
|
|
171
195
|
}
|
|
172
196
|
},
|
|
173
|
-
dependencies: getDefaultDependencies(templateName
|
|
197
|
+
dependencies: getDefaultDependencies(templateName),
|
|
174
198
|
};
|
|
175
199
|
}
|
|
176
200
|
}
|
|
177
201
|
|
|
178
|
-
function getDefaultDependencies(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Prefer shared SDK helpers when running on bun (or node with TS support, if that lands later).
|
|
185
|
-
if (runtime === 'bun') {
|
|
186
|
-
deps['@cli4ai/lib'] = '^1.0.0';
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
deps['commander'] = '^14.0.0';
|
|
202
|
+
function getDefaultDependencies(templateName: string): Record<string, string> | undefined {
|
|
203
|
+
const deps: Record<string, string> = {
|
|
204
|
+
'@cli4ai/lib': '^1.0.0',
|
|
205
|
+
'commander': '^14.0.0',
|
|
206
|
+
};
|
|
190
207
|
|
|
191
208
|
if (templateName === 'browser') {
|
|
192
209
|
deps['puppeteer'] = '^24.0.0';
|
|
@@ -195,28 +212,24 @@ function getDefaultDependencies(
|
|
|
195
212
|
return deps;
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
function getTemplate(templateName: string,
|
|
199
|
-
if (runtime === 'node') {
|
|
200
|
-
return getNodeTemplate(templateName, manifest);
|
|
201
|
-
}
|
|
202
|
-
|
|
215
|
+
function getTemplate(templateName: string, manifest: Manifest): string {
|
|
203
216
|
switch (templateName) {
|
|
204
217
|
case 'api':
|
|
205
|
-
return
|
|
218
|
+
return getApiTemplate(manifest);
|
|
206
219
|
case 'browser':
|
|
207
|
-
return
|
|
220
|
+
return getBrowserTemplate(manifest);
|
|
208
221
|
default:
|
|
209
|
-
return
|
|
222
|
+
return getBasicTemplate(manifest);
|
|
210
223
|
}
|
|
211
224
|
}
|
|
212
225
|
|
|
213
|
-
function
|
|
214
|
-
return `#!/usr/bin/env
|
|
226
|
+
function getBasicTemplate(manifest: Manifest): string {
|
|
227
|
+
return `#!/usr/bin/env npx tsx
|
|
215
228
|
/**
|
|
216
229
|
* ${manifest.name} - ${manifest.description || 'A cli4ai tool'}
|
|
217
230
|
*/
|
|
218
231
|
|
|
219
|
-
import { cli, output } from '@cli4ai/lib
|
|
232
|
+
import { cli, output } from '@cli4ai/lib';
|
|
220
233
|
|
|
221
234
|
const program = cli('${manifest.name}', '${manifest.version}', '${manifest.description || manifest.name}');
|
|
222
235
|
|
|
@@ -232,13 +245,13 @@ program.parse();
|
|
|
232
245
|
`;
|
|
233
246
|
}
|
|
234
247
|
|
|
235
|
-
function
|
|
236
|
-
return `#!/usr/bin/env
|
|
248
|
+
function getApiTemplate(manifest: Manifest): string {
|
|
249
|
+
return `#!/usr/bin/env npx tsx
|
|
237
250
|
/**
|
|
238
251
|
* ${manifest.name} - ${manifest.description || 'API wrapper tool'}
|
|
239
252
|
*/
|
|
240
253
|
|
|
241
|
-
import { cli, env, output, outputError, withErrorHandling } from '@cli4ai/lib
|
|
254
|
+
import { cli, env, output, outputError, withErrorHandling } from '@cli4ai/lib';
|
|
242
255
|
|
|
243
256
|
const program = cli('${manifest.name}', '${manifest.version}', '${manifest.description || manifest.name}');
|
|
244
257
|
|
|
@@ -249,33 +262,29 @@ program
|
|
|
249
262
|
// Use \`cli4ai secrets set API_KEY\` to store this securely (cli4ai injects it at runtime).
|
|
250
263
|
const apiKey = env('API_KEY');
|
|
251
264
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
if (!res.ok) {
|
|
258
|
-
outputError('API_ERROR', \`HTTP \${res.status}: \${res.statusText}\`);
|
|
259
|
-
}
|
|
265
|
+
const res = await fetch(\`https://api.example.com/\${endpoint}\`, {
|
|
266
|
+
headers: { 'Authorization': \`Bearer \${apiKey}\` }
|
|
267
|
+
});
|
|
260
268
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
outputError('NETWORK_ERROR', err instanceof Error ? err.message : String(err));
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
outputError('API_ERROR', \`HTTP \${res.status}: \${res.statusText}\`);
|
|
264
271
|
}
|
|
272
|
+
|
|
273
|
+
output(await res.json());
|
|
265
274
|
}));
|
|
266
275
|
|
|
267
276
|
program.parse();
|
|
268
277
|
`;
|
|
269
278
|
}
|
|
270
279
|
|
|
271
|
-
function
|
|
272
|
-
return `#!/usr/bin/env
|
|
280
|
+
function getBrowserTemplate(manifest: Manifest): string {
|
|
281
|
+
return `#!/usr/bin/env npx tsx
|
|
273
282
|
/**
|
|
274
283
|
* ${manifest.name} - ${manifest.description || 'Browser automation tool'}
|
|
275
284
|
*/
|
|
276
285
|
|
|
277
286
|
import puppeteer from 'puppeteer';
|
|
278
|
-
import { cli, output, outputError, withErrorHandling } from '@cli4ai/lib
|
|
287
|
+
import { cli, output, outputError, withErrorHandling } from '@cli4ai/lib';
|
|
279
288
|
|
|
280
289
|
const program = cli('${manifest.name}', '${manifest.version}', '${manifest.description || manifest.name}');
|
|
281
290
|
|
|
@@ -297,161 +306,6 @@ program
|
|
|
297
306
|
screenshot: options.output,
|
|
298
307
|
timestamp: new Date().toISOString()
|
|
299
308
|
});
|
|
300
|
-
} catch (err) {
|
|
301
|
-
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
302
|
-
} finally {
|
|
303
|
-
await browser.close();
|
|
304
|
-
}
|
|
305
|
-
}));
|
|
306
|
-
|
|
307
|
-
program.parse();
|
|
308
|
-
`;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function getNodeTemplate(templateName: string, manifest: Manifest): string {
|
|
312
|
-
switch (templateName) {
|
|
313
|
-
case 'api':
|
|
314
|
-
return getNodeApiTemplate(manifest);
|
|
315
|
-
case 'browser':
|
|
316
|
-
return getNodeBrowserTemplate(manifest);
|
|
317
|
-
default:
|
|
318
|
-
return getNodeBasicTemplate(manifest);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function getNodeBasicTemplate(manifest: Manifest): string {
|
|
323
|
-
return `#!/usr/bin/env node
|
|
324
|
-
/**
|
|
325
|
-
* ${manifest.name} - ${manifest.description || 'A cli4ai tool'}
|
|
326
|
-
*/
|
|
327
|
-
|
|
328
|
-
import { Command } from 'commander';
|
|
329
|
-
|
|
330
|
-
const output = (data) => console.log(JSON.stringify(data, null, 2));
|
|
331
|
-
const outputError = (code, message, details) => {
|
|
332
|
-
console.error(JSON.stringify({ error: code, message, ...(details || {}) }));
|
|
333
|
-
process.exit(1);
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const withErrorHandling = (fn) => async (...args) => {
|
|
337
|
-
try {
|
|
338
|
-
await fn(...args);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const program = new Command()
|
|
345
|
-
.name('${manifest.name}')
|
|
346
|
-
.version('${manifest.version}', '-v, --version', 'Show version')
|
|
347
|
-
.description('${manifest.description || manifest.name}');
|
|
348
|
-
|
|
349
|
-
program
|
|
350
|
-
.command('hello [name]')
|
|
351
|
-
.description('Say hello')
|
|
352
|
-
.action(withErrorHandling(async (name) => {
|
|
353
|
-
const who = (name || '').trim() || 'world';
|
|
354
|
-
output({ message: \`Hello, \${who}!\` });
|
|
355
|
-
}));
|
|
356
|
-
|
|
357
|
-
program.parse();
|
|
358
|
-
`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function getNodeApiTemplate(manifest: Manifest): string {
|
|
362
|
-
return `#!/usr/bin/env node
|
|
363
|
-
/**
|
|
364
|
-
* ${manifest.name} - ${manifest.description || 'API wrapper tool'}
|
|
365
|
-
*/
|
|
366
|
-
|
|
367
|
-
import { Command } from 'commander';
|
|
368
|
-
|
|
369
|
-
const output = (data) => console.log(JSON.stringify(data, null, 2));
|
|
370
|
-
const outputError = (code, message, details) => {
|
|
371
|
-
console.error(JSON.stringify({ error: code, message, ...(details || {}) }));
|
|
372
|
-
process.exit(1);
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
const withErrorHandling = (fn) => async (...args) => {
|
|
376
|
-
try {
|
|
377
|
-
await fn(...args);
|
|
378
|
-
} catch (err) {
|
|
379
|
-
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
const env = (name) => {
|
|
384
|
-
const value = process.env[name];
|
|
385
|
-
if (!value) outputError('ENV_MISSING', \`Missing required environment variable: \${name}\`);
|
|
386
|
-
return value;
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
const program = new Command()
|
|
390
|
-
.name('${manifest.name}')
|
|
391
|
-
.version('${manifest.version}', '-v, --version', 'Show version')
|
|
392
|
-
.description('${manifest.description || manifest.name}');
|
|
393
|
-
|
|
394
|
-
program
|
|
395
|
-
.command('fetch <endpoint>')
|
|
396
|
-
.description('Fetch JSON from an API endpoint')
|
|
397
|
-
.action(withErrorHandling(async (endpoint) => {
|
|
398
|
-
// Use \`cli4ai secrets set API_KEY\` to store this securely (cli4ai injects it at runtime).
|
|
399
|
-
const apiKey = env('API_KEY');
|
|
400
|
-
const res = await fetch(\`https://api.example.com/\${endpoint}\`, {
|
|
401
|
-
headers: { Authorization: \`Bearer \${apiKey}\` },
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
if (!res.ok) {
|
|
405
|
-
outputError('API_ERROR', \`HTTP \${res.status}: \${res.statusText}\`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
output(await res.json());
|
|
409
|
-
}));
|
|
410
|
-
|
|
411
|
-
program.parse();
|
|
412
|
-
`;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function getNodeBrowserTemplate(manifest: Manifest): string {
|
|
416
|
-
return `#!/usr/bin/env node
|
|
417
|
-
/**
|
|
418
|
-
* ${manifest.name} - ${manifest.description || 'Browser automation tool'}
|
|
419
|
-
*/
|
|
420
|
-
|
|
421
|
-
import { Command } from 'commander';
|
|
422
|
-
import puppeteer from 'puppeteer';
|
|
423
|
-
|
|
424
|
-
const output = (data) => console.log(JSON.stringify(data, null, 2));
|
|
425
|
-
const outputError = (code, message, details) => {
|
|
426
|
-
console.error(JSON.stringify({ error: code, message, ...(details || {}) }));
|
|
427
|
-
process.exit(1);
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const withErrorHandling = (fn) => async (...args) => {
|
|
431
|
-
try {
|
|
432
|
-
await fn(...args);
|
|
433
|
-
} catch (err) {
|
|
434
|
-
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const program = new Command()
|
|
439
|
-
.name('${manifest.name}')
|
|
440
|
-
.version('${manifest.version}', '-v, --version', 'Show version')
|
|
441
|
-
.description('${manifest.description || manifest.name}');
|
|
442
|
-
|
|
443
|
-
program
|
|
444
|
-
.command('screenshot <url>')
|
|
445
|
-
.description('Take a screenshot of a webpage')
|
|
446
|
-
.option('-o, --output <file>', 'Output file', 'screenshot.png')
|
|
447
|
-
.option('--full-page', 'Capture full page')
|
|
448
|
-
.action(withErrorHandling(async (url, options) => {
|
|
449
|
-
const browser = await puppeteer.launch({ headless: true });
|
|
450
|
-
try {
|
|
451
|
-
const page = await browser.newPage();
|
|
452
|
-
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
453
|
-
await page.screenshot({ path: options.output, fullPage: options.fullPage });
|
|
454
|
-
output({ url, screenshot: options.output, timestamp: new Date().toISOString() });
|
|
455
309
|
} finally {
|
|
456
310
|
await browser.close();
|
|
457
311
|
}
|
|
@@ -461,11 +315,7 @@ program.parse();
|
|
|
461
315
|
`;
|
|
462
316
|
}
|
|
463
317
|
|
|
464
|
-
function getReadmeTemplate(templateName: string,
|
|
465
|
-
const runCmd =
|
|
466
|
-
runtime === 'node' ? `node ${manifest.entry}` :
|
|
467
|
-
`bun run ${manifest.entry}`;
|
|
468
|
-
|
|
318
|
+
function getReadmeTemplate(templateName: string, manifest: Manifest): string {
|
|
469
319
|
const exampleCmd =
|
|
470
320
|
templateName === 'api' ? 'fetch users' :
|
|
471
321
|
templateName === 'browser' ? 'screenshot https://example.com' :
|
|
@@ -475,10 +325,16 @@ function getReadmeTemplate(templateName: string, runtime: 'bun' | 'node', manife
|
|
|
475
325
|
|
|
476
326
|
${manifest.description || ''}
|
|
477
327
|
|
|
328
|
+
## Setup
|
|
329
|
+
|
|
330
|
+
\`\`\`bash
|
|
331
|
+
npm install
|
|
332
|
+
\`\`\`
|
|
333
|
+
|
|
478
334
|
## Run directly
|
|
479
335
|
|
|
480
336
|
\`\`\`bash
|
|
481
|
-
|
|
337
|
+
npx tsx run.ts ${exampleCmd}
|
|
482
338
|
\`\`\`
|
|
483
339
|
|
|
484
340
|
## Install with cli4ai (project-scoped)
|
|
@@ -489,6 +345,12 @@ cli4ai add --local /path/to/${manifest.name} -y
|
|
|
489
345
|
cli4ai run ${manifest.name} ${exampleCmd}
|
|
490
346
|
\`\`\`
|
|
491
347
|
|
|
348
|
+
## Testing
|
|
349
|
+
|
|
350
|
+
\`\`\`bash
|
|
351
|
+
npm test
|
|
352
|
+
\`\`\`
|
|
353
|
+
|
|
492
354
|
## MCP
|
|
493
355
|
|
|
494
356
|
\`\`\`bash
|
|
@@ -504,12 +366,93 @@ function getGitignoreTemplate(): string {
|
|
|
504
366
|
.env.*
|
|
505
367
|
dist
|
|
506
368
|
.DS_Store
|
|
369
|
+
*.log
|
|
370
|
+
coverage
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function getTsconfigTemplate(): string {
|
|
375
|
+
return `{
|
|
376
|
+
"compilerOptions": {
|
|
377
|
+
"target": "ES2022",
|
|
378
|
+
"module": "NodeNext",
|
|
379
|
+
"moduleResolution": "NodeNext",
|
|
380
|
+
"strict": true,
|
|
381
|
+
"esModuleInterop": true,
|
|
382
|
+
"skipLibCheck": true,
|
|
383
|
+
"forceConsistentCasingInFileNames": true,
|
|
384
|
+
"resolveJsonModule": true,
|
|
385
|
+
"declaration": false,
|
|
386
|
+
"noEmit": true
|
|
387
|
+
},
|
|
388
|
+
"include": ["*.ts", "lib/**/*.ts"],
|
|
389
|
+
"exclude": ["node_modules"]
|
|
390
|
+
}
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getVitestConfigTemplate(): string {
|
|
395
|
+
return `import { defineConfig } from 'vitest/config';
|
|
396
|
+
|
|
397
|
+
export default defineConfig({
|
|
398
|
+
test: {
|
|
399
|
+
globals: true,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getTestTemplate(templateName: string, manifest: Manifest): string {
|
|
406
|
+
const testCmd = templateName === 'api' ? 'fetch' : templateName === 'browser' ? 'screenshot' : 'hello';
|
|
407
|
+
const envSetup = templateName === 'api' ? `
|
|
408
|
+
// Set required env vars before importing
|
|
409
|
+
process.env.API_KEY = 'test-api-key';
|
|
410
|
+
` : '';
|
|
411
|
+
|
|
412
|
+
return `import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
413
|
+
${envSetup}
|
|
414
|
+
describe('${manifest.name}', () => {
|
|
415
|
+
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
|
416
|
+
let logs: string[];
|
|
417
|
+
|
|
418
|
+
beforeEach(() => {
|
|
419
|
+
logs = [];
|
|
420
|
+
consoleSpy = vi.spyOn(console, 'log').mockImplementation((msg: string) => {
|
|
421
|
+
logs.push(msg);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
afterEach(() => {
|
|
426
|
+
consoleSpy.mockRestore();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('${testCmd} command', () => {
|
|
430
|
+
test('should output valid JSON', async () => {
|
|
431
|
+
// TODO: Import and test your command handler
|
|
432
|
+
// Example:
|
|
433
|
+
// import { ${testCmd}Command } from './run';
|
|
434
|
+
// await ${testCmd}Command('test-arg');
|
|
435
|
+
// expect(logs.length).toBeGreaterThan(0);
|
|
436
|
+
// const result = JSON.parse(logs[0]);
|
|
437
|
+
// expect(result).toBeDefined();
|
|
438
|
+
|
|
439
|
+
expect(true).toBe(true); // Placeholder - replace with real test
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('should handle invalid input', async () => {
|
|
443
|
+
// TODO: Test error handling
|
|
444
|
+
// Example:
|
|
445
|
+
// expect(() => ${testCmd}Command('')).toThrow();
|
|
446
|
+
|
|
447
|
+
expect(true).toBe(true); // Placeholder - replace with real test
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
});
|
|
507
451
|
`;
|
|
508
452
|
}
|
|
509
453
|
|
|
510
454
|
function getDevPackageJson(
|
|
511
455
|
templateName: string,
|
|
512
|
-
runtime: 'bun' | 'node',
|
|
513
456
|
manifest: Manifest
|
|
514
457
|
): Record<string, unknown> {
|
|
515
458
|
const deps = manifest.dependencies || {};
|
|
@@ -520,11 +463,19 @@ function getDevPackageJson(
|
|
|
520
463
|
private: true,
|
|
521
464
|
type: 'module',
|
|
522
465
|
bin: {
|
|
523
|
-
[manifest.name]:
|
|
466
|
+
[manifest.name]: `./run.ts`,
|
|
524
467
|
},
|
|
525
468
|
dependencies: deps,
|
|
469
|
+
devDependencies: {
|
|
470
|
+
'typescript': '^5.0.0',
|
|
471
|
+
'tsx': '^4.0.0',
|
|
472
|
+
'vitest': '^2.0.0',
|
|
473
|
+
'@types/node': '^22.0.0',
|
|
474
|
+
},
|
|
526
475
|
scripts: {
|
|
527
|
-
dev:
|
|
476
|
+
dev: 'tsx run.ts',
|
|
477
|
+
test: 'vitest run',
|
|
478
|
+
'test:watch': 'vitest',
|
|
528
479
|
},
|
|
529
480
|
};
|
|
530
481
|
}
|
|
@@ -533,7 +484,6 @@ function getNextSteps(
|
|
|
533
484
|
nameArg: string | undefined,
|
|
534
485
|
targetDir: string,
|
|
535
486
|
templateName: string,
|
|
536
|
-
runtime: 'bun' | 'node',
|
|
537
487
|
manifest: Manifest
|
|
538
488
|
): Array<{ description: string; command: string }> {
|
|
539
489
|
const exampleArgs =
|
|
@@ -541,16 +491,20 @@ function getNextSteps(
|
|
|
541
491
|
templateName === 'browser' ? 'screenshot https://example.com' :
|
|
542
492
|
'hello world';
|
|
543
493
|
|
|
544
|
-
const runDirect =
|
|
545
|
-
runtime === 'node' ? `node ${manifest.entry} ${exampleArgs}` :
|
|
546
|
-
`bun run ${manifest.entry} ${exampleArgs}`;
|
|
547
|
-
|
|
548
494
|
const installPath = nameArg ? `./${basename(targetDir)}` : '.';
|
|
549
495
|
|
|
550
496
|
return [
|
|
551
497
|
{
|
|
552
|
-
description: '
|
|
553
|
-
command:
|
|
498
|
+
description: 'Install dependencies',
|
|
499
|
+
command: 'npm install',
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
description: 'Run the tool directly',
|
|
503
|
+
command: `npx tsx run.ts ${exampleArgs}`,
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
description: 'Run tests',
|
|
507
|
+
command: 'npm test',
|
|
554
508
|
},
|
|
555
509
|
{
|
|
556
510
|
description: 'Install into a project and run via cli4ai',
|
package/src/commands/routines.ts
CHANGED
|
@@ -384,9 +384,24 @@ export async function routinesEditCommand(name: string, options: { global?: bool
|
|
|
384
384
|
return;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
387
|
+
// SECURITY: Parse editor command to prevent shell injection
|
|
388
|
+
// Common editors may have arguments like "code --wait" or "vim -c 'set nu'"
|
|
389
|
+
// We split on spaces but this is a simple approach; complex cases should use exec wrapper
|
|
390
|
+
const editorParts = editor.trim().split(/\s+/);
|
|
391
|
+
const editorCmd = editorParts[0];
|
|
392
|
+
const editorArgs = [...editorParts.slice(1), routine.path];
|
|
393
|
+
|
|
394
|
+
// Validate editor command doesn't contain shell metacharacters in the base command
|
|
395
|
+
if (!/^[a-zA-Z0-9_.\-\/]+$/.test(editorCmd)) {
|
|
396
|
+
outputError('INVALID_INPUT', 'Invalid $EDITOR value - contains unsafe characters', {
|
|
397
|
+
editor: editorCmd,
|
|
398
|
+
hint: 'Set $EDITOR to a simple command like "vim", "nano", or "code --wait"'
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const result = spawnSync(editorCmd, editorArgs, {
|
|
403
|
+
stdio: 'inherit'
|
|
404
|
+
// SECURITY: shell: false (default) prevents command injection
|
|
390
405
|
});
|
|
391
406
|
|
|
392
407
|
process.exit(result.status ?? 1);
|
|
@@ -79,7 +79,7 @@ export async function schedulerStartCommand(options: SchedulerStartOptions): Pro
|
|
|
79
79
|
const daemonScript = resolve(dirname(fileURLToPath(import.meta.url)), '../core/scheduler-daemon.ts');
|
|
80
80
|
|
|
81
81
|
// Spawn with detached mode
|
|
82
|
-
const child = spawn('
|
|
82
|
+
const child = spawn('npx', ['tsx', daemonScript, '--project-dir', process.cwd()], {
|
|
83
83
|
detached: true,
|
|
84
84
|
stdio: 'ignore',
|
|
85
85
|
env: {
|
package/src/commands/update.ts
CHANGED
|
@@ -99,14 +99,30 @@ export async function checkForUpdates(): Promise<{ hasUpdate: boolean; current:
|
|
|
99
99
|
*/
|
|
100
100
|
function getUpdateablePackages(): Array<{ name: string; npmName: string; version: string }> {
|
|
101
101
|
const packages: Array<{ name: string; npmName: string; version: string }> = [];
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
|
|
104
|
+
// Get packages from ~/.cli4ai/packages/ (where cli4ai add -g installs)
|
|
105
|
+
for (const pkg of getGlobalPackages()) {
|
|
106
|
+
if (!seen.has(pkg.name)) {
|
|
107
|
+
seen.add(pkg.name);
|
|
108
|
+
packages.push({
|
|
109
|
+
name: pkg.name,
|
|
110
|
+
npmName: `@cli4ai/${pkg.name}`,
|
|
111
|
+
version: pkg.version
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
102
115
|
|
|
103
|
-
//
|
|
116
|
+
// Also check npm global @cli4ai packages (for packages installed via npm directly)
|
|
104
117
|
for (const pkg of getNpmGlobalPackages()) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
if (!seen.has(pkg.name)) {
|
|
119
|
+
seen.add(pkg.name);
|
|
120
|
+
packages.push({
|
|
121
|
+
name: pkg.name,
|
|
122
|
+
npmName: `@cli4ai/${pkg.name}`,
|
|
123
|
+
version: pkg.version
|
|
124
|
+
});
|
|
125
|
+
}
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
return packages;
|