@swarp/cli 0.0.3-rc.25 → 0.0.3-rc.27
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 +1 -1
- package/src/deploy/wireguard.mjs +54 -0
- package/src/deploy/wireguard.test.mjs +91 -0
- package/src/init/index.mjs +49 -0
- package/src/init/index.test.mjs +49 -0
package/package.json
CHANGED
|
@@ -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
|
+
});
|
package/src/init/index.mjs
CHANGED
|
@@ -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),
|
package/src/init/index.test.mjs
CHANGED
|
@@ -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
|
});
|