@michaelhartmayer/agentctl 1.0.3 → 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.
@@ -0,0 +1,172 @@
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.Logic = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const utils_1 = require("./utils");
9
+ const skills_1 = require("../skills");
10
+ /**
11
+ * THE BRAIN: Pure functions that calculate "Plans" (Effect arrays).
12
+ * NO IMPURE IMPORTS (no fs, no os, no process).
13
+ */
14
+ exports.Logic = {
15
+ planScaffold(args, ctx, options) {
16
+ if (args.length === 0)
17
+ throw new Error('No command path provided');
18
+ if (options.exists) {
19
+ throw new Error(`Command ${args.join(' ')} already exists`);
20
+ }
21
+ if (options.cappedAncestor) {
22
+ throw new Error(`Cannot nest command under capped command: ${options.cappedAncestor.relPath}`);
23
+ }
24
+ const localRoot = ctx.localRoot || ctx.cwd;
25
+ const agentctlDir = path_1.default.join(localRoot, '.agentctl');
26
+ const cmdPath = args.join(path_1.default.sep);
27
+ const targetDir = path_1.default.join(agentctlDir, cmdPath);
28
+ const name = args[args.length - 1];
29
+ const type = options.type || 'scaffold';
30
+ const isWin = ctx.platform === 'win32';
31
+ const effects = [{ type: 'mkdir', path: targetDir }];
32
+ const manifest = {
33
+ name,
34
+ description: '',
35
+ type,
36
+ };
37
+ if (type === 'scaffold') {
38
+ const scriptName = isWin ? 'command.cmd' : 'command.sh';
39
+ const scriptPath = path_1.default.join(targetDir, scriptName);
40
+ const scriptContent = isWin
41
+ ? '@echo off\r\nREM Add your command logic here\r\necho Not implemented'
42
+ : '#!/usr/bin/env bash\n# Add your command logic here\necho "Not implemented"';
43
+ manifest.run = `./${scriptName}`;
44
+ effects.push({ type: 'writeFile', path: scriptPath, content: scriptContent });
45
+ if (!isWin)
46
+ effects.push({ type: 'chmod', path: scriptPath, mode: 0o755 });
47
+ }
48
+ else if (type === 'alias') {
49
+ manifest.run = options.target;
50
+ }
51
+ effects.push({ type: 'writeJson', path: path_1.default.join(targetDir, 'manifest.json'), content: manifest });
52
+ const logMsg = type === 'scaffold' ? `Scaffolded command: ${args.join(' ')}` :
53
+ type === 'alias' ? `Aliased command: ${args.join(' ')} -> ${options.target}` :
54
+ `Created group: ${args.join(' ')}`;
55
+ effects.push({ type: 'log', message: logMsg });
56
+ return { effects };
57
+ },
58
+ planPushGlobal(args, ctx, options) {
59
+ if (!ctx.localRoot)
60
+ throw new Error('Not in a local context');
61
+ if (!options.existsInLocal)
62
+ throw new Error(`Local command ${args.join(' ')} not found`);
63
+ if (options.existsInGlobal)
64
+ throw new Error(`Global command ${args.join(' ')} already exists`);
65
+ const cmdPathStr = args.join(path_1.default.sep);
66
+ const srcDir = path_1.default.join(ctx.localRoot, '.agentctl', cmdPathStr);
67
+ const destDir = path_1.default.join(ctx.globalRoot, cmdPathStr);
68
+ const effects = [
69
+ { type: 'mkdir', path: path_1.default.dirname(destDir) }
70
+ ];
71
+ if (options.move) {
72
+ effects.push({ type: 'move', src: srcDir, dest: destDir });
73
+ effects.push({ type: 'log', message: `Moved ${args.join(' ')} to global scope` });
74
+ }
75
+ else {
76
+ effects.push({ type: 'copy', src: srcDir, dest: destDir });
77
+ effects.push({ type: 'log', message: `Copied ${args.join(' ')} to global scope` });
78
+ }
79
+ return effects;
80
+ },
81
+ planPullLocal(args, ctx, options) {
82
+ if (!ctx.localRoot)
83
+ throw new Error('Not in a local context');
84
+ if (!options.existsInGlobal)
85
+ throw new Error(`Global command ${args.join(' ')} not found`);
86
+ if (options.existsInLocal)
87
+ throw new Error(`Local command ${args.join(' ')} already exists`);
88
+ const cmdPathStr = args.join(path_1.default.sep);
89
+ const srcDir = path_1.default.join(ctx.globalRoot, cmdPathStr);
90
+ const destDir = path_1.default.join(ctx.localRoot, '.agentctl', cmdPathStr);
91
+ const effects = [
92
+ { type: 'mkdir', path: path_1.default.dirname(destDir) }
93
+ ];
94
+ if (options.move) {
95
+ effects.push({ type: 'move', src: srcDir, dest: destDir });
96
+ effects.push({ type: 'log', message: `Moved ${args.join(' ')} to local scope` });
97
+ }
98
+ else {
99
+ effects.push({ type: 'copy', src: srcDir, dest: destDir });
100
+ effects.push({ type: 'log', message: `Copied ${args.join(' ')} to local scope` });
101
+ }
102
+ return effects;
103
+ },
104
+ planRemove(args, ctx, options) {
105
+ if (!options.resolvedPath) {
106
+ throw new Error(`Command ${args.join(' ')} not found${options.global ? ' in global scope' : ''}`);
107
+ }
108
+ const targetDir = path_1.default.dirname(options.resolvedPath);
109
+ return [
110
+ { type: 'remove', path: targetDir },
111
+ { type: 'log', message: `Removed ${options.scope} command: ${args.join(' ')}` }
112
+ ];
113
+ },
114
+ planMove(srcArgs, destArgs, ctx, options) {
115
+ if (!options.resolvedSrc)
116
+ throw new Error(`Command ${srcArgs.join(' ')} not found`);
117
+ if (!options.rootDir || !options.agentctlDir)
118
+ throw new Error('Cannot determine root for move');
119
+ if (options.destExists)
120
+ throw new Error(`Destination ${destArgs.join(' ')} already exists`);
121
+ if (options.cappedAncestor)
122
+ throw new Error(`Cannot nest command under capped command: ${options.cappedAncestor.relPath}`);
123
+ const srcDir = path_1.default.dirname(options.resolvedSrc.manifestPath);
124
+ const destPathStr = destArgs.join(path_1.default.sep);
125
+ const destDir = path_1.default.join(options.agentctlDir, destPathStr);
126
+ const effects = [
127
+ { type: 'move', src: srcDir, dest: destDir }
128
+ ];
129
+ const updatedManifest = { ...options.resolvedSrc.manifest, name: destArgs[destArgs.length - 1] };
130
+ effects.push({ type: 'writeJson', path: path_1.default.join(destDir, 'manifest.json'), content: updatedManifest });
131
+ effects.push({ type: 'log', message: `Moved ${srcArgs.join(' ')} to ${destArgs.join(' ')}` });
132
+ return effects;
133
+ },
134
+ planInstallSkill(agent, ctx, options) {
135
+ if (!skills_1.SUPPORTED_AGENTS.includes(agent)) {
136
+ throw new Error(`Agent '${agent}' not supported. Supported agents: ${skills_1.SUPPORTED_AGENTS.join(', ')}`);
137
+ }
138
+ let targetDir;
139
+ if (agent === 'cursor') {
140
+ targetDir = path_1.default.join(ctx.cwd, '.cursor', 'skills');
141
+ }
142
+ else if (agent === 'antigravity') {
143
+ if (options.global) {
144
+ const globalRoot = options.antigravityGlobalDir || utils_1.UtilsLogic.getAntigravityGlobalRoot({
145
+ platform: ctx.platform,
146
+ env: {}, // UtilsLogic should probably take full ctx or we just use ctx.homedir
147
+ homedir: ctx.homedir
148
+ });
149
+ targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
150
+ }
151
+ else {
152
+ targetDir = path_1.default.join(ctx.cwd, '.agent', 'skills', 'agentctl');
153
+ }
154
+ }
155
+ else if (agent === 'agentsmd') {
156
+ targetDir = path_1.default.join(ctx.cwd, '.agents', 'skills', 'agentctl');
157
+ }
158
+ else if (agent === 'gemini') {
159
+ if (options.global) {
160
+ const globalRoot = options.geminiGlobalDir || path_1.default.join(ctx.homedir, '.gemini');
161
+ targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
162
+ }
163
+ else {
164
+ targetDir = path_1.default.join(ctx.cwd, '.gemini', 'skills', 'agentctl');
165
+ }
166
+ }
167
+ else {
168
+ throw new Error(`Agent logic for '${agent}' not implemented.`);
169
+ }
170
+ return [{ type: 'installSkill', targetDir, agent }];
171
+ }
172
+ };
@@ -0,0 +1,64 @@
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.AppLogic = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ exports.AppLogic = {
9
+ planApp(args, result) {
10
+ if (!result) {
11
+ return [{ type: 'log', message: `Command '${args.join(' ')}' not found. Run 'agentctl list' to see available commands.` }];
12
+ }
13
+ const { manifest, args: remainingArgs, scope, manifestPath, cmdPath } = result;
14
+ if (manifest.run) {
15
+ const cmdDir = path_1.default.dirname(manifestPath);
16
+ let runCmd = manifest.run;
17
+ if (runCmd.startsWith('./') || runCmd.startsWith('.\\')) {
18
+ runCmd = path_1.default.resolve(cmdDir, runCmd);
19
+ }
20
+ runCmd = runCmd.replace(/{{DIR}}/g, cmdDir);
21
+ const fullCommand = `${runCmd} ${remainingArgs.join(' ')}`;
22
+ return [
23
+ { type: 'log', message: `[${scope}] Running: ${fullCommand}` },
24
+ {
25
+ type: 'spawn',
26
+ command: fullCommand,
27
+ options: {
28
+ cwd: process.cwd(),
29
+ shell: true,
30
+ stdio: 'inherit',
31
+ env: { ...process.env, AGENTCTL_SCOPE: scope }
32
+ },
33
+ onExit: (code) => {
34
+ process.exit(code || 0);
35
+ }
36
+ }
37
+ ];
38
+ }
39
+ else {
40
+ return exports.AppLogic.planGroupList(manifest, cmdPath, []); // Shell will call this again with actual children
41
+ }
42
+ },
43
+ planGroupList(manifest, cmdPath, allCommands) {
44
+ const effects = [
45
+ { type: 'log', message: manifest.name },
46
+ { type: 'log', message: manifest.description || 'No description' },
47
+ { type: 'log', message: '\nSubcommands:' }
48
+ ];
49
+ const prefix = cmdPath + ' ';
50
+ const depth = cmdPath.split(' ').length;
51
+ const children = allCommands.filter(c => c.path.startsWith(prefix) && c.path !== cmdPath);
52
+ const direct = children.filter(c => c.path.split(' ').length === depth + 1);
53
+ if (direct.length === 0) {
54
+ effects.push({ type: 'log', message: ' (No subcommands found)' });
55
+ }
56
+ else {
57
+ for (const child of direct) {
58
+ const name = child.path.split(' ').pop();
59
+ effects.push({ type: 'log', message: ` ${name}\t${child.description}` });
60
+ }
61
+ }
62
+ return effects;
63
+ }
64
+ };
@@ -0,0 +1,57 @@
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.planInstallClone = planInstallClone;
7
+ exports.planInstallCopy = planInstallCopy;
8
+ const path_1 = __importDefault(require("path"));
9
+ function planInstallClone(ctx, deps) {
10
+ if (!ctx.global && !ctx.localRoot) {
11
+ throw new Error('Not in a local context. Run inside a project or use --global');
12
+ }
13
+ const tempDir = path_1.default.join(ctx.osTmpdir, deps.tempFolderName);
14
+ const effects = [];
15
+ // 1. Create a log message
16
+ effects.push({
17
+ type: 'log',
18
+ message: `Fetching ${ctx.repoUrl}...`
19
+ });
20
+ // 2. Clone the repository shallowly into a temp directory
21
+ effects.push({
22
+ type: 'gitClone',
23
+ url: ctx.repoUrl,
24
+ dest: tempDir
25
+ });
26
+ return { effects, tempDir };
27
+ }
28
+ function planInstallCopy(ctx, deps) {
29
+ if (!ctx.global && !ctx.localRoot) {
30
+ throw new Error('Not in a local context. Run inside a project or use --global');
31
+ }
32
+ const rootDir = ctx.global ? ctx.globalRoot : ctx.localRoot;
33
+ const agentctlDir = ctx.global ? rootDir : path_1.default.join(rootDir, '.agentctl');
34
+ const targetDir = path_1.default.join(agentctlDir, ...ctx.pathParts);
35
+ const effects = [];
36
+ if (!ctx.allowCollisions) {
37
+ // Simple collision detection based on paths relative to targetDir
38
+ const collisions = deps.existingItems.filter(item => deps.downloadedItems.includes(item));
39
+ if (collisions.length > 0) {
40
+ effects.push({ type: 'remove', path: path_1.default.dirname(deps.tempAgentctlDir) }); // cleanup
41
+ throw new Error(`Installation aborted due to collisions without --allow-collisions flag: \n${collisions.join('\n')}`);
42
+ }
43
+ }
44
+ // Ensure target path exists
45
+ effects.push({ type: 'mkdir', path: targetDir });
46
+ // Copy contents
47
+ effects.push({
48
+ type: 'copy',
49
+ src: deps.tempAgentctlDir,
50
+ dest: targetDir,
51
+ options: { overwrite: ctx.allowCollisions }
52
+ });
53
+ // Clean up temp dir
54
+ effects.push({ type: 'remove', path: path_1.default.dirname(deps.tempAgentctlDir) });
55
+ effects.push({ type: 'log', message: `Successfully installed commands to ${targetDir}` });
56
+ return { effects };
57
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ManifestLogic = void 0;
4
+ exports.ManifestLogic = {
5
+ isCappedManifest(m) {
6
+ return !!m.run || m.type === 'scaffold' || m.type === 'alias';
7
+ }
8
+ };
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResolveLogic = void 0;
4
+ exports.ResolveLogic = {
5
+ decide(local, global, isCapped) {
6
+ if (!local?.manifest && !global.manifest) {
7
+ return { currentMatch: null, finalResult: null, shouldStop: true };
8
+ }
9
+ const localManifest = local?.manifest;
10
+ const globalManifest = global.manifest;
11
+ // 1. Local Capped -> Return Match immediately.
12
+ if (localManifest && isCapped(localManifest)) {
13
+ return {
14
+ currentMatch: null,
15
+ finalResult: {
16
+ scope: 'local',
17
+ manifest: localManifest,
18
+ manifestPath: local.path
19
+ },
20
+ shouldStop: true
21
+ };
22
+ }
23
+ // 2. Global Capped
24
+ if (globalManifest && isCapped(globalManifest)) {
25
+ if (localManifest) {
26
+ // Local Group shadows Global Capped
27
+ return {
28
+ currentMatch: {
29
+ scope: 'local',
30
+ manifest: localManifest,
31
+ manifestPath: local.path
32
+ },
33
+ finalResult: null,
34
+ shouldStop: false
35
+ };
36
+ }
37
+ else {
38
+ return {
39
+ currentMatch: null,
40
+ finalResult: {
41
+ scope: 'global',
42
+ manifest: globalManifest,
43
+ manifestPath: global.path
44
+ },
45
+ shouldStop: true
46
+ };
47
+ }
48
+ }
49
+ // 3. Neither capped
50
+ if (localManifest) {
51
+ return {
52
+ currentMatch: {
53
+ scope: 'local',
54
+ manifest: localManifest,
55
+ manifestPath: local.path
56
+ },
57
+ finalResult: null,
58
+ shouldStop: false
59
+ };
60
+ }
61
+ else {
62
+ return {
63
+ currentMatch: {
64
+ scope: 'global',
65
+ manifest: globalManifest,
66
+ manifestPath: global.path
67
+ },
68
+ finalResult: null,
69
+ shouldStop: false
70
+ };
71
+ }
72
+ }
73
+ };
@@ -0,0 +1,20 @@
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.SkillsLogic = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ exports.SkillsLogic = {
9
+ getSourcePaths(ctx) {
10
+ return [
11
+ path_1.default.resolve(ctx.dirname, '../../skills/agentctl/SKILL.md'),
12
+ path_1.default.resolve(ctx.dirname, '../skills/agentctl/SKILL.md'),
13
+ path_1.default.resolve(ctx.dirname, '../../../skills/agentctl/SKILL.md')
14
+ ];
15
+ },
16
+ getTargetPath(targetDir, agent) {
17
+ const filename = agent === 'cursor' ? 'agentctl.md' : 'SKILL.md';
18
+ return path_1.default.join(targetDir, filename);
19
+ }
20
+ };
@@ -0,0 +1,31 @@
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.UtilsLogic = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ exports.UtilsLogic = {
9
+ getGlobalRoot(ctx) {
10
+ if (ctx.platform === 'win32') {
11
+ return path_1.default.join(ctx.env.APPDATA || path_1.default.join(ctx.homedir, 'AppData', 'Roaming'), 'agentctl');
12
+ }
13
+ return path_1.default.join(ctx.homedir, '.config', 'agentctl');
14
+ },
15
+ getAntigravityGlobalRoot(ctx) {
16
+ return path_1.default.join(ctx.homedir, '.gemini', 'antigravity');
17
+ },
18
+ findLocalRoot(cwd, existsSync) {
19
+ let current = path_1.default.resolve(cwd);
20
+ const root = path_1.default.parse(current).root;
21
+ for (;;) {
22
+ if (existsSync(path_1.default.join(current, '.agentctl'))) {
23
+ return current;
24
+ }
25
+ if (current === root) {
26
+ return null;
27
+ }
28
+ current = path_1.default.dirname(current);
29
+ }
30
+ }
31
+ };
package/dist/manifest.js CHANGED
@@ -1,12 +1,17 @@
1
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
- });
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
10
15
  };
