@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.
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { group, scaffold } from '../src/ctl';
6
+
7
+ describe('ctl group', () => {
8
+ let cwd: string;
9
+
10
+ beforeEach(async () => {
11
+ cwd = await createTestDir();
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await cleanupTestDir(cwd);
16
+ });
17
+
18
+ it('creates an uncapped command (group) with minimal manifest', async () => {
19
+ await group(['dev'], { cwd });
20
+
21
+ const groupDir = path.join(cwd, '.agentctl', 'dev');
22
+ expect(await fs.pathExists(groupDir)).toBe(true);
23
+
24
+ const manifestPath = path.join(groupDir, 'manifest.json');
25
+ expect(await fs.pathExists(manifestPath)).toBe(true);
26
+ const manifest = await fs.readJson(manifestPath);
27
+
28
+ expect(manifest).toEqual(expect.objectContaining({
29
+ name: 'dev',
30
+ type: 'group',
31
+ }));
32
+ expect(manifest.run).toBeUndefined();
33
+ });
34
+
35
+ it('fails if path exists as capped command', async () => {
36
+ // Create capped command 'dev'
37
+ await scaffold(['dev'], { cwd });
38
+
39
+ await expect(group(['dev'], { cwd }))
40
+ .rejects.toThrow(/already exists/);
41
+ });
42
+
43
+ it('fails if path exists as directory (group)', async () => {
44
+ await group(['dev'], { cwd });
45
+ await expect(group(['dev'], { cwd }))
46
+ .rejects.toThrow(/already exists/);
47
+ });
48
+ });
@@ -0,0 +1,16 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+
6
+ const BASE_TEST_DIR = path.join(os.tmpdir(), 'agentctl-tests');
7
+
8
+ export async function createTestDir() {
9
+ const dir = path.join(BASE_TEST_DIR, crypto.randomUUID());
10
+ await fs.ensureDir(dir);
11
+ return dir;
12
+ }
13
+
14
+ export async function cleanupTestDir(dir: string) {
15
+ await fs.remove(dir);
16
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { list, inspect } from '../src/ctl';
6
+ import { scaffold } from '../src/ctl';
7
+
8
+ describe('ctl introspection', () => {
9
+ let localRoot: string;
10
+ let globalRoot: string;
11
+ let baseDir: string;
12
+
13
+ beforeEach(async () => {
14
+ baseDir = await createTestDir();
15
+ localRoot = path.join(baseDir, 'local');
16
+ globalRoot = path.join(baseDir, 'global');
17
+ await fs.ensureDir(localRoot);
18
+ await fs.ensureDir(globalRoot);
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await cleanupTestDir(baseDir);
23
+ });
24
+
25
+ it('list commands recursively from both scopes', async () => {
26
+ // Local: dev start (scaffold), tools (group)
27
+ await scaffold(['dev', 'start'], { cwd: localRoot });
28
+ await fs.ensureDir(path.join(localRoot, '.agentctl', 'tools')); // implicit group
29
+
30
+ // Global: gh (alias), tools lint (alias)
31
+ const ghDir = path.join(globalRoot, 'gh');
32
+ await fs.ensureDir(ghDir);
33
+ await fs.writeJson(path.join(ghDir, 'manifest.json'), { name: 'gh', type: 'alias', run: 'gh' });
34
+
35
+ const toolsLintDir = path.join(globalRoot, 'tools', 'lint');
36
+ await fs.ensureDir(toolsLintDir);
37
+ await fs.writeJson(path.join(toolsLintDir, 'manifest.json'), { name: 'lint', type: 'alias', run: 'lint' });
38
+
39
+ const results = await list({ cwd: localRoot, globalDir: globalRoot });
40
+
41
+ const paths = results.map(r => r.path).sort();
42
+ expect(paths).toContain('dev');
43
+ expect(paths).toContain('dev start');
44
+ expect(paths).toContain('gh');
45
+ expect(paths).toContain('tools');
46
+ expect(paths).toContain('tools lint');
47
+
48
+ const dev = results.find(r => r.path === 'dev');
49
+ expect(dev.scope).toBe('local');
50
+ expect(dev.type).toBe('group');
51
+
52
+ const gh = results.find(r => r.path === 'gh');
53
+ expect(gh.scope).toBe('global');
54
+ expect(gh.type).toBe('alias');
55
+
56
+ // Ensure tools is merged/handled
57
+ const tools = results.find(r => r.path === 'tools');
58
+ // It resides in both (implicitly in global as parent of lint, explicitly in local).
59
+ // Local wins metadata. It's a group.
60
+ expect(tools.type).toBe('group');
61
+ });
62
+
63
+ it('inspect shows command details', async () => {
64
+ await scaffold(['deploy'], { cwd: localRoot });
65
+
66
+ const details = await inspect(['deploy'], { cwd: localRoot, globalDir: globalRoot });
67
+ expect(details).not.toBeNull();
68
+ expect(details.manifest.name).toBe('deploy');
69
+ expect(details.resolvedPath).toContain(path.join(localRoot, '.agentctl', 'deploy'));
70
+ });
71
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { mv, scaffold } from '../src/ctl';
6
+
7
+ describe('lifecycle nesting guards', () => {
8
+ let testDir: string;
9
+ let agentctlDir: string;
10
+
11
+ beforeEach(async () => {
12
+ testDir = await createTestDir();
13
+ agentctlDir = path.join(testDir, '.agentctl');
14
+ await fs.ensureDir(agentctlDir);
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await cleanupTestDir(testDir);
19
+ });
20
+
21
+ it('mv throws when trying to nest under a capped command', async () => {
22
+ // Create a capped command 'calc'
23
+ await scaffold(['calc'], { cwd: testDir });
24
+
25
+ // Create another command 'git'
26
+ await scaffold(['git'], { cwd: testDir });
27
+
28
+ // Try to move 'git' to 'calc sub'
29
+ // This should fail because 'calc' is capped (has run script)
30
+ await expect(
31
+ mv(['git'], ['calc', 'sub'], { cwd: testDir })
32
+ ).rejects.toThrow('Cannot nest command under capped command');
33
+ });
34
+
35
+ it('scaffold throws when trying to nest under a capped command', async () => {
36
+ // Create a capped command 'calc'
37
+ await scaffold(['calc'], { cwd: testDir });
38
+
39
+ // Try to scaffold 'calc sub'
40
+ await expect(
41
+ scaffold(['calc', 'sub'], { cwd: testDir })
42
+ ).rejects.toThrow('Cannot nest command under capped command');
43
+ });
44
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { rm, mv, scaffold } from '../src/ctl';
6
+
7
+ describe('ctl lifecycle (rm, mv)', () => {
8
+ let cwd: string;
9
+ beforeEach(async () => {
10
+ cwd = await createTestDir();
11
+ });
12
+ afterEach(async () => {
13
+ await cleanupTestDir(cwd);
14
+ });
15
+
16
+ it('rm: removes command logic', async () => {
17
+ await scaffold(['deploy'], { cwd });
18
+ const cmdDir = path.join(cwd, '.agentctl', 'deploy');
19
+ expect(await fs.pathExists(cmdDir)).toBe(true);
20
+
21
+ await rm(['deploy'], { cwd });
22
+ expect(await fs.pathExists(cmdDir)).toBe(false);
23
+ });
24
+
25
+ it('rm: removes group', async () => {
26
+ await scaffold(['dev', 'start'], { cwd });
27
+ const devDir = path.join(cwd, '.agentctl', 'dev');
28
+ expect(await fs.pathExists(devDir)).toBe(true);
29
+ expect(await fs.pathExists(path.join(devDir, 'start'))).toBe(true);
30
+
31
+ await rm(['dev'], { cwd });
32
+ expect(await fs.pathExists(devDir)).toBe(false);
33
+ });
34
+
35
+ it('mv: moves command and updates name', async () => {
36
+ await scaffold(['deploy'], { cwd });
37
+ const oldDir = path.join(cwd, '.agentctl', 'deploy');
38
+ const manifestPath = path.join(oldDir, 'manifest.json');
39
+ expect((await fs.readJson(manifestPath)).name).toBe('deploy');
40
+
41
+ // Move deploy -> release
42
+ await mv(['deploy'], ['release'], { cwd });
43
+
44
+ const newDir = path.join(cwd, '.agentctl', 'release');
45
+ expect(await fs.pathExists(oldDir)).toBe(false);
46
+ expect(await fs.pathExists(newDir)).toBe(true);
47
+
48
+ const newManifest = await fs.readJson(path.join(newDir, 'manifest.json'));
49
+ expect(newManifest.name).toBe('release');
50
+ });
51
+
52
+ it('mv: fails if destination exists', async () => {
53
+ await scaffold(['deploy'], { cwd });
54
+ await scaffold(['release'], { cwd });
55
+
56
+ await expect(mv(['deploy'], ['release'], { cwd }))
57
+ .rejects.toThrow(/already exists/);
58
+ });
59
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isCappedManifest, Manifest } from '../src/manifest';
3
+
4
+ describe('isCappedManifest', () => {
5
+ it('returns true when manifest has a run command', () => {
6
+ const m: Manifest = { name: 'test', run: './script.sh' };
7
+ expect(isCappedManifest(m)).toBe(true);
8
+ });
9
+
10
+ it('returns true when manifest type is scaffold', () => {
11
+ const m: Manifest = { name: 'test', type: 'scaffold' };
12
+ expect(isCappedManifest(m)).toBe(true);
13
+ });
14
+
15
+ it('returns true when manifest type is alias', () => {
16
+ const m: Manifest = { name: 'test', type: 'alias' };
17
+ expect(isCappedManifest(m)).toBe(true);
18
+ });
19
+
20
+ it('returns false when manifest is a plain group', () => {
21
+ const m: Manifest = { name: 'test', type: 'group' };
22
+ expect(isCappedManifest(m)).toBe(false);
23
+ });
24
+
25
+ it('returns false when manifest has no type and no run', () => {
26
+ const m: Manifest = { name: 'test' };
27
+ expect(isCappedManifest(m)).toBe(false);
28
+ });
29
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { resolveCommand } from '../src/resolve';
6
+
7
+ describe('resolve scope priority', () => {
8
+ let testDir: string;
9
+ let globalDir: string;
10
+
11
+ beforeEach(async () => {
12
+ testDir = await createTestDir();
13
+ globalDir = await createTestDir();
14
+ await fs.ensureDir(path.join(testDir, '.agentctl'));
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await cleanupTestDir(testDir);
19
+ await cleanupTestDir(globalDir);
20
+ });
21
+
22
+ it('local group shadows global group at the same path', async () => {
23
+ // Create a group in both local and global with the same name
24
+ const localCmd = path.join(testDir, '.agentctl', 'tools');
25
+ await fs.ensureDir(localCmd);
26
+ await fs.writeJson(path.join(localCmd, 'manifest.json'), {
27
+ name: 'tools', type: 'group', description: 'local tools'
28
+ });
29
+
30
+ const globalCmd = path.join(globalDir, 'tools');
31
+ await fs.ensureDir(globalCmd);
32
+ await fs.writeJson(path.join(globalCmd, 'manifest.json'), {
33
+ name: 'tools', type: 'group', description: 'global tools'
34
+ });
35
+
36
+ const result = await resolveCommand(['tools'], { cwd: testDir, globalDir });
37
+ expect(result).not.toBeNull();
38
+ expect(result!.scope).toBe('local');
39
+ expect(result!.manifest.description).toBe('local tools');
40
+ });
41
+
42
+ it('local capped command shadows global capped command', async () => {
43
+ const localCmd = path.join(testDir, '.agentctl', 'deploy');
44
+ await fs.ensureDir(localCmd);
45
+ await fs.writeJson(path.join(localCmd, 'manifest.json'), {
46
+ name: 'deploy', type: 'scaffold', run: './local-deploy.sh'
47
+ });
48
+
49
+ const globalCmd = path.join(globalDir, 'deploy');
50
+ await fs.ensureDir(globalCmd);
51
+ await fs.writeJson(path.join(globalCmd, 'manifest.json'), {
52
+ name: 'deploy', type: 'scaffold', run: './global-deploy.sh'
53
+ });
54
+
55
+ const result = await resolveCommand(['deploy'], { cwd: testDir, globalDir });
56
+ expect(result).not.toBeNull();
57
+ expect(result!.scope).toBe('local');
58
+ expect(result!.manifest.run).toBe('./local-deploy.sh');
59
+ });
60
+
61
+ it('global command resolves when no local exists', async () => {
62
+ const globalCmd = path.join(globalDir, 'deploy');
63
+ await fs.ensureDir(globalCmd);
64
+ await fs.writeJson(path.join(globalCmd, 'manifest.json'), {
65
+ name: 'deploy', type: 'scaffold', run: './deploy.sh'
66
+ });
67
+
68
+ const result = await resolveCommand(['deploy'], { cwd: testDir, globalDir });
69
+ expect(result).not.toBeNull();
70
+ expect(result!.scope).toBe('global');
71
+ });
72
+ });
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { resolveCommand } from '../src/resolve';
6
+ import { scaffold } from '../src/ctl';
7
+
8
+ describe('resolveCommand', () => {
9
+ let localRoot: string;
10
+ let globalRoot: string;
11
+ let baseDir: string;
12
+
13
+ beforeEach(async () => {
14
+ baseDir = await createTestDir();
15
+ localRoot = path.join(baseDir, 'local');
16
+ globalRoot = path.join(baseDir, 'global');
17
+ await fs.ensureDir(localRoot);
18
+ await fs.ensureDir(globalRoot);
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await cleanupTestDir(baseDir);
23
+ });
24
+
25
+ it('resolves local command', async () => {
26
+ // Create local command 'deploy' in localRoot/.agentctl/deploy
27
+ await scaffold(['deploy'], { cwd: localRoot });
28
+
29
+ const result = await resolveCommand(['deploy'], { cwd: localRoot, globalDir: globalRoot });
30
+ expect(result).not.toBeNull();
31
+ expect(result!.manifest.name).toBe('deploy');
32
+ expect(result!.scope).toBe('local');
33
+ expect(result!.args).toEqual([]);
34
+ });
35
+
36
+ it('resolves global command', async () => {
37
+ // Create global alias 'gh' in globalRoot/gh
38
+ const cmdDir = path.join(globalRoot, 'gh');
39
+ await fs.ensureDir(cmdDir);
40
+ await fs.writeJson(path.join(cmdDir, 'manifest.json'), { name: 'gh', type: 'alias', run: 'gh' });
41
+
42
+ const result = await resolveCommand(['gh'], { cwd: localRoot, globalDir: globalRoot });
43
+ expect(result).not.toBeNull();
44
+ expect(result!.manifest.name).toBe('gh');
45
+ expect(result!.scope).toBe('global');
46
+ });
47
+
48
+ it('prioritizes local over global', async () => {
49
+ // Local 'deploy' (manifest type scaffold)
50
+ await scaffold(['deploy'], { cwd: localRoot });
51
+
52
+ // Global 'deploy' (manifest type alias)
53
+ const cmdDir = path.join(globalRoot, 'deploy');
54
+ await fs.ensureDir(cmdDir);
55
+ await fs.writeJson(path.join(cmdDir, 'manifest.json'), { name: 'deploy', type: 'alias', run: 'echo global' });
56
+
57
+ const result = await resolveCommand(['deploy'], { cwd: localRoot, globalDir: globalRoot });
58
+ expect(result).not.toBeNull();
59
+ expect(result!.scope).toBe('local');
60
+ // Ensure we got the scaffold manifest (which has type 'scaffold')
61
+ expect(result!.manifest.type).toBe('scaffold');
62
+ });
63
+
64
+ it('returns null if not found', async () => {
65
+ const result = await resolveCommand(['missing'], { cwd: localRoot, globalDir: globalRoot });
66
+ expect(result).toBeNull();
67
+ });
68
+
69
+ it('handles nested properties', async () => {
70
+ // Local group 'dev', command 'start'
71
+ await scaffold(['dev', 'start'], { cwd: localRoot });
72
+
73
+ const result = await resolveCommand(['dev', 'start'], { cwd: localRoot, globalDir: globalRoot });
74
+ expect(result).not.toBeNull();
75
+ expect(result!.manifest.name).toBe('start');
76
+ expect(result!.cmdPath).toBe('dev start'); // Should construct cmdPath?
77
+ });
78
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { scaffold } from '../src/ctl';
6
+
7
+ describe('ctl scaffold', () => {
8
+ let cwd: string;
9
+
10
+ beforeEach(async () => {
11
+ cwd = await createTestDir();
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await cleanupTestDir(cwd);
16
+ });
17
+
18
+ it('creates a capped command with manifest and script', async () => {
19
+ await scaffold(['deploy'], { cwd });
20
+
21
+ const cmdDir = path.join(cwd, '.agentctl', 'deploy');
22
+ expect(await fs.pathExists(cmdDir)).toBe(true);
23
+
24
+ const manifestPath = path.join(cmdDir, 'manifest.json');
25
+ expect(await fs.pathExists(manifestPath)).toBe(true);
26
+ const manifest = await fs.readJson(manifestPath);
27
+
28
+ expect(manifest).toEqual(expect.objectContaining({
29
+ name: 'deploy',
30
+ type: 'scaffold',
31
+ description: '',
32
+ }));
33
+
34
+ const scriptPath = path.join(cmdDir, manifest.run);
35
+ expect(await fs.pathExists(scriptPath)).toBe(true);
36
+
37
+ const scriptContent = await fs.readFile(scriptPath, 'utf-8');
38
+ if (process.platform === 'win32') {
39
+ expect(scriptContent).toContain('@echo off');
40
+ } else {
41
+ expect(scriptContent).toContain('#!/usr/bin/env');
42
+ }
43
+ });
44
+
45
+ it('creates nested commands implicitly creating groups', async () => {
46
+ await scaffold(['dev', 'start'], { cwd });
47
+
48
+ const groupDir = path.join(cwd, '.agentctl', 'dev');
49
+ expect(await fs.pathExists(groupDir)).toBe(true);
50
+
51
+ const cmdDir = path.join(groupDir, 'start');
52
+ expect(await fs.pathExists(cmdDir)).toBe(true);
53
+ });
54
+
55
+ it('fails if command already exists', async () => {
56
+ await scaffold(['deploy'], { cwd });
57
+
58
+ await expect(scaffold(['deploy'], { cwd }))
59
+ .rejects.toThrow(/already exists/);
60
+ });
61
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { pushGlobal, pullLocal } from '../src/ctl';
6
+
7
+ describe('scoping error guards', () => {
8
+ let testDir: string;
9
+ let globalDir: string;
10
+
11
+ beforeEach(async () => {
12
+ testDir = await createTestDir();
13
+ globalDir = await createTestDir();
14
+ // Create .agentctl so findLocalRoot works
15
+ await fs.ensureDir(path.join(testDir, '.agentctl'));
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await cleanupTestDir(testDir);
20
+ await cleanupTestDir(globalDir);
21
+ });
22
+
23
+ it('pushGlobal throws if local command does not exist', async () => {
24
+ await expect(
25
+ pushGlobal(['nonexistent'], { cwd: testDir, globalDir })
26
+ ).rejects.toThrow('not found');
27
+ });
28
+
29
+ it('pullLocal throws if global command does not exist', async () => {
30
+ await expect(
31
+ pullLocal(['nonexistent'], { cwd: testDir, globalDir })
32
+ ).rejects.toThrow('not found');
33
+ });
34
+
35
+ it('pushGlobal throws if global command already exists', async () => {
36
+ // Create local command
37
+ const localCmd = path.join(testDir, '.agentctl', 'mycmd');
38
+ await fs.ensureDir(localCmd);
39
+ await fs.writeJson(path.join(localCmd, 'manifest.json'), {
40
+ name: 'mycmd', type: 'scaffold', run: './script.sh'
41
+ });
42
+
43
+ // Create conflicting global command
44
+ const globalCmd = path.join(globalDir, 'mycmd');
45
+ await fs.ensureDir(globalCmd);
46
+ await fs.writeJson(path.join(globalCmd, 'manifest.json'), {
47
+ name: 'mycmd', type: 'scaffold', run: './script.sh'
48
+ });
49
+
50
+ await expect(
51
+ pushGlobal(['mycmd'], { cwd: testDir, globalDir })
52
+ ).rejects.toThrow('already exists');
53
+ });
54
+
55
+ it('pullLocal throws if local command already exists', async () => {
56
+ // Create global command
57
+ const globalCmd = path.join(globalDir, 'mycmd');
58
+ await fs.ensureDir(globalCmd);
59
+ await fs.writeJson(path.join(globalCmd, 'manifest.json'), {
60
+ name: 'mycmd', type: 'scaffold', run: './script.sh'
61
+ });
62
+
63
+ // Create conflicting local command
64
+ const localCmd = path.join(testDir, '.agentctl', 'mycmd');
65
+ await fs.ensureDir(localCmd);
66
+ await fs.writeJson(path.join(localCmd, 'manifest.json'), {
67
+ name: 'mycmd', type: 'scaffold', run: './script.sh'
68
+ });
69
+
70
+ await expect(
71
+ pullLocal(['mycmd'], { cwd: testDir, globalDir })
72
+ ).rejects.toThrow('already exists');
73
+ });
74
+ });
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { createTestDir, cleanupTestDir } from './helpers';
5
+ import { pushGlobal, pullLocal, scaffold } from '../src/ctl';
6
+
7
+ describe('ctl scoping (global, local)', () => {
8
+ let localRoot: string;
9
+ let globalRoot: string;
10
+ let baseDir: string;
11
+
12
+ beforeEach(async () => {
13
+ baseDir = await createTestDir();
14
+ localRoot = path.join(baseDir, 'local');
15
+ globalRoot = path.join(baseDir, 'global');
16
+ await fs.ensureDir(localRoot);
17
+ await fs.ensureDir(globalRoot);
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await cleanupTestDir(baseDir);
22
+ });
23
+
24
+ it('pushGlobal: copies local to global with --copy', async () => {
25
+ await scaffold(['deploy'], { cwd: localRoot });
26
+ const localPath = path.join(localRoot, '.agentctl', 'deploy');
27
+ const globalPath = path.join(globalRoot, 'deploy');
28
+
29
+ expect(await fs.pathExists(localPath)).toBe(true);
30
+ expect(await fs.pathExists(globalPath)).toBe(false);
31
+
32
+ await pushGlobal(['deploy'], { cwd: localRoot, globalDir: globalRoot, copy: true });
33
+
34
+ expect(await fs.pathExists(localPath)).toBe(true);
35
+ expect(await fs.pathExists(globalPath)).toBe(true);
36
+ const gManifest = await fs.readJson(path.join(globalPath, 'manifest.json'));
37
+ expect(gManifest.name).toBe('deploy');
38
+ });
39
+
40
+ it('pushGlobal: moves local to global with --move', async () => {
41
+ await scaffold(['deploy'], { cwd: localRoot });
42
+ const localPath = path.join(localRoot, '.agentctl', 'deploy');
43
+ const globalPath = path.join(globalRoot, 'deploy');
44
+
45
+ await pushGlobal(['deploy'], { cwd: localRoot, globalDir: globalRoot, move: true });
46
+
47
+ expect(await fs.pathExists(localPath)).toBe(false);
48
+ expect(await fs.pathExists(globalPath)).toBe(true);
49
+ });
50
+
51
+ it('pullLocal: moves global to local', async () => {
52
+ // Setup global manually
53
+ const globalPath = path.join(globalRoot, 'deploy');
54
+ await fs.ensureDir(globalPath);
55
+ await fs.writeJson(path.join(globalPath, 'manifest.json'), { name: 'deploy', type: 'scaffold' });
56
+
57
+ // Ensure local .agentctl exists
58
+ await fs.ensureDir(path.join(localRoot, '.agentctl'));
59
+ const localPath = path.join(localRoot, '.agentctl', 'deploy');
60
+
61
+ await pullLocal(['deploy'], { cwd: localRoot, globalDir: globalRoot, move: true });
62
+
63
+ expect(await fs.pathExists(globalPath)).toBe(false);
64
+ expect(await fs.pathExists(localPath)).toBe(true);
65
+ });
66
+ });