@michaelhartmayer/agentctl 1.1.0 → 1.1.3

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,23 @@ 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');
16
18
  program
17
19
  .name('agentctl')
18
20
  .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
- });
21
+ .version(pkg.version);
22
+ // --- Subcommand: ctl ---
125
23
  const ctl = program.command('ctl')
126
24
  .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
131
25
  const withErrorHandling = (fn) => {
132
26
  return async (...args) => {
133
27
  try {
@@ -145,19 +39,21 @@ const withErrorHandling = (fn) => {
145
39
  };
146
40
  };
147
41
  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")')
42
+ .description('Scaffold a new command directory with a manifest and starter script.')
43
+ .argument('[path...]', 'Hierarchical path for the new command (e.g., "dev start" or "utils/cleanup")')
44
+ .summary('create a new command')
150
45
  .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.
46
+ Additional Info:
47
+ This command creates a folder in your local .agentctl directory.
48
+ Inside, it generates:
49
+ - manifest.json: Metadata about the command.
50
+ - command.sh/cmd: A starter script for your logic.
155
51
 
156
52
  Examples:
157
- $ agentctl ctl scaffold dev start
53
+ $ agentctl ctl scaffold build front
158
54
  $ agentctl ctl scaffold utils/backup
159
55
  `)
160
- .action(withErrorHandling(async (pathParts, _options, command) => {
56
+ .action(withErrorHandling(async (pathParts, opts, command) => {
161
57
  if (!pathParts || pathParts.length === 0) {
162
58
  command.help();
163
59
  return;
@@ -165,9 +61,15 @@ Examples:
165
61
  await (0, ctl_1.scaffold)(pathParts);
166
62
  }));
167
63
  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) => {
64
+ .description('Create a command that executes a raw shell string.')
65
+ .argument('[args...]', 'Hierarchical path segments followed by the shell command target')
66
+ .summary('create a shell alias')
67
+ .addHelpText('after', `
68
+ Examples:
69
+ $ agentctl ctl alias dev logs "docker compose logs -f"
70
+ $ agentctl ctl alias list-files "ls -la"
71
+ `)
72
+ .action(withErrorHandling(async (args, opts, command) => {
171
73
  if (!args || args.length < 2) {
172
74
  command.help();
173
75
  return;
@@ -175,34 +77,21 @@ ctl.command('alias')
175
77
  const target = args.pop();
176
78
  const name = args;
177
79
  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
- `);
80
+ }));
193
81
  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")')
82
+ .description('Create a command group (namespace) to organize subcommands.')
83
+ .argument('[path...]', 'Hierarchical path for the group (e.g., "dev" or "cloud/aws")')
84
+ .summary('create a namespace group')
196
85
  .addHelpText('after', `
197
- Description:
198
- Groups are essentially folders that contain other commands.
199
- They don't execute anything themselves but provide organization.
86
+ Additional Info:
87
+ Groups allow you to categorize commands. Running a group command without
88
+ subcommands will list all direct subcommands within that group.
200
89
 
201
90
  Examples:
202
91
  $ agentctl ctl group dev
203
- $ agentctl ctl group tools/internal
92
+ $ agentctl ctl group data/pipelines
204
93
  `)
