@michaelhartmayer/agentctl 1.1.0 → 1.1.4

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/dist/index.js CHANGED
@@ -5,129 +5,54 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
- const path_1 = __importDefault(require("path"));
9
- require("fs-extra");
8
+ const chalk_1 = __importDefault(require("chalk"));
10
9
  const ctl_1 = require("./ctl");
11
10
  const resolve_1 = require("./resolve");
12
- const child_process_1 = require("child_process");
13
- const chalk_1 = __importDefault(require("chalk"));
11
+ const effects_1 = require("./effects");
12
+ const index_1 = require("./logic/index");
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const pkgPath = path_1.default.join(__dirname, '../package.json');
16
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8'));
14
17
  const program = new commander_1.Command();
15
- const pkg = require('../package.json');
18
+ program.addHelpCommand(false);
16
19
  program
17
20
  .name('agentctl')
18
21
  .description('Agent Controller CLI - Unified control plane for humans and agents')
19
- .version(pkg.version)
20
- .allowUnknownOption()
21
- .helpOption(false) // Disable default help to allow pass-through
22
- .argument('[command...]', 'Command to run')
23
- .action(async (args, _options, _command) => {
24
- // If no args, check for help flag or just show help
25
- if (!args || args.length === 0) {
26
- // If they passed --help or -h, show help. If no args at all, show help.
27
- // Since we ate options, we check raw args or just treat empty args as help.
28
- // command.opts() won't have help if we disabled it?
29
- // Actually, if we disable helpOption, --help becomes an unknown option or arg.
30
- // Let's check process.argv for -h or --help if args is empty?
31
- // "agentctl --help" -> args=[], options might contain help if we didn't disable it?
32
- // With helpOption(false), --help is just a flag in argv.
33
- // If args is empty and we see help flag, show help.
34
- if (process.argv.includes('--help') || process.argv.includes('-h') || process.argv.length <= 2) {
35
- program.help();
36
- return;
37
- }
38
- }
39
- // If args are present, we try to resolve.
40
- // BUT, "agentctl --help" will result in args being empty if it's parsed as option?
41
- // Wait, if helpOption(false), then --help is an unknown option.
42
- // If allowUnknownOption is true, it might not be in 'args' if it looks like a flag.
43
- // Let's rely on resolveCommand. passed args are variadic.
44
- // However, "agentctl dev --help" -> args=["dev", "--help"]?
45
- // My repro says yes: [ 'dev-tools', 'gh', '--help' ].
46
- // So for "agentctl --help", args might be ["--help"].
47
- if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
48
- program.help();
49
- return;
50
- }
51
- // Bypass for ctl subcommand if it slipped through (shouldn't if registered)
52
- if (args[0] === 'ctl')
53
- return;
54
- try {
55
- // resolveCommand needs to handle flags in args if they are part of the path?
56
- // No, flags usually come after. resolveCommand stops at first non-matching path part?
57
- // resolveCommand logic: iterates args.
58
- // "dev-tools gh --help" -> path "dev-tools gh", remaining "--help"
59
- const result = await (0, resolve_1.resolveCommand)(args);
60
- if (!result) {
61
- // If not found, and they asked for help, show root help?
62
- // Or if they just typed a wrong command.
63
- if (args.includes('--help') || args.includes('-h')) {
64
- // Try to show help for the partial command?
65
- // For now, just show root list/help or error.
66
- // If it's "agentctl dev --help" and "dev" is a group, resolveCommand SHOULD return the group.
67
- }
68
- console.error(chalk_1.default.red(`Command '${args.join(' ')}' not found.`));
69
- console.log(`Run ${chalk_1.default.cyan('agentctl list')} to see available commands.`);
70
- process.exit(1);
71
- }
72
- const { manifest, args: remainingArgs, scope } = result;
73
- if (manifest.run) {
74
- // ... run logic ...
75
- // remainingArgs should contain --help if it was passed.
76
- const cmdDir = path_1.default.dirname(result.manifestPath);
77
- let runCmd = manifest.run;
78
- // Resolve relative path
79
- if (runCmd.startsWith('./') || runCmd.startsWith('.\\')) {
80
- runCmd = path_1.default.resolve(cmdDir, runCmd);
81
- }
82
- // Interpolate {{DIR}}
83
- runCmd = runCmd.replace(/{{DIR}}/g, cmdDir);
84
- const fullCommand = `${runCmd} ${remainingArgs.join(' ')}`;
85
- console.log(chalk_1.default.dim(`[${scope}] Running: ${fullCommand}`));
86
- const child = (0, child_process_1.spawn)(fullCommand, {
87
- cwd: process.cwd(), // Execute in CWD as discussed
88
- shell: true,
89
- stdio: 'inherit',
90
- env: { ...process.env, AGENTCTL_SCOPE: scope }
91
- });
92
- child.on('exit', (code) => {
93
- process.exit(code || 0);
94
- });
95
- }
96
- else {
97
- // Group
98
- console.log(chalk_1.default.blue(chalk_1.default.bold(`${manifest.name}`)));
99
- console.log(manifest.description || 'No description');
100
- console.log('\nSubcommands:');
101
- const all = await (0, ctl_1.list)();
102
- const prefix = result.cmdPath + ' ';
103
- // Filter logic roughly for direct children
104
- const depth = result.cmdPath.split(' ').length;
105
- const children = all.filter(c => c.path.startsWith(prefix) && c.path !== result.cmdPath);
106
- const direct = children.filter(c => c.path.split(' ').length === depth + 1);
107
- if (direct.length === 0 && children.length === 0) {
108
- console.log(chalk_1.default.dim(' (No subcommands found)'));
109
- }
110
- for (const child of direct) {
111
- console.log(` ${child.path.split(' ').pop()}\t${chalk_1.default.dim(child.description)}`);
112
- }
113
- }
114
- }
115
- catch (e) {
116
- if (e instanceof Error) {
117
- console.error(chalk_1.default.red(e.message));
118
- }
119
- else {
120
- console.error(chalk_1.default.red('An unknown error occurred'));
121
- }
122
- process.exit(1);
123
- }
124
- });
22
+ .version(pkg.version);
23
+ // --- Subcommand: ctl ---
125
24
  const ctl = program.command('ctl')
