@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/README.md +136 -17
- package/dist/ctl.js +175 -228
- package/dist/effects.js +61 -0
- package/dist/fs-utils.js +26 -19
- package/dist/index.js +266 -227
- package/dist/logic/ctl.js +173 -0
- package/dist/logic/index.js +79 -0
- package/dist/logic/install.js +60 -0
- package/dist/logic/manifest.js +8 -0
- package/dist/logic/resolve.js +73 -0
- package/dist/logic/skills.js +20 -0
- package/dist/logic/utils.js +31 -0
- package/dist/manifest.js +17 -1
- package/dist/resolve.js +30 -76
- package/dist/skills.js +11 -23
- package/package.json +1 -1
- package/dist/package.json +0 -60
- package/dist/src/ctl.js +0 -316
- package/dist/src/fs-utils.js +0 -35
- package/dist/src/index.js +0 -351
- package/dist/src/manifest.js +0 -19
- package/dist/src/resolve.js +0 -112
- package/dist/src/skills.js +0 -39
package/README.md
CHANGED
|
@@ -90,14 +90,31 @@ Teach your AI agent how to use `agentctl` by installing a skill file.
|
|
|
90
90
|
agentctl ctl --install-skill cursor
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
###
|
|
94
|
-
|
|
93
|
+
### Provide the proper help copy
|
|
94
|
+
When creating commands, make sure the `manifest.json` properly scopes its information:
|
|
95
|
+
- `description`: A short summary of the command, displayed when viewing the list of available commands.
|
|
96
|
+
- `help`: A longer set of instructions/usage string. For `group` types, this is displayed when calling the group without a subcommand. For `scaffold` types, this is currently for reference only; your command script must manually handle an `--help` flag for detailed usage.
|
|
97
|
+
|
|
98
|
+
#### Scaffold Schema
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"name": "<command_folder_name>",
|
|
102
|
+
"description": "<insert command summary here>",
|
|
103
|
+
"help": "<insert longer usage/help instructions here>",
|
|
104
|
+
"type": "scaffold",
|
|
105
|
+
"run": "./command.cmd"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
#### Group Schema
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"name": "<group_folder_name>",
|
|
113
|
+
"description": "<insert group summary here>",
|
|
114
|
+
"help": "<insert longer group description/instructions here>",
|
|
115
|
+
"type": "group"
|
|
116
|
+
}
|
|
99
117
|
```
|
|
100
|
-
*(Requires `gemini-cli` installed and authenticated)*
|
|
101
118
|
|
|
102
119
|
---
|
|
103
120
|
|
|
@@ -105,17 +122,119 @@ agentctl agent headless-gemini . "refactor the formatting in src/"
|
|
|
105
122
|
|
|
106
123
|
The `ctl` subcommand is the meta-layer for managing `agentctl` itself.
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
### `agentctl ctl scaffold <path>`
|
|
126
|
+
Create a new script-based command. This generates a folder containing a `manifest.json` and a starter script (`command.cmd`/`command.sh`) for your logic.
|
|
127
|
+
- **Why it's great:** Automates the boilerplate of creating a new CLI tool.
|
|
128
|
+
- **When to use it:** When you have a complex script or an arbitrary executable (Node, Python, Go) you want to expose easily via the CLI.
|
|
129
|
+
```bash
|
|
130
|
+
agentctl ctl scaffold build:front
|
|
131
|
+
agentctl ctl scaffold "build server" # Creates group 'build' and subcommand 'server'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `agentctl ctl group <path>`
|
|
135
|
+
Create a new namespace to organize subcommands.
|
|
136
|
+
- **Why it's great:** Keeps your CLI clean by grouping related tools without forcing you to write a parent CLI router.
|
|
137
|
+
- **When to use it:** When you have a collection of similar commands (e.g., `db migrate`, `db reset`) and want an organized help menu grouping them under `db`.
|
|
138
|
+
```bash
|
|
139
|
+
agentctl ctl group data # Creates 'data' group
|
|
140
|
+
agentctl ctl group "cloud aws" # Creates nested 'cloud/aws' groups
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `agentctl ctl alias <path> <target>`
|
|
144
|
+
Create a command that simply runs an existing binary, string, or target.
|
|
145
|
+
- **Why it's great:** Replaces bash aliases with structured, documentable commands that work locally or globally across environments and OSes.
|
|
146
|
+
- **When to use it:** When you have a long, frequently used string (`docker compose run --rm node npm install`) and want a short name (`agentctl npm install`) without needing a whole generated shell script.
|
|
147
|
+
```bash
|
|
148
|
+
agentctl ctl alias "tools gh" gh
|
|
149
|
+
agentctl ctl alias list-files "ls -la"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `agentctl ctl rm [options] <path>`
|
|
153
|
+
Permanently remove a command or entire group from your workspace.
|
|
154
|
+
- **Why it's great:** Quickly clean up old tooling right from the CLI without needing to manually delete `.agentctl` directories.
|
|
155
|
+
- **When to use it:** When retiring a script or cleaning up an obsolete group.
|
|
156
|
+
|
|
157
|
+
**Options:**
|
|
158
|
+
- `-g, --global`: Remove from global scope instead of local
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
agentctl ctl rm build:front # Removes local 'build front' command
|
|
162
|
+
agentctl ctl rm mytool --global # Removes global 'mytool' command
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `agentctl ctl mv [options] <src> <dest>`
|
|
166
|
+
Rename a command or move it to a new group/namespace.
|
|
167
|
+
- **Why it's great:** Refactor your CLI commands like you'd refactor code, moving logic around namespaces without breaking the underlying executed scripts.
|
|
168
|
+
- **When to use it:** When restructuring your toolbelt from a flat list to a nested categorization.
|
|
169
|
+
|
|
170
|
+
**Options:**
|
|
171
|
+
- `-g, --global`: Operate in global scope
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
agentctl ctl mv mytool my-new-tool
|
|
175
|
+
agentctl ctl mv "tools ping" "network ping"
|
|
176
|
+
agentctl ctl mv "tools ping" "network ping" --global # Moves in global scope
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `agentctl ctl list [options]`
|
|
180
|
+
List all currently available commands across both your local and global scopes.
|
|
181
|
+
- **Why it's great:** Generates an easy-to-read JSON summary of everything your agent can do.
|
|
182
|
+
- **When to use it:** When programming an agent or exploring what tooling is available in a scoped project.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
agentctl ctl list
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `agentctl ctl inspect [options] <path>`
|
|
189
|
+
Dump the resolved manifest and location of a given command path.
|
|
190
|
+
- **Why it's great:** Eliminates the "where did this command come from?" problem when debugging complex workspaces.
|
|
191
|
+
- **When to use it:** When you have local/global conflicts or want to quickly see the JSON source manifest for a command.
|
|
192
|
+
```bash
|
|
193
|
+
agentctl ctl inspect dev:start
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `agentctl ctl global [options] <path>`
|
|
197
|
+
Promote a local command or group to your globally available toolbelt.
|
|
198
|
+
- **Why it's great:** Eject highly useful, generic project abstractions directly into your personal universal toolbelt.
|
|
199
|
+
- **When to use it:** You wrote a script specific to a project and realized "I want this CLI tool in every repo I touch."
|
|
200
|
+
|
|
201
|
+
**Options:**
|
|
202
|
+
- `-c, --copy`: Copy the command (keep local version, default)
|
|
203
|
+
- `-m, --move`: Move the command (delete local after copying)
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
agentctl ctl global "dev toolkit" # Copies the command to global scope
|
|
207
|
+
agentctl ctl global "dev toolkit" --move # Moves it instead of copy
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `agentctl ctl local [options] <path>`
|
|
211
|
+
Bring a global command down into the current local project environment.
|
|
212
|
+
- **Why it's great:** Promotes collaboration seamlessly. You can take your personal script, localize it, and commit it to git for your entire team.
|
|
213
|
+
- **When to use it:** You have a generic tool that is suddenly very relevant to a specific repository, and you want CI or your coworkers to access it.
|
|
214
|
+
|
|
215
|
+
**Options:**
|
|
216
|
+
- `-c, --copy`: Copy the command (keep global version, default)
|
|
217
|
+
- `-m, --move`: Move the command (delete global after pulling)
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
agentctl ctl local "my global-tool"
|
|
221
|
+
agentctl ctl local "my global-tool" --move
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `agentctl ctl install [options] <repo-url> [path...]`
|
|
225
|
+
Install commands or groups directly from a remote Git repository's `.agentctl` folder into your local or global scope.
|
|
226
|
+
- **Why it's great:** Provides dependency-free tool sharing! Distribute entire suites of scripts/commands without using a package manager.
|
|
227
|
+
- **When to use it:** When you want to pull down shared utility commands that your team maintains in a central repository, or distribute your own tools to others.
|
|
228
|
+
|
|
229
|
+
**Options:**
|
|
230
|
+
- `-g, --global`: Install globally instead of locally
|
|
231
|
+
- `--allow-collisions`: Allow overwriting existing commands or merging into groups
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
agentctl ctl install https://github.com/my-org/tools
|
|
235
|
+
agentctl ctl install https://github.com/my-org/tools --global
|
|
236
|
+
agentctl ctl install https://github.com/my-org/tools deploy --allow-collisions
|
|
237
|
+
```
|
|
119
238
|
|
|
120
239
|
---
|
|
121
240
|
|
package/dist/ctl.js
CHANGED
|
@@ -8,206 +8,208 @@ exports.alias = alias;
|
|
|
8
8
|
exports.group = group;
|
|
9
9
|
exports.pushGlobal = pushGlobal;
|
|
10
10
|
exports.pullLocal = pullLocal;
|
|
11
|
-
exports.installSkill = installSkill;
|
|
12
11
|
exports.rm = rm;
|
|
13
12
|
exports.mv = mv;
|
|
14
13
|
exports.inspect = inspect;
|
|
14
|
+
exports.installSkill = installSkill;
|
|
15
|
+
exports.install = install;
|
|
15
16
|
exports.list = list;
|
|
16
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
-
const
|
|
18
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
19
20
|
const fs_utils_1 = require("./fs-utils");
|
|
20
21
|
const manifest_1 = require("./manifest");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
const resolve_1 = require("./resolve");
|
|
23
|
+
const effects_1 = require("./effects");
|
|
24
|
+
const ctl_1 = require("./logic/ctl");
|
|
25
|
+
const install_1 = require("./logic/install");
|
|
26
|
+
async function getCappedAncestor(dir, baseDir) {
|
|
27
|
+
let current = path_1.default.dirname(dir);
|
|
28
|
+
while (current.length >= baseDir.length && current !== path_1.default.dirname(baseDir)) {
|
|
29
|
+
const mPath = path_1.default.join(current, 'manifest.json');
|
|
30
|
+
if (await fs_extra_1.default.pathExists(mPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const m = await fs_extra_1.default.readJson(mPath);
|
|
33
|
+
if ((0, manifest_1.isCappedManifest)(m)) {
|
|
34
|
+
return { path: current, relPath: path_1.default.relative(baseDir, current) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
current = path_1.default.dirname(current);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
async function getContext(options) {
|
|
46
|
+
const cwd = options.cwd || process.cwd();
|
|
47
|
+
return {
|
|
48
|
+
cwd: path_1.default.resolve(cwd),
|
|
49
|
+
platform: process.platform,
|
|
50
|
+
localRoot: (0, fs_utils_1.findLocalRoot)(cwd),
|
|
51
|
+
globalRoot: options.globalDir || (0, fs_utils_1.getGlobalRoot)(),
|
|
52
|
+
homedir: process.env.HOME || process.env.USERPROFILE || os_1.default.homedir()
|
|
36
53
|
};
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
}
|
|
55
|
+
async function scaffold(args, options = {}) {
|
|
56
|
+
const ctx = await getContext(options);
|
|
57
|
+
const localRoot = ctx.localRoot || ctx.cwd;
|
|
58
|
+
const agentctlDir = path_1.default.join(localRoot, '.agentctl');
|
|
59
|
+
const targetDir = path_1.default.join(agentctlDir, args.join(path_1.default.sep));
|
|
60
|
+
const exists = await fs_extra_1.default.pathExists(targetDir);
|
|
61
|
+
const cappedAncestor = await getCappedAncestor(targetDir, agentctlDir);
|
|
62
|
+
const { effects } = ctl_1.Logic.planScaffold(args, ctx, { exists, cappedAncestor: cappedAncestor || undefined, type: 'scaffold' });
|
|
63
|
+
await (0, effects_1.execute)(effects);
|
|
39
64
|
}
|
|
40
65
|
async function alias(args, target, options = {}) {
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
const ctx = await getContext(options);
|
|
67
|
+
const localRoot = ctx.localRoot || ctx.cwd;
|
|
68
|
+
const agentctlDir = path_1.default.join(localRoot, '.agentctl');
|
|
69
|
+
const targetDir = path_1.default.join(agentctlDir, args.join(path_1.default.sep));
|
|
70
|
+
const exists = await fs_extra_1.default.pathExists(targetDir);
|
|
71
|
+
const cappedAncestor = await getCappedAncestor(targetDir, agentctlDir);
|
|
72
|
+
const { effects } = ctl_1.Logic.planScaffold(args, ctx, {
|
|
73
|
+
exists,
|
|
74
|
+
cappedAncestor: cappedAncestor || undefined,
|
|
45
75
|
type: 'alias',
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
await
|
|
49
|
-
console.log(`Aliased command: ${args.join(' ')} -> ${target}`);
|
|
76
|
+
target
|
|
77
|
+
});
|
|
78
|
+
await (0, effects_1.execute)(effects);
|
|
50
79
|
}
|
|
51
80
|
async function group(args, options = {}) {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
const ctx = await getContext(options);
|
|
82
|
+
const localRoot = ctx.localRoot || ctx.cwd;
|
|
83
|
+
const agentctlDir = path_1.default.join(localRoot, '.agentctl');
|
|
84
|
+
const targetDir = path_1.default.join(agentctlDir, args.join(path_1.default.sep));
|
|
85
|
+
const exists = await fs_extra_1.default.pathExists(targetDir);
|
|
86
|
+
const cappedAncestor = await getCappedAncestor(targetDir, agentctlDir);
|
|
87
|
+
const { effects } = ctl_1.Logic.planScaffold(args, ctx, {
|
|
88
|
+
exists,
|
|
89
|
+
cappedAncestor: cappedAncestor || undefined,
|
|
90
|
+
type: 'group'
|
|
91
|
+
});
|
|
92
|
+
await (0, effects_1.execute)(effects);
|
|
60
93
|
}
|
|
61
94
|
async function pushGlobal(args, options = {}) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (!localRoot)
|
|
95
|
+
const ctx = await getContext(options);
|
|
96
|
+
if (!ctx.localRoot)
|
|
65
97
|
throw new Error('Not in a local context');
|
|
66
|
-
const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
|
|
67
|
-
const localAgentctl = path_1.default.join(localRoot, '.agentctl');
|
|
68
98
|
const cmdPathStr = args.join(path_1.default.sep);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await fs_extra_1.default.ensureDir(path_1.default.dirname(destDir));
|
|
78
|
-
if (options.move) {
|
|
79
|
-
await fs_extra_1.default.move(srcDir, destDir);
|
|
80
|
-
console.log(`Moved ${args.join(' ')} to global scope`);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
await fs_extra_1.default.copy(srcDir, destDir);
|
|
84
|
-
console.log(`Copied ${args.join(' ')} to global scope`);
|
|
85
|
-
}
|
|
99
|
+
const existsInLocal = await fs_extra_1.default.pathExists(path_1.default.join(ctx.localRoot, '.agentctl', cmdPathStr));
|
|
100
|
+
const existsInGlobal = await fs_extra_1.default.pathExists(path_1.default.join(ctx.globalRoot, cmdPathStr));
|
|
101
|
+
const effects = ctl_1.Logic.planPushGlobal(args, ctx, {
|
|
102
|
+
move: options.move,
|
|
103
|
+
existsInLocal,
|
|
104
|
+
existsInGlobal
|
|
105
|
+
});
|
|
106
|
+
await (0, effects_1.execute)(effects);
|
|
86
107
|
}
|
|
87
108
|
async function pullLocal(args, options = {}) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
if (!localRoot)
|
|
109
|
+
const ctx = await getContext(options);
|
|
110
|
+
if (!ctx.localRoot)
|
|
91
111
|
throw new Error('Not in a local context');
|
|
92
|
-
const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
|
|
93
112
|
const cmdPathStr = args.join(path_1.default.sep);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
await fs_extra_1.default.ensureDir(path_1.default.dirname(destDir));
|
|
104
|
-
if (options.move) {
|
|
105
|
-
await fs_extra_1.default.move(srcDir, destDir);
|
|
106
|
-
console.log(`Moved ${args.join(' ')} to local scope`);
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
await fs_extra_1.default.copy(srcDir, destDir);
|
|
110
|
-
console.log(`Copied ${args.join(' ')} to local scope`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const skills_1 = require("./skills");
|
|
114
|
-
async function installSkill(agent, options = {}) {
|
|
115
|
-
const cwd = options.cwd || process.cwd();
|
|
116
|
-
if (!skills_1.SUPPORTED_AGENTS.includes(agent)) {
|
|
117
|
-
throw new Error(`Agent '${agent}' not supported. Supported agents: ${skills_1.SUPPORTED_AGENTS.join(', ')}`);
|
|
118
|
-
}
|
|
119
|
-
let targetDir;
|
|
120
|
-
if (agent === 'cursor') {
|
|
121
|
-
targetDir = path_1.default.join(cwd, '.cursor', 'skills');
|
|
122
|
-
}
|
|
123
|
-
else if (agent === 'antigravity') {
|
|
124
|
-
if (options.global) {
|
|
125
|
-
const globalRoot = options.antigravityGlobalDir || (0, fs_utils_1.getAntigravityGlobalRoot)();
|
|
126
|
-
targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
targetDir = path_1.default.join(cwd, '.agent', 'skills', 'agentctl');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
else if (agent === 'agentsmd') {
|
|
133
|
-
targetDir = path_1.default.join(cwd, '.agents', 'skills', 'agentctl');
|
|
134
|
-
}
|
|
135
|
-
else if (agent === 'gemini') {
|
|
136
|
-
if (options.global) {
|
|
137
|
-
const globalRoot = options.geminiGlobalDir || path_1.default.join(process.env.HOME || process.env.USERPROFILE, '.gemini');
|
|
138
|
-
targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
targetDir = path_1.default.join(cwd, '.gemini', 'skills', 'agentctl');
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
throw new Error(`Agent logic for '${agent}' not implemented.`);
|
|
146
|
-
}
|
|
147
|
-
const p = await (0, skills_1.copySkill)(targetDir, agent);
|
|
148
|
-
console.log(`Installed skill for ${agent} at ${p}`);
|
|
113
|
+
const existsInGlobal = await fs_extra_1.default.pathExists(path_1.default.join(ctx.globalRoot, cmdPathStr));
|
|
114
|
+
const existsInLocal = await fs_extra_1.default.pathExists(path_1.default.join(ctx.localRoot, '.agentctl', cmdPathStr));
|
|
115
|
+
const effects = ctl_1.Logic.planPullLocal(args, ctx, {
|
|
116
|
+
move: options.move,
|
|
117
|
+
existsInLocal,
|
|
118
|
+
existsInGlobal
|
|
119
|
+
});
|
|
120
|
+
await (0, effects_1.execute)(effects);
|
|
149
121
|
}
|
|
150
122
|
async function rm(args, options = {}) {
|
|
151
123
|
const resolved = await (0, resolve_1.resolveCommand)(args, options);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
124
|
+
const ctx = await getContext(options);
|
|
125
|
+
const effects = ctl_1.Logic.planRemove(args, ctx, {
|
|
126
|
+
resolvedPath: resolved?.manifestPath || null,
|
|
127
|
+
scope: resolved?.scope || 'unknown',
|
|
128
|
+
global: options.global
|
|
129
|
+
});
|
|
130
|
+
await (0, effects_1.execute)(effects);
|
|
158
131
|
}
|
|
159
132
|
async function mv(srcArgs, destArgs, options = {}) {
|
|
160
133
|
const resolved = await (0, resolve_1.resolveCommand)(srcArgs, options);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let current = path_1.default.dirname(destDir);
|
|
179
|
-
while (current.length >= agentctlDir.length && !isSamePath(current, path_1.default.dirname(agentctlDir))) {
|
|
180
|
-
if (await isCapped(current)) {
|
|
181
|
-
const relPath = path_1.default.relative(agentctlDir, current); // relative to base
|
|
182
|
-
throw new Error(`Cannot nest command under capped command: ${relPath}`);
|
|
134
|
+
const ctx = await getContext(options);
|
|
135
|
+
let rootDir = null;
|
|
136
|
+
let agentctlDir = null;
|
|
137
|
+
let destExists = false;
|
|
138
|
+
let cappedAncestor = null;
|
|
139
|
+
if (resolved) {
|
|
140
|
+
rootDir = resolved.scope === 'local'
|
|
141
|
+
? (0, fs_utils_1.findLocalRoot)(options.cwd || process.cwd())
|
|
142
|
+
: (options.globalDir || (0, fs_utils_1.getGlobalRoot)());
|
|
143
|
+
if (rootDir) {
|
|
144
|
+
agentctlDir = resolved.scope === 'local' ? path_1.default.join(rootDir, '.agentctl') : rootDir;
|
|
145
|
+
const destPathStr = destArgs.join(path_1.default.sep);
|
|
146
|
+
const destDir = path_1.default.join(agentctlDir, destPathStr);
|
|
147
|
+
destExists = await fs_extra_1.default.pathExists(destDir);
|
|
148
|
+
const ancestor = await getCappedAncestor(destDir, agentctlDir);
|
|
149
|
+
if (ancestor)
|
|
150
|
+
cappedAncestor = { relPath: ancestor.relPath };
|
|
183
151
|
}
|
|
184
|
-
current = path_1.default.dirname(current);
|
|
185
152
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
console.log(`Moved ${srcArgs.join(' ')} to ${destArgs.join(' ')}`);
|
|
153
|
+
const effects = ctl_1.Logic.planMove(srcArgs, destArgs, ctx, {
|
|
154
|
+
resolvedSrc: resolved ? { manifestPath: resolved.manifestPath, scope: resolved.scope, manifest: resolved.manifest } : null,
|
|
155
|
+
destExists,
|
|
156
|
+
cappedAncestor: cappedAncestor || undefined,
|
|
157
|
+
rootDir,
|
|
158
|
+
agentctlDir
|
|
159
|
+
});
|
|
160
|
+
await (0, effects_1.execute)(effects);
|
|
195
161
|
}
|
|
196
162
|
async function inspect(args, options = {}) {
|
|
197
163
|
const resolved = await (0, resolve_1.resolveCommand)(args, options);
|
|
198
|
-
if (!resolved)
|
|
164
|
+
if (!resolved)
|
|
199
165
|
return null;
|
|
200
|
-
}
|
|
201
166
|
return {
|
|
202
167
|
manifest: resolved.manifest,
|
|
203
168
|
resolvedPath: resolved.manifestPath,
|
|
204
169
|
scope: resolved.scope
|
|
205
170
|
};
|
|
206
171
|
}
|
|
172
|
+
async function installSkill(agent, options = {}) {
|
|
173
|
+
const ctx = await getContext(options);
|
|
174
|
+
const effects = ctl_1.Logic.planInstallSkill(agent, ctx, options);
|
|
175
|
+
await (0, effects_1.execute)(effects);
|
|
176
|
+
}
|
|
177
|
+
async function install(repoUrl, pathArgs, options = {}) {
|
|
178
|
+
const ctx = await getContext(options);
|
|
179
|
+
const installCtx = {
|
|
180
|
+
repoUrl,
|
|
181
|
+
pathParts: pathArgs,
|
|
182
|
+
global: !!options.global,
|
|
183
|
+
allowCollisions: !!options.allowCollisions,
|
|
184
|
+
localRoot: ctx.localRoot || ctx.cwd,
|
|
185
|
+
isNewLocalRoot: !options.global && !ctx.localRoot,
|
|
186
|
+
globalRoot: ctx.globalRoot,
|
|
187
|
+
osTmpdir: os_1.default.tmpdir()
|
|
188
|
+
};
|
|
189
|
+
// Phase 1: Clone
|
|
190
|
+
const tempFolderName = `agentctl-install-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
|
191
|
+
const { effects: cloneEffects, tempDir } = (0, install_1.planInstallClone)(installCtx, { tempFolderName });
|
|
192
|
+
await (0, effects_1.execute)(cloneEffects);
|
|
193
|
+
// Phase 2: Copy
|
|
194
|
+
const tempAgentctlDir = path_1.default.join(tempDir, '.agentctl');
|
|
195
|
+
if (!(await fs_extra_1.default.pathExists(tempAgentctlDir))) {
|
|
196
|
+
await fs_extra_1.default.remove(tempDir);
|
|
197
|
+
throw new Error(`Repository does not contain an .agentctl directory at the root.`);
|
|
198
|
+
}
|
|
199
|
+
const downloadedItems = await fs_extra_1.default.readdir(tempAgentctlDir);
|
|
200
|
+
const rootDir = installCtx.global ? installCtx.globalRoot : installCtx.localRoot;
|
|
201
|
+
const agentctlDir = installCtx.global ? rootDir : path_1.default.join(rootDir, '.agentctl');
|
|
202
|
+
const targetDir = path_1.default.join(agentctlDir, ...installCtx.pathParts);
|
|
203
|
+
const existingItems = (await fs_extra_1.default.pathExists(targetDir)) ? await fs_extra_1.default.readdir(targetDir) : [];
|
|
204
|
+
const { effects: copyEffects } = (0, install_1.planInstallCopy)(installCtx, {
|
|
205
|
+
tempAgentctlDir,
|
|
206
|
+
existingItems,
|
|
207
|
+
downloadedItems
|
|
208
|
+
});
|
|
209
|
+
await (0, effects_1.execute)(copyEffects);
|
|
210
|
+
}
|
|
207
211
|
async function list(options = {}) {
|
|
208
|
-
const
|
|
209
|
-
const localRoot = (0, fs_utils_1.findLocalRoot)(cwd);
|
|
210
|
-
const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
|
|
212
|
+
const ctx = await getContext(options);
|
|
211
213
|
const commands = new Map();
|
|
212
214
|
async function walk(dir, prefix, scope) {
|
|
213
215
|
if (!await fs_extra_1.default.pathExists(dir))
|
|
@@ -228,89 +230,34 @@ async function list(options = {}) {
|
|
|
228
230
|
const cmdPath = cmdPathParts.join(' ');
|
|
229
231
|
let manifest = null;
|
|
230
232
|
const mPath = path_1.default.join(filePath, 'manifest.json');
|
|
231
|
-
if (await fs_extra_1.default.pathExists(mPath))
|
|
233
|
+
if (await fs_extra_1.default.pathExists(mPath))
|
|
232
234
|
manifest = await (0, manifest_1.readManifest)(mPath);
|
|
233
|
-
}
|
|
234
235
|
let type = 'group';
|
|
235
236
|
if (manifest) {
|
|
236
|
-
if ((0, manifest_1.isCappedManifest)(manifest))
|
|
237
|
+
if ((0, manifest_1.isCappedManifest)(manifest))
|
|
237
238
|
type = manifest.type || 'scaffold';
|
|
238
|
-
|
|
239
|
-
else if (manifest.type) {
|
|
239
|
+
else if (manifest.type)
|
|
240
240
|
type = manifest.type;
|
|
241
|
-
}
|
|
242
241
|
}
|
|
243
|
-
const item = {
|
|
244
|
-
path: cmdPath,
|
|
245
|
-
type,
|
|
246
|
-
scope,
|
|
247
|
-
description: manifest?.description || ''
|
|
248
|
-
};
|
|
249
242
|
if (!commands.has(cmdPath)) {
|
|
250
|
-
|
|
243
|
+
if (manifest) {
|
|
244
|
+
const item = { path: cmdPath, type, scope, description: manifest.description || '' };
|
|
245
|
+
commands.set(cmdPath, item);
|
|
246
|
+
}
|
|
251
247
|
const effectiveManifest = manifest || { name: file, type: 'group' };
|
|
252
|
-
if (!(0, manifest_1.isCappedManifest)(effectiveManifest))
|
|
248
|
+
if (!(0, manifest_1.isCappedManifest)(effectiveManifest))
|
|
253
249
|
await walk(filePath, cmdPathParts, scope);
|
|
254
|
-
}
|
|
255
250
|
}
|
|
256
251
|
else {
|
|
257
252
|
const existing = commands.get(cmdPath);
|
|
258
|
-
if (existing && existing.scope === 'local') {
|
|
259
|
-
|
|
260
|
-
await walk(filePath, cmdPathParts, scope);
|
|
261
|
-
}
|
|
253
|
+
if (existing && existing.scope === 'local' && existing.type === 'group' && type === 'group') {
|
|
254
|
+
await walk(filePath, cmdPathParts, scope);
|
|
262
255
|
}
|
|
263
256
|
}
|
|
264
257
|
}
|
|
265
258
|
}
|
|
266
|
-
if (localRoot)
|
|
267
|
-
await walk(path_1.default.join(localRoot, '.agentctl'), [], 'local');
|
|
268
|
-
|
|
269
|
-
await walk(globalRoot, [], 'global');
|
|
259
|
+
if (ctx.localRoot)
|
|
260
|
+
await walk(path_1.default.join(ctx.localRoot, '.agentctl'), [], 'local');
|
|
261
|
+
await walk(ctx.globalRoot, [], 'global');
|
|
270
262
|
return Array.from(commands.values());
|
|
271
263
|
}
|
|
272
|
-
// Helpers
|
|
273
|
-
async function prepareCommand(args, options = {}) {
|
|
274
|
-
const cwd = options.cwd || process.cwd();
|
|
275
|
-
const rootDir = cwd;
|
|
276
|
-
const agentctlDir = path_1.default.join(rootDir, '.agentctl');
|
|
277
|
-
if (args.length === 0)
|
|
278
|
-
throw new Error('No command path provided');
|
|
279
|
-
const cmdPath = args.join(path_1.default.sep);
|
|
280
|
-
const targetDir = path_1.default.join(agentctlDir, cmdPath);
|
|
281
|
-
if (await fs_extra_1.default.pathExists(targetDir)) {
|
|
282
|
-
throw new Error(`Command ${args.join(' ')} already exists`);
|
|
283
|
-
}
|
|
284
|
-
let current = path_1.default.dirname(targetDir);
|
|
285
|
-
while (current.length >= agentctlDir.length && !isSamePath(current, path_1.default.dirname(agentctlDir))) {
|
|
286
|
-
if (await isCapped(current)) {
|
|
287
|
-
const relPath = path_1.default.relative(agentctlDir, current); // This uses cwd for resolution if agentctlDir is cwd resolved
|
|
288
|
-
// Need to verify relative works predictably
|
|
289
|
-
// agentctlDir comes from path.join(cwd, '.agentctl').
|
|
290
|
-
throw new Error(`Cannot nest command under capped command: ${relPath}`);
|
|
291
|
-
}
|
|
292
|
-
current = path_1.default.dirname(current);
|
|
293
|
-
}
|
|
294
|
-
await fs_extra_1.default.ensureDir(targetDir);
|
|
295
|
-
return {
|
|
296
|
-
targetDir,
|
|
297
|
-
name: args[args.length - 1],
|
|
298
|
-
isWin: process.platform === 'win32'
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
async function isCapped(dir) {
|
|
302
|
-
const manifestPath = path_1.default.join(dir, 'manifest.json');
|
|
303
|
-
if (await fs_extra_1.default.pathExists(manifestPath)) {
|
|
304
|
-
try {
|
|
305
|
-
const m = await fs_extra_1.default.readJson(manifestPath);
|
|
306
|
-
return (0, manifest_1.isCappedManifest)(m);
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
function isSamePath(p1, p2) {
|
|
315
|
-
return path_1.default.relative(p1, p2) === '';
|
|
316
|
-
}
|