@michaelhartmayer/agentctl 1.2.0 → 1.2.2

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/ctl.js CHANGED
@@ -42,6 +42,18 @@ async function getCappedAncestor(dir, baseDir) {
42
42
  }
43
43
  return null;
44
44
  }
45
+ async function getMissingAncestorGroups(args, agentctlDir) {
46
+ const missing = [];
47
+ for (let i = 1; i < args.length; i++) {
48
+ const segments = args.slice(0, i);
49
+ const dir = path_1.default.join(agentctlDir, ...segments);
50
+ const manifestPath = path_1.default.join(dir, 'manifest.json');
51
+ if (!(await fs_extra_1.default.pathExists(manifestPath))) {
52
+ missing.push({ dir, name: segments[segments.length - 1] });
53
+ }
54
+ }
55
+ return missing;
56
+ }
45
57
  async function getContext(options) {
46
58
  const cwd = options.cwd || process.cwd();
47
59
  return {
@@ -59,7 +71,13 @@ async function scaffold(args, options = {}) {
59
71
  const targetDir = path_1.default.join(agentctlDir, args.join(path_1.default.sep));
60
72
  const exists = await fs_extra_1.default.pathExists(targetDir);
61
73
  const cappedAncestor = await getCappedAncestor(targetDir, agentctlDir);
62
- const { effects } = ctl_1.Logic.planScaffold(args, ctx, { exists, cappedAncestor: cappedAncestor || undefined, type: 'scaffold' });
74
+ const missingAncestorGroups = await getMissingAncestorGroups(args, agentctlDir);
75
+ const { effects } = ctl_1.Logic.planScaffold(args, ctx, {
76
+ exists,
77
+ cappedAncestor: cappedAncestor || undefined,
78
+ type: 'scaffold',
79
+ missingAncestorGroups
80
+ });
63
81
  await (0, effects_1.execute)(effects);
64
82
  }
65
83
  async function alias(args, target, options = {}) {
@@ -69,11 +87,13 @@ async function alias(args, target, options = {}) {
69
87
  const targetDir = path_1.default.join(agentctlDir, args.join(path_1.default.sep));
70
88
  const exists = await fs_extra_1.default.pathExists(targetDir);
71
89
  const cappedAncestor = await getCappedAncestor(targetDir, agentctlDir);
90
+ const missingAncestorGroups = await getMissingAncestorGroups(args, agentctlDir);
72
91
  const { effects } = ctl_1.Logic.planScaffold(args, ctx, {
73
92
  exists,
74
93
  cappedAncestor: cappedAncestor || undefined,
75
94
  type: 'alias',
76
- target
95
+ target,
96
+ missingAncestorGroups
77
97
  });
78
98
  await (0, effects_1.execute)(effects);
79
99
  }
@@ -84,10 +104,12 @@ async function group(args, options = {}) {
84
104
  const targetDir = path_1.default.join(agentctlDir, args.join(path_1.default.sep));
85
105
  const exists = await fs_extra_1.default.pathExists(targetDir);
86
106
  const cappedAncestor = await getCappedAncestor(targetDir, agentctlDir);
107
+ const missingAncestorGroups = await getMissingAncestorGroups(args, agentctlDir);
87
108
  const { effects } = ctl_1.Logic.planScaffold(args, ctx, {
88
109
  exists,
89
110
  cappedAncestor: cappedAncestor || undefined,
90
- type: 'group'
111
+ type: 'group',
112
+ missingAncestorGroups
91
113
  });
92
114
  await (0, effects_1.execute)(effects);
93
115
  }
package/dist/effects.js CHANGED
@@ -46,7 +46,9 @@ async function execute(effects) {
46
46
  break;
47
47
  }
48
48
  case 'spawn': {
49
- const child = (0, child_process_1.spawn)(effect.command, effect.options);
49
+ const child = effect.args
50
+ ? (0, child_process_1.spawn)(effect.command, effect.args, effect.options)
51
+ : (0, child_process_1.spawn)(effect.command, effect.options);
50
52
  if (effect.onExit) {
51
53
  child.on('exit', effect.onExit);
52
54
  }
package/dist/index.js CHANGED
@@ -28,33 +28,33 @@ const ctl = program.command('ctl')
28
28
  .action((opts, command) => {
29
29
  command.help();
30
30
  })
31
- .addHelpText('after', `
32
- ${chalk_1.default.bold('Agentctl Paradigm:')}
33
- Agentctl acts as a unified control plane allowing both Humans and AI Agents
34
- to create, discover, and execute local shell commands. By running \`agentctl ctl scaffold <name>\`
35
- you create a directory in the \`.agentctl\` folder containing a \`manifest.json\`
36
- and a run script. This dynamically creates a new \`agentctl <name>\` command that
37
- is easily callable by agents and globally executable on your machine.
38
-
39
- Commands:
40
-
41
- ${chalk_1.default.bold('Creation')}
42
- scaffold [path...] create a new command
43
- alias [args...] create a shell alias
44
- group [path...] create a namespace group
45
-
46
- ${chalk_1.default.bold('Organize & Scope')}
47
- rm [options] [path...] delete a command
48
- mv [options] [src] [dest] rename/move a command
49
- global [options] [path...] make a command global
50
- local [options] [path...] make a command local
51
-
52
- ${chalk_1.default.bold('Information')}
53
- list list all commands
54
- inspect [path...] inspect command details
55
-
56
- ${chalk_1.default.bold('Integration')}
57
- install [options] [repoUrl] [pathParts...] install remote command group
31
+ .addHelpText('after', `
32
+ ${chalk_1.default.bold('Agentctl Paradigm:')}
33
+ Agentctl acts as a unified control plane allowing both Humans and AI Agents
34
+ to create, discover, and execute local shell commands. By running \`agentctl ctl scaffold <name>\`
35
+ you create a directory in the \`.agentctl\` folder containing a \`manifest.json\`
36
+ and a run script. This dynamically creates a new \`agentctl <name>\` command that
37
+ is easily callable by agents and globally executable on your machine.
38
+
39
+ Commands:
40
+
41
+ ${chalk_1.default.bold('Creation')}
42
+ scaffold [path...] create a new command
43
+ alias [args...] create a shell alias
44
+ group [path...] create a namespace group
45
+
46
+ ${chalk_1.default.bold('Organize & Scope')}
47
+ rm [options] [path...] delete a command
48
+ mv [options] [src] [dest] rename/move a command
49
+ global [options] [path...] make a command global
50
+ local [options] [path...] make a command local
51
+
52
+ ${chalk_1.default.bold('Information')}
53
+ list list all commands
54
+ inspect [path...] inspect command details
55
+
56
+ ${chalk_1.default.bold('Integration')}
57
+ install [options] [repoUrl] [pathParts...] install remote command group
58
58
  `);
59
59
  const withErrorHandling = (fn) => {
60
60
  return async (...args) => {
@@ -77,29 +77,29 @@ ctl.command('scaffold')
77
77
  .description('Scaffold a new command directory with a manifest and starter script.')
78
78
  .argument('[path...]', 'Hierarchical path for the new command (e.g., "dev start" or "utils/cleanup")')
79
79
  .summary('create a new command')
80
- .addHelpText('after', `
81
- Additional Info:
82
- This command creates a folder in your local .agentctl directory.
83
- Inside this newly created folder, it generates:
84
- - manifest.json: Metadata config to edit for your command.
85
- - command.sh/cmd: A starter script to edit for your actual logic.
86
-
87
- Manifest Schema (manifest.json) to edit:
88
- {
89
- "name": "<command_folder_name>",
90
- "description": "<insert command summary here>",
91
- "help": "<insert longer usage/help instructions here>",
92
- "type": "scaffold", // do not change!
93
- "run": "./command.cmd" // points to the script to execute
94
- }
95
-
96
- Note:
97
- - The "description" is displayed when you view this command in a list.
98
- - Commands must provide their own help implementation (e.g. by handling --help inside your script).
99
-
100
- Examples:
101
- $ agentctl ctl scaffold build:front
102
- $ agentctl ctl scaffold "build front" # Creates group 'build' and subcommand 'front'
80
+ .addHelpText('after', `
81
+ Additional Info:
82
+ This command creates a folder in your local .agentctl directory.
83
+ Inside this newly created folder, it generates:
84
+ - manifest.json: Metadata config to edit for your command.
85
+ - command.sh/cmd: A starter script to edit for your actual logic.
86
+
87
+ Manifest Schema (manifest.json) to edit:
88
+ {
89
+ "name": "<command_folder_name>",
90
+ "description": "<insert command summary here>",
91
+ "help": "<insert longer usage/help instructions here>",
92
+ "type": "scaffold", // do not change!
93
+ "run": "./command.cmd" // points to the script to execute
94
+ }
95
+
96
+ Note:
97
+ - The "description" is displayed when you view this command in a list.
98
+ - Commands must provide their own help implementation (e.g. by handling --help inside your script).
99
+
100
+ Examples:
101
+ $ agentctl ctl scaffold build:front
102
+ $ agentctl ctl scaffold "build front" # Creates group 'build' and subcommand 'front'
103
103
  `)
104
104
  .action(withErrorHandling(async (pathParts, opts, command) => {
105
105
  const normalized = normalizePath(pathParts);
@@ -113,10 +113,10 @@ ctl.command('alias')
113
113
  .description('Create a command that executes a raw shell string.')
114
114
  .argument('[args...]', 'Hierarchical path segments followed by the shell command target')
115
115
  .summary('create a shell alias')
116
- .addHelpText('after', `
117
- Examples:
118
- $ agentctl ctl alias dev logs "docker compose logs -f"
119
- $ agentctl ctl alias list-files "ls -la"
116
+ .addHelpText('after', `
117
+ Examples:
118
+ $ agentctl ctl alias dev logs "docker compose logs -f"
119
+ $ agentctl ctl alias list-files "ls -la"
120
120
  `)
121
121
  .action(withErrorHandling(async (args, opts, command) => {
122
122
  if (!args || args.length < 2) {
@@ -135,29 +135,29 @@ ctl.command('group')
135
135
  .description('Create a command group (namespace) to organize subcommands.')
136
136
  .argument('[path...]', 'Hierarchical path for the group (e.g., "dev" or "cloud/aws")')
137
137
  .summary('create a namespace group')
138
- .addHelpText('after', `
139
- Additional Info:
140
- Groups allow you to categorize commands. Running a group command without
141
- subcommands will list all direct subcommands within that group.
142
-
143
- This command creates a folder in your local .agentctl directory.
144
- Inside this newly created folder, it generates a manifest.json.
145
-
146
- Group Schema (manifest.json) to edit:
147
- {
148
- "name": "<group_folder_name>",
149
- "description": "<insert group summary here>",
150
- "help": "<insert longer group description/instructions here>",
151
- "type": "group" // do not change!
152
- }
153
-
154
- Note:
155
- - The "description" is displayed when you view this group in a list.
156
- - The "help" is displayed when you call this group without a subcommand.
157
-
158
- Examples:
159
- $ agentctl ctl group dev
160
- $ agentctl ctl group "data pipelines" # Creates group 'data' and subgroup 'pipelines'
138
+ .addHelpText('after', `
139
+ Additional Info:
140
+ Groups allow you to categorize commands. Running a group command without
141
+ subcommands will list all direct subcommands within that group.
142
+
143
+ This command creates a folder in your local .agentctl directory.
144
+ Inside this newly created folder, it generates a manifest.json.
145
+
146
+ Group Schema (manifest.json) to edit:
147
+ {
148
+ "name": "<group_folder_name>",
149
+ "description": "<insert group summary here>",
150
+ "help": "<insert longer group description/instructions here>",
151
+ "type": "group" // do not change!
152
+ }
153
+
154
+ Note:
155
+ - The "description" is displayed when you view this group in a list.
156
+ - The "help" is displayed when you call this group without a subcommand.
157
+
158
+ Examples:
159
+ $ agentctl ctl group dev
160
+ $ agentctl ctl group "data pipelines" # Creates group 'data' and subgroup 'pipelines'
161
161
  `)
162
162
  .action(withErrorHandling(async (parts, opts, command) => {
163
163
  const normalized = normalizePath(parts);
@@ -172,10 +172,10 @@ ctl.command('rm')
172
172
  .argument('[path...]', 'Command path to remove')
173
173
  .option('-g, --global', 'Remove from global scope instead of local')
174
174
  .summary('delete a command')
175
- .addHelpText('after', `
176
- Examples:
177
- $ agentctl ctl rm dev start
178
- $ agentctl ctl rm utils--global
175
+ .addHelpText('after', `
176
+ Examples:
177
+ $ agentctl ctl rm dev start
178
+ $ agentctl ctl rm utils--global
179
179
  `)
180
180
  .action(withErrorHandling(async (parts, opts, command) => {
181
181
  const normalized = normalizePath(parts);
@@ -191,10 +191,10 @@ ctl.command('mv')
191
191
  .argument('[dest]', 'New path (space-separated or quoted)')
192
192
  .option('-g, --global', 'Operate in global scope')
193
193
  .summary('rename/move a command')
194
- .addHelpText('after', `
195
- Examples:
196
- $ agentctl ctl mv "dev start" "dev begin"
197
- $ agentctl ctl mv utils scripts--global
194
+ .addHelpText('after', `
195
+ Examples:
196
+ $ agentctl ctl mv "dev start" "dev begin"
197
+ $ agentctl ctl mv utils scripts--global
198
198
  `)
199
199
  .action(withErrorHandling(async (src, dest, opts, command) => {
200
200
  const normalizedSrc = normalizePath(src ? [src] : []);
@@ -208,12 +208,12 @@ Examples:
208
208
  ctl.command('list')
209
209
  .description('List all available commands across local and global scopes.')
210
210
  .summary('list all commands')
211
- .addHelpText('after', `
212
- Output Columns:
213
- TYPE - scaffold, alias, or group
214
- SCOPE - local(project - specific) or global(user - wide)
215
- COMMAND - The path used to invoke the command
216
- DESCRIPTION - Brief text from the command's manifest
211
+ .addHelpText('after', `
212
+ Output Columns:
213
+ TYPE - scaffold, alias, or group
214
+ SCOPE - local(project - specific) or global(user - wide)
215
+ COMMAND - The path used to invoke the command
216
+ DESCRIPTION - Brief text from the command's manifest
217
217
  `)
218
218
  .action(withErrorHandling(async () => {
219
219
  const items = await (0, ctl_1.list)();
@@ -228,9 +228,9 @@ ctl.command('inspect')
228
228
  .description('Show the internal manifest and file system path of a command.')
229
229
  .argument('[path...]', 'Command path to inspect')
230
230
  .summary('inspect command details')
231
- .addHelpText('after', `
232
- Examples:
233
- $ agentctl ctl inspect dev start
231
+ .addHelpText('after', `
232
+ Examples:
233
+ $ agentctl ctl inspect dev start
234
234
  `)
235
235
  .action(withErrorHandling(async (parts, opts, command) => {
236
236
  const normalized = normalizePath(parts);
@@ -253,13 +253,13 @@ ctl.command('global')
253
253
  .option('-m, --move', 'Move the command (delete local after copying)')
254
254
  .option('-c, --copy', 'Copy the command (keep local version, default)')
255
255
  .summary('make a command global')
256
- .addHelpText('after', `
257
- Additional Info:
258
- Global commands are stored in your home directory and are available in any project.
259
-
260
- Examples:
261
- $ agentctl ctl global utils / cleanup
262
- $ agentctl ctl global dev / deploy--move
256
+ .addHelpText('after', `
257
+ Additional Info:
258
+ Global commands are stored in your home directory and are available in any project.
259
+
260
+ Examples:
261
+ $ agentctl ctl global utils / cleanup
262
+ $ agentctl ctl global dev / deploy--move
263
263
  `)
264
264
  .action(withErrorHandling(async (parts, opts, command) => {
265
265
  const normalized = normalizePath(parts);
@@ -275,10 +275,10 @@ ctl.command('local')
275
275
  .option('-m, --move', 'Move the command (delete global after pulling)')
276
276
  .option('-c, --copy', 'Copy the command (keep global version, default)')
277
277
  .summary('make a command local')
278
- .addHelpText('after', `
279
- Examples:
280
- $ agentctl ctl local utils / shared
281
- $ agentctl ctl local snippets / js--move
278
+ .addHelpText('after', `
279
+ Examples:
280
+ $ agentctl ctl local utils / shared
281
+ $ agentctl ctl local snippets / js--move
282
282
  `)
283
283
  .action(withErrorHandling(async (parts, opts, command) => {
284
284
  const normalized = normalizePath(parts);
@@ -295,14 +295,14 @@ ctl.command('install')
295
295
  .option('-g, --global', 'Install globally instead of locally')
296
296
  .option('--allow-collisions', 'Allow overwriting existing commands or merging into groups')
297
297
  .summary('install remote command group')
298
- .addHelpText('after', `
299
- Additional Info:
300
- Fetches the.agentctl folder from the remote repository and installs it into
301
- your local or global agentctl environment.
302
-
303
- Examples:
304
- $ agentctl ctl install https://github.com/org/repo-tools
305
- $ agentctl ctl install https://github.com/org/deploy-scripts deploy --global
298
+ .addHelpText('after', `
299
+ Additional Info:
300
+ Fetches the.agentctl folder from the remote repository and installs it into
301
+ your local or global agentctl environment.
302
+
303
+ Examples:
304
+ $ agentctl ctl install https://github.com/org/repo-tools
305
+ $ agentctl ctl install https://github.com/org/deploy-scripts deploy --global
306
306
  `)
307
307
  .action(withErrorHandling(async (repoUrl, pathParts, opts, command) => {
308
308
  if (!repoUrl) {
package/dist/logic/ctl.js CHANGED
@@ -29,6 +29,13 @@ exports.Logic = {
29
29
  const type = options.type || 'scaffold';
30
30
  const isWin = ctx.platform === 'win32';
31
31
  const effects = [{ type: 'mkdir', path: targetDir }];
32
+ for (const group of options.missingAncestorGroups || []) {
33
+ effects.push({
34
+ type: 'writeJson',
35
+ path: path_1.default.join(group.dir, 'manifest.json'),
36
+ content: { name: group.name, type: 'group' }
37
+ });
38
+ }
32
39
  const manifest = {
33
40
  name,
34
41
  description: '<insert summary>',
@@ -27,13 +27,17 @@ exports.AppLogic = {
27
27
  runCmd = path_1.default.resolve(cmdDir, runCmd);
28
28
  }
29
29
  runCmd = runCmd.replace(/{{DIR}}/g, cmdDir);
30
- const fullCommand = `${runCmd} ${remainingArgs.join(' ')}`;
30
+ const quoteArg = (arg) => /[\s"'\\$`]/.test(arg) ? JSON.stringify(arg) : arg;
31
+ const displayedArgs = remainingArgs.map(quoteArg).join(' ');
32
+ const fullCommand = displayedArgs ? `${runCmd} ${displayedArgs}` : runCmd;
33
+ const isAlias = manifest.type === 'alias';
31
34
  effects.push({ type: 'log', message: `[${scope}] Running: ${fullCommand}` }, {
32
35
  type: 'spawn',
33
- command: fullCommand,
36
+ command: isAlias ? fullCommand : runCmd,
37
+ args: isAlias ? undefined : remainingArgs,
34
38
  options: {
35
39
  cwd: process.cwd(),
36
- shell: true,
40
+ shell: isAlias,
37
41
  stdio: 'inherit',
38
42
  env: { ...process.env, AGENTCTL_SCOPE: scope }
39
43
  },
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "description": "Agent Controller - A unified interface for humans and AI agents",
7
- "version": "1.2.0",
7
+ "version": "1.2.2",
8
8
  "main": "dist/index.js",
9
9
  "bin": {
10
10
  "agentctl": "./dist/index.js"