126
- .description('Agent Controller Management - Create, organize, and manage commands');
127
- // --- Lifecycle Commands ---
128
- // We'll stick to flat list but with good descriptions.
129
- // Helper for consistent error handling
130
- // Helper for consistent error handling
25
+ .description('Agent Controller Management - Create, organize, and manage commands')
26
+ .addHelpCommand(false)
27
+ .configureHelp({ visibleCommands: () => [] })
28
+ .addHelpText('after', `
29
+ ${chalk_1.default.bold('Agentctl Paradigm:')}
30
+ Agentctl acts as a unified control plane allowing both Humans and AI Agents
31
+ to create, discover, and execute local shell commands. By running \`agentctl ctl scaffold <name>\`
32
+ you create a directory in the \`.agentctl\` folder containing a \`manifest.json\`
33
+ and a run script. This dynamically creates a new \`agentctl <name>\` command that
34
+ is easily callable by agents and globally executable on your machine.
35
+
36
+ Commands:
37
+
38
+ ${chalk_1.default.bold('Creation')}
39
+ scaffold [path...] create a new command
40
+ alias [args...] create a shell alias
41
+ group [path...] create a namespace group
42
+
43
+ ${chalk_1.default.bold('Organize & Scope')}
44
+ rm [options] [path...] delete a command
45
+ mv [options] [src] [dest] rename/move a command
46
+ global [options] [path...] make a command global
47
+ local [options] [path...] make a command local
48
+
49
+ ${chalk_1.default.bold('Information')}
50
+ list list all commands
51
+ inspect [path...] inspect command details
52
+
53
+ ${chalk_1.default.bold('Integration')}
54
+ install [options] [repoUrl] [pathParts...] install remote command group
55
+ `);
131
56
  const withErrorHandling = (fn) => {
132
57
  return async (...args) => {
133
58
  try {
@@ -144,208 +69,322 @@ const withErrorHandling = (fn) => {
144
69
  }
145
70
  };
146
71
  };
72
+ const normalizePath = (parts) => parts.flatMap(p => p.split(/[\s/\\:]+/).filter(Boolean));
147
73
  ctl.command('scaffold')
148
- .description('Scaffold a new command script (creates a manifest and a .sh/.cmd file)')
149
- .argument('[path...]', 'The hierarchical path for the new command (e.g. "dev start")')
74
+ .description('Scaffold a new command directory with a manifest and starter script.')
75
+ .argument('[path...]', 'Hierarchical path for the new command (e.g., "dev start" or "utils/cleanup")')
76
+ .summary('create a new command')
150
77
  .addHelpText('after', `
151
- Description:
152
- Scaffolding creates a new directory for your command containing a 'manifest.json'
153
- and a boilerplate script file (.sh on Linux/Mac, .cmd on Windows).
154
- You can then edit the script to add your own logic.
78
+ Additional Info:
79
+ This command creates a folder in your local .agentctl directory.
80
+ Inside this newly created folder, it generates:
81
+ - manifest.json: Metadata config to edit for your command.
82
+ - command.sh/cmd: A starter script to edit for your actual logic.
83
+
84
+ Manifest Schema (manifest.json) to edit:
85
+ {
86
+ "name": "<command_folder_name>",
87
+ "description": "<insert command summary here>",
88
+ "help": "<insert longer usage/help instructions here>",
89
+ "type": "scaffold", // do not change!
90
+ "run": "./command.cmd" // points to the script to execute
91
+ }
92
+
93
+ Note:
94
+ - The "description" is displayed when you view this command in a list.
95
+ - Commands must provide their own help implementation (e.g. by handling --help inside your script).
155
96
 
156
97
  Examples:
157
- $ agentctl ctl scaffold dev start
158
- $ agentctl ctl scaffold utils/backup
98
+ $ agentctl ctl scaffold build:front
99
+ $ agentctl ctl scaffold "build front" # Creates group 'build' and subcommand 'front'
159
100
  `)
160
- .action(withErrorHandling(async (pathParts, _options, command) => {
161
- if (!pathParts || pathParts.length === 0) {
101
+ .action(withErrorHandling(async (pathParts, opts, command) => {
102
+ const normalized = normalizePath(pathParts);
103
+ if (normalized.length === 0) {
162
104
  command.help();
163
105
  return;
164
106
  }
165
- await (0, ctl_1.scaffold)(pathParts);
107
+ await (0, ctl_1.scaffold)(normalized);
166
108
  }));
167
109
  ctl.command('alias')
168
- .description('Create an alias command that executes a shell string')
169
- .argument('[path_and_cmd...]', 'Hierarchical path segments followed by the shell command')
170
- .action(withErrorHandling(async (args, _options, command) => {
110
+ .description('Create a command that executes a raw shell string.')
111
+ .argument('[args...]', 'Hierarchical path segments followed by the shell command target')
112
+ .summary('create a shell alias')
113
+ .addHelpText('after', `
114
+ Examples:
115
+ $ agentctl ctl alias dev logs "docker compose logs -f"
116
+ $ agentctl ctl alias list-files "ls -la"
117
+ `)
118
+ .action(withErrorHandling(async (args, opts, command) => {
171
119
  if (!args || args.length < 2) {
172
120
  command.help();
173
121
  return;
174
122
  }
175
123
  const target = args.pop();
176
- const name = args;
124
+ const name = normalizePath(args);
125
+ if (name.length === 0) {
126
+ command.help();
127
+ return;
128
+ }
177
129
  await (0, ctl_1.alias)(name, target);
178
- }))
179
- .addHelpText('after', `
180
- How it works:
181
- The last argument is always treated as the shell command to execute.
182
- All preceding arguments form the hierarchical path.
183
-
184
- If the shell command contains spaces, wrap it in quotes.
185
-
186
- Examples:
187
- $ agentctl ctl alias tools git-status "git status"
188
- -> Creates 'agentctl tools git-status' which runs 'git status'.
189
-
190
- $ agentctl ctl alias dev build "npm run build"
191
- -> Creates 'agentctl dev build' which runs 'npm run build'.
192
- `);
130
+ }));
193
131
  ctl.command('group')
194
- .description('Create a command group (namespace) to organize related commands')
195
- .argument('[path...]', 'Hierarchical path for the group (e.g. "dev")')
132
+ .description('Create a command group (namespace) to organize subcommands.')
133
+ .argument('[path...]', 'Hierarchical path for the group (e.g., "dev" or "cloud/aws")')
134
+ .summary('create a namespace group')
196
135
  .addHelpText('after', `
197
- Description:
198
- Groups are essentially folders that contain other commands.
199
- They don't execute anything themselves but provide organization.
136
+ Additional Info:
137
+ Groups allow you to categorize commands. Running a group command without
138
+ subcommands will list all direct subcommands within that group.
139
+
140
+ This command creates a folder in your local .agentctl directory.
141
+ Inside this newly created folder, it generates a manifest.json.
142
+
143
+ Group Schema (manifest.json) to edit:
144
+ {
145
+ "name": "<group_folder_name>",
146
+ "description": "<insert group summary here>",
147
+ "help": "<insert longer group description/instructions here>",
148
+ "type": "group" // do not change!
149
+ }
150
+
151
+ Note:
152
+ - The "description" is displayed when you view this group in a list.
153
+ - The "help" is displayed when you call this group without a subcommand.
200
154
 
201
155
  Examples:
202
156
  $ agentctl ctl group dev
203
- $ agentctl ctl group tools/internal
157
+ $ agentctl ctl group "data pipelines" # Creates group 'data' and subgroup 'pipelines'
204
158
  `)
205
- .action(withErrorHandling(async (parts, _options, command) => {
206
- if (!parts || parts.length === 0) {
159
+ .action(withErrorHandling(async (parts, opts, command) => {
160
+ const normalized = normalizePath(parts);
161
+ if (normalized.length === 0) {
207
162
  command.help();
208
163
  return;
209
164
  }
210
- await (0, ctl_1.group)(parts);
165
+ await (0, ctl_1.group)(normalized);
211
166
  }));
212
167
  ctl.command('rm')
213
- .description('Remove a command or group permanently')
168
+ .description('Permanently remove a command or group.')
214
169
  .argument('[path...]', 'Command path to remove')
215
- .option('--global', 'Remove from global scope')
170
+ .option('-g, --global', 'Remove from global scope instead of local')
171
+ .summary('delete a command')
216
172
  .addHelpText('after', `
217
173
  Examples:
218
- $ agentctl ctl rm dev start
219
- $ agentctl ctl rm tools --global
220
- `)
174
+ $ agentctl ctl rm dev start
175
+ $ agentctl ctl rm utils--global
176
+ `)
221
177
  .action(withErrorHandling(async (parts, opts, command) => {
222
- if (!parts || parts.length === 0) {
178
+ const normalized = normalizePath(parts);
179
+ if (normalized.length === 0) {
223
180
  command.help();
224
181
  return;
225
182
  }
226
- await (0, ctl_1.rm)(parts, { global: opts.global });
183
+ await (0, ctl_1.rm)(normalized, { global: opts.global });
227
184
  }));
228
185
  ctl.command('mv')
229
- .description('Move or rename a command or group')
230
- .argument('[src]', 'Current path of the command')
231
- .argument('[dest]', 'New path for the command')
232
- .option('--global', 'Perform operation in global scope')
186
+ .description('Move or rename a command/group within its current scope.')
187
+ .argument('[src]', 'Current path (space-separated or quoted)')
188
+ .argument('[dest]', 'New path (space-separated or quoted)')
189
+ .option('-g, --global', 'Operate in global scope')
190
+ .summary('rename/move a command')
233
191
  .addHelpText('after', `
234
192
  Examples:
235
- $ agentctl ctl mv "dev start" "dev boot"
236
- $ agentctl ctl mv tools/gh tools/github --global
237
- `)
193
+ $ agentctl ctl mv "dev start" "dev begin"
194
+ $ agentctl ctl mv utils scripts--global
195
+ `)
238
196
  .action(withErrorHandling(async (src, dest, opts, command) => {
239
- if (!src || !dest) {
197
+ const normalizedSrc = normalizePath(src ? [src] : []);
198
+ const normalizedDest = normalizePath(dest ? [dest] : []);
199
+ if (normalizedSrc.length === 0 || normalizedDest.length === 0) {
240
200
  command.help();
241
201
  return;
242
202
  }
243
- await (0, ctl_1.mv)(src.split(' '), dest.split(' '), { global: opts.global });
203
+ await (0, ctl_1.mv)(normalizedSrc, normalizedDest, { global: opts.global });
244
204
  }));
245
- // --- Introspection ---
246
205
  ctl.command('list')
247
- .description('List all available commands across local and global scopes')
206
+ .description('List all available commands across local and global scopes.')
207
+ .summary('list all commands')
208
+ .addHelpText('after', `
209
+ Output Columns:
210
+ TYPE - scaffold, alias, or group
211
+ SCOPE - local(project - specific) or global(user - wide)
212
+ COMMAND - The path used to invoke the command
213
+ DESCRIPTION - Brief text from the command's manifest
214
+ `)
248
215
  .action(withErrorHandling(async () => {
249
216
  const items = await (0, ctl_1.list)();
250
- console.log('TYPE SCOPE COMMAND DESCRIPTION');
217
+ console.log(chalk_1.default.bold('TYPE SCOPE COMMAND DESCRIPTION'));
251
218
  for (const item of items) {
252
- console.log(`${item.type.padEnd(9)} ${item.scope.padEnd(9)} ${item.path.padEnd(19)} ${item.description}`);
219
+ const typePipe = item.type.padEnd(9);
220
+ const scopePipe = item.scope === 'local' ? chalk_1.default.cyan(item.scope.padEnd(9)) : chalk_1.default.magenta(item.scope.padEnd(9));
221
+ console.log(`${typePipe} ${scopePipe} ${chalk_1.default.yellow(item.path.padEnd(19))} ${item.description}`);
253
222
  }
254
223
  }));
255
224
  ctl.command('inspect')
256
- .description('Inspect the internal manifest and details of a command')
225
+ .description('Show the internal manifest and file system path of a command.')
257
226
  .argument('[path...]', 'Command path to inspect')
258
- .action(withErrorHandling(async (parts, _options, command) => {
259
- if (!parts || parts.length === 0) {
227
+ .summary('inspect command details')
228
+ .addHelpText('after', `
229
+ Examples:
230
+ $ agentctl ctl inspect dev start
231
+ `)
232
+ .action(withErrorHandling(async (parts, opts, command) => {
233
+ const normalized = normalizePath(parts);
234
+ if (normalized.length === 0) {
260
235
  command.help();
261
236
  return;
262
237
  }
263
- const info = await (0, ctl_1.inspect)(parts);
238
+ const info = await (0, ctl_1.inspect)(normalized);
264
239
  if (info) {
265
240
  console.log(JSON.stringify(info, null, 2));
266
241
  }
267
242
  else {
268
- console.error('Command not found');
243
+ console.error(chalk_1.default.red('Command not found'));
269
244
  process.exit(1);
270
245
  }
271
246
  }));
272
- // --- Scoping ---
273
247
  ctl.command('global')
274
- .description('Push a local command to the global scope')
275
- .argument('[path...]', 'Local command path')
276
- .option('--move', 'Move instead of copy')
277
- .option('--copy', 'Copy (default)')
248
+ .description('Promote a local command to the global scope.')
249
+ .argument('[path...]', 'Local command path to promote')
250
+ .option('-m, --move', 'Move the command (delete local after copying)')
251
+ .option('-c, --copy', 'Copy the command (keep local version, default)')
252
+ .summary('make a command global')
278
253
  .addHelpText('after', `
279
- Examples:
280
- $ agentctl ctl global sys --move
281
- $ agentctl ctl global tools --copy
282
- `)
254
+ Additional Info:
255
+ Global commands are stored in your home directory and are available in any project.
256
+
257
+ Examples:
258
+ $ agentctl ctl global utils / cleanup
259
+ $ agentctl ctl global dev / deploy--move
260
+ `)
283
261
  .action(withErrorHandling(async (parts, opts, command) => {
284
- if (!parts || parts.length === 0) {
262
+ const normalized = normalizePath(parts);
263
+ if (normalized.length === 0) {
285
264
  command.help();
286
265
  return;
287
266
  }
288
- await (0, ctl_1.pushGlobal)(parts, { move: opts.move, copy: opts.copy || !opts.move });
267
+ await (0, ctl_1.pushGlobal)(normalized, { move: opts.move, copy: opts.copy || !opts.move });
289
268
  }));
290
269
  ctl.command('local')
291
- .description('Pull a global command to the local scope')
292
- .argument('[path...]', 'Global command path')
293
- .option('--move', 'Move instead of copy')
294
- .option('--copy', 'Copy (default)')
270
+ .description('Pull a global command into the current local project.')
271
+ .argument('[path...]', 'Global command path to pull')
272
+ .option('-m, --move', 'Move the command (delete global after pulling)')
273
+ .option('-c, --copy', 'Copy the command (keep global version, default)')
274
+ .summary('make a command local')
295
275
  .addHelpText('after', `
296
276
  Examples:
297
- $ agentctl ctl local tools --copy
298
- `)
277
+ $ agentctl ctl local utils / shared
278
+ $ agentctl ctl local snippets / js--move
279
+ `)
299
280
  .action(withErrorHandling(async (parts, opts, command) => {
300
- if (!parts || parts.length === 0) {
281
+ const normalized = normalizePath(parts);
282
+ if (normalized.length === 0) {
301
283
  command.help();
302
284
  return;
303
285
  }
304
- await (0, ctl_1.pullLocal)(parts, { move: opts.move, copy: opts.copy || !opts.move });
286
+ await (0, ctl_1.pullLocal)(normalized, { move: opts.move, copy: opts.copy || !opts.move });
305
287
  }));
306
- // --- Agent Integration ---
307
- // We attach this to the root `ctl` as options or a sub-command?
308
- // Original code had it as options on `ctl`. We can make it a command for better help.
309
- // But sticking to options maintains compatibility. We'll improve the option help.
310
- ctl.option('--install-skill <agent>', 'Install skill for agent (cursor, antigravity, agentsmd, gemini)')
311
- .option('--global', 'Install skill globally (for supported agents)')
288
+ ctl.command('install')
289
+ .description('Install a command group from a remote Git repository.')
290
+ .argument('[repoUrl]', 'URL of the remote Git repository containing an .agentctl folder')
291
+ .argument('[pathParts...]', 'Optional local namespace/group to install into')
292
+ .option('-g, --global', 'Install globally instead of locally')
293
+ .option('--allow-collisions', 'Allow overwriting existing commands or merging into groups')
294
+ .summary('install remote command group')
312
295
  .addHelpText('after', `
313
- Examples:
314
- $ agentctl ctl --install-skill cursor
315
- $ agentctl ctl --install-skill antigravity --global
316
- $ agentctl ctl --install-skill gemini
317
- `)
318
- .action(withErrorHandling(async (op, command) => {
319
- const opts = ctl.opts();
320
- if (opts.installSkill) {
321
- await (0, ctl_1.installSkill)(opts.installSkill, { global: opts.global });
296
+ Additional Info:
297
+ Fetches the.agentctl folder from the remote repository and installs it into
298
+ your local or global agentctl environment.
299
+
300
+ Examples:
301
+ $ agentctl ctl install https://github.com/org/repo-tools
302
+ $ agentctl ctl install https://github.com/org/deploy-scripts deploy --global
303
+ `)
304
+ .action(withErrorHandling(async (repoUrl, pathParts, opts, command) => {
305
+ if (!repoUrl) {
306
+ command.help();
307
+ return;
322
308
  }
323
- else {
324
- // If no subcmd and no option, show help
325
- if (command.args.length === 0) {
326
- ctl.help();
309
+ const normalized = normalizePath(pathParts || []);
310
+ await (0, ctl_1.install)(repoUrl, normalized, { global: opts.global, allowCollisions: opts.allowCollisions });
311
+ }));
312
+ // --- Dynamic Command Logic ---
313
+ async function handleDynamicCommand(args) {
314
+ try {
315
+ const result = await (0, resolve_1.resolveCommand)(args);
316
+ if (!result) {
317
+ const effects = index_1.AppLogic.planApp(args, null);
318
+ await (0, effects_1.execute)(effects.map(e => e.type === 'log' ? { ...e, message: chalk_1.default.red(e.message) } : e));
319
+ process.exit(1);
320
+ }
321
+ if (result.manifest.run) {
322
+ const effects = index_1.AppLogic.planApp(args, result);
323
+ await (0, effects_1.execute)(effects);
324
+ }
325
+ else {
326
+ const all = await (0, ctl_1.list)();
327
+ const effects = index_1.AppLogic.planGroupList(result.manifest, result.cmdPath, all);
328
+ await (0, effects_1.execute)(effects.map(e => {
329
+ if (e.type === 'log') {
330
+ if (e.message === result.manifest.name)
331
+ return { ...e, message: chalk_1.default.blue(chalk_1.default.bold(e.message)) };
332
+ if (e.message === '\nSubcommands:')
333
+ return e;
334
+ if (e.message.startsWith(' '))
335
+ return e;
336
+ if (e.message === 'Command group containing subcommands')
337
+ return { ...e, message: chalk_1.default.dim(e.message) };
338
+ }
339
+ return e;
340
+ }));
327
341
  }
328
342
  }
329
- }));
330
- // Inject dynamic commands into root help
331
- // We need to do this before parsing
343
+ catch (e) {
344
+ if (e instanceof Error) {
345
+ console.error(chalk_1.default.red(e.message));
346
+ }
347
+ else {
348
+ console.error(chalk_1.default.red('An unknown error occurred'));
349
+ }
350
+ process.exit(1);
351
+ }
352
+ }
332
353
  (async () => {
354
+ // Add help text for user commands dynamically
333
355
  try {
334
356
  const allCommands = await (0, ctl_1.list)();
335
- const topLevel = allCommands.filter(c => !c.path.includes(' ')); // Only top level
357
+ const topLevel = allCommands.filter(c => !c.path.includes(' '));
336
358
  if (topLevel.length > 0) {
337
359
  const lines = [''];
338
- lines.push('User Commands:');
360
+ lines.push(chalk_1.default.bold('User Commands:'));
339
361
  for (const cmd of topLevel) {
340
- // simple padding
341
- lines.push(` ${cmd.path.padEnd(27)}${cmd.description}`);
362
+ const desc = cmd.description || 'Command group containing subcommands';
363
+ lines.push(` ${chalk_1.default.yellow(cmd.path.padEnd(27))}${desc}`);
342
364
  }
343
365
  lines.push('');
344
366
  program.addHelpText('after', lines.join('\n'));
345
367
  }
346
368
  }
347
369
  catch {
348
- // Ignore errors during help generation (e.g. if not initialized)
370
+ // Ignore errors during help generation
371
+ }
372
+ // Process arguments
373
+ const args = process.argv.slice(2);
374
+ // If no args, show help
375
+ if (args.length === 0) {
376
+ program.help();
377
+ return;
378
+ }
379
+ // Special case: if it starts with 'ctl', or is '--help', '--version', '-h', etc.,
380
+ // let commander handle it natively.
381
+ const firstArg = args[0];
382
+ const isStandardCommand = ['ctl', '--help', '-h', '--version', '-V'].includes(firstArg) || firstArg.startsWith('-');
383
+ if (isStandardCommand) {
384
+ program.parse(process.argv);
385
+ }
386
+ else {
387
+ // It's a dynamic command
388
+ await handleDynamicCommand(args);
349
389
  }
350
- program.parse(process.argv);
351
390
  })();