11
16
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -15,16 +20,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
20
  exports.readManifest = readManifest;
16
21
  exports.isCappedManifest = isCappedManifest;
17
22
  const fs_extra_1 = __importDefault(require("fs-extra"));
18
- function readManifest(p) {
19
- return __awaiter(this, void 0, void 0, function* () {
20
- try {
21
- return yield fs_extra_1.default.readJson(p);
22
- }
23
- catch (_a) {
24
- return null;
25
- }
26
- });
23
+ const manifest_1 = require("./logic/manifest");
24
+ __exportStar(require("./logic/manifest"), exports);
25
+ async function readManifest(p) {
26
+ try {
27
+ return await fs_extra_1.default.readJson(p);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
27
32
  }
28
33
  function isCappedManifest(m) {
29
- return !!m.run || m.type === 'scaffold' || m.type === 'alias';
34
+ return manifest_1.ManifestLogic.isCappedManifest(m);
30
35
  }
package/dist/resolve.js CHANGED
@@ -1,13 +1,4 @@
1
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
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
4
  };
@@ -17,107 +8,59 @@ const path_1 = __importDefault(require("path"));
17
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
18
9
  const fs_utils_1 = require("./fs-utils");
19
10
  const manifest_1 = require("./manifest");
20
- function resolveCommand(args_1) {
21
- return __awaiter(this, arguments, void 0, function* (args, options = {}) {
22
- const cwd = options.cwd || process.cwd();
23
- const localRoot = !options.global ? (0, fs_utils_1.findLocalRoot)(cwd) : null;
24
- const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
25
- const localAgentctl = localRoot ? path_1.default.join(localRoot, '.agentctl') : null;
26
- let currentMatch = null;
27
- // Iterate through args to find longest match
28
- for (let i = 0; i < args.length; i++) {
29
- // Path corresponding to args[0..i]
30
- const currentArgs = args.slice(0, i + 1);
31
- const relPath = currentArgs.join(path_1.default.sep);
32
- const cmdPath = currentArgs.join(' ');
33
- const localPath = localAgentctl ? path_1.default.join(localAgentctl, relPath) : null;
34
- const globalPath = path_1.default.join(globalRoot, relPath);
35
- let localManifest = null;
36
- let globalManifest = null;
37
- // check local
38
- if (localPath && (yield fs_extra_1.default.pathExists(localPath))) {
39
- const mPath = path_1.default.join(localPath, 'manifest.json');
40
- if (yield fs_extra_1.default.pathExists(mPath)) {
41
- localManifest = yield (0, manifest_1.readManifest)(mPath);
42
- }
43
- if (!localManifest && (yield fs_extra_1.default.stat(localPath)).isDirectory()) {
44
- // Implicit group
45
- localManifest = { name: args[i], type: 'group' };
46
- }
47
- }
48
- // check global
49
- if (yield fs_extra_1.default.pathExists(globalPath)) {
50
- const mPath = path_1.default.join(globalPath, 'manifest.json');
51
- if (yield fs_extra_1.default.pathExists(mPath)) {
52
- globalManifest = yield (0, manifest_1.readManifest)(mPath);
53
- }
54
- if (!globalManifest && (yield fs_extra_1.default.stat(globalPath)).isDirectory()) {
55
- globalManifest = { name: args[i], type: 'group' };
56
- }
57
- }
58
- if (!localManifest && !globalManifest) {
59
- break;
60
- }
61
- const remainingArgs = args.slice(i + 1);
62
- // Priority logic
63
- // 1. Local Capped -> Return Match immediately.
64
- if (localManifest && (0, manifest_1.isCappedManifest)(localManifest)) {
65
- return {
66
- manifest: localManifest,
67
- manifestPath: path_1.default.join(localPath, 'manifest.json'),
68
- args: remainingArgs,
69
- scope: 'local',
70
- cmdPath
71
- };
11
+ const resolve_1 = require("./logic/resolve");
12
+ async function resolveCommand(args, options = {}) {
13
+ const cwd = options.cwd || process.cwd();
14
+ const localRoot = !options.global ? (0, fs_utils_1.findLocalRoot)(cwd) : null;
15
+ const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
16
+ const localAgentctl = localRoot ? path_1.default.join(localRoot, '.agentctl') : null;
17
+ let currentMatch = null;
18
+ for (let i = 0; i < args.length; i++) {
19
+ const currentArgs = args.slice(0, i + 1);
20
+ const relPath = currentArgs.join(path_1.default.sep);
21
+ const cmdPath = currentArgs.join(' ');
22
+ const localPath = localAgentctl ? path_1.default.join(localAgentctl, relPath) : null;
23
+ const globalPath = path_1.default.join(globalRoot, relPath);
24
+ const getFacts = async (p) => {
25
+ if (!p || !(await fs_extra_1.default.pathExists(p)))
26
+ return null;
27
+ const mPath = path_1.default.join(p, 'manifest.json');
28
+ let manifest = null;
29
+ if (await fs_extra_1.default.pathExists(mPath)) {
30
+ manifest = await (0, manifest_1.readManifest)(mPath);
72
31
  }
73
- // 2. Global Capped
74
- if (globalManifest && (0, manifest_1.isCappedManifest)(globalManifest)) {
75
- // Check if shadowed by Local Group
76
- if (localManifest) {
77
- // Local exists (must be group since checked capped above).
78
- // Shadowed. Treat as Local Group.
79
- currentMatch = {
80
- manifest: localManifest,
81
- manifestPath: path_1.default.join(localPath, 'manifest.json'),
82
- args: remainingArgs,
83
- scope: 'local',
84
- cmdPath
85
- };
86
- }
87
- else {
88
- // Not shadowed. Global Capped wins. Return immediately.
89
- return {
90
- manifest: globalManifest,
91
- manifestPath: path_1.default.join(globalPath, 'manifest.json'),
92
- args: remainingArgs,
93
- scope: 'global',
94
- cmdPath
95
- };
96
- }
97
- }
98
- else {
99
- // Neither is capped. Both are groups (or one is).
100
- // Local wins if exists.
101
- if (localManifest) {
102
- currentMatch = {
103
- manifest: localManifest,
104
- manifestPath: path_1.default.join(localPath, 'manifest.json'),
105
- args: remainingArgs,
106
- scope: 'local',
107
- cmdPath
108
- };
109
- }
110
- else {
111
- currentMatch = {
112
- manifest: globalManifest,
113
- manifestPath: path_1.default.join(globalPath, 'manifest.json'),
114
- args: remainingArgs,
115
- scope: 'global',
116
- cmdPath
117
- };
118
- }
32
+ const stats = await fs_extra_1.default.stat(p);
33
+ const isDir = stats.isDirectory();
34
+ if (!manifest && isDir) {
35
+ manifest = { name: args[i], type: 'group' };
119
36
  }
37
+ return { exists: true, manifest, isDir, path: mPath };
38
+ };
39
+ const localFacts = await getFacts(localPath);
40
+ const globalFacts = (await getFacts(globalPath)) || { exists: false, manifest: null, isDir: false, path: path_1.default.join(globalPath, 'manifest.json') };
41
+ const decision = resolve_1.ResolveLogic.decide(localFacts, globalFacts, manifest_1.isCappedManifest);
42
+ const remainingArgs = args.slice(i + 1);
43
+ if (decision.finalResult) {
44
+ return {
45
+ manifest: decision.finalResult.manifest,
46
+ manifestPath: decision.finalResult.manifestPath,
47
+ args: remainingArgs,
48
+ scope: decision.finalResult.scope,
49
+ cmdPath
50
+ };
51
+ }
52
+ if (decision.currentMatch) {
53
+ currentMatch = {
54
+ manifest: decision.currentMatch.manifest,
55
+ manifestPath: decision.currentMatch.manifestPath,
56
+ args: remainingArgs,
57
+ scope: decision.currentMatch.scope,
58
+ cmdPath
59
+ };
60
+ }
61
+ if (decision.shouldStop) {
62
+ break;
120
63
  }
121
- return currentMatch;
122
- });
64
+ }
65
+ return currentMatch;
123
66
  }