@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,699 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { rm, mv, inspect, pullLocal, list, pushGlobal, installSkill, scaffold, group } from '../src/ctl';
6
+ import { resolveCommand } from '../src/resolve';
7
+ import { readManifest } from '../src/manifest';
8
+ import { getGlobalRoot, getAntigravityGlobalRoot } from '../src/fs-utils';
9
+ import * as fsUtils from '../src/fs-utils';
10
+ import { SUPPORTED_AGENTS, copySkill } from '../src/skills';
11
+
12
+ describe('edge cases for 100% coverage', () => {
13
+ const tmpDir = path.join(os.tmpdir(), 'agentctl-coverage-tests');
14
+ const localDir = path.join(tmpDir, 'local');
15
+ const globalDir = path.join(tmpDir, 'global');
16
+
17
+ beforeEach(async () => {
18
+ await fs.ensureDir(localDir);
19
+ await fs.ensureDir(globalDir);
20
+ await fs.ensureDir(path.join(localDir, '.agentctl'));
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await fs.remove(tmpDir);
25
+ });
26
+
27
+ it('covers getAntigravityGlobalRoot', () => {
28
+ const root = getAntigravityGlobalRoot();
29
+ expect(root).toContain('.gemini');
30
+ expect(root).toContain('antigravity');
31
+ });
32
+
33
+ it('covers non-win32 getGlobalRoot', () => {
34
+ const originalPlatform = process.platform;
35
+ Object.defineProperty(process, 'platform', { value: 'linux' });
36
+ const root = getGlobalRoot();
37
+ expect(root).toContain('.config');
38
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
39
+ });
40
+
41
+ it('covers win32 getGlobalRoot without APPDATA', () => {
42
+ if (process.platform !== 'win32') return;
43
+ const originalAppData = process.env.APPDATA;
44
+ delete process.env.APPDATA;
45
+ const root = getGlobalRoot();
46
+ expect(root).toContain('AppData');
47
+ process.env.APPDATA = originalAppData;
48
+ });
49
+
50
+ it('covers readManifest failure (corrupt JSON)', async () => {
51
+ const p = path.join(tmpDir, 'corrupt.json');
52
+ await fs.writeFile(p, '{ invalid json }');
53
+ const m = await readManifest(p);
54
+ expect(m).toBeNull();
55
+ });
56
+
57
+ it('covers rm failure (command not found)', async () => {
58
+ await expect(rm(['notfound'], { cwd: localDir, globalDir })).rejects.toThrow('Command notfound not found');
59
+ });
60
+
61
+ it('covers mv failure (command not found)', async () => {
62
+ await expect(mv(['notfound'], ['dest'], { cwd: localDir, globalDir })).rejects.toThrow('Command notfound not found');
63
+ });
64
+
65
+ it('covers inspect failure (command not found)', async () => {
66
+ const result = await inspect(['notfound'], { cwd: localDir, globalDir });
67
+ expect(result).toBeNull();
68
+ });
69
+
70
+ it('covers pullLocal copy (not move)', async () => {
71
+ const cmdDir = path.join(globalDir, 'cmd');
72
+ await fs.ensureDir(cmdDir);
73
+ await fs.writeJson(path.join(cmdDir, 'manifest.json'), { name: 'cmd', type: 'scaffold' });
74
+
75
+ await pullLocal(['cmd'], { cwd: localDir, globalDir, copy: true });
76
+
77
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'cmd'))).toBe(true);
78
+ expect(await fs.pathExists(path.join(globalDir, 'cmd'))).toBe(true);
79
+ });
80
+
81
+ it('covers implicit global group and group-winning-over-capped shadowing', async () => {
82
+ // Global has a capped command "work"
83
+ const globalWork = path.join(globalDir, 'work');
84
+ await fs.ensureDir(globalWork);
85
+ await fs.writeJson(path.join(globalWork, 'manifest.json'), { name: 'work', run: 'echo global' });
86
+
87
+ // Local has a group "work" (implicit)
88
+ const localWork = path.join(localDir, '.agentctl', 'work');
89
+ await fs.ensureDir(localWork);
90
+
91
+ // Global has an implicit group "other"
92
+ const globalOther = path.join(globalDir, 'other');
93
+ await fs.ensureDir(globalOther);
94
+
95
+ const resWork = await resolveCommand(['work'], { cwd: localDir, globalDir });
96
+ expect(resWork?.scope).toBe('local');
97
+ expect(resWork?.manifest.type).toBe('group');
98
+
99
+ const resOther = await resolveCommand(['other'], { cwd: localDir, globalDir });
100
+ expect(resOther?.scope).toBe('global');
101
+ expect(resOther?.manifest.type).toBe('group');
102
+ });
103
+
104
+ it('covers list with manifest type logic and corrupt manifest', async () => {
105
+ const localWork = path.join(localDir, '.agentctl', 'work');
106
+ await fs.ensureDir(localWork);
107
+ // Type provided in manifest, but not capped
108
+ await fs.writeJson(path.join(localWork, 'manifest.json'), { name: 'work', type: 'group' });
109
+
110
+ const corruptCmd = path.join(localDir, '.agentctl', 'corrupt');
111
+ await fs.ensureDir(corruptCmd);
112
+ await fs.writeFile(path.join(corruptCmd, 'manifest.json'), '{ bad json }');
113
+
114
+ const cmds = await list({ cwd: localDir, globalDir });
115
+ const work = cmds.find(c => c.path === 'work');
116
+ expect(work?.type).toBe('group');
117
+
118
+ const corrupt = cmds.find(c => c.path === 'corrupt');
119
+ expect(corrupt?.type).toBe('group'); // Falls back to group on corrupt json
120
+ });
121
+
122
+ it('covers isCapped failure in prepareCommand', async () => {
123
+ const cappedDir = path.join(localDir, '.agentctl', 'capped');
124
+ await fs.ensureDir(cappedDir);
125
+ await fs.writeFile(path.join(cappedDir, 'manifest.json'), '{ bad json }');
126
+
127
+ // scaffold will call prepareCommand, which calls isCapped on parent dirs
128
+ await scaffold(['capped', 'sub'], { cwd: localDir });
129
+
130
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'capped', 'sub'))).toBe(true);
131
+ });
132
+
133
+ it('covers list stat failure', async () => {
134
+ const originalStat = (await import('fs-extra')).default.stat;
135
+ const spy = vi.spyOn(fs, 'stat').mockImplementation(async (p: fs.PathLike) => {
136
+ if (p.toString().includes('failme')) {
137
+ throw new Error('stat error');
138
+ }
139
+ return originalStat(p);
140
+ });
141
+
142
+ const failDir = path.join(globalDir, 'failme');
143
+ await fs.ensureDir(failDir);
144
+
145
+ const cmds = await list({ cwd: localDir, globalDir });
146
+ expect(cmds.find(c => c.path === 'failme')).toBeUndefined();
147
+
148
+ spy.mockRestore();
149
+ });
150
+
151
+ it('covers default options in pushGlobal and pullLocal', async () => {
152
+ // Need to be in a local root for these to not throw
153
+ const originalCwd = process.cwd();
154
+ process.chdir(localDir);
155
+ try {
156
+ await expect(pushGlobal(['notfound'])).rejects.toThrow('Local command notfound not found');
157
+ await expect(pullLocal(['notfound'])).rejects.toThrow('Global command notfound not found');
158
+ } finally {
159
+ process.chdir(originalCwd);
160
+ }
161
+ });
162
+
163
+ it('covers installSkill errors and agents', async () => {
164
+ // We already have a test for supported agents but let's hit the ctl branch
165
+ await expect(installSkill('unknown', { cwd: localDir })).rejects.toThrow('Agent \'unknown\' not supported');
166
+
167
+ await installSkill('antigravity', { cwd: localDir });
168
+ expect(await fs.pathExists(path.join(localDir, '.agent', 'skills', 'agentctl', 'SKILL.md'))).toBe(true);
169
+
170
+ const fakeGlobal = path.join(tmpDir, 'fakeGlobal');
171
+ await installSkill('antigravity', { global: true, antigravityGlobalDir: fakeGlobal });
172
+ expect(await fs.pathExists(path.join(fakeGlobal, 'skills', 'agentctl', 'SKILL.md'))).toBe(true);
173
+
174
+ await installSkill('cursor', { cwd: localDir });
175
+ expect(await fs.pathExists(path.join(localDir, '.cursor', 'skills', 'agentctl.md'))).toBe(true);
176
+ });
177
+
178
+ it('covers mv in global scope', async () => {
179
+ const globalSrc = path.join(globalDir, 'src');
180
+ await fs.ensureDir(globalSrc);
181
+ await fs.writeJson(path.join(globalSrc, 'manifest.json'), { name: 'src', type: 'scaffold' });
182
+
183
+ await mv(['src'], ['dest'], { globalDir });
184
+ expect(await fs.pathExists(path.join(globalDir, 'dest'))).toBe(true);
185
+ expect(await fs.pathExists(path.join(globalDir, 'src'))).toBe(false);
186
+ });
187
+
188
+ it('covers prepareCommand empty args', async () => {
189
+ await expect(scaffold([], { cwd: localDir })).rejects.toThrow('No command path provided');
190
+ });
191
+
192
+ it('covers list with existing local group shadowing global group', async () => {
193
+ const localGroup = path.join(localDir, '.agentctl', 'group');
194
+ const globalGroup = path.join(globalDir, 'group');
195
+ await fs.ensureDir(localGroup);
196
+ await fs.ensureDir(globalGroup);
197
+
198
+ // This will hit the branch where it already has the path and scope is local
199
+ const cmds = await list({ cwd: localDir, globalDir });
200
+ expect(cmds.find(c => c.path === 'group')?.scope).toBe('local');
201
+ });
202
+
203
+ it('covers non-win32 scaffold', async () => {
204
+ const originalPlatform = process.platform;
205
+ Object.defineProperty(process, 'platform', { value: 'linux' });
206
+ await scaffold(['linuxcmd'], { cwd: localDir });
207
+ const scriptPath = path.join(localDir, '.agentctl', 'linuxcmd', 'command.sh');
208
+ expect(await fs.pathExists(scriptPath)).toBe(true);
209
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
210
+ });
211
+
212
+ it('covers installSkill unimplemented agent logic', async () => {
213
+ // We modify the array to trick the check for coverage of the "else" branch
214
+ SUPPORTED_AGENTS.push('test-agent');
215
+ try {
216
+ await expect(installSkill('test-agent', { cwd: localDir })).rejects.toThrow("Agent logic for 'test-agent' not implemented");
217
+ } finally {
218
+ SUPPORTED_AGENTS.pop();
219
+ }
220
+ });
221
+
222
+ it('covers copySkill skill file not found paths', async () => {
223
+ const originalExists = fs.existsSync;
224
+ let callCount = 0;
225
+ const spy = vi.spyOn(fs, 'existsSync').mockImplementation((p: fs.PathLike) => {
226
+ const pStr = p.toString();
227
+ // Count calls for SKILL.md checks
228
+ if (pStr.includes('SKILL.md')) {
229
+ callCount++;
230
+ }
231
+ return originalExists(p);
232
+ });
233
+
234
+ const targetDir = path.join(localDir, 'skills');
235
+
236
+ vi.spyOn(fs, 'copy').mockResolvedValue(undefined);
237
+
238
+ // Case 0: Succeed on 1st attempt (immediate)
239
+ callCount = 0;
240
+ spy.mockImplementation((p: fs.PathLike) => {
241
+ const pStr = p.toString();
242
+ if (pStr.endsWith('SKILL.md')) {
243
+ return true;
244
+ }
245
+ return originalExists(p);
246
+ });
247
+ await copySkill(targetDir, 'antigravity');
248
+
249
+ // Case 1: Succeed on 2nd attempt
250
+ callCount = 0;
251
+ spy.mockImplementation((p: fs.PathLike) => {
252
+ const pStr = p.toString();
253
+ if (pStr.endsWith('SKILL.md')) {
254
+ callCount++;
255
+ if (callCount < 2) return false;
256
+ return true;
257
+ }
258
+ return originalExists(p);
259
+ });
260
+ await copySkill(targetDir, 'antigravity');
261
+
262
+ // Case 2: Succeed on 3rd attempt
263
+ callCount = 0;
264
+ spy.mockImplementation((p: fs.PathLike) => {
265
+ const pStr = p.toString();
266
+ if (pStr.endsWith('SKILL.md')) {
267
+ callCount++;
268
+ if (callCount < 3) return false;
269
+ return true;
270
+ }
271
+ return originalExists(p);
272
+ });
273
+
274
+ await copySkill(targetDir, 'antigravity');
275
+
276
+ // Case 3: Fail all attempts
277
+ callCount = 0;
278
+ spy.mockImplementation((p: fs.PathLike) => {
279
+ const pStr = p.toString();
280
+ if (pStr.endsWith('SKILL.md')) {
281
+ return false;
282
+ }
283
+ return originalExists(p);
284
+ });
285
+
286
+ await expect(copySkill(targetDir, 'antigravity')).rejects.toThrow('Could not locate source SKILL.md');
287
+
288
+ spy.mockRestore();
289
+ vi.restoreAllMocks();
290
+ });
291
+
292
+ it('covers resolveCommand global check and file-as-directory cases', async () => {
293
+ // Test global: true in resolveCommand
294
+ const res = await resolveCommand(['nothing'], { global: true, globalDir });
295
+ expect(res).toBeNull();
296
+
297
+ // Test file at path preventing implicit group (local)
298
+ const localFile = path.join(localDir, '.agentctl', 'file');
299
+ await fs.ensureDir(path.dirname(localFile));
300
+ await fs.writeFile(localFile, 'content');
301
+
302
+ const resLocalFile = await resolveCommand(['file'], { cwd: localDir, globalDir });
303
+ expect(resLocalFile).toBeNull(); // Should not treat as group
304
+
305
+ // Test file at path preventing implicit group (global)
306
+ const globalFile = path.join(globalDir, 'file');
307
+ await fs.writeFile(globalFile, 'content');
308
+
309
+ const resGlobalFile = await resolveCommand(['file'], { cwd: localDir, globalDir });
310
+ expect(resGlobalFile).toBeNull();
311
+ });
312
+
313
+ it('covers list with file in directory (not directory)', async () => {
314
+ // Create a file inside .agentctl that list() iterates over but ignores because it's not a dir
315
+ const localCtl = path.join(localDir, '.agentctl');
316
+ await fs.ensureDir(localCtl);
317
+ const localFile = path.join(localCtl, 'notadir');
318
+ await fs.writeFile(localFile, 'content');
319
+
320
+ const cmds = await list({ cwd: localDir, globalDir });
321
+ expect(cmds.find(c => c.path === 'notadir')).toBeUndefined();
322
+ });
323
+
324
+ it('covers isCapped exception', async () => {
325
+ const cappedDir = path.join(localDir, '.agentctl', 'badjson');
326
+ await fs.ensureDir(cappedDir);
327
+ await fs.writeFile(path.join(cappedDir, 'manifest.json'), '{ bad }');
328
+
329
+ // We try to scaffold inside it.
330
+ // The parent check for isCapped should fail to parse JSON and return false (safe).
331
+ await scaffold(['badjson', 'sub'], { cwd: localDir });
332
+ expect(await fs.pathExists(path.join(cappedDir, 'sub'))).toBe(true);
333
+ });
334
+
335
+ it('covers list shadowing branches', async () => {
336
+ // Case: Global (Group) checked when local doesn't exist? (Implicit logic) -> Covered by standard flows.
337
+ // We want to verify `existing.scope === 'local'` check (line 289 ctl.ts)
338
+ // If we have a Global command, then Local command that is NOT group? (conflict?)
339
+ // Or Global command, then finding another Global command with same name (impossible in fs unless case sensitivity issues, but iteration logic handles files once).
340
+
341
+ // Let's cover explicit code path where we find a Global item and it shadows nothing (normal).
342
+ // Let's Try: Local Group exists. Global Scan finds same name.
343
+ // Walk Global:
344
+ // finds 'common'. Adds to map? No, map has 'common' from local scan (scope='local').
345
+ // entering else block (line 288).
346
+ // `existing.scope === 'local'` is true.
347
+ // `existing.type === 'group'` is true.
348
+ // `type` (of global item) is 'group'.
349
+ // -> recursively walk global.
350
+
351
+ // We need:
352
+ // 1. Local Group 'common'
353
+ // 2. Global Group 'common'
354
+ // This should trigger the merge walk.
355
+
356
+ const localCommon = path.join(localDir, '.agentctl', 'common');
357
+ await fs.ensureDir(localCommon);
358
+
359
+ const globalCommon = path.join(globalDir, 'common');
360
+ await fs.ensureDir(globalCommon);
361
+ await fs.ensureDir(path.join(globalCommon, 'sub')); // Subcommand in global common
362
+
363
+ const cmds = await list({ cwd: localDir, globalDir });
364
+ expect(cmds.find(c => c.path === 'common sub')).toBeDefined();
365
+
366
+ // Local Group shadows Global Capped
367
+ const localGroup = path.join(localDir, '.agentctl', 'shadow1');
368
+ await fs.ensureDir(localGroup);
369
+
370
+ const globalCapped = path.join(globalDir, 'shadow1');
371
+ await fs.ensureDir(globalCapped);
372
+ await fs.writeJson(path.join(globalCapped, 'manifest.json'), { name: 'shadow1', run: 'echo' });
373
+
374
+ const cmds2 = await list({ cwd: localDir, globalDir });
375
+ const shadow1 = cmds2.find(c => c.path === 'shadow1');
376
+ expect(shadow1?.scope).toBe('local');
377
+ expect(shadow1?.type).toBe('group');
378
+
379
+ // Local Capped shadows Global Group
380
+ const localCapped = path.join(localDir, '.agentctl', 'shadow2');
381
+ await fs.ensureDir(localCapped);
382
+ await fs.writeJson(path.join(localCapped, 'manifest.json'), { name: 'shadow2', run: 'echo' });
383
+
384
+ const globalGroup2 = path.join(globalDir, 'shadow2');
385
+ await fs.ensureDir(globalGroup2);
386
+
387
+ const cmds3 = await list({ cwd: localDir, globalDir });
388
+ const shadow2 = cmds3.find(c => c.path === 'shadow2');
389
+ expect(shadow2?.scope).toBe('local');
390
+ expect(shadow2?.type).not.toBe('group'); // capped/scaffold
391
+ });
392
+
393
+ it('covers mv implicit group (no manifest)', async () => {
394
+ // Create implicit group
395
+ const groupDir = path.join(localDir, '.agentctl', 'implicit');
396
+ await fs.ensureDir(groupDir);
397
+
398
+ // Move it
399
+ await mv(['implicit'], ['moved_implicit'], { cwd: localDir, globalDir });
400
+
401
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'moved_implicit'))).toBe(true);
402
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'implicit'))).toBe(false);
403
+ // And manifest check should simply pass (if exists logic)
404
+ });
405
+
406
+ it('covers list outside local context', async () => {
407
+ const nonProjectDir = path.join(tmpDir, 'nonproject');
408
+ await fs.ensureDir(nonProjectDir);
409
+
410
+ const cmds = await list({ cwd: nonProjectDir, globalDir });
411
+ // Should only return global commands
412
+ // Ensure there is at least one global command for verification or empty is fine
413
+ // verification:
414
+ expect(Array.isArray(cmds)).toBe(true);
415
+ });
416
+
417
+ it('covers deep nesting (isCapped checks)', async () => {
418
+ // Create a group
419
+ await group(['L1'], { cwd: localDir });
420
+ // Create L2
421
+ await group(['L1', 'L2'], { cwd: localDir });
422
+ // Create L3
423
+ await scaffold(['L1', 'L2', 'L3'], { cwd: localDir });
424
+
425
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'L1', 'L2', 'L3'))).toBe(true);
426
+ });
427
+
428
+ it('covers capped manifest without type (defaults to scaffold)', async () => {
429
+ const cappedDir = path.join(localDir, '.agentctl', 'notype');
430
+ await fs.ensureDir(cappedDir);
431
+ // Explicitly undefined type but has run
432
+ await fs.writeJson(path.join(cappedDir, 'manifest.json'), { name: 'notype', run: 'echo' });
433
+
434
+ const cmds = await list({ cwd: localDir, globalDir });
435
+ const cmd = cmds.find(c => c.path === 'notype');
436
+ expect(cmd?.type).toBe('scaffold');
437
+ });
438
+
439
+ it('covers mv rootDir failure logic', async () => {
440
+ // We mock findLocalRoot to succeed for resolveCommand (first call)
441
+ // and return null for mv (second call).
442
+
443
+ // This requires 'resolveCommand' to find something local.
444
+ // We can create a local command.
445
+ const cmdDir = path.join(localDir, '.agentctl', 'failmv');
446
+ await fs.ensureDir(cmdDir);
447
+ await fs.writeJson(path.join(cmdDir, 'manifest.json'), { name: 'failmv', type: 'scaffold' });
448
+
449
+ // Use spyOn with the wildcard import
450
+ const spy = vi.spyOn(fsUtils, 'findLocalRoot');
451
+ let calls = 0;
452
+ spy.mockImplementation(() => {
453
+ calls++;
454
+ if (calls === 1) return localDir; // For resolveCommand
455
+ return null; // For mv
456
+ });
457
+
458
+ await expect(mv(['failmv'], ['dest'], { cwd: localDir })).rejects.toThrow('Cannot determine root for move');
459
+
460
+ spy.mockRestore();
461
+ });
462
+
463
+ it('covers rm global failure message', async () => {
464
+ // Ensure it doesn't exist
465
+ await expect(rm(['failrm'], { global: true, globalDir })).rejects.toThrow('in global scope');
466
+ });
467
+
468
+ it('covers group manifest without type (implicit default)', async () => {
469
+ const groupDir = path.join(localDir, '.agentctl', 'implicittype');
470
+ await fs.ensureDir(groupDir);
471
+ await fs.writeJson(path.join(groupDir, 'manifest.json'), { name: 'implicittype', description: 'desc' });
472
+
473
+ const cmds = await list({ cwd: localDir, globalDir });
474
+ const cmd = cmds.find(c => c.path === 'implicittype');
475
+ expect(cmd?.type).toBe('group');
476
+ });
477
+
478
+ it('covers installSkill gemini default path', async () => {
479
+ const origHome = process.env.HOME;
480
+ const origUserProfile = process.env.USERPROFILE;
481
+ const mockHome = path.join(tmpDir, 'mock_home');
482
+
483
+ process.env.HOME = mockHome;
484
+ process.env.USERPROFILE = mockHome;
485
+
486
+ try {
487
+ await installSkill('gemini', { global: true });
488
+
489
+ const skillPath = path.join(mockHome, '.gemini', 'skills', 'agentctl', 'SKILL.md');
490
+ expect(await fs.pathExists(skillPath)).toBe(true);
491
+ } finally {
492
+ process.env.HOME = origHome;
493
+ process.env.USERPROFILE = origUserProfile;
494
+ }
495
+ });
496
+
497
+ it('covers list/prepareCommand without cwd option', async () => {
498
+ const origCwd = process.cwd();
499
+ try {
500
+ process.chdir(localDir);
501
+
502
+ const cmds = await list();
503
+ expect(Array.isArray(cmds)).toBe(true);
504
+
505
+ await scaffold(['newcmd']);
506
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'newcmd'))).toBe(true);
507
+
508
+ // Also test mv without cwd
509
+ await mv(['newcmd'], ['newcmd_moved']);
510
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'newcmd_moved'))).toBe(true);
511
+
512
+ } finally {
513
+ process.chdir(origCwd);
514
+ }
515
+ });
516
+
517
+ it('covers pushGlobal/pullLocal outside local context', async () => {
518
+ const outsideDir = path.join(tmpDir, 'outside');
519
+ await fs.ensureDir(outsideDir);
520
+
521
+ await expect(pushGlobal(['any'], { cwd: outsideDir, globalDir })).rejects.toThrow('Not in a local context');
522
+ await expect(pullLocal(['any'], { cwd: outsideDir, globalDir })).rejects.toThrow('Not in a local context');
523
+ });
524
+
525
+ it('covers mv global default root (no globalDir)', async () => {
526
+ const mockHome = path.join(tmpDir, 'mock_home_mv');
527
+ const origHome = process.env.HOME;
528
+ const origAppData = process.env.APPDATA;
529
+
530
+ process.env.HOME = mockHome;
531
+ process.env.APPDATA = mockHome;
532
+
533
+ try {
534
+ let globalRoot;
535
+ if (process.platform === 'win32') {
536
+ globalRoot = path.join(mockHome, 'agentctl');
537
+ } else {
538
+ globalRoot = path.join(mockHome, '.config', 'agentctl');
539
+ }
540
+
541
+ await fs.ensureDir(globalRoot);
542
+ const cmdDir = path.join(globalRoot, 'mv_def');
543
+ await fs.ensureDir(cmdDir);
544
+ await fs.writeJson(path.join(cmdDir, 'manifest.json'), { name: 'mv_def', type: 'scaffold' });
545
+
546
+ await mv(['mv_def'], ['mv_def_moved'], { cwd: localDir });
547
+ expect(await fs.pathExists(path.join(globalRoot, 'mv_def_moved'))).toBe(true);
548
+ } finally {
549
+ process.env.HOME = origHome;
550
+ process.env.APPDATA = origAppData;
551
+ }
552
+ });
553
+
554
+ it('covers installSkill antigravity default path', async () => {
555
+ const mockHome = path.join(tmpDir, 'mock_home_ag');
556
+ await fs.ensureDir(mockHome);
557
+ const origAppData = process.env.APPDATA;
558
+ const origHome = process.env.HOME;
559
+ const origUserProfile = process.env.USERPROFILE;
560
+
561
+ process.env.APPDATA = mockHome;
562
+ process.env.HOME = mockHome;
563
+ process.env.USERPROFILE = mockHome;
564
+
565
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
566
+ try {
567
+ await installSkill('antigravity', { global: true });
568
+
569
+ // Check logs to see where it installed
570
+ const calls = logSpy.mock.calls.map(c => c[0]);
571
+ const installMsg = calls.find(msg => msg.includes('Installed skill for antigravity'));
572
+ if (!installMsg) {
573
+ throw new Error('Install message not found');
574
+ }
575
+
576
+ // Expected path should be in mockHome
577
+ // On Windows: mockHome/.gemini/antigravity or mockHome/antigravity...
578
+ // getAntigravityGlobalRoot uses os.homedir(). we mocked USERPROFILE/HOME, so it should be mockHome.
579
+ // .gemini/antigravity is the standard path in fs-utils.
580
+
581
+ expect(installMsg).toContain(mockHome);
582
+ expect(installMsg).toContain('antigravity');
583
+ expect(installMsg).toContain('skills');
584
+
585
+ // Also check file existence based on path extracted?
586
+ // "Installed skill for antigravity at <path>"
587
+ const installedPath = installMsg.split(' at ')[1].trim();
588
+ expect(await fs.pathExists(installedPath)).toBe(true);
589
+
590
+ } finally {
591
+ logSpy.mockRestore();
592
+ process.env.APPDATA = origAppData;
593
+ process.env.HOME = origHome;
594
+ process.env.USERPROFILE = origUserProfile;
595
+ }
596
+ });
597
+
598
+ it('covers installSkill gemini global options and env vars', async () => {
599
+ const mockHome = path.join(tmpDir, 'mock_gemini');
600
+ await fs.ensureDir(mockHome);
601
+
602
+ const origHome = process.env.HOME;
603
+ const origUserProfile = process.env.USERPROFILE;
604
+
605
+ process.env.HOME = '';
606
+ process.env.USERPROFILE = '';
607
+
608
+ try {
609
+ // Case 1: explicit globalDir
610
+ const explicitDir = path.join(tmpDir, 'explicit_gemini');
611
+ await installSkill('gemini', { global: true, geminiGlobalDir: explicitDir });
612
+ expect(await fs.pathExists(path.join(explicitDir, 'skills', 'agentctl', 'SKILL.md'))).toBe(true);
613
+
614
+ // Case 2: default via HOME
615
+ process.env.HOME = mockHome;
616
+ await installSkill('gemini', { global: true });
617
+ expect(await fs.pathExists(path.join(mockHome, '.gemini', 'skills', 'agentctl', 'SKILL.md'))).toBe(true);
618
+
619
+ // Case 3: default via USERPROFILE (HOME empty)
620
+ process.env.HOME = '';
621
+ process.env.USERPROFILE = mockHome;
622
+ // Clean up previous run
623
+ await fs.remove(path.join(mockHome, '.gemini'));
624
+ await installSkill('gemini', { global: true });
625
+ expect(await fs.pathExists(path.join(mockHome, '.gemini', 'skills', 'agentctl', 'SKILL.md'))).toBe(true);
626
+
627
+ } finally {
628
+ process.env.HOME = origHome;
629
+ process.env.USERPROFILE = origUserProfile;
630
+ }
631
+ });
632
+
633
+ it('covers mv with explicit options (cwd/globalDir)', async () => {
634
+ // Local: explicit cwd
635
+ const subDir = path.join(localDir, 'subdir');
636
+ await fs.ensureDir(subDir);
637
+ // We need .agentctl relative to that cwd? No, findLocalRoot walks up.
638
+ // If we pass cwd=subDir, findLocalRoot finds localDir. Correct.
639
+
640
+ // Create source command
641
+ const srcCmd = path.join(localDir, '.agentctl', 'mv_src');
642
+ await fs.ensureDir(srcCmd);
643
+ await fs.writeJson(path.join(srcCmd, 'manifest.json'), { name: 'mv_src', type: 'scaffold' });
644
+
645
+ await mv(['mv_src'], ['mv_dest_cwd'], { cwd: subDir });
646
+ expect(await fs.pathExists(path.join(localDir, '.agentctl', 'mv_dest_cwd'))).toBe(true);
647
+
648
+ // Global: explicit globalDir
649
+ const customGlobal = path.join(tmpDir, 'custom_global');
650
+ await fs.ensureDir(customGlobal);
651
+ const globalSrc = path.join(customGlobal, 'g_src');
652
+ await fs.ensureDir(globalSrc);
653
+ await fs.writeJson(path.join(globalSrc, 'manifest.json'), { name: 'g_src', type: 'scaffold' });
654
+
655
+ await mv(['g_src'], ['g_dest'], { global: true, globalDir: customGlobal });
656
+ expect(await fs.pathExists(path.join(customGlobal, 'g_dest'))).toBe(true);
657
+ });
658
+
659
+ it('covers list with non-existent global dir', async () => {
660
+ const missingGlobal = path.join(tmpDir, 'missing_global');
661
+ // Ensure it doesn't exist
662
+ await fs.remove(missingGlobal);
663
+
664
+ const cmds = await list({ cwd: localDir, globalDir: missingGlobal });
665
+ // Should rely on walking local only.
666
+ // We verify no error thrown and correct behavior.
667
+ expect(Array.isArray(cmds)).toBe(true);
668
+ });
669
+
670
+ it('covers list collision (Global vs Global)', async () => {
671
+ // Setup: global/a (dir) -> global/a/b (subcommand) => logical path "a b"
672
+ // Setup: global/a b (dir) => logical path "a b"
673
+
674
+ const gRoot = path.join(tmpDir, 'g_collision');
675
+ await fs.ensureDir(gRoot);
676
+
677
+ const dirA = path.join(gRoot, 'a');
678
+ await fs.ensureDir(dirA);
679
+ const dirAB = path.join(dirA, 'b'); // "a b"
680
+ await fs.ensureDir(dirAB);
681
+ await fs.writeJson(path.join(dirAB, 'manifest.json'), { name: 'b', description: 'nested' });
682
+
683
+ const dirSpace = path.join(gRoot, 'a b'); // "a b"
684
+ await fs.ensureDir(dirSpace);
685
+ await fs.writeJson(path.join(dirSpace, 'manifest.json'), { name: 'a b', description: 'top' });
686
+
687
+ // Depending on iteration order, one will register first.
688
+ // The second one will hit `commands.has() -> true`.
689
+ // Inspecting `ctl.ts`: `commands.set` only happens if `!commands.has`.
690
+ // So the first one wins.
691
+ // We just need to ensure `scope !== 'local'` branch is hit.
692
+ // Since both are global, existing.scope is 'global'.
693
+
694
+ const cmds = await list({ cwd: localDir, globalDir: gRoot });
695
+ const collision = cmds.filter(c => c.path === 'a b');
696
+ expect(collision.length).toBe(1); // Should only have one entry
697
+ // And we verified logic didn't crash
698
+ });
699
+ });