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.
@@ -0,0 +1,273 @@
1
+ /**
2
+ * cli4ai secrets - Manage secrets for CLI tools
3
+ */
4
+
5
+ import { createInterface } from 'readline';
6
+ import { resolve } from 'path';
7
+ import { existsSync, readFileSync } from 'fs';
8
+ import { output, outputError, log } from '../lib/cli.js';
9
+ import {
10
+ getSecret,
11
+ setSecret,
12
+ deleteSecret,
13
+ listSecretKeys,
14
+ getSecretSource,
15
+ hasSecret
16
+ } from '../core/secrets.js';
17
+ import { findPackage } from '../core/config.js';
18
+
19
+ interface SecretsOptions {
20
+ package?: string;
21
+ }
22
+
23
+ /**
24
+ * Prompt for input (hidden for secrets)
25
+ */
26
+ async function prompt(message: string, hidden = false): Promise<string> {
27
+ const rl = createInterface({
28
+ input: process.stdin,
29
+ output: process.stderr
30
+ });
31
+
32
+ return new Promise((resolve) => {
33
+ if (hidden && process.stdin.isTTY) {
34
+ // Hide input for secrets
35
+ process.stderr.write(message);
36
+ let value = '';
37
+
38
+ process.stdin.setRawMode(true);
39
+ process.stdin.resume();
40
+ process.stdin.setEncoding('utf8');
41
+
42
+ const onData = (char: string) => {
43
+ if (char === '\n' || char === '\r') {
44
+ process.stdin.setRawMode(false);
45
+ process.stdin.removeListener('data', onData);
46
+ process.stderr.write('\n');
47
+ rl.close();
48
+ resolve(value);
49
+ } else if (char === '\u0003') {
50
+ // Ctrl+C
51
+ process.exit(1);
52
+ } else if (char === '\u007F') {
53
+ // Backspace
54
+ if (value.length > 0) {
55
+ value = value.slice(0, -1);
56
+ }
57
+ } else {
58
+ value += char;
59
+ }
60
+ };
61
+
62
+ process.stdin.on('data', onData);
63
+ } else {
64
+ rl.question(message, (answer) => {
65
+ rl.close();
66
+ resolve(answer);
67
+ });
68
+ }
69
+ });
70
+ }
71
+
72
+ /**
73
+ * cli4ai secrets set <key> [value]
74
+ */
75
+ export async function secretsSetCommand(key: string, value?: string): Promise<void> {
76
+ if (!key) {
77
+ outputError('INVALID_INPUT', 'Secret key is required');
78
+ }
79
+
80
+ // Prompt for value if not provided
81
+ if (!value) {
82
+ value = await prompt(`Enter value for ${key}: `, true);
83
+ }
84
+
85
+ if (!value) {
86
+ outputError('INVALID_INPUT', 'Secret value cannot be empty');
87
+ }
88
+
89
+ setSecret(key, value);
90
+ log(`✓ Secret '${key}' saved to vault`);
91
+
92
+ output({ key, status: 'saved' });
93
+ }
94
+
95
+ /**
96
+ * cli4ai secrets get <key>
97
+ */
98
+ export async function secretsGetCommand(key: string): Promise<void> {
99
+ if (!key) {
100
+ outputError('INVALID_INPUT', 'Secret key is required');
101
+ }
102
+
103
+ const value = getSecret(key);
104
+ const source = getSecretSource(key);
105
+
106
+ if (!value) {
107
+ outputError('NOT_FOUND', `Secret '${key}' not found`, {
108
+ hint: `Set it with: cli4ai secrets set ${key}`
109
+ });
110
+ }
111
+
112
+ // Output value to stdout (for piping)
113
+ console.log(value);
114
+ }
115
+
116
+ /**
117
+ * cli4ai secrets list
118
+ */
119
+ export async function secretsListCommand(options: SecretsOptions): Promise<void> {
120
+ const vaultKeys = listSecretKeys();
121
+
122
+ // If package specified, show status of required secrets
123
+ if (options.package) {
124
+ const pkg = findPackage(options.package, process.cwd());
125
+ if (!pkg) {
126
+ outputError('NOT_FOUND', `Package not found: ${options.package}`);
127
+ }
128
+
129
+ const cli4aiPath = resolve(pkg.path, 'cli4ai.json');
130
+ if (!existsSync(cli4aiPath)) {
131
+ outputError('NOT_FOUND', 'Package has no cli4ai.json');
132
+ }
133
+
134
+ const manifest = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
135
+ const envDefs = manifest.env || {};
136
+
137
+ log(`\nSecrets for ${options.package}:\n`);
138
+
139
+ const secrets: Array<{ key: string; required: boolean; source: string; description?: string }> = [];
140
+
141
+ for (const [key, def] of Object.entries(envDefs) as [string, { required?: boolean; description?: string }][]) {
142
+ const source = getSecretSource(key);
143
+ secrets.push({
144
+ key,
145
+ required: def.required ?? false,
146
+ source,
147
+ description: def.description
148
+ });
149
+
150
+ const status = source === 'env' ? '✓ (env)' :
151
+ source === 'vault' ? '✓ (vault)' :
152
+ def.required ? '✗ missing' : '○ optional';
153
+
154
+ log(` ${status.padEnd(12)} ${key}`);
155
+ if (def.description) {
156
+ log(` ${def.description}`);
157
+ }
158
+ }
159
+
160
+ log('');
161
+ output({ package: options.package, secrets });
162
+ return;
163
+ }
164
+
165
+ // List all secrets in vault
166
+ if (vaultKeys.length === 0) {
167
+ log('No secrets stored in vault.\n');
168
+ log('Set a secret: cli4ai secrets set SLACK_BOT_TOKEN');
169
+ log('From env var: Secrets are also read from environment variables\n');
170
+ output({ secrets: [] });
171
+ return;
172
+ }
173
+
174
+ log('\nSecrets in vault:\n');
175
+
176
+ const secrets = vaultKeys.map(key => {
177
+ const source = getSecretSource(key);
178
+ const overridden = source === 'env';
179
+ log(` ${key}${overridden ? ' (overridden by env)' : ''}`);
180
+ return { key, overriddenByEnv: overridden };
181
+ });
182
+
183
+ log('\n');
184
+ output({ secrets });
185
+ }
186
+
187
+ /**
188
+ * cli4ai secrets delete <key>
189
+ */
190
+ export async function secretsDeleteCommand(key: string): Promise<void> {
191
+ if (!key) {
192
+ outputError('INVALID_INPUT', 'Secret key is required');
193
+ }
194
+
195
+ const deleted = deleteSecret(key);
196
+
197
+ if (!deleted) {
198
+ outputError('NOT_FOUND', `Secret '${key}' not found in vault`);
199
+ }
200
+
201
+ log(`✓ Secret '${key}' deleted from vault`);
202
+ output({ key, status: 'deleted' });
203
+ }
204
+
205
+ /**
206
+ * cli4ai secrets init [package]
207
+ * Interactive setup for a package's required secrets
208
+ */
209
+ export async function secretsInitCommand(packageName?: string, options?: SecretsOptions): Promise<void> {
210
+ const pkgName = packageName || options?.package;
211
+
212
+ if (!pkgName) {
213
+ outputError('INVALID_INPUT', 'Package name required', {
214
+ hint: 'Usage: cli4ai secrets init <package>'
215
+ });
216
+ }
217
+
218
+ const pkg = findPackage(pkgName, process.cwd());
219
+ if (!pkg) {
220
+ outputError('NOT_FOUND', `Package not found: ${pkgName}`);
221
+ }
222
+
223
+ const cli4aiPath = resolve(pkg.path, 'cli4ai.json');
224
+ if (!existsSync(cli4aiPath)) {
225
+ outputError('NOT_FOUND', 'Package has no cli4ai.json');
226
+ }
227
+
228
+ const manifest = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
229
+ const envDefs = manifest.env || {};
230
+
231
+ const required = Object.entries(envDefs)
232
+ .filter(([_, def]) => (def as { required?: boolean }).required)
233
+ .map(([key, def]) => ({ key, ...(def as { description?: string }) }));
234
+
235
+ if (required.length === 0) {
236
+ log(`\n${pkgName} has no required secrets.\n`);
237
+ output({ package: pkgName, configured: [] });
238
+ return;
239
+ }
240
+
241
+ log(`\nConfiguring secrets for ${pkgName}:\n`);
242
+
243
+ const configured: string[] = [];
244
+ const skipped: string[] = [];
245
+
246
+ for (const { key, description } of required) {
247
+ const source = getSecretSource(key);
248
+
249
+ if (source !== 'missing') {
250
+ log(` ✓ ${key} (already set via ${source})`);
251
+ skipped.push(key);
252
+ continue;
253
+ }
254
+
255
+ if (description) {
256
+ log(`\n ${key}: ${description}`);
257
+ }
258
+
259
+ const value = await prompt(` Enter ${key}: `, true);
260
+
261
+ if (value) {
262
+ setSecret(key, value);
263
+ log(` ✓ ${key} saved`);
264
+ configured.push(key);
265
+ } else {
266
+ log(` ○ ${key} skipped`);
267
+ skipped.push(key);
268
+ }
269
+ }
270
+
271
+ log('\n');
272
+ output({ package: pkgName, configured, skipped });
273
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * cli4ai start - Start MCP server for a package
3
+ */
4
+
5
+ import { outputError } from '../lib/cli.js';
6
+ import { findPackage } from '../core/config.js';
7
+ import { loadManifest } from '../core/manifest.js';
8
+ import { startMcpServer } from '../mcp/server.js';
9
+
10
+ interface StartOptions {
11
+ stdio?: boolean;
12
+ }
13
+
14
+ export async function startCommand(
15
+ packageName: string,
16
+ options: StartOptions
17
+ ): Promise<void> {
18
+ const cwd = process.cwd();
19
+
20
+ // Find the package
21
+ const pkg = findPackage(packageName, cwd);
22
+ if (!pkg) {
23
+ outputError('NOT_FOUND', `Package not found: ${packageName}`, {
24
+ hint: 'Install the package first with "cli4ai add <package>"'
25
+ });
26
+ }
27
+
28
+ // Load manifest
29
+ const manifest = loadManifest(pkg!.path);
30
+
31
+ // Check if MCP is enabled
32
+ if (!manifest.mcp?.enabled) {
33
+ outputError('INVALID_INPUT', `Package ${packageName} does not have MCP enabled`, {
34
+ hint: 'Add "mcp": { "enabled": true } to cli4ai.json'
35
+ });
36
+ }
37
+
38
+ // Start MCP server (stdio mode is default/only mode for now)
39
+ await startMcpServer(manifest, pkg!.path);
40
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * cli4ai update - Update installed packages and cli4ai itself
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import { log, outputError } from '../lib/cli.js';
7
+ import {
8
+ getNpmGlobalPackages,
9
+ getGlobalPackages,
10
+ getLocalPackages
11
+ } from '../core/config.js';
12
+
13
+ interface UpdateOptions {
14
+ self?: boolean;
15
+ all?: boolean;
16
+ }
17
+
18
+ interface UpdateResult {
19
+ name: string;
20
+ from: string;
21
+ to: string;
22
+ status: 'updated' | 'current' | 'failed';
23
+ }
24
+
25
+ // ANSI colors
26
+ const RESET = '\x1B[0m';
27
+ const BOLD = '\x1B[1m';
28
+ const DIM = '\x1B[2m';
29
+ const CYAN = '\x1B[36m';
30
+ const GREEN = '\x1B[32m';
31
+ const YELLOW = '\x1B[33m';
32
+
33
+ /**
34
+ * Get latest version from npm registry API (fast!)
35
+ */
36
+ async function getLatestVersion(packageName: string): Promise<string | null> {
37
+ try {
38
+ // URL-encode the package name to handle scoped packages like @cli4ai/foo
39
+ // npm registry expects: @scope%2Fpackage (slash encoded, @ not encoded)
40
+ const encodedName = packageName.replace('/', '%2F');
41
+ const response = await fetch(`https://registry.npmjs.org/${encodedName}/latest`, {
42
+ headers: { 'Accept': 'application/json' }
43
+ });
44
+ if (!response.ok) return null;
45
+ const data = await response.json() as { version: string };
46
+ return data.version;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Get latest versions for multiple packages in parallel
54
+ */
55
+ async function getLatestVersions(packageNames: string[]): Promise<Map<string, string | null>> {
56
+ const results = new Map<string, string | null>();
57
+ const promises = packageNames.map(async (name) => {
58
+ const version = await getLatestVersion(name);
59
+ results.set(name, version);
60
+ });
61
+ await Promise.all(promises);
62
+ return results;
63
+ }
64
+
65
+ /**
66
+ * Update a single package (async)
67
+ */
68
+ async function updatePackageAsync(packageName: string): Promise<boolean> {
69
+ return new Promise((resolve) => {
70
+ const proc = spawn('npm', ['install', '-g', `${packageName}@latest`], {
71
+ stdio: 'pipe'
72
+ });
73
+ proc.on('close', (code) => {
74
+ resolve(code === 0);
75
+ });
76
+ proc.on('error', () => {
77
+ resolve(false);
78
+ });
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Check for cli4ai framework updates
84
+ */
85
+ export async function checkForUpdates(): Promise<{ hasUpdate: boolean; current: string; latest: string }> {
86
+ const { VERSION } = await import('../lib/cli.js');
87
+ const latest = await getLatestVersion('cli4ai');
88
+
89
+ if (!latest) {
90
+ return { hasUpdate: false, current: VERSION, latest: VERSION };
91
+ }
92
+
93
+ const hasUpdate = latest !== VERSION;
94
+ return { hasUpdate, current: VERSION, latest };
95
+ }
96
+
97
+ /**
98
+ * Get all updateable packages
99
+ */
100
+ function getUpdateablePackages(): Array<{ name: string; npmName: string; version: string }> {
101
+ const packages: Array<{ name: string; npmName: string; version: string }> = [];
102
+
103
+ // Get npm global @cli4ai packages
104
+ for (const pkg of getNpmGlobalPackages()) {
105
+ packages.push({
106
+ name: pkg.name,
107
+ npmName: `@cli4ai/${pkg.name}`,
108
+ version: pkg.version
109
+ });
110
+ }
111
+
112
+ return packages;
113
+ }
114
+
115
+ export async function updateCommand(options: UpdateOptions): Promise<void> {
116
+ const results: UpdateResult[] = [];
117
+
118
+ // Update cli4ai itself first if --self or --all
119
+ if (options.self || options.all) {
120
+ log(`\n${BOLD}Checking cli4ai framework...${RESET}`);
121
+
122
+ const { hasUpdate, current, latest } = await checkForUpdates();
123
+
124
+ if (hasUpdate) {
125
+ log(` ${CYAN}↑${RESET} cli4ai ${DIM}${current}${RESET} → ${GREEN}${latest}${RESET}`);
126
+ const success = await updatePackageAsync('cli4ai');
127
+
128
+ if (success) {
129
+ log(` ${GREEN}✓${RESET} cli4ai updated\n`);
130
+ results.push({ name: 'cli4ai', from: current, to: latest, status: 'updated' });
131
+ } else {
132
+ log(` ${YELLOW}✗${RESET} Failed to update cli4ai\n`);
133
+ results.push({ name: 'cli4ai', from: current, to: latest, status: 'failed' });
134
+ }
135
+ } else {
136
+ log(` ${GREEN}✓${RESET} cli4ai is up to date (${current})\n`);
137
+ results.push({ name: 'cli4ai', from: current, to: current, status: 'current' });
138
+ }
139
+
140
+ if (options.self && !options.all) {
141
+ return;
142
+ }
143
+ }
144
+
145
+ // Get all installed packages
146
+ const packages = getUpdateablePackages();
147
+
148
+ if (packages.length === 0) {
149
+ log(`${DIM}No @cli4ai packages installed.${RESET}`);
150
+ log(`${DIM}Run ${RESET}cli4ai browse${DIM} to install packages.${RESET}\n`);
151
+ return;
152
+ }
153
+
154
+ log(`${BOLD}Checking ${packages.length} package${packages.length !== 1 ? 's' : ''} for updates...${RESET}\n`);
155
+
156
+ // Fetch all versions in parallel (fast!)
157
+ const npmNames = packages.map(p => p.npmName);
158
+ const latestVersions = await getLatestVersions(npmNames);
159
+
160
+ // Separate packages that need updates vs current
161
+ const toUpdate: Array<{ name: string; npmName: string; from: string; to: string }> = [];
162
+
163
+ for (const pkg of packages) {
164
+ const latest = latestVersions.get(pkg.npmName);
165
+
166
+ if (!latest) {
167
+ log(` ${DIM}${pkg.name}${RESET} - ${YELLOW}could not check${RESET}`);
168
+ continue;
169
+ }
170
+
171
+ if (latest === pkg.version) {
172
+ log(` ${GREEN}✓${RESET} ${pkg.name} ${DIM}${pkg.version}${RESET}`);
173
+ results.push({ name: pkg.name, from: pkg.version, to: pkg.version, status: 'current' });
174
+ } else {
175
+ log(` ${CYAN}↑${RESET} ${pkg.name} ${DIM}${pkg.version}${RESET} → ${GREEN}${latest}${RESET}`);
176
+ toUpdate.push({ name: pkg.name, npmName: pkg.npmName, from: pkg.version, to: latest });
177
+ }
178
+ }
179
+
180
+ // Update all packages in parallel
181
+ if (toUpdate.length > 0) {
182
+ log(`\n${BOLD}Updating ${toUpdate.length} package${toUpdate.length !== 1 ? 's' : ''} in parallel...${RESET}`);
183
+
184
+ const updatePromises = toUpdate.map(async (pkg) => {
185
+ const success = await updatePackageAsync(pkg.npmName);
186
+ return { ...pkg, success };
187
+ });
188
+
189
+ const updateResults = await Promise.all(updatePromises);
190
+
191
+ log('');
192
+ for (const result of updateResults) {
193
+ if (result.success) {
194
+ log(` ${GREEN}✓${RESET} ${result.name} updated`);
195
+ results.push({ name: result.name, from: result.from, to: result.to, status: 'updated' });
196
+ } else {
197
+ log(` ${YELLOW}✗${RESET} ${result.name} failed`);
198
+ results.push({ name: result.name, from: result.from, to: result.to, status: 'failed' });
199
+ }
200
+ }
201
+ }
202
+
203
+ // Summary
204
+ const updated = results.filter(r => r.status === 'updated').length;
205
+ const failed = results.filter(r => r.status === 'failed').length;
206
+
207
+ console.log('');
208
+ if (updated > 0) {
209
+ log(`${GREEN}${updated} package${updated !== 1 ? 's' : ''} updated${RESET}`);
210
+ }
211
+ if (failed > 0) {
212
+ log(`${YELLOW}${failed} package${failed !== 1 ? 's' : ''} failed to update${RESET}`);
213
+ }
214
+ if (updated === 0 && failed === 0) {
215
+ log(`${GREEN}All packages are up to date${RESET}`);
216
+ }
217
+ console.log('');
218
+ }