agent-gear 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.
Files changed (65) hide show
  1. package/AGENTS.md +67 -0
  2. package/dist/acp/bridge.d.ts +56 -0
  3. package/dist/acp/bridge.d.ts.map +1 -0
  4. package/dist/acp/bridge.js +295 -0
  5. package/dist/acp/bridge.js.map +1 -0
  6. package/dist/acp/detector.d.ts +13 -0
  7. package/dist/acp/detector.d.ts.map +1 -0
  8. package/dist/acp/detector.js +29 -0
  9. package/dist/acp/detector.js.map +1 -0
  10. package/dist/acp/spawner.d.ts +24 -0
  11. package/dist/acp/spawner.d.ts.map +1 -0
  12. package/dist/acp/spawner.js +82 -0
  13. package/dist/acp/spawner.js.map +1 -0
  14. package/dist/acp/sync-bridge.d.ts +21 -0
  15. package/dist/acp/sync-bridge.d.ts.map +1 -0
  16. package/dist/acp/sync-bridge.js +75 -0
  17. package/dist/acp/sync-bridge.js.map +1 -0
  18. package/dist/config.d.ts +22 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +63 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/daemon.d.ts +5 -0
  23. package/dist/daemon.d.ts.map +1 -0
  24. package/dist/daemon.js +32 -0
  25. package/dist/daemon.js.map +1 -0
  26. package/dist/git-handler.d.ts +17 -0
  27. package/dist/git-handler.d.ts.map +1 -0
  28. package/dist/git-handler.js +60 -0
  29. package/dist/git-handler.js.map +1 -0
  30. package/dist/git-handler.test.d.ts +2 -0
  31. package/dist/git-handler.test.d.ts.map +1 -0
  32. package/dist/git-handler.test.js +127 -0
  33. package/dist/git-handler.test.js.map +1 -0
  34. package/dist/heartbeat.d.ts +6 -0
  35. package/dist/heartbeat.d.ts.map +1 -0
  36. package/dist/heartbeat.js +16 -0
  37. package/dist/heartbeat.js.map +1 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +52 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/socketio-client.d.ts +6 -0
  43. package/dist/socketio-client.d.ts.map +1 -0
  44. package/dist/socketio-client.js +385 -0
  45. package/dist/socketio-client.js.map +1 -0
  46. package/package.json +34 -0
  47. package/src/acp/bridge.ts +327 -0
  48. package/src/acp/detector.ts +40 -0
  49. package/src/acp/spawner.ts +94 -0
  50. package/src/acp/sync-bridge.ts +111 -0
  51. package/src/config.ts +75 -0
  52. package/src/daemon.ts +33 -0
  53. package/src/git-handler.test.ts +153 -0
  54. package/src/git-handler.ts +75 -0
  55. package/src/heartbeat.ts +18 -0
  56. package/src/index.ts +60 -0
  57. package/src/socketio-client.ts +497 -0
  58. package/tests/acp-bridge.test.ts +473 -0
  59. package/tests/acp-detector.test.ts +70 -0
  60. package/tests/acp-spawner.test.ts +149 -0
  61. package/tests/acp-sync-bridge.test.ts +263 -0
  62. package/tests/config.test.ts +166 -0
  63. package/tests/socketio-client.test.ts +196 -0
  64. package/tsconfig.json +21 -0
  65. package/vitest.config.ts +14 -0
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { exec } from 'child_process';
3
+
4
+ vi.mock('child_process', () => ({
5
+ exec: vi.fn(),
6
+ }));
7
+
8
+ import { GitHandler } from './git-handler.js';
9
+
10
+ function mockExecResolved(stdout: string) {
11
+ return vi.mocked(exec).mockImplementation(((_cmd: string, _opts: unknown, cb: (err: unknown, result: { stdout: string }) => void) => {
12
+ if (typeof cb === 'function') {
13
+ cb(null, { stdout });
14
+ }
15
+ }) as never);
16
+ }
17
+
18
+ function mockExecRejected(errorMsg: string) {
19
+ return vi.mocked(exec).mockImplementation(((_cmd: string, _opts: unknown, cb: (err: unknown, result: unknown) => void) => {
20
+ if (typeof cb === 'function') {
21
+ cb(new Error(errorMsg), null);
22
+ }
23
+ }) as never);
24
+ }
25
+
26
+ describe('GitHandler', () => {
27
+ let handler: GitHandler;
28
+
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ handler = new GitHandler();
32
+ });
33
+
34
+ describe('check', () => {
35
+ it('should detect a valid git repo with branches', async () => {
36
+ let callCount = 0;
37
+ vi.mocked(exec).mockImplementation(((_cmd: string, _opts: unknown, cb: (err: unknown, result: { stdout: string }) => void) => {
38
+ callCount++;
39
+ const outputs: Record<number, string> = {
40
+ 1: '.git\n',
41
+ 2: 'main\n',
42
+ 3: 'main\ndevelop\nfeature-x\n',
43
+ };
44
+ if (typeof cb === 'function') {
45
+ cb(null, { stdout: outputs[callCount] || '' });
46
+ }
47
+ }) as never);
48
+
49
+ const result = await handler.check('/fake/path');
50
+
51
+ expect(result.isGitRepo).toBe(true);
52
+ expect(result.currentBranch).toBe('main');
53
+ expect(result.branches).toEqual(['main', 'develop', 'feature-x']);
54
+ expect(result.defaultBranch).toBe('main');
55
+ });
56
+
57
+ it('should detect master as default if main does not exist', async () => {
58
+ let callCount = 0;
59
+ vi.mocked(exec).mockImplementation(((_cmd: string, _opts: unknown, cb: (err: unknown, result: { stdout: string }) => void) => {
60
+ callCount++;
61
+ const outputs: Record<number, string> = {
62
+ 1: '.git\n',
63
+ 2: 'master\n',
64
+ 3: 'develop\nmaster\n',
65
+ };
66
+ if (typeof cb === 'function') {
67
+ cb(null, { stdout: outputs[callCount] || '' });
68
+ }
69
+ }) as never);
70
+
71
+ const result = await handler.check('/fake/path');
72
+
73
+ expect(result.isGitRepo).toBe(true);
74
+ expect(result.defaultBranch).toBe('master');
75
+ });
76
+
77
+ it('should sort alphabetically if neither main nor master exists', async () => {
78
+ let callCount = 0;
79
+ vi.mocked(exec).mockImplementation(((_cmd: string, _opts: unknown, cb: (err: unknown, result: { stdout: string }) => void) => {
80
+ callCount++;
81
+ const outputs: Record<number, string> = {
82
+ 1: '.git\n',
83
+ 2: 'feature-x\n',
84
+ 3: 'develop\nfeature-x\n',
85
+ };
86
+ if (typeof cb === 'function') {
87
+ cb(null, { stdout: outputs[callCount] || '' });
88
+ }
89
+ }) as never);
90
+
91
+ const result = await handler.check('/fake/path');
92
+
93
+ expect(result.isGitRepo).toBe(true);
94
+ expect(result.defaultBranch).toBe('develop');
95
+ });
96
+
97
+ it('should return isGitRepo=false when not a git repo', async () => {
98
+ mockExecRejected('fatal: not a git repository');
99
+
100
+ const result = await handler.check('/fake/path');
101
+
102
+ expect(result.isGitRepo).toBe(false);
103
+ expect(result.currentBranch).toBeUndefined();
104
+ expect(result.branches).toBeUndefined();
105
+ });
106
+
107
+ it('should handle git-dir check returning empty string', async () => {
108
+ mockExecResolved('');
109
+
110
+ const result = await handler.check('/fake/path');
111
+
112
+ expect(result.isGitRepo).toBe(false);
113
+ });
114
+
115
+ it('should return error when branch listing fails on a valid repo', async () => {
116
+ vi.mocked(exec).mockImplementationOnce(((_cmd: string, _opts: unknown, cb: (err: unknown, result: { stdout: string }) => void) => {
117
+ if (typeof cb === 'function') cb(null, { stdout: '.git\n' });
118
+ }) as never);
119
+ vi.mocked(exec).mockImplementation(((_cmd: string, _opts: unknown, cb: (err: unknown, result: unknown) => void) => {
120
+ if (typeof cb === 'function') cb(new Error('git command failed'), null);
121
+ }) as never);
122
+
123
+ const result = await handler.check('/fake/path');
124
+
125
+ expect(result.isGitRepo).toBe(true);
126
+ expect(result.error).toBe('git command failed');
127
+ });
128
+ });
129
+
130
+ describe('init', () => {
131
+ it('should initialize git repo and rename branch to main', async () => {
132
+ vi.mocked(exec).mockImplementation(((_cmd: string, _opts: unknown, cb: (err: unknown, result: { stdout: string }) => void) => {
133
+ if (typeof cb === 'function') cb(null, { stdout: '' });
134
+ }) as never);
135
+
136
+ const result = await handler.init('/fake/path');
137
+
138
+ expect(result.success).toBe(true);
139
+ expect(exec).toHaveBeenCalledTimes(2);
140
+ expect(vi.mocked(exec).mock.calls[0][0]).toBe('git init');
141
+ expect(vi.mocked(exec).mock.calls[1][0]).toBe('git branch -m main');
142
+ });
143
+
144
+ it('should handle git init failure', async () => {
145
+ mockExecRejected('permission denied');
146
+
147
+ const result = await handler.init('/fake/path');
148
+
149
+ expect(result.success).toBe(false);
150
+ expect(result.error).toBe('permission denied');
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,75 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ const execAsync = promisify(exec);
5
+
6
+ export interface GitCheckResult {
7
+ isGitRepo: boolean;
8
+ currentBranch?: string;
9
+ branches?: string[];
10
+ defaultBranch?: string;
11
+ error?: string;
12
+ }
13
+
14
+ export interface GitInitResult {
15
+ success: boolean;
16
+ error?: string;
17
+ }
18
+
19
+ export class GitHandler {
20
+ async check(path: string): Promise<GitCheckResult> {
21
+ try {
22
+ const { stdout: gitDir } = await execAsync('git rev-parse --git-dir', { cwd: path });
23
+ if (!gitDir.trim()) {
24
+ return { isGitRepo: false };
25
+ }
26
+ } catch {
27
+ return { isGitRepo: false };
28
+ }
29
+
30
+ try {
31
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: path });
32
+ const { stdout: branchesOutput } = await execAsync('git branch --format="%(refname:short)"', { cwd: path });
33
+
34
+ const branches = branchesOutput.trim().split('\n').filter(Boolean);
35
+ const current = currentBranch.trim();
36
+
37
+ let defaultBranch: string;
38
+ if (branches.includes('main')) {
39
+ defaultBranch = 'main';
40
+ } else if (branches.includes('master')) {
41
+ defaultBranch = 'master';
42
+ } else {
43
+ const sorted = [...branches].sort();
44
+ defaultBranch = sorted[0] || current;
45
+ }
46
+
47
+ return {
48
+ isGitRepo: true,
49
+ currentBranch: current,
50
+ branches,
51
+ defaultBranch,
52
+ };
53
+ } catch (err) {
54
+ return {
55
+ isGitRepo: true,
56
+ error: err instanceof Error ? err.message : 'Unknown error',
57
+ };
58
+ }
59
+ }
60
+
61
+ async init(path: string): Promise<GitInitResult> {
62
+ try {
63
+ await execAsync('git init', { cwd: path });
64
+ await execAsync('git branch -m main', { cwd: path });
65
+ return { success: true };
66
+ } catch (err) {
67
+ return {
68
+ success: false,
69
+ error: err instanceof Error ? err.message : 'Unknown error',
70
+ };
71
+ }
72
+ }
73
+ }
74
+
75
+ export const gitHandler = new GitHandler();
@@ -0,0 +1,18 @@
1
+ const HEARTBEAT_INTERVAL_MS = 30_000;
2
+
3
+ export class Heartbeat {
4
+ private intervalId: ReturnType<typeof setInterval> | null = null;
5
+
6
+ start(sendFn: () => void): void {
7
+ this.intervalId = setInterval(() => {
8
+ sendFn();
9
+ }, HEARTBEAT_INTERVAL_MS);
10
+ }
11
+
12
+ stop(): void {
13
+ if (this.intervalId) {
14
+ clearInterval(this.intervalId);
15
+ this.intervalId = null;
16
+ }
17
+ }
18
+ }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { hostname } from 'os';
4
+ import { resolveKey, resolveEndpoint, saveKeyToConfig, saveEndpointToConfig } from './config.js';
5
+ import { connectToServer, unregister } from './socketio-client.js';
6
+ import { writePid, removePid, isRunning } from './daemon.js';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('agent-gear')
12
+ .description('Agent Controller - manages connection to controller-api')
13
+ .version('1.0.0')
14
+ .option('-k, --key <key>', 'Controller key')
15
+ .option('-s, --server <endpoint>', 'Server endpoint URL')
16
+ .option('-n, --name <name>', 'Controller name')
17
+ .action(async (options) => {
18
+ if (isRunning()) {
19
+ console.error('Controller is already running. Stop it first or remove the PID file.');
20
+ process.exit(1);
21
+ }
22
+
23
+ const key = resolveKey(options.key);
24
+ if (!key) {
25
+ console.error('No key provided. Use --key, ZAG_CONTROLLER_KEY env, or configure in ~/.zag/config.toml');
26
+ process.exit(1);
27
+ }
28
+
29
+ const endpoint = resolveEndpoint(options.server);
30
+ if (!endpoint) {
31
+ console.error('No server endpoint provided. Use --server, ZAG_SERVER_ENDPOINT env, or configure in ~/.zag/config.toml');
32
+ process.exit(1);
33
+ }
34
+
35
+ if (options.key) saveKeyToConfig(options.key);
36
+ if (options.server) saveEndpointToConfig(options.server);
37
+
38
+ writePid();
39
+
40
+ const socket = connectToServer(endpoint, key, {
41
+ serverName: options.name || hostname(),
42
+ });
43
+
44
+ const shutdown = () => {
45
+ console.log('Shutting down...');
46
+ unregister(socket);
47
+ socket.disconnect();
48
+ removePid();
49
+ process.exit(0);
50
+ };
51
+
52
+ process.on('SIGINT', shutdown);
53
+ process.on('SIGTERM', shutdown);
54
+
55
+ process.on('exit', () => {
56
+ removePid();
57
+ });
58
+ });
59
+
60
+ program.parse();