@swarp/cli 0.0.3-rc.25 → 0.0.3-rc.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarp/cli",
3
- "version": "0.0.3-rc.25",
3
+ "version": "0.0.3-rc.28",
4
4
  "description": "SWARP agent orchestration platform — CLI, MCP server, generator",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+
4
+ export function createWireGuardPeer(org, region, agentName) {
5
+ const confPath = `/tmp/${agentName}.conf`;
6
+ execFileSync("flyctl", ["wireguard", "create", org, region, agentName, confPath]);
7
+ return confPath;
8
+ }
9
+
10
+ export function extractDnsIP(confPath) {
11
+ const contents = readFileSync(confPath, "utf8");
12
+ for (const line of contents.split("\n")) {
13
+ const trimmed = line.trim();
14
+ if (trimmed.startsWith("DNS")) {
15
+ const [, value] = trimmed.split("=");
16
+ return value.trim();
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export function addKeepalive(confPath, interval = 25) {
23
+ const contents = readFileSync(confPath, "utf8");
24
+ if (contents.includes("PersistentKeepalive")) {
25
+ return confPath;
26
+ }
27
+ const updated = contents.replace(
28
+ /(\[Peer\])/,
29
+ `$1\nPersistentKeepalive = ${interval}`
30
+ );
31
+ writeFileSync(confPath, updated, "utf8");
32
+ return confPath;
33
+ }
34
+
35
+ export function pushWireGuardToSprite(agentName, confPath) {
36
+ const conf = readFileSync(confPath, "utf8");
37
+ execFileSync(
38
+ "sprite",
39
+ [
40
+ "exec",
41
+ "-s",
42
+ agentName,
43
+ "--",
44
+ "bash",
45
+ "-c",
46
+ "mkdir -p /etc/wireguard && cat > /etc/wireguard/wg0.conf && wg-quick up wg0",
47
+ ],
48
+ { input: conf }
49
+ );
50
+ }
51
+
52
+ export function removeWireGuardPeer(org, agentName) {
53
+ execFileSync("flyctl", ["wireguard", "remove", org, agentName]);
54
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { writeFileSync, readFileSync } from "node:fs";
5
+
6
+ vi.mock("node:child_process", () => ({ execFileSync: vi.fn() }));
7
+
8
+ import { execFileSync } from "node:child_process";
9
+ import {
10
+ createWireGuardPeer,
11
+ extractDnsIP,
12
+ addKeepalive,
13
+ pushWireGuardToSprite,
14
+ removeWireGuardPeer,
15
+ } from "./wireguard.mjs";
16
+
17
+ const SAMPLE_CONF = `[Interface]
18
+ PrivateKey = abc123
19
+ Address = fdaa:0:5e4f:a7b:23c:0:a:2/120
20
+ DNS = fdaa:0:5e4f::3
21
+
22
+ [Peer]
23
+ PublicKey = xyz789
24
+ AllowedIPs = fdaa:0:5e4f::/48
25
+ Endpoint = sea1.gateway.6pn.dev:51820
26
+ `;
27
+
28
+ function writeTempConf(contents = SAMPLE_CONF) {
29
+ const path = join(tmpdir(), `wg-test-${Date.now()}.conf`);
30
+ writeFileSync(path, contents, "utf8");
31
+ return path;
32
+ }
33
+
34
+ describe("createWireGuardPeer", () => {
35
+ beforeEach(() => vi.clearAllMocks());
36
+
37
+ it("calls flyctl with correct args and returns conf path", () => {
38
+ const result = createWireGuardPeer("my-org", "sea", "my-agent");
39
+ expect(execFileSync).toHaveBeenCalledWith("flyctl", [
40
+ "wireguard",
41
+ "create",
42
+ "my-org",
43
+ "sea",
44
+ "my-agent",
45
+ "/tmp/my-agent.conf",
46
+ ]);
47
+ expect(result).toBe("/tmp/my-agent.conf");
48
+ });
49
+ });
50
+
51
+ describe("extractDnsIP", () => {
52
+ it("parses the DNS IP from a WG config", () => {
53
+ const confPath = writeTempConf();
54
+ expect(extractDnsIP(confPath)).toBe("fdaa:0:5e4f::3");
55
+ });
56
+ });
57
+
58
+ describe("addKeepalive", () => {
59
+ it("inserts PersistentKeepalive after [Peer]", () => {
60
+ const confPath = writeTempConf();
61
+ addKeepalive(confPath);
62
+ const result = readFileSync(confPath, "utf8");
63
+ expect(result).toContain("PersistentKeepalive = 25");
64
+ expect(result.indexOf("[Peer]")).toBeLessThan(
65
+ result.indexOf("PersistentKeepalive")
66
+ );
67
+ });
68
+
69
+ it("is idempotent — does not add keepalive twice", () => {
70
+ const confPath = writeTempConf();
71
+ addKeepalive(confPath);
72
+ addKeepalive(confPath);
73
+ const result = readFileSync(confPath, "utf8");
74
+ const count = (result.match(/PersistentKeepalive/g) || []).length;
75
+ expect(count).toBe(1);
76
+ });
77
+ });
78
+
79
+ describe("removeWireGuardPeer", () => {
80
+ beforeEach(() => vi.clearAllMocks());
81
+
82
+ it("calls flyctl with correct args", () => {
83
+ removeWireGuardPeer("my-org", "my-agent");
84
+ expect(execFileSync).toHaveBeenCalledWith("flyctl", [
85
+ "wireguard",
86
+ "remove",
87
+ "my-org",
88
+ "my-agent",
89
+ ]);
90
+ });
91
+ });
@@ -136,6 +136,52 @@ function updateMcpJson(cwd) {
136
136
  console.log(` update ${mcpPath} — added swarp entry`);
137
137
  }
138
138
 
139
+ // ── GitHub environment setup ──────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Creates the `swarp-certs` GitHub environment with the authenticated user as
143
+ * a required reviewer. Best-effort — logs a skip message if `gh` is unavailable
144
+ * or the API call fails.
145
+ */
146
+ function createGithubCertsEnvironment() {
147
+ try {
148
+ const repo = execFileSync('gh', ['repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner'], {
149
+ encoding: 'utf8',
150
+ }).trim();
151
+
152
+ execFileSync(
153
+ 'gh',
154
+ ['api', `repos/${repo}/environments/swarp-certs`, '-X', 'PUT', '--input', '-'],
155
+ { input: JSON.stringify({ deployment_branch_policy: null }), encoding: 'utf8' },
156
+ );
157
+
158
+ const userId = execFileSync('gh', ['api', 'user', '-q', '.id'], { encoding: 'utf8' }).trim();
159
+
160
+ execFileSync(
161
+ 'gh',
162
+ [
163
+ 'api',
164
+ `repos/${repo}/environments/swarp-certs`,
165
+ '-X',
166
+ 'PUT',
167
+ '--input',
168
+ '-',
169
+ ],
170
+ {
171
+ input: JSON.stringify({
172
+ reviewers: [{ type: 'User', id: Number(userId) }],
173
+ deployment_branch_policy: null,
174
+ }),
175
+ encoding: 'utf8',
176
+ },
177
+ );
178
+
179
+ console.log(' create GitHub environment swarp-certs (required reviewer set)');
180
+ } catch {
181
+ console.log(' skip GitHub environment swarp-certs (gh unavailable or request failed)');
182
+ }
183
+ }
184
+
139
185
  // ── Main entry point ──────────────────────────────────────────────────────────
140
186
 
141
187
  /**
@@ -177,6 +223,9 @@ export async function runInit({ cwd = process.cwd(), wizardAnswers, input, outpu
177
223
  // 7. .mcp.json
178
224
  updateMcpJson(cwd);
179
225
 
226
+ // 7b. GitHub swarp-certs environment
227
+ createGithubCertsEnvironment();
228
+
180
229
  // 8. .claude/skills/swarp/SKILL.md — generated from agent configs
181
230
  const skillContent = generateSkill({
182
231
  agentsDir: path.resolve(cwd, agentsDir),
@@ -4,6 +4,24 @@ import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import { runInit } from './index.mjs';
6
6
 
7
+ // Track calls made to execFileSync by the module under test.
8
+ // We cannot vi.spyOn a CJS binding in ESM, so we use a shared call log
9
+ // populated via vi.mock instead.
10
+ const execFileSyncCalls = [];
11
+ let execFileSyncImpl = null;
12
+
13
+ vi.mock('node:child_process', async (importOriginal) => {
14
+ const original = await importOriginal();
15
+ return {
16
+ ...original,
17
+ execFileSync: (...args) => {
18
+ execFileSyncCalls.push(args);
19
+ if (execFileSyncImpl) return execFileSyncImpl(...args);
20
+ return original.execFileSync(...args);
21
+ },
22
+ };
23
+ });
24
+
7
25
  // ── Helpers ───────────────────────────────────────────────────────────────────
8
26
 
9
27
  function makeTmpDir() {
@@ -28,6 +46,8 @@ describe('runInit', () => {
28
46
 
29
47
  beforeEach(() => {
30
48
  tmpDir = makeTmpDir();
49
+ execFileSyncCalls.length = 0;
50
+ execFileSyncImpl = null;
31
51
  // Silence stdout/stderr during tests
32
52
  vi.spyOn(console, 'log').mockImplementation(() => {});
33
53
  vi.spyOn(console, 'warn').mockImplementation(() => {});
@@ -35,6 +55,7 @@ describe('runInit', () => {
35
55
 
36
56
  afterEach(() => {
37
57
  cleanDir(tmpDir);
58
+ execFileSyncImpl = null;
38
59
  vi.restoreAllMocks();
39
60
  });
40
61
 
@@ -165,4 +186,32 @@ describe('runInit', () => {
165
186
  const messages = logSpy.mock.calls.map((c) => c[0]).join('\n');
166
187
  expect(messages).toContain('/swarp');
167
188
  });
189
+
190
+ describe('GitHub swarp-certs environment', () => {
191
+ it('calls gh api to create the swarp-certs environment', async () => {
192
+ execFileSyncImpl = (cmd, args) => {
193
+ if (cmd === 'gh' && args[0] === 'repo') return 'myorg/myrepo\n';
194
+ if (cmd === 'gh' && args[0] === 'api' && args[1] === 'user') return '12345\n';
195
+ if (cmd === 'gh' && args[0] === 'api') return '{}';
196
+ // which checks — return empty string (tool found)
197
+ return '';
198
+ };
199
+
200
+ await runInit({ cwd: tmpDir, wizardAnswers: DEFAULT_ANSWERS });
201
+
202
+ const apiCalls = execFileSyncCalls.filter(
203
+ ([cmd, args]) => cmd === 'gh' && args[0] === 'api' && args[1]?.includes('swarp-certs'),
204
+ );
205
+ expect(apiCalls.length).toBeGreaterThanOrEqual(1);
206
+ });
207
+
208
+ it('skips environment creation gracefully when gh fails', async () => {
209
+ execFileSyncImpl = (cmd, args) => {
210
+ if (cmd === 'gh' && args[0] === 'repo') throw new Error('gh not found');
211
+ return '';
212
+ };
213
+
214
+ await expect(runInit({ cwd: tmpDir, wizardAnswers: DEFAULT_ANSWERS })).resolves.toBeUndefined();
215
+ });
216
+ });
168
217
  });