cli4ai 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # cli4ai
2
+
3
+ > Official CLI4AI package • https://cli4ai.com
4
+
5
+ The package manager for AI CLI tools.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ # Install cli4ai
11
+ npm i -g cli4ai
12
+ # or (optional)
13
+ bun install -g cli4ai
14
+
15
+ # Install a package
16
+ cli4ai add -g github
17
+
18
+ # Run it
19
+ cli4ai run github notifs
20
+ ```
21
+
22
+ ## Why cli4ai?
23
+
24
+ AI agents need tools. MCP servers are complex to set up. cli4ai makes it simple:
25
+
26
+ - **Install tools in seconds** - `cli4ai add github` just works
27
+ - **Run from anywhere** - `cli4ai run <tool> <command>`
28
+ - **No MCP boilerplate** - cli4ai wraps CLI tools as MCP servers automatically
29
+ - **Local-first** - Tools are just scripts, no cloud required
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ # Add a local registry (directory containing tools)
35
+ cli4ai config --add-registry ~/my-tools
36
+
37
+ # Search for tools
38
+ cli4ai search github
39
+
40
+ # Install globally (available everywhere)
41
+ cli4ai add -g github
42
+
43
+ # Install locally (project-scoped)
44
+ cd ~/my-project
45
+ cli4ai add mongodb
46
+
47
+ # Run tools
48
+ cli4ai run github notifs
49
+ cli4ai run mongodb databases
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ ### Package Management
55
+
56
+ | Command | Description |
57
+ |---------|-------------|
58
+ | `cli4ai add <pkg>` | Install package locally |
59
+ | `cli4ai add -g <pkg>` | Install package globally |
60
+ | `cli4ai remove <pkg>` | Uninstall package |
61
+ | `cli4ai remove -g <pkg>` | Uninstall global package |
62
+ | `cli4ai list` | List local packages |
63
+ | `cli4ai list -g` | List global packages |
64
+
65
+ ### Discovery
66
+
67
+ | Command | Description |
68
+ |---------|-------------|
69
+ | `cli4ai search <query>` | Search for packages |
70
+ | `cli4ai info <pkg>` | Show package details |
71
+
72
+ ### Execution
73
+
74
+ | Command | Description |
75
+ |---------|-------------|
76
+ | `cli4ai run <pkg> <cmd> [args]` | Run a tool command |
77
+ | `cli4ai start <pkg>` | Start package as MCP server |
78
+
79
+ Notes:
80
+ - Tool flags are supported: `cli4ai run chrome screenshot https://example.com --full-page`
81
+ - If a flag conflicts with `cli4ai run` options, use `--` to pass-through: `cli4ai run <pkg> <cmd> -- --help`
82
+
83
+ ### Configuration
84
+
85
+ | Command | Description |
86
+ |---------|-------------|
87
+ | `cli4ai config` | Show current config |
88
+ | `cli4ai config --add-registry <path>` | Add local registry |
89
+ | `cli4ai config --remove-registry <path>` | Remove registry |
90
+
91
+ ### Project Setup
92
+
93
+ | Command | Description |
94
+ |---------|-------------|
95
+ | `cli4ai init [name]` | Create a new tool project |
96
+ | `cli4ai mcp-config` | Generate Claude Code MCP config |
97
+
98
+ ## Package Format
99
+
100
+ Tools are defined by a `cli4ai.json` manifest:
101
+
102
+ ```json
103
+ {
104
+ "name": "github",
105
+ "version": "1.0.0",
106
+ "description": "GitHub CLI wrapper",
107
+ "entry": "run.ts",
108
+ "runtime": "bun",
109
+ "commands": {
110
+ "notifs": {
111
+ "description": "Your notifications"
112
+ },
113
+ "repos": {
114
+ "description": "List repositories",
115
+ "args": [
116
+ { "name": "user", "required": false }
117
+ ]
118
+ }
119
+ },
120
+ "env": {
121
+ "GITHUB_TOKEN": {
122
+ "required": false,
123
+ "description": "Personal access token"
124
+ }
125
+ },
126
+ "mcp": {
127
+ "enabled": true
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### Manifest Fields
133
+
134
+ | Field | Required | Description |
135
+ |-------|----------|-------------|
136
+ | `name` | Yes | Package name (lowercase, hyphens ok) |
137
+ | `version` | Yes | Semver version |
138
+ | `entry` | Yes | Entry point script |
139
+ | `description` | No | Package description |
140
+ | `runtime` | No | Runtime: `bun` (default), `node`, `deno` |
141
+ | `commands` | No | Command definitions for MCP |
142
+ | `env` | No | Environment variable requirements |
143
+ | `mcp.enabled` | No | Enable MCP server mode |
144
+
145
+ ## Storage Locations
146
+
147
+ ```
148
+ ~/.cli4ai/
149
+ ├── config.json # Global configuration
150
+ ├── packages/ # Globally installed packages
151
+ │ └── github/
152
+ └── bin/ # PATH-linked executables (optional)
153
+
154
+ ./
155
+ ├── cli4ai.json # Project manifest (if a tool)
156
+ ├── cli4ai.lock # Lock file (reproducible installs)
157
+ └── .cli4ai/
158
+ └── packages/ # Locally installed packages
159
+ ```
160
+
161
+ ## MCP Integration
162
+
163
+ cli4ai can generate configuration for Claude Code:
164
+
165
+ ```bash
166
+ # Generate config for all installed packages
167
+ cli4ai mcp-config
168
+
169
+ # Generate for specific packages
170
+ cli4ai mcp-config --packages github,slack
171
+
172
+ # Get snippet for a single package
173
+ cli4ai mcp-config --package github --snippet
174
+ ```
175
+
176
+ Output:
177
+ ```json
178
+ {
179
+ "mcpServers": {
180
+ "cli4ai-github": {
181
+ "command": "cli4ai",
182
+ "args": ["start", "github"]
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ Add this to your Claude Code MCP settings.
189
+
190
+ ## Local Registries
191
+
192
+ cli4ai discovers packages from local directories:
193
+
194
+ ```bash
195
+ # Add a registry
196
+ cli4ai config --add-registry ~/agent-tools
197
+
198
+ # Now packages in ~/agent-tools are discoverable
199
+ cli4ai search github # Finds ~/agent-tools/github/
200
+ ```
201
+
202
+ A registry is just a directory containing tool folders, each with a `cli4ai.json`.
203
+
204
+ ## Creating Tools
205
+
206
+ ```bash
207
+ # Create a new tool
208
+ cli4ai init my-tool
209
+ cd my-tool
210
+
211
+ # Edit `run.ts` and `cli4ai.json`
212
+
213
+ # Test locally
214
+ bun run run.ts hello world
215
+
216
+ # Install for testing
217
+ cd ~/some-project
218
+ cli4ai add --local ~/path/to/my-tool -y
219
+ cli4ai run my-tool hello world
220
+ ```
221
+
222
+ ### Tool Entry Point
223
+
224
+ Tools are simple CLI scripts that output JSON:
225
+
226
+ ```typescript
227
+ #!/usr/bin/env bun
228
+
229
+ import { cli, output } from '@cli4ai/lib/cli.ts';
230
+
231
+ const program = cli('my-tool', '1.0.0', 'Example tool');
232
+
233
+ program
234
+ .command('hello [name]')
235
+ .description('Say hello')
236
+ .action((name?: string) => {
237
+ output({ message: `Hello, ${name || 'world'}!` });
238
+ });
239
+
240
+ program.parse();
241
+ ```
242
+
243
+ When run via `cli4ai run`, these env vars are set for convenience:
244
+ - `CLI4AI_CWD`: directory where you invoked `cli4ai`
245
+ - `C4AI_PACKAGE_DIR`: installed package directory
246
+ - `C4AI_PACKAGE_NAME`: resolved package name
247
+ - `C4AI_ENTRY`: absolute path to the tool entry file
248
+
249
+ ## Global vs Local Install
250
+
251
+ | Aspect | Global (`-g`) | Local (default) |
252
+ |--------|---------------|-----------------|
253
+ | Location | `~/.cli4ai/packages/` | `./.cli4ai/packages/` |
254
+ | Scope | Available everywhere | Project only |
255
+ | Lock file | No | Yes (`cli4ai.lock`) |
256
+ | Use case | Utilities (github, slack) | Project-specific (mongodb) |
257
+
258
+ ## Environment Variables
259
+
260
+ Tools can require environment variables:
261
+
262
+ ```json
263
+ {
264
+ "env": {
265
+ "API_KEY": { "required": true },
266
+ "DEBUG": { "required": false, "default": "false" }
267
+ }
268
+ }
269
+ ```
270
+
271
+ Set them in your shell or `.env` file.
272
+
273
+ ## License
274
+
275
+ MIT
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "cli4ai",
3
+ "version": "0.8.0",
4
+ "description": "The package manager for AI CLI tools - cliforai.com",
5
+ "type": "module",
6
+ "bin": {
7
+ "cli4ai": "./src/bin.ts"
8
+ },
9
+ "scripts": {
10
+ "dev": "bun run src/bin.ts",
11
+ "build": "bun build src/bin.ts --outdir dist --target node",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "bun test",
14
+ "test:watch": "bun test --watch",
15
+ "test:coverage": "bun test --coverage"
16
+ },
17
+ "dependencies": {
18
+ "commander": "^14.0.0",
19
+ "semver": "^7.6.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "^1.3.4",
23
+ "@types/node": "^22.0.0",
24
+ "@types/semver": "^7.5.0",
25
+ "typescript": "^5.7.0"
26
+ },
27
+ "author": "cliforai",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/cliforai/framework"
32
+ },
33
+ "homepage": "https://cliforai.com",
34
+ "keywords": [
35
+ "cli",
36
+ "ai",
37
+ "tools",
38
+ "mcp",
39
+ "package-manager",
40
+ "cli4ai"
41
+ ],
42
+ "files": [
43
+ "src/**/*.ts",
44
+ "templates/**/*"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * cli4ai - The package manager for AI CLI tools
4
+ * cliforai.com
5
+ */
6
+
7
+ import { createProgram } from './cli.js';
8
+ import { VERSION } from './lib/cli.js';
9
+ import { getNpmGlobalPackages } from './core/config.js';
10
+
11
+ // ANSI codes
12
+ const RESET = '\x1B[0m';
13
+ const BOLD = '\x1B[1m';
14
+ const DIM = '\x1B[2m';
15
+ const CYAN = '\x1B[36m';
16
+ const WHITE = '\x1B[37m';
17
+ const GREEN = '\x1B[32m';
18
+ const YELLOW = '\x1B[33m';
19
+ const MAGENTA = '\x1B[35m';
20
+
21
+ /**
22
+ * Sleep helper
23
+ */
24
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
25
+
26
+ /**
27
+ * Robot eating dots animation
28
+ */
29
+ async function showAnimation(): Promise<void> {
30
+ const width = 35;
31
+ const robot = ['[•_•]', '[•_•]', '[°_°]', '[•_•]'];
32
+ const food = '·';
33
+
34
+ for (let pos = 0; pos <= width; pos++) {
35
+ process.stdout.write('\r\x1B[K');
36
+ const frame = robot[pos % robot.length];
37
+ const trail = ' '.repeat(pos) + food.repeat(width - pos);
38
+ process.stdout.write(` ${CYAN}${frame}${RESET}${DIM}${trail}${RESET}`);
39
+ await sleep(20);
40
+ }
41
+ process.stdout.write('\r\x1B[K');
42
+ }
43
+
44
+ /**
45
+ * Animate the robot face (blinking)
46
+ */
47
+ async function animateRobotFace(): Promise<void> {
48
+ const faces = ['[•_•]', '[•_•]', '[-_-]', '[•_•]', '[•_•]', '[°_°]', '[•_•]'];
49
+ const line = ` ${BOLD}${CYAN}cli4ai${RESET} ${DIM}─${RESET} ${WHITE}cliforai.com${RESET}`;
50
+
51
+ for (const face of faces) {
52
+ process.stdout.write(`\r ${CYAN}${face}${RESET}${line}`);
53
+ await sleep(120);
54
+ }
55
+ console.log('');
56
+ }
57
+
58
+ async function showBanner(): Promise<void> {
59
+ if (!process.stdout.isTTY) return;
60
+
61
+ console.log('');
62
+
63
+ // Fun robot eating animation
64
+ await showAnimation();
65
+
66
+ // Animated robot branding (blinking)
67
+ await animateRobotFace();
68
+
69
+ console.log(` ${DIM}The package manager for AI CLI tools${RESET}`);
70
+ console.log(` ${DIM}v${VERSION}${RESET}`);
71
+ console.log('');
72
+ console.log(` ${BOLD}Commands${RESET}`);
73
+ console.log(` ${DIM}${'─'.repeat(40)}${RESET}`);
74
+ console.log(` ${GREEN}browse${RESET} ${DIM}Browse & install packages${RESET}`);
75
+ console.log(` ${GREEN}run${RESET} ${CYAN}<pkg> <cmd>${RESET} ${DIM}Run a tool command${RESET}`);
76
+ console.log(` ${GREEN}ls${RESET} ${DIM}List installed packages${RESET}`);
77
+ console.log(` ${GREEN}update${RESET} ${DIM}Update all packages${RESET}`);
78
+ console.log('');
79
+ console.log(` ${DIM}Run`${RESET} ${WHITE}cli4ai --help${RESET} ${DIM}for all commands${RESET}`);
80
+ console.log('');
81
+
82
+ // Check for updates in background (non-blocking)
83
+ checkUpdatesInBackground();
84
+ }
85
+
86
+ /**
87
+ * Check for updates without blocking - spawns detached process
88
+ */
89
+ function checkUpdatesInBackground(): void {
90
+ // Quick local check only - don't hit network
91
+ try {
92
+ const packages = getNpmGlobalPackages();
93
+ if (packages.length > 0) {
94
+ // Spawn a background process to check updates and cache results
95
+ const child = Bun.spawn(['sh', '-c', `
96
+ # Quick npm check (with timeout)
97
+ timeout 3 npm view cli4ai version 2>/dev/null > /tmp/.cli4ai-update-check 2>&1 &
98
+ `], {
99
+ detached: true,
100
+ stdio: ['ignore', 'ignore', 'ignore']
101
+ });
102
+ child.unref();
103
+ }
104
+ } catch {
105
+ // Ignore errors - this is best effort
106
+ }
107
+ }
108
+
109
+ // Check if running without command (just `cli4ai`)
110
+ const args = process.argv.slice(2);
111
+ const hasCommand = args.length > 0 && !args[0].startsWith('-');
112
+ const isHelp = args.includes('--help') || args.includes('-h');
113
+ const isVersion = args.includes('--version') || args.includes('-v');
114
+
115
+ if (!hasCommand && !isHelp && !isVersion) {
116
+ showBanner().then(() => process.exit(0));
117
+ } else {
118
+ const program = createProgram();
119
+ program.parse(process.argv);
120
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,256 @@
1
+ /**
2
+ * cli4ai CLI - Main program setup
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { BRAND, VERSION, withErrorHandling } from './lib/cli.js';
7
+
8
+ // Commands
9
+ import { initCommand } from './commands/init.js';
10
+ import { addCommand } from './commands/add.js';
11
+ import { removeCommand } from './commands/remove.js';
12
+ import { listCommand } from './commands/list.js';
13
+ import { runCommand } from './commands/run.js';
14
+ import { infoCommand } from './commands/info.js';
15
+ import { searchCommand } from './commands/search.js';
16
+ import { configCommand } from './commands/config.js';
17
+ import { mcpConfigCommand } from './commands/mcp-config.js';
18
+ import { startCommand } from './commands/start.js';
19
+ import {
20
+ secretsSetCommand,
21
+ secretsGetCommand,
22
+ secretsListCommand,
23
+ secretsDeleteCommand,
24
+ secretsInitCommand
25
+ } from './commands/secrets.js';
26
+ import { browseCommand } from './commands/browse.js';
27
+ import { updateCommand } from './commands/update.js';
28
+ import { routinesListCommand, routinesRunCommand, routinesShowCommand, routinesCreateCommand, routinesEditCommand, routinesRemoveCommand } from './commands/routines.js';
29
+
30
+ export function createProgram(): Command {
31
+ const program = new Command()
32
+ .name('cli4ai')
33
+ .version(VERSION, '-v, --version', 'Show version')
34
+ .description('The package manager for AI CLI tools')
35
+ .addHelpText('beforeAll', `\n${BRAND}\n`)
36
+ .configureOutput({
37
+ writeErr: (str) => {
38
+ if (!str.includes('"error"')) {
39
+ process.stderr.write(str);
40
+ }
41
+ }
42
+ });
43
+
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+ // PACKAGE MANAGEMENT
46
+ // ═══════════════════════════════════════════════════════════════════════════
47
+
48
+ program
49
+ .command('init [name]')
50
+ .description('Create a new cli4ai tool project')
51
+ .option('-t, --template <template>', 'Template to use (basic, api, browser)', 'basic')
52
+ .option('-r, --runtime <runtime>', 'Runtime (bun, node)', 'bun')
53
+ .option('-y, --yes', 'Skip prompts, use defaults')
54
+ .action(withErrorHandling(initCommand));
55
+
56
+ program
57
+ .command('add <packages...>')
58
+ .description('Install packages')
59
+ .option('-l, --local', 'Install from local path')
60
+ .option('-g, --global', 'Install globally')
61
+ .option('-D, --save-dev', 'Save as dev dependency')
62
+ .option('-y, --yes', 'Skip confirmation prompt')
63
+ .action(withErrorHandling(addCommand));
64
+
65
+ program
66
+ .command('remove <packages...>')
67
+ .alias('rm')
68
+ .alias('uninstall')
69
+ .description('Remove packages')
70
+ .option('-g, --global', 'Remove from global packages')
71
+ .action(withErrorHandling(removeCommand));
72
+
73
+ program
74
+ .command('list')
75
+ .alias('ls')
76
+ .description('List installed packages')
77
+ .option('-g, --global', 'List global packages')
78
+ .option('--json', 'Output as JSON')
79
+ .action(withErrorHandling(listCommand));
80
+
81
+ // ═══════════════════════════════════════════════════════════════════════════
82
+ // EXECUTION
83
+ // ═══════════════════════════════════════════════════════════════════════════
84
+
85
+ program
86
+ .command('run <package> [command] [args...]')
87
+ .description('Run a tool command')
88
+ .option('-e, --env <vars...>', 'Environment variables (KEY=value)')
89
+ // Allow passing tool flags through (e.g. `cli4ai run chrome screenshot --full-page`)
90
+ .allowUnknownOption(true)
91
+ .addHelpText('after', `
92
+
93
+ Examples:
94
+ cli4ai run github trending
95
+ cli4ai run chrome screenshot https://example.com --full-page
96
+
97
+ Pass-through:
98
+ Use "--" to pass flags that would otherwise be parsed by cli4ai:
99
+ cli4ai run <pkg> <cmd> -- --help
100
+ `)
101
+ .action(withErrorHandling(runCommand));
102
+
103
+ // ═══════════════════════════════════════════════════════════════════════════
104
+ // DISCOVERY
105
+ // ═══════════════════════════════════════════════════════════════════════════
106
+
107
+ program
108
+ .command('info <package>')
109
+ .description('Show package information')
110
+ .option('--versions', 'Show all available versions')
111
+ .action(withErrorHandling(infoCommand));
112
+
113
+ program
114
+ .command('search <query>')
115
+ .description('Search for packages')
116
+ .option('-l, --limit <n>', 'Max results', '20')
117
+ .action(withErrorHandling(searchCommand));
118
+
119
+ program
120
+ .command('browse')
121
+ .description('Interactive package browser')
122
+ .action(withErrorHandling(browseCommand));
123
+
124
+ program
125
+ .command('update')
126
+ .alias('upgrade')
127
+ .description('Update installed packages')
128
+ .option('-s, --self', 'Update cli4ai framework only')
129
+ .option('-a, --all', 'Update cli4ai framework and all packages')
130
+ .option('-y, --yes', 'Skip confirmation prompt')
131
+ .action(withErrorHandling(updateCommand));
132
+
133
+ // ═══════════════════════════════════════════════════════════════════════════
134
+ // CONFIGURATION
135
+ // ═══════════════════════════════════════════════════════════════════════════
136
+
137
+ program
138
+ .command('config [key] [value]')
139
+ .description('Get or set configuration')
140
+ .option('--list', 'List all config values')
141
+ .option('--add-registry <path>', 'Add local registry path')
142
+ .option('--remove-registry <path>', 'Remove local registry path')
143
+ .action(withErrorHandling(configCommand));
144
+
145
+ // ═══════════════════════════════════════════════════════════════════════════
146
+ // MCP INTEGRATION
147
+ // ═══════════════════════════════════════════════════════════════════════════
148
+
149
+ program
150
+ .command('mcp-config')
151
+ .description('Generate MCP configuration for Claude Code')
152
+ .option('-g, --global', 'Use global packages only')
153
+ .option('-p, --package <name>', 'Generate for specific package')
154
+ .option('--snippet', 'Output as config snippet (use with --package)')
155
+ .action(withErrorHandling(mcpConfigCommand));
156
+
157
+ program
158
+ .command('start <package>')
159
+ .description('Start MCP server for a package')
160
+ .option('--stdio', 'Use stdio transport (default)')
161
+ .action(withErrorHandling(startCommand));
162
+
163
+ // ═══════════════════════════════════════════════════════════════════════════
164
+ // SECRETS MANAGEMENT
165
+ // ═══════════════════════════════════════════════════════════════════════════
166
+
167
+ const secrets = program
168
+ .command('secrets')
169
+ .description('Manage secrets for CLI tools');
170
+
171
+ secrets
172
+ .command('set <key> [value]')
173
+ .description('Set a secret (prompts for value if not provided)')
174
+ .action(withErrorHandling(secretsSetCommand));
175
+
176
+ secrets
177
+ .command('get <key>')
178
+ .description('Get a secret value')
179
+ .action(withErrorHandling(secretsGetCommand));
180
+
181
+ secrets
182
+ .command('list')
183
+ .alias('ls')
184
+ .description('List all secrets')
185
+ .option('-p, --package <name>', 'Show secrets for a specific package')
186
+ .action(withErrorHandling(secretsListCommand));
187
+
188
+ secrets
189
+ .command('delete <key>')
190
+ .alias('rm')
191
+ .description('Delete a secret')
192
+ .action(withErrorHandling(secretsDeleteCommand));
193
+
194
+ secrets
195
+ .command('init <package>')
196
+ .description('Interactive setup for a package\'s required secrets')
197
+ .action(withErrorHandling(secretsInitCommand));
198
+
199
+ // ═══════════════════════════════════════════════════════════════════════════
200
+ // HELP
201
+ // ═══════════════════════════════════════════════════════════════════════════
202
+
203
+ program.addHelpCommand('help [command]', 'Show help for command');
204
+
205
+ // ═══════════════════════════════════════════════════════════════════════════
206
+ // ROUTINES
207
+ // ═══════════════════════════════════════════════════════════════════════════
208
+
209
+ const routines = program
210
+ .command('routines')
211
+ .description('Manage and run routines');
212
+
213
+ routines
214
+ .command('list')
215
+ .alias('ls')
216
+ .description('List available routines')
217
+ .option('-g, --global', 'Use global routines only')
218
+ .option('--json', 'Output as JSON')
219
+ .action(withErrorHandling(routinesListCommand));
220
+
221
+ routines
222
+ .command('show <name>')
223
+ .description('Show routine definition')
224
+ .option('-g, --global', 'Use global routines only')
225
+ .action(withErrorHandling(routinesShowCommand));
226
+
227
+ routines
228
+ .command('create <name>')
229
+ .description('Create a new routine')
230
+ .option('-g, --global', 'Create routine globally')
231
+ .option('--type <type>', 'Routine type (bash, json)', 'bash')
232
+ .action(withErrorHandling(routinesCreateCommand));
233
+
234
+ routines
235
+ .command('edit <name>')
236
+ .description('Edit a routine in $EDITOR')
237
+ .option('-g, --global', 'Use global routine only')
238
+ .action(withErrorHandling(routinesEditCommand));
239
+
240
+ routines
241
+ .command('remove <name>')
242
+ .alias('rm')
243
+ .description('Remove a routine')
244
+ .option('-g, --global', 'Use global routine only')
245
+ .action(withErrorHandling(routinesRemoveCommand));
246
+
247
+ routines
248
+ .command('run <name> [args...]')
249
+ .description('Run a routine')
250
+ .option('-g, --global', 'Use global routine only')
251
+ .option('--var <vars...>', 'Variables (KEY=value)')
252
+ .option('--dry-run', 'Show execution plan without running')
253
+ .action(withErrorHandling(routinesRunCommand));
254
+
255
+ return program;
256
+ }