205
- .action(withErrorHandling(async (parts, _options, command) => {
94
+ .action(withErrorHandling(async (parts, opts, command) => {
206
95
  if (!parts || parts.length === 0) {
207
96
  command.help();
208
97
  return;
@@ -210,13 +99,14 @@ Examples:
210
99
  await (0, ctl_1.group)(parts);
211
100
  }));
212
101
  ctl.command('rm')
213
- .description('Remove a command or group permanently')
102
+ .description('Permanently remove a command or group.')
214
103
  .argument('[path...]', 'Command path to remove')
215
- .option('--global', 'Remove from global scope')
104
+ .option('-g, --global', 'Remove from global scope instead of local')
105
+ .summary('delete a command')
216
106
  .addHelpText('after', `
217
107
  Examples:
218
108
  $ agentctl ctl rm dev start
219
- $ agentctl ctl rm tools --global
109
+ $ agentctl ctl rm utils --global
220
110
  `)
221
111
  .action(withErrorHandling(async (parts, opts, command) => {
222
112
  if (!parts || parts.length === 0) {
@@ -226,14 +116,15 @@ Examples:
226
116
  await (0, ctl_1.rm)(parts, { global: opts.global });
227
117
  }));
228
118
  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')
119
+ .description('Move or rename a command/group within its current scope.')
120
+ .argument('[src]', 'Current path (space-separated or quoted)')
121
+ .argument('[dest]', 'New path (space-separated or quoted)')
122
+ .option('-g, --global', 'Operate in global scope')
123
+ .summary('rename/move a command')
233
124
  .addHelpText('after', `
234
125
  Examples:
235
- $ agentctl ctl mv "dev start" "dev boot"
236
- $ agentctl ctl mv tools/gh tools/github --global
126
+ $ agentctl ctl mv "dev start" "dev begin"
127
+ $ agentctl ctl mv utils scripts --global
237
128
  `)
238
129
  .action(withErrorHandling(async (src, dest, opts, command) => {
239
130
  if (!src || !dest) {
@@ -242,20 +133,34 @@ Examples:
242
133
  }
243
134
  await (0, ctl_1.mv)(src.split(' '), dest.split(' '), { global: opts.global });
244
135
  }));
245
- // --- Introspection ---
246
136
  ctl.command('list')
247
- .description('List all available commands across local and global scopes')
137
+ .description('List all available commands across local and global scopes.')
138
+ .summary('list all commands')
139
+ .addHelpText('after', `
140
+ Output Columns:
141
+ TYPE - scaffold, alias, or group
142
+ SCOPE - local (project-specific) or global (user-wide)
143
+ COMMAND - The path used to invoke the command
144
+ DESCRIPTION - Brief text from the command's manifest
145
+ `)
248
146
  .action(withErrorHandling(async () => {
249
147
  const items = await (0, ctl_1.list)();
250
- console.log('TYPE SCOPE COMMAND DESCRIPTION');
148
+ console.log(chalk_1.default.bold('TYPE SCOPE COMMAND DESCRIPTION'));
251
149
  for (const item of items) {
252
- console.log(`${item.type.padEnd(9)} ${item.scope.padEnd(9)} ${item.path.padEnd(19)} ${item.description}`);
150
+ const typePipe = item.type.padEnd(9);
151
+ const scopePipe = item.scope === 'local' ? chalk_1.default.cyan(item.scope.padEnd(9)) : chalk_1.default.magenta(item.scope.padEnd(9));
152
+ console.log(`${typePipe} ${scopePipe} ${chalk_1.default.yellow(item.path.padEnd(19))} ${item.description}`);
253
153
  }
254
154
  }));
255
155
  ctl.command('inspect')
256
- .description('Inspect the internal manifest and details of a command')
156
+ .description('Show the internal manifest and file system path of a command.')
257
157
  .argument('[path...]', 'Command path to inspect')
258
- .action(withErrorHandling(async (parts, _options, command) => {
158
+ .summary('inspect command details')
159
+ .addHelpText('after', `
160
+ Examples:
161
+ $ agentctl ctl inspect dev start
162
+ `)
163
+ .action(withErrorHandling(async (parts, opts, command) => {
259
164
  if (!parts || parts.length === 0) {
260
165
  command.help();
261
166
  return;
@@ -265,20 +170,23 @@ ctl.command('inspect')
265
170
  console.log(JSON.stringify(info, null, 2));
266
171
  }
267
172
  else {
268
- console.error('Command not found');
173
+ console.error(chalk_1.default.red('Command not found'));
269
174
  process.exit(1);
270
175
  }
271
176
  }));
272
- // --- Scoping ---
273
177
  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)')
178
+ .description('Promote a local command to the global scope.')
179
+ .argument('[path...]', 'Local command path to promote')
180
+ .option('-m, --move', 'Move the command (delete local after copying)')
181
+ .option('-c, --copy', 'Copy the command (keep local version, default)')
182
+ .summary('make a command global')
278
183
  .addHelpText('after', `
184
+ Additional Info:
185
+ Global commands are stored in your home directory and are available in any project.
186
+
279
187
  Examples:
280
- $ agentctl ctl global sys --move
281
- $ agentctl ctl global tools --copy
188
+ $ agentctl ctl global utils/cleanup
189
+ $ agentctl ctl global dev/deploy --move
282
190
  `)
283
191
  .action(withErrorHandling(async (parts, opts, command) => {
284
192
  if (!parts || parts.length === 0) {
@@ -288,13 +196,15 @@ Examples:
288
196
  await (0, ctl_1.pushGlobal)(parts, { move: opts.move, copy: opts.copy || !opts.move });
289
197
  }));
290
198
  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)')
199
+ .description('Pull a global command into the current local project.')
200
+ .argument('[path...]', 'Global command path to pull')
201
+ .option('-m, --move', 'Move the command (delete global after pulling)')
202
+ .option('-c, --copy', 'Copy the command (keep global version, default)')
203
+ .summary('make a command local')
295
204
  .addHelpText('after', `
296
205
  Examples:
297
- $ agentctl ctl local tools --copy
206
+ $ agentctl ctl local utils/shared
207
+ $ agentctl ctl local snippets/js --move
298
208
  `)
299
209
  .action(withErrorHandling(async (parts, opts, command) => {
300
210
  if (!parts || parts.length === 0) {
@@ -303,49 +213,127 @@ Examples:
303
213
  }
304
214
  await (0, ctl_1.pullLocal)(parts, { move: opts.move, copy: opts.copy || !opts.move });
305
215
  }));
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)')
216
+ ctl.command('install-skill')
217
+ .description('Configure a supported AI agent (like Cursor or Gemini) to natively use Agentctl.')
218
+ .argument('[agent]', 'Agent name (cursor, antigravity, agentsmd, gemini)')
219
+ .option('-g, --global', 'Install globally for the agent (if supported)')
220
+ .summary('configure AI agent integration')
312
221
  .addHelpText('after', `
222
+ Supported Agents:
223
+ - cursor (Installs to .cursor/skills)
224
+ - antigravity (Installs to .agent/skills or ~/.gemini/antigravity)
225
+ - agentsmd (Installs to .agents/skills)
226
+ - gemini (Installs to .gemini/skills or ~/.gemini/skills)
227
+
313
228
  Examples:
314
- $ agentctl ctl --install-skill cursor
315
- $ agentctl ctl --install-skill antigravity --global
316
- $ agentctl ctl --install-skill gemini
229
+ $ agentctl ctl install-skill cursor
230
+ $ agentctl ctl install-skill antigravity --global
317
231
  `)
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 });
232
+ .action(withErrorHandling(async (agent, opts, command) => {
233
+ if (!agent) {
234
+ command.help();
235
+ return;
322
236
  }
323
- else {
324
- // If no subcmd and no option, show help
325
- if (command.args.length === 0) {
326
- ctl.help();
327
- }
237
+ await (0, ctl_1.installSkill)(agent, { global: opts.global });
238
+ }));
239
+ ctl.command('install')
240
+ .description('Install a command group from a remote Git repository.')
241
+ .argument('[repoUrl]', 'URL of the remote Git repository containing an .agentctl folder')
242
+ .argument('[pathParts...]', 'Optional local namespace/group to install into')
243
+ .option('-g, --global', 'Install globally instead of locally')
244
+ .option('--allow-collisions', 'Allow overwriting existing commands or merging into groups')
245
+ .summary('install remote command group')
246
+ .addHelpText('after', `
247
+ Additional Info:
248
+ Fetches the .agentctl folder from the remote repository and installs it into
249
+ your local or global agentctl environment.
250
+
251
+ Examples:
252
+ $ agentctl ctl install https://github.com/org/repo-tools
253
+ $ agentctl ctl install https://github.com/org/deploy-scripts deploy --global
254
+ `)
255
+ .action(withErrorHandling(async (repoUrl, pathParts, opts, command) => {
256
+ if (!repoUrl) {
257
+ command.help();
258
+ return;
328
259
  }
260
+ await (0, ctl_1.install)(repoUrl, pathParts, { global: opts.global, allowCollisions: opts.allowCollisions });
329
261
  }));
330
- // Inject dynamic commands into root help
331
- // We need to do this before parsing
262
+ // --- Dynamic Command Logic ---
263
+ async function handleDynamicCommand(args) {
264
+ try {
265
+ const result = await (0, resolve_1.resolveCommand)(args);
266
+ if (!result) {
267
+ const effects = index_1.AppLogic.planApp(args, null);
268
+ await (0, effects_1.execute)(effects.map(e => e.type === 'log' ? { ...e, message: chalk_1.default.red(e.message) } : e));
269
+ process.exit(1);
270
+ }
271
+ if (result.manifest.run) {
272
+ const effects = index_1.AppLogic.planApp(args, result);
273
+ await (0, effects_1.execute)(effects);
274
+ }
275
+ else {
276
+ const all = await (0, ctl_1.list)();
277
+ const effects = index_1.AppLogic.planGroupList(result.manifest, result.cmdPath, all);
278
+ await (0, effects_1.execute)(effects.map(e => {
279
+ if (e.type === 'log') {
280
+ if (e.message === result.manifest.name)
281
+ return { ...e, message: chalk_1.default.blue(chalk_1.default.bold(e.message)) };
282
+ if (e.message === '\nSubcommands:')
283
+ return e;
284
+ if (e.message.startsWith(' '))
285
+ return e;
286
+ if (e.message === 'No description')
287
+ return { ...e, message: chalk_1.default.dim(e.message) };
288
+ }
289
+ return e;
290
+ }));
291
+ }
292
+ }
293
+ catch (e) {
294
+ if (e instanceof Error) {
295
+ console.error(chalk_1.default.red(e.message));
296
+ }
297
+ else {
298
+ console.error(chalk_1.default.red('An unknown error occurred'));
299
+ }
300
+ process.exit(1);
301
+ }
302
+ }
332
303
  (async () => {
304
+ // Add help text for user commands dynamically
333
305
  try {
334
306
  const allCommands = await (0, ctl_1.list)();
335
- const topLevel = allCommands.filter(c => !c.path.includes(' ')); // Only top level
307
+ const topLevel = allCommands.filter(c => !c.path.includes(' '));
336
308
  if (topLevel.length > 0) {
337
309
  const lines = [''];
338
- lines.push('User Commands:');
310
+ lines.push(chalk_1.default.bold('User Commands:'));
339
311
  for (const cmd of topLevel) {
340
- // simple padding
341
- lines.push(` ${cmd.path.padEnd(27)}${cmd.description}`);
312
+ lines.push(` ${chalk_1.default.yellow(cmd.path.padEnd(27))}${cmd.description}`);
342
313
  }
343
314
  lines.push('');
344
315
  program.addHelpText('after', lines.join('\n'));
345
316
  }
346
317
  }
347
318
  catch {
348
- // Ignore errors during help generation (e.g. if not initialized)
319
+ // Ignore errors during help generation
320
+ }
321
+ // Process arguments
322
+ const args = process.argv.slice(2);
323
+ // If no args, show help
324
+ if (args.length === 0) {
325
+ program.help();
326
+ return;
327
+ }
328
+ // Special case: if it starts with 'ctl', or is '--help', '--version', '-h', etc.,
329
+ // let commander handle it natively.
330
+ const firstArg = args[0];
331
+ const isStandardCommand = ['ctl', '--help', '-h', '--version', '-V'].includes(firstArg) || firstArg.startsWith('-');
332
+ if (isStandardCommand) {
333
+ program.parse(process.argv);
334
+ }
335
+ else {
336
+ // It's a dynamic command
337
+ await handleDynamicCommand(args);
349
338
  }
350
- program.parse(process.argv);
351
339
  })();