@michaelhartmayer/agentctl 1.0.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/.eslintignore +5 -0
- package/.eslintrc.json +22 -0
- package/.husky/pre-commit +2 -0
- package/README.md +124 -0
- package/agentctl.cmd +2 -0
- package/dist/ctl.js +351 -0
- package/dist/fs-utils.js +35 -0
- package/dist/index.js +313 -0
- package/dist/manifest.js +30 -0
- package/dist/resolve.js +123 -0
- package/dist/skills.js +50 -0
- package/package.json +49 -0
- package/scripts/register-path.js +32 -0
- package/scripts/unregister-path.js +30 -0
- package/skills/agentctl/SKILL.md +59 -0
- package/src/ctl.ts +356 -0
- package/src/fs-utils.ts +30 -0
- package/src/index.ts +331 -0
- package/src/manifest.ts +21 -0
- package/src/resolve.ts +124 -0
- package/src/skills.ts +42 -0
- package/tests/alias.test.ts +48 -0
- package/tests/edge_cases.test.ts +699 -0
- package/tests/group.test.ts +48 -0
- package/tests/helpers.ts +16 -0
- package/tests/introspection.test.ts +71 -0
- package/tests/lifecycle-guards.test.ts +44 -0
- package/tests/lifecycle.test.ts +59 -0
- package/tests/manifest.test.ts +29 -0
- package/tests/resolve-priority.test.ts +72 -0
- package/tests/resolve.test.ts +78 -0
- package/tests/scaffold.test.ts +61 -0
- package/tests/scoping-guards.test.ts +74 -0
- package/tests/scoping.test.ts +66 -0
- package/tests/skills.test.ts +62 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
package/.eslintignore
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"parser": "@typescript-eslint/parser",
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@typescript-eslint",
|
|
5
|
+
"eslint-comments"
|
|
6
|
+
],
|
|
7
|
+
"extends": [
|
|
8
|
+
"eslint:recommended",
|
|
9
|
+
"plugin:@typescript-eslint/recommended",
|
|
10
|
+
"plugin:eslint-comments/recommended"
|
|
11
|
+
],
|
|
12
|
+
"rules": {
|
|
13
|
+
"eslint-comments/no-use": "error",
|
|
14
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
15
|
+
"@typescript-eslint/no-unused-vars": [
|
|
16
|
+
"error",
|
|
17
|
+
{
|
|
18
|
+
"argsIgnorePattern": "^_"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# agentctl 🤖🛠️
|
|
2
|
+
|
|
3
|
+
**One CLI to rule them all.**
|
|
4
|
+
|
|
5
|
+
> **agentctl** is a unified control plane for humans and AI agents. It lets you build, organize, and manage command-line tools without frameworks or complex protocols. Just structured shell commands that anyone (or anything) can run.
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
- **Zero Friction**: Turn any script or one-liner into a structured CLI command in seconds.
|
|
10
|
+
- **Universal Interface**: Humans get a nice help menu; Agents get a discoverable toolset.
|
|
11
|
+
- **Scoped Execution**: Keep project-specific commands in your repo (`.agentctl/`), and user-wide tools in your global config (`~/.config/agentctl/`).
|
|
12
|
+
- **AI Native**: Built-in support for installing "skills" into AI coding assistants (Cursor, Antigravity, Gemini).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g agentctl
|
|
20
|
+
# or run directly
|
|
21
|
+
npx agentctl <command>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Create a command
|
|
27
|
+
Scaffold a new command. This creates a script file you can edit.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
agentctl ctl scaffold "dev start"
|
|
31
|
+
# Created .agentctl/dev/start/command.sh (or .cmd on Windows)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Run it
|
|
35
|
+
```bash
|
|
36
|
+
agentctl dev start
|
|
37
|
+
# Executes your script!
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Alias an existing tool
|
|
41
|
+
Group your favorite tools under a unified namespace.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
agentctl ctl group tools
|
|
45
|
+
agentctl ctl alias "tools gh" gh
|
|
46
|
+
agentctl ctl alias "tools slack" slack-cli
|
|
47
|
+
|
|
48
|
+
agentctl tools
|
|
49
|
+
# Lists available tools: gh, slack
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Core Concepts
|
|
55
|
+
|
|
56
|
+
### 📂 Groups & Commands
|
|
57
|
+
- **Group**: A namespace (folder) that contains other commands.
|
|
58
|
+
- **Command**: A leaf node that executes a script or binary.
|
|
59
|
+
|
|
60
|
+
The filesystem **IS** the command tree.
|
|
61
|
+
`.agentctl/dev/build/` → `agentctl dev build`
|
|
62
|
+
|
|
63
|
+
### 🌍 Scoping
|
|
64
|
+
|
|
65
|
+
**Local Scope**: `.agentctl/` in your current directory.
|
|
66
|
+
- Ideal for project-specific workflows (build, test, deploy).
|
|
67
|
+
- Check it into git to share with your team!
|
|
68
|
+
|
|
69
|
+
**Global Scope**: `~/.config/agentctl/` (or `%APPDATA%\agentctl` on Windows).
|
|
70
|
+
- Ideal for your personal toolbelt.
|
|
71
|
+
- Access these commands from anywhere.
|
|
72
|
+
|
|
73
|
+
**Move between scopes:**
|
|
74
|
+
```bash
|
|
75
|
+
# Move a local command to global scope
|
|
76
|
+
agentctl ctl global "my-script" --move
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Agent Integration 🧠
|
|
82
|
+
|
|
83
|
+
`agentctl` is designed to be the bridge between you and your AI agents.
|
|
84
|
+
|
|
85
|
+
### Install Skills
|
|
86
|
+
Teach your AI agent how to use `agentctl` by installing a skill file.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# For Cursor / Windsurf / Antigravity
|
|
90
|
+
agentctl ctl --install-skill cursor
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Headless Execution
|
|
94
|
+
Run agentic workflows directly from the CLI.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Run a headless Gemini session in the current directory
|
|
98
|
+
agentctl agent headless-gemini . "refactor the formatting in src/"
|
|
99
|
+
```
|
|
100
|
+
*(Requires `gemini-cli` installed and authenticated)*
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Management Commands (`ctl`)
|
|
105
|
+
|
|
106
|
+
The `ctl` subcommand is the meta-layer for managing `agentctl` itself.
|
|
107
|
+
|
|
108
|
+
| Command | Description |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `scaffold <path>` | Create a new script-based command |
|
|
111
|
+
| `alias <path> <target>` | Create a command that runs another command |
|
|
112
|
+
| `group <path>` | Create a new namespace |
|
|
113
|
+
| `rm <path>` | Remove a command or group |
|
|
114
|
+
| `mv <src> <dest>` | and/or rename a command |
|
|
115
|
+
| `list` | Show all commands (local + global) |
|
|
116
|
+
| `inspect <path>` | JSON dump of command manifest |
|
|
117
|
+
| `global <path>` | Promote local command to global |
|
|
118
|
+
| `local <path>` | Pull global command to local |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
package/agentctl.cmd
ADDED
package/dist/ctl.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.scaffold = scaffold;
|
|
16
|
+
exports.alias = alias;
|
|
17
|
+
exports.group = group;
|
|
18
|
+
exports.pushGlobal = pushGlobal;
|
|
19
|
+
exports.pullLocal = pullLocal;
|
|
20
|
+
exports.installSkill = installSkill;
|
|
21
|
+
exports.rm = rm;
|
|
22
|
+
exports.mv = mv;
|
|
23
|
+
exports.inspect = inspect;
|
|
24
|
+
exports.list = list;
|
|
25
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const resolve_1 = require("./resolve");
|
|
28
|
+
const fs_utils_1 = require("./fs-utils");
|
|
29
|
+
const manifest_1 = require("./manifest");
|
|
30
|
+
function scaffold(args_1) {
|
|
31
|
+
return __awaiter(this, arguments, void 0, function* (args, options = {}) {
|
|
32
|
+
const { targetDir, name, isWin } = yield prepareCommand(args, options);
|
|
33
|
+
const scriptName = isWin ? 'command.cmd' : 'command.sh';
|
|
34
|
+
const scriptPath = path_1.default.join(targetDir, scriptName);
|
|
35
|
+
const scriptContent = isWin
|
|
36
|
+
? '@echo off\r\nREM Add your command logic here\r\necho Not implemented'
|
|
37
|
+
: '#!/usr/bin/env bash\n# Add your command logic here\necho "Not implemented"';
|
|
38
|
+
yield fs_extra_1.default.writeFile(scriptPath, scriptContent);
|
|
39
|
+
if (!isWin)
|
|
40
|
+
yield fs_extra_1.default.chmod(scriptPath, 0o755);
|
|
41
|
+
const manifest = {
|
|
42
|
+
name,
|
|
43
|
+
description: '',
|
|
44
|
+
type: 'scaffold',
|
|
45
|
+
run: `./${scriptName}`,
|
|
46
|
+
};
|
|
47
|
+
yield fs_extra_1.default.writeJson(path_1.default.join(targetDir, 'manifest.json'), manifest, { spaces: 2 });
|
|
48
|
+
console.log(`Scaffolded command: ${args.join(' ')}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function alias(args_1, target_1) {
|
|
52
|
+
return __awaiter(this, arguments, void 0, function* (args, target, options = {}) {
|
|
53
|
+
const { targetDir, name } = yield prepareCommand(args, options);
|
|
54
|
+
const manifest = {
|
|
55
|
+
name,
|
|
56
|
+
description: '',
|
|
57
|
+
type: 'alias',
|
|
58
|
+
run: target,
|
|
59
|
+
};
|
|
60
|
+
yield fs_extra_1.default.writeJson(path_1.default.join(targetDir, 'manifest.json'), manifest, { spaces: 2 });
|
|
61
|
+
console.log(`Aliased command: ${args.join(' ')} -> ${target}`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function group(args_1) {
|
|
65
|
+
return __awaiter(this, arguments, void 0, function* (args, options = {}) {
|
|
66
|
+
const { targetDir, name } = yield prepareCommand(args, options);
|
|
67
|
+
const manifest = {
|
|
68
|
+
name,
|
|
69
|
+
description: '',
|
|
70
|
+
type: 'group',
|
|
71
|
+
};
|
|
72
|
+
yield fs_extra_1.default.writeJson(path_1.default.join(targetDir, 'manifest.json'), manifest, { spaces: 2 });
|
|
73
|
+
console.log(`Created group: ${args.join(' ')}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function pushGlobal(args_1) {
|
|
77
|
+
return __awaiter(this, arguments, void 0, function* (args, options = {}) {
|
|
78
|
+
const cwd = options.cwd || process.cwd();
|
|
79
|
+
const localRoot = (0, fs_utils_1.findLocalRoot)(cwd);
|
|
80
|
+
if (!localRoot)
|
|
81
|
+
throw new Error('Not in a local context');
|
|
82
|
+
const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
|
|
83
|
+
const localAgentctl = path_1.default.join(localRoot, '.agentctl');
|
|
84
|
+
const cmdPathStr = args.join(path_1.default.sep);
|
|
85
|
+
const srcDir = path_1.default.join(localAgentctl, cmdPathStr);
|
|
86
|
+
if (!(yield fs_extra_1.default.pathExists(srcDir))) {
|
|
87
|
+
throw new Error(`Local command ${args.join(' ')} not found`);
|
|
88
|
+
}
|
|
89
|
+
const destDir = path_1.default.join(globalRoot, cmdPathStr);
|
|
90
|
+
if (yield fs_extra_1.default.pathExists(destDir)) {
|
|
91
|
+
throw new Error(`Global command ${args.join(' ')} already exists`);
|
|
92
|
+
}
|
|
93
|
+
yield fs_extra_1.default.ensureDir(path_1.default.dirname(destDir));
|
|
94
|
+
if (options.move) {
|
|
95
|
+
yield fs_extra_1.default.move(srcDir, destDir);
|
|
96
|
+
console.log(`Moved ${args.join(' ')} to global scope`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
yield fs_extra_1.default.copy(srcDir, destDir);
|
|
100
|
+
console.log(`Copied ${args.join(' ')} to global scope`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function pullLocal(args_1) {
|
|
105
|
+
return __awaiter(this, arguments, void 0, function* (args, options = {}) {
|
|
106
|
+
const cwd = options.cwd || process.cwd();
|
|
107
|
+
const localRoot = (0, fs_utils_1.findLocalRoot)(cwd);
|
|
108
|
+
if (!localRoot)
|
|
109
|
+
throw new Error('Not in a local context');
|
|
110
|
+
const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
|
|
111
|
+
const cmdPathStr = args.join(path_1.default.sep);
|
|
112
|
+
const srcDir = path_1.default.join(globalRoot, cmdPathStr);
|
|
113
|
+
if (!(yield fs_extra_1.default.pathExists(srcDir))) {
|
|
114
|
+
throw new Error(`Global command ${args.join(' ')} not found`);
|
|
115
|
+
}
|
|
116
|
+
const localAgentctl = path_1.default.join(localRoot, '.agentctl');
|
|
117
|
+
const destDir = path_1.default.join(localAgentctl, cmdPathStr);
|
|
118
|
+
if (yield fs_extra_1.default.pathExists(destDir)) {
|
|
119
|
+
throw new Error(`Local command ${args.join(' ')} already exists`);
|
|
120
|
+
}
|
|
121
|
+
yield fs_extra_1.default.ensureDir(path_1.default.dirname(destDir));
|
|
122
|
+
if (options.move) {
|
|
123
|
+
yield fs_extra_1.default.move(srcDir, destDir);
|
|
124
|
+
console.log(`Moved ${args.join(' ')} to local scope`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
yield fs_extra_1.default.copy(srcDir, destDir);
|
|
128
|
+
console.log(`Copied ${args.join(' ')} to local scope`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const skills_1 = require("./skills");
|
|
133
|
+
function installSkill(agent_1) {
|
|
134
|
+
return __awaiter(this, arguments, void 0, function* (agent, options = {}) {
|
|
135
|
+
const cwd = options.cwd || process.cwd();
|
|
136
|
+
if (!skills_1.SUPPORTED_AGENTS.includes(agent)) {
|
|
137
|
+
throw new Error(`Agent '${agent}' not supported. Supported agents: ${skills_1.SUPPORTED_AGENTS.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
let targetDir;
|
|
140
|
+
if (agent === 'cursor') {
|
|
141
|
+
targetDir = path_1.default.join(cwd, '.cursor', 'skills');
|
|
142
|
+
}
|
|
143
|
+
else if (agent === 'antigravity') {
|
|
144
|
+
if (options.global) {
|
|
145
|
+
const globalRoot = options.antigravityGlobalDir || (0, fs_utils_1.getAntigravityGlobalRoot)();
|
|
146
|
+
targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
targetDir = path_1.default.join(cwd, '.agent', 'skills', 'agentctl');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (agent === 'agentsmd') {
|
|
153
|
+
targetDir = path_1.default.join(cwd, '.agents', 'skills', 'agentctl');
|
|
154
|
+
}
|
|
155
|
+
else if (agent === 'gemini') {
|
|
156
|
+
if (options.global) {
|
|
157
|
+
const globalRoot = options.geminiGlobalDir || path_1.default.join(process.env.HOME || process.env.USERPROFILE, '.gemini');
|
|
158
|
+
targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
targetDir = path_1.default.join(cwd, '.gemini', 'skills', 'agentctl');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
throw new Error(`Agent logic for '${agent}' not implemented.`);
|
|
166
|
+
}
|
|
167
|
+
const p = yield (0, skills_1.copySkill)(targetDir, agent);
|
|
168
|
+
console.log(`Installed skill for ${agent} at ${p}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function rm(args_1) {
|
|
172
|
+
return __awaiter(this, arguments, void 0, function* (args, options = {}) {
|
|
173
|
+
const resolved = yield (0, resolve_1.resolveCommand)(args, options);
|
|
174
|
+
if (!resolved) {
|
|
175
|
+
throw new Error(`Command ${args.join(' ')} not found${options.global ? ' in global scope' : ''}`);
|
|
176
|
+
}
|
|
177
|
+
const targetDir = path_1.default.dirname(resolved.manifestPath);
|
|
178
|
+
yield fs_extra_1.default.remove(targetDir);
|
|
179
|
+
console.log(`Removed ${resolved.scope} command: ${args.join(' ')}`);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function mv(srcArgs_1, destArgs_1) {
|
|
183
|
+
return __awaiter(this, arguments, void 0, function* (srcArgs, destArgs, options = {}) {
|
|
184
|
+
const resolved = yield (0, resolve_1.resolveCommand)(srcArgs, options);
|
|
185
|
+
if (!resolved) {
|
|
186
|
+
throw new Error(`Command ${srcArgs.join(' ')} not found`);
|
|
187
|
+
}
|
|
188
|
+
const srcDir = path_1.default.dirname(resolved.manifestPath);
|
|
189
|
+
const rootDir = resolved.scope === 'local'
|
|
190
|
+
? (0, fs_utils_1.findLocalRoot)(options.cwd || process.cwd())
|
|
191
|
+
: (options.globalDir || (0, fs_utils_1.getGlobalRoot)());
|
|
192
|
+
if (!rootDir)
|
|
193
|
+
throw new Error('Cannot determine root for move');
|
|
194
|
+
const agentctlDir = resolved.scope === 'local' ? path_1.default.join(rootDir, '.agentctl') : rootDir;
|
|
195
|
+
// For global, rootDir IS the agentctl dir (config dir). Local has .agentctl subdir.
|
|
196
|
+
const destPathStr = destArgs.join(path_1.default.sep);
|
|
197
|
+
const destDir = path_1.default.join(agentctlDir, destPathStr);
|
|
198
|
+
if (yield fs_extra_1.default.pathExists(destDir)) {
|
|
199
|
+
throw new Error(`Destination ${destArgs.join(' ')} already exists`);
|
|
200
|
+
}
|
|
201
|
+
// Check parent validity (nesting under capped)
|
|
202
|
+
let current = path_1.default.dirname(destDir);
|
|
203
|
+
while (current.length >= agentctlDir.length && !isSamePath(current, path_1.default.dirname(agentctlDir))) {
|
|
204
|
+
if (yield isCapped(current)) {
|
|
205
|
+
const relPath = path_1.default.relative(agentctlDir, current); // relative to base
|
|
206
|
+
throw new Error(`Cannot nest command under capped command: ${relPath}`);
|
|
207
|
+
}
|
|
208
|
+
current = path_1.default.dirname(current);
|
|
209
|
+
}
|
|
210
|
+
yield fs_extra_1.default.move(srcDir, destDir);
|
|
211
|
+
// Update manifest name
|
|
212
|
+
const manifestPath = path_1.default.join(destDir, 'manifest.json');
|
|
213
|
+
if (yield fs_extra_1.default.pathExists(manifestPath)) {
|
|
214
|
+
const manifest = yield fs_extra_1.default.readJson(manifestPath);
|
|
215
|
+
manifest.name = destArgs[destArgs.length - 1];
|
|
216
|
+
yield fs_extra_1.default.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
217
|
+
}
|
|
218
|
+
console.log(`Moved ${srcArgs.join(' ')} to ${destArgs.join(' ')}`);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function inspect(args_1) {
|
|
222
|
+
return __awaiter(this, arguments, void 0, function* (args, options = {}) {
|
|
223
|
+
const resolved = yield (0, resolve_1.resolveCommand)(args, options);
|
|
224
|
+
if (!resolved) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
manifest: resolved.manifest,
|
|
229
|
+
resolvedPath: resolved.manifestPath,
|
|
230
|
+
scope: resolved.scope
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function list() {
|
|
235
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
236
|
+
const cwd = options.cwd || process.cwd();
|
|
237
|
+
const localRoot = (0, fs_utils_1.findLocalRoot)(cwd);
|
|
238
|
+
const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
|
|
239
|
+
const commands = new Map();
|
|
240
|
+
function walk(dir, prefix, scope) {
|
|
241
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
242
|
+
if (!(yield fs_extra_1.default.pathExists(dir)))
|
|
243
|
+
return;
|
|
244
|
+
const files = yield fs_extra_1.default.readdir(dir);
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
const filePath = path_1.default.join(dir, file);
|
|
247
|
+
let stats;
|
|
248
|
+
try {
|
|
249
|
+
stats = yield fs_extra_1.default.stat(filePath);
|
|
250
|
+
}
|
|
251
|
+
catch (_a) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!stats.isDirectory())
|
|
255
|
+
continue;
|
|
256
|
+
const cmdPathParts = [...prefix, file];
|
|
257
|
+
const cmdPath = cmdPathParts.join(' ');
|
|
258
|
+
let manifest = null;
|
|
259
|
+
const mPath = path_1.default.join(filePath, 'manifest.json');
|
|
260
|
+
if (yield fs_extra_1.default.pathExists(mPath)) {
|
|
261
|
+
manifest = yield (0, manifest_1.readManifest)(mPath);
|
|
262
|
+
}
|
|
263
|
+
let type = 'group';
|
|
264
|
+
if (manifest) {
|
|
265
|
+
if ((0, manifest_1.isCappedManifest)(manifest)) {
|
|
266
|
+
type = manifest.type || 'scaffold';
|
|
267
|
+
}
|
|
268
|
+
else if (manifest.type) {
|
|
269
|
+
type = manifest.type;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const item = {
|
|
273
|
+
path: cmdPath,
|
|
274
|
+
type,
|
|
275
|
+
scope,
|
|
276
|
+
description: (manifest === null || manifest === void 0 ? void 0 : manifest.description) || ''
|
|
277
|
+
};
|
|
278
|
+
if (!commands.has(cmdPath)) {
|
|
279
|
+
commands.set(cmdPath, item);
|
|
280
|
+
const effectiveManifest = manifest || { name: file, type: 'group' };
|
|
281
|
+
if (!(0, manifest_1.isCappedManifest)(effectiveManifest)) {
|
|
282
|
+
yield walk(filePath, cmdPathParts, scope);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const existing = commands.get(cmdPath);
|
|
287
|
+
if (existing && existing.scope === 'local') {
|
|
288
|
+
if (existing.type === 'group' && type === 'group') {
|
|
289
|
+
yield walk(filePath, cmdPathParts, scope);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (localRoot) {
|
|
297
|
+
yield walk(path_1.default.join(localRoot, '.agentctl'), [], 'local');
|
|
298
|
+
}
|
|
299
|
+
yield walk(globalRoot, [], 'global');
|
|
300
|
+
return Array.from(commands.values());
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Helpers
|
|
304
|
+
function prepareCommand(args_1) {
|
|
305
|
+
return __awaiter(this, arguments, void 0, function* (args, options = {}) {
|
|
306
|
+
const cwd = options.cwd || process.cwd();
|
|
307
|
+
const rootDir = cwd;
|
|
308
|
+
const agentctlDir = path_1.default.join(rootDir, '.agentctl');
|
|
309
|
+
if (args.length === 0)
|
|
310
|
+
throw new Error('No command path provided');
|
|
311
|
+
const cmdPath = args.join(path_1.default.sep);
|
|
312
|
+
const targetDir = path_1.default.join(agentctlDir, cmdPath);
|
|
313
|
+
if (yield fs_extra_1.default.pathExists(targetDir)) {
|
|
314
|
+
throw new Error(`Command ${args.join(' ')} already exists`);
|
|
315
|
+
}
|
|
316
|
+
let current = path_1.default.dirname(targetDir);
|
|
317
|
+
while (current.length >= agentctlDir.length && !isSamePath(current, path_1.default.dirname(agentctlDir))) {
|
|
318
|
+
if (yield isCapped(current)) {
|
|
319
|
+
const relPath = path_1.default.relative(agentctlDir, current); // This uses cwd for resolution if agentctlDir is cwd resolved
|
|
320
|
+
// Need to verify relative works predictably
|
|
321
|
+
// agentctlDir comes from path.join(cwd, '.agentctl').
|
|
322
|
+
throw new Error(`Cannot nest command under capped command: ${relPath}`);
|
|
323
|
+
}
|
|
324
|
+
current = path_1.default.dirname(current);
|
|
325
|
+
}
|
|
326
|
+
yield fs_extra_1.default.ensureDir(targetDir);
|
|
327
|
+
return {
|
|
328
|
+
targetDir,
|
|
329
|
+
name: args[args.length - 1],
|
|
330
|
+
isWin: process.platform === 'win32'
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function isCapped(dir) {
|
|
335
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
336
|
+
const manifestPath = path_1.default.join(dir, 'manifest.json');
|
|
337
|
+
if (yield fs_extra_1.default.pathExists(manifestPath)) {
|
|
338
|
+
try {
|
|
339
|
+
const m = yield fs_extra_1.default.readJson(manifestPath);
|
|
340
|
+
return (0, manifest_1.isCappedManifest)(m);
|
|
341
|
+
}
|
|
342
|
+
catch (_a) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function isSamePath(p1, p2) {
|
|
350
|
+
return path_1.default.relative(p1, p2) === '';
|
|
351
|
+
}
|
package/dist/fs-utils.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getGlobalRoot = getGlobalRoot;
|
|
7
|
+
exports.getAntigravityGlobalRoot = getAntigravityGlobalRoot;
|
|
8
|
+
exports.findLocalRoot = findLocalRoot;
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
12
|
+
function getGlobalRoot() {
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
return path_1.default.join(process.env.APPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Roaming'), 'agentctl');
|
|
15
|
+
}
|
|
16
|
+
return path_1.default.join(os_1.default.homedir(), '.config', 'agentctl');
|
|
17
|
+
}
|
|
18
|
+
function getAntigravityGlobalRoot() {
|
|
19
|
+
return path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity');
|
|
20
|
+
}
|
|
21
|
+
function findLocalRoot(cwd = process.cwd()) {
|
|
22
|
+
let current = path_1.default.resolve(cwd);
|
|
23
|
+
const root = path_1.default.parse(current).root;
|
|
24
|
+
// Safety break and root check
|
|
25
|
+
// Using for(;;) to avoid no-constant-condition
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (fs_extra_1.default.existsSync(path_1.default.join(current, '.agentctl'))) {
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
if (current === root) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
current = path_1.default.dirname(current);
|
|
34
|
+
}
|
|
35
|
+
}
|