cadet-agent 0.15.0 → 0.15.2
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/README.md +28 -9
- package/package.json +4 -1
- package/src/install.mjs +472 -337
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Cadet-Agent is **not a one-shot code generator**. It won't spit out a finished g
|
|
|
7
7
|
## Repository Layout
|
|
8
8
|
- `.cadet/agent/core/` contains the shared Cadet-Agent framework documents.
|
|
9
9
|
- `.cadet/agent/docs/` contains setup guides for each supported IDE.
|
|
10
|
-
- `.github/agents/` contains the Copilot custom agent
|
|
10
|
+
- `.github/agents/` contains the Copilot custom agent definitions (Cadet Agent + Cadet Agent Reviewer).
|
|
11
11
|
- `.cursor/` contains Cursor-specific authored files.
|
|
12
12
|
- `.continue/` contains Continue-specific authored files.
|
|
13
13
|
- `.claude/` contains Claude Code-specific authored files.
|
|
@@ -27,6 +27,20 @@ This downloads the latest framework release and extracts it into your current di
|
|
|
27
27
|
npx cadet-agent@latest init --target ./my-unity-project
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
### Keeping the Framework Updated
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx cadet-agent@latest sync
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
When a new release is available, `sync` downloads the updated framework and replaces managed files (`.cadet/agent/core/`, IDE integration shims, agent definitions). Your local policies (`.cadet/agent/policies/`) and project plans (`.cadet/agent/project-plans/`) are automatically preserved. After syncing, start a fresh chat for the changes to take effect.
|
|
37
|
+
|
|
38
|
+
To sync a specific directory:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx cadet-agent@latest sync --target ./my-unity-project
|
|
42
|
+
```
|
|
43
|
+
|
|
30
44
|
## Manual Install (fallback)
|
|
31
45
|
|
|
32
46
|
If you prefer to install from a packaged release artifact, download `cadet-agent.zip` from [GitHub Releases](https://github.com/naishtech/cadet-agent/releases) and extract it into your Unity project root:
|
|
@@ -36,25 +50,29 @@ Expand-Archive .\cadet-agent.zip -DestinationPath . -Force
|
|
|
36
50
|
```
|
|
37
51
|
|
|
38
52
|
## Getting Started
|
|
39
|
-
- For
|
|
40
|
-
-
|
|
41
|
-
- For GitHub Copilot setup, see [.cadet/agent/docs/github-copilot.md](.cadet/agent/docs/github-copilot.md).
|
|
42
|
-
- For Cursor setup, see [.cadet/agent/docs/cursor.md](.cadet/agent/docs/cursor.md).
|
|
43
|
-
- For Continue setup, see [.cadet/agent/docs/continue.md](.cadet/agent/docs/continue.md).
|
|
44
|
-
- For Claude Code setup, see [.cadet/agent/docs/claude-code.md](.cadet/agent/docs/claude-code.md).
|
|
53
|
+
- For framework navigation after install, see `.cadet/agent/core/README.md`.
|
|
54
|
+
- IDE setup guides and full documentation are at the [canonical repository](https://github.com/naishtech/cadet-agent) (GitHub Pages).
|
|
45
55
|
|
|
46
56
|
## Examples
|
|
47
57
|
|
|
48
58
|
### GitHub Copilot
|
|
49
59
|
Run `npx cadet-agent@latest init` in your Unity project root, then open the repo in VS Code.
|
|
50
60
|
|
|
51
|
-
**Agent mode:** Select the **Cadet** agent from the agent picker in Copilot Chat. The agent definition at `.github/agents/cadet.agent.md` provides focused instructions and tool configuration.
|
|
61
|
+
**Agent mode:** Select the **Cadet Agent** agent from the agent picker in Copilot Chat. The agent definition at `.github/agents/cadet.agent.md` provides focused instructions and tool configuration.
|
|
52
62
|
|
|
53
63
|
```text
|
|
54
64
|
[Describe your game dev task...]
|
|
55
65
|
```
|
|
56
66
|
|
|
57
|
-
Cadet will use the shared framework in `.cadet/agent/core` to route the conversation through learner calibration, bootstrap checks, and planning.
|
|
67
|
+
Cadet Agent will use the shared framework in `.cadet/agent/core` to route the conversation through learner calibration, bootstrap checks, and planning.
|
|
68
|
+
|
|
69
|
+
**Review mode:** After the Cadet Agent completes a task, select the **Cadet Agent Reviewer** from the agent picker. Provide the task, story, or PR to review:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
Review the PR at https://github.com/... or Review story-1 in epic-1-player-movement
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The reviewer will read `.cadet/agent/core/cadet-agent.md` as the rulebook, audit `.cadet/state.json` for gate compliance, and check the code and artifacts against every non-negotiable rule. It produces a structured report with a gate audit, process deviations, and recommendations — it does not edit code.
|
|
58
76
|
|
|
59
77
|
### Cursor feature request
|
|
60
78
|
After opening the repository in Cursor, the always-apply rule in `.cursor/rules/cadet-agent.md` should load automatically. A typical request looks like this:
|
|
@@ -84,6 +102,7 @@ If a specific game repository needs local conventions, add a policy file under `
|
|
|
84
102
|
Running `./package-agent.ps1` produces `cadet-agent.zip` with this layout:
|
|
85
103
|
- `.cadet/agent/core/`
|
|
86
104
|
- `.github/agents/cadet.agent.md`
|
|
105
|
+
- `.github/agents/cadet-agent-reviewer.agent.md`
|
|
87
106
|
- `.cursor/rules/cadet-agent.md`
|
|
88
107
|
- `.continue/rules/cadet-agent.md`
|
|
89
108
|
- `.claude/skills/cadet-agent.md`
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cadet-agent",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cadet-agent": "bin/cli.mjs"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.mjs"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"bin/",
|
|
11
14
|
"src/"
|
package/src/install.mjs
CHANGED
|
@@ -1,337 +1,472 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync, existsSync,
|
|
2
|
-
import { join, dirname } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
console.log(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
console.log('
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
1
|
+
import { createWriteStream, mkdirSync, readFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, relative } from 'node:path';
|
|
3
|
+
import { inflateRawSync } from 'node:zlib';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
|
|
6
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const DEFAULT_API = 'https://api.github.com/repos/naishtech/cadet-agent/releases/latest';
|
|
9
|
+
const USER_AGENT = 'cadet-agent-cli';
|
|
10
|
+
|
|
11
|
+
function resolveApiUrl(override) {
|
|
12
|
+
return override || process.env.CADET_AGENT_RELEASE_URL || DEFAULT_API;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeVersion(v) {
|
|
16
|
+
return (v || '').replace(/^v/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildHeaders(extra = {}) {
|
|
20
|
+
const headers = {
|
|
21
|
+
'User-Agent': USER_AGENT,
|
|
22
|
+
...extra,
|
|
23
|
+
};
|
|
24
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
25
|
+
if (token) {
|
|
26
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
27
|
+
}
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── ZIP parser (zero-dependency, handles store + deflate) ───────────────────
|
|
32
|
+
|
|
33
|
+
const SIG_EOCD = 0x06054b50;
|
|
34
|
+
const SIG_CD = 0x02014b50;
|
|
35
|
+
const SIG_LFH = 0x04034b50;
|
|
36
|
+
|
|
37
|
+
function read32(buf, off) { return buf.readUInt32LE(off); }
|
|
38
|
+
function read16(buf, off) { return buf.readUInt16LE(off); }
|
|
39
|
+
|
|
40
|
+
function findEocd(buf) {
|
|
41
|
+
// Search backwards from end for EOCD signature (comment is max 65535 bytes)
|
|
42
|
+
const maxStart = Math.max(0, buf.length - 65535 - 22);
|
|
43
|
+
for (let i = buf.length - 22; i >= maxStart; i--) {
|
|
44
|
+
if (read32(buf, i) === SIG_EOCD) return i;
|
|
45
|
+
}
|
|
46
|
+
throw new Error('Not a valid ZIP file: EOCD signature not found');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function* centralDirectoryEntries(buf, cdOffset, cdSize) {
|
|
50
|
+
let off = cdOffset;
|
|
51
|
+
const end = cdOffset + cdSize;
|
|
52
|
+
while (off < end) {
|
|
53
|
+
if (read32(buf, off) !== SIG_CD) break;
|
|
54
|
+
const method = read16(buf, off + 10);
|
|
55
|
+
const compressedSize = read32(buf, off + 20);
|
|
56
|
+
const uncompressedSize = read32(buf, off + 24);
|
|
57
|
+
const filenameLen = read16(buf, off + 28);
|
|
58
|
+
const extraLen = read16(buf, off + 30);
|
|
59
|
+
const commentLen = read16(buf, off + 32);
|
|
60
|
+
const localHeaderOff = read32(buf, off + 42);
|
|
61
|
+
const filename = buf.toString('utf-8', off + 46, off + 46 + filenameLen);
|
|
62
|
+
|
|
63
|
+
// Skip directory entries (trailing / in filename, or uncompressedSize == 0 with no method)
|
|
64
|
+
if (!filename.endsWith('/')) {
|
|
65
|
+
yield { filename, method, compressedSize, uncompressedSize, localHeaderOff };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
off += 46 + filenameLen + extraLen + commentLen;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractFile(buf, entry, targetDir) {
|
|
73
|
+
const { filename, method, compressedSize, localHeaderOff } = entry;
|
|
74
|
+
|
|
75
|
+
// Read local file header to get filename + extra lengths (they may differ from CD)
|
|
76
|
+
const lfhFilenameLen = read16(buf, localHeaderOff + 26);
|
|
77
|
+
const lfhExtraLen = read16(buf, localHeaderOff + 28);
|
|
78
|
+
|
|
79
|
+
const dataStart = localHeaderOff + 30 + lfhFilenameLen + lfhExtraLen;
|
|
80
|
+
const compressed = buf.subarray(dataStart, dataStart + compressedSize);
|
|
81
|
+
|
|
82
|
+
let data;
|
|
83
|
+
if (method === 0) {
|
|
84
|
+
// Stored — no compression
|
|
85
|
+
data = compressed;
|
|
86
|
+
} else if (method === 8) {
|
|
87
|
+
// Deflate
|
|
88
|
+
data = inflateRawSync(compressed);
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Unsupported compression method ${method} for ${filename}.\n` +
|
|
92
|
+
`This zip uses a compression format this CLI doesn't support.\n` +
|
|
93
|
+
`Try downloading cadet-agent.zip manually from:\n` +
|
|
94
|
+
` https://github.com/naishtech/cadet-agent/releases/latest`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const outPath = join(targetDir, filename);
|
|
99
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const rs = Readable.from(data);
|
|
103
|
+
const ws = createWriteStream(outPath);
|
|
104
|
+
rs.pipe(ws);
|
|
105
|
+
ws.on('finish', () => resolve(outPath));
|
|
106
|
+
ws.on('error', (err) => reject(new Error(`Write failed for ${filename}: ${err.message}`)));
|
|
107
|
+
rs.on('error', (err) => reject(new Error(`Read failed for ${filename}: ${err.message}`)));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function extractZip(buf, targetDir) {
|
|
112
|
+
const eocdOff = findEocd(buf);
|
|
113
|
+
const cdSize = read32(buf, eocdOff + 12);
|
|
114
|
+
const cdOff = read32(buf, eocdOff + 16);
|
|
115
|
+
|
|
116
|
+
// ZIP64 detection — 0xFFFFFFFF in 32-bit fields means real values are in ZIP64 extra records
|
|
117
|
+
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
120
|
+
'Try downloading cadet-agent.zip manually from:\n' +
|
|
121
|
+
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const paths = [];
|
|
126
|
+
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
127
|
+
const outPath = await extractFile(buf, entry, targetDir);
|
|
128
|
+
paths.push(outPath);
|
|
129
|
+
}
|
|
130
|
+
return paths;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── GitHub release download ─────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function fetchLatestRelease(apiUrl) {
|
|
136
|
+
const url = resolveApiUrl(apiUrl);
|
|
137
|
+
console.log('🔍 Fetching latest Cadet-Agent release...');
|
|
138
|
+
|
|
139
|
+
const res = await fetch(url, {
|
|
140
|
+
headers: buildHeaders({ 'Accept': 'application/vnd.github+json' }),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
if (res.status === 403 || res.status === 429) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`GitHub API rate-limited (${res.status}). ` +
|
|
147
|
+
'Set GITHUB_TOKEN or GH_TOKEN env var for authenticated requests, or try again later.'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`GitHub API returned ${res.status}: ${res.statusText}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return res.json();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function findZipAsset(release) {
|
|
157
|
+
const asset = release.assets?.find(a => a.name === 'cadet-agent.zip');
|
|
158
|
+
if (!asset) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Release ${release.tag_name} does not contain cadet-agent.zip.\n` +
|
|
161
|
+
`Available assets: ${(release.assets || []).map(a => a.name).join(', ') || 'none'}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return asset;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function downloadZip(url) {
|
|
168
|
+
console.log('⬇️ Downloading cadet-agent.zip...');
|
|
169
|
+
|
|
170
|
+
const res = await fetch(url, {
|
|
171
|
+
headers: buildHeaders({ 'Accept': 'application/octet-stream' }),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const contentLength = res.headers.get('content-length');
|
|
179
|
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
180
|
+
|
|
181
|
+
// Stream to buffer with progress
|
|
182
|
+
const chunks = [];
|
|
183
|
+
let downloaded = 0;
|
|
184
|
+
const reader = res.body.getReader();
|
|
185
|
+
|
|
186
|
+
while (true) {
|
|
187
|
+
const { done, value } = await reader.read();
|
|
188
|
+
if (done) break;
|
|
189
|
+
chunks.push(value);
|
|
190
|
+
downloaded += value.length;
|
|
191
|
+
if (total > 0) {
|
|
192
|
+
const pct = Math.round((downloaded / total) * 100);
|
|
193
|
+
process.stdout.write(`\r ${pct}% (${(downloaded / 1024).toFixed(0)} KB / ${(total / 1024).toFixed(0)} KB)`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (total > 0) process.stdout.write('\n');
|
|
197
|
+
|
|
198
|
+
return Buffer.concat(chunks);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Public install entry ────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export async function install(targetDir, opts = {}) {
|
|
204
|
+
console.log(`📦 Cadet-Agent — installing to ${targetDir}\n`);
|
|
205
|
+
|
|
206
|
+
// 1. Fetch release metadata
|
|
207
|
+
const release = await fetchLatestRelease(opts.sourceUrl);
|
|
208
|
+
const releaseVersion = normalizeVersion(release.tag_name);
|
|
209
|
+
console.log(` Latest: v${releaseVersion} (published ${release.published_at})\n`);
|
|
210
|
+
|
|
211
|
+
// 2. Find zip asset
|
|
212
|
+
const asset = findZipAsset(release);
|
|
213
|
+
|
|
214
|
+
// 3. Download
|
|
215
|
+
const zipBuf = await downloadZip(asset.browser_download_url);
|
|
216
|
+
console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
|
|
217
|
+
|
|
218
|
+
// 4. Extract
|
|
219
|
+
console.log('📂 Extracting...');
|
|
220
|
+
const extracted = await extractZip(zipBuf, targetDir);
|
|
221
|
+
|
|
222
|
+
// 5. Report
|
|
223
|
+
console.log(`\n✅ Cadet-Agent v${releaseVersion} installed! Extracted ${extracted.length} files.\n`);
|
|
224
|
+
|
|
225
|
+
// Print per-IDE next steps
|
|
226
|
+
console.log('── Next steps ──');
|
|
227
|
+
console.log(' GitHub Copilot:');
|
|
228
|
+
console.log(' Select "Cadet Agent" from the agent picker in Copilot Chat');
|
|
229
|
+
console.log(' Cursor:');
|
|
230
|
+
console.log(' Already active — .cursor\\rules\\cadet-agent.md loads automatically');
|
|
231
|
+
console.log(' Continue:');
|
|
232
|
+
console.log(' Already active — .continue\\rules\\cadet-agent.md loads automatically');
|
|
233
|
+
console.log(' Claude Code:');
|
|
234
|
+
console.log(' Already active — .claude\\skills\\cadet-agent.md loads as a project skill');
|
|
235
|
+
console.log('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Manifest-aware extraction ───────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function matchesPreservedPath(filename, preservedPaths) {
|
|
241
|
+
// Normalize: strip leading dot (zip paths like ".cadet/agent/policies/...")
|
|
242
|
+
const normalized = filename.replace(/^\.?\/?/, '');
|
|
243
|
+
for (const preserved of preservedPaths) {
|
|
244
|
+
const p = preserved.replace(/^\.?\/?/, '');
|
|
245
|
+
if (normalized === p || normalized.startsWith(p + '/') || normalized.startsWith(p + '\\')) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function matchesManagedPath(filename, managedPaths) {
|
|
253
|
+
const normalized = filename.replace(/^\.?\/?/, '').replace(/\\/g, '/');
|
|
254
|
+
for (const m of managedPaths) {
|
|
255
|
+
const mn = m.replace(/^\.?\/?/, '').replace(/\\/g, '/');
|
|
256
|
+
if (normalized === mn || normalized.startsWith(mn + '/')) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function walkDir(dir, fn) {
|
|
264
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
const fullPath = join(dir, entry.name);
|
|
267
|
+
if (entry.isDirectory()) {
|
|
268
|
+
walkDir(fullPath, fn);
|
|
269
|
+
} else {
|
|
270
|
+
fn(fullPath, relative(dir, fullPath));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function deleteObsoleteManagedFiles(targetDir, managedPaths, zipFilenames) {
|
|
276
|
+
const normalizedZip = new Set(
|
|
277
|
+
zipFilenames.map(f => f.replace(/^\.\//, '').replace(/\\/g, '/'))
|
|
278
|
+
);
|
|
279
|
+
const deleted = [];
|
|
280
|
+
|
|
281
|
+
for (const managed of managedPaths) {
|
|
282
|
+
const mn = managed.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
283
|
+
const managedPath = join(targetDir, managed.replace(/^\.\//, ''));
|
|
284
|
+
const stat = (() => { try { return statSync(managedPath); } catch { return null; } })();
|
|
285
|
+
if (!stat) continue;
|
|
286
|
+
|
|
287
|
+
if (stat.isFile()) {
|
|
288
|
+
// Single-file managed path — check if it's in the zip
|
|
289
|
+
if (!normalizedZip.has(mn)) continue;
|
|
290
|
+
} else if (stat.isDirectory()) {
|
|
291
|
+
walkDir(managedPath, (filePath, rel) => {
|
|
292
|
+
const relNorm = rel.replace(/\\/g, '/');
|
|
293
|
+
const fullRel = mn + '/' + relNorm;
|
|
294
|
+
if (!normalizedZip.has(fullRel)) {
|
|
295
|
+
try {
|
|
296
|
+
unlinkSync(filePath);
|
|
297
|
+
deleted.push(filePath);
|
|
298
|
+
} catch {
|
|
299
|
+
// File may already be gone or locked — skip
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return deleted;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function extractZipWithManifest(buf, targetDir, { preserved, managed }) {
|
|
309
|
+
const eocdOff = findEocd(buf);
|
|
310
|
+
const cdSize = read32(buf, eocdOff + 12);
|
|
311
|
+
const cdOff = read32(buf, eocdOff + 16);
|
|
312
|
+
|
|
313
|
+
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
316
|
+
'Try downloading cadet-agent.zip manually from:\n' +
|
|
317
|
+
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const updated = [];
|
|
322
|
+
const preserved_list = [];
|
|
323
|
+
const added = [];
|
|
324
|
+
const zipFilenames = [];
|
|
325
|
+
|
|
326
|
+
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
327
|
+
zipFilenames.push(entry.filename);
|
|
328
|
+
if (matchesPreservedPath(entry.filename, preserved)) {
|
|
329
|
+
preserved_list.push(entry.filename);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const outPath = await extractFile(buf, entry, targetDir);
|
|
333
|
+
if (matchesManagedPath(entry.filename, managed)) {
|
|
334
|
+
updated.push(outPath);
|
|
335
|
+
} else {
|
|
336
|
+
added.push(outPath);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Delete obsolete managed files no longer in the zip (renamed/removed managed paths)
|
|
341
|
+
const deleted = deleteObsoleteManagedFiles(targetDir, managed, zipFilenames);
|
|
342
|
+
|
|
343
|
+
return { updated, preserved: preserved_list, added, deleted, zipFilenames };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── Removed-managed-path cleanup ─────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
export function findManagedPathsInZip(buf) {
|
|
349
|
+
try {
|
|
350
|
+
const eocdOff = findEocd(buf);
|
|
351
|
+
const cdSize = read32(buf, eocdOff + 12);
|
|
352
|
+
const cdOff = read32(buf, eocdOff + 16);
|
|
353
|
+
|
|
354
|
+
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
355
|
+
const fn = entry.filename.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
356
|
+
if (fn === '.cadet/agent/core/FrameworkManifest.json') {
|
|
357
|
+
// Extract just this one entry to read managedPaths
|
|
358
|
+
const lfhFilenameLen = read16(buf, entry.localHeaderOff + 26);
|
|
359
|
+
const lfhExtraLen = read16(buf, entry.localHeaderOff + 28);
|
|
360
|
+
const dataStart = entry.localHeaderOff + 30 + lfhFilenameLen + lfhExtraLen;
|
|
361
|
+
const compressed = buf.subarray(dataStart, dataStart + entry.compressedSize);
|
|
362
|
+
let data;
|
|
363
|
+
if (entry.method === 0) data = compressed;
|
|
364
|
+
else if (entry.method === 8) data = inflateRawSync(compressed);
|
|
365
|
+
else break;
|
|
366
|
+
const manifest = JSON.parse(data.toString('utf-8'));
|
|
367
|
+
return manifest.managedPaths || [];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// Not a valid zip or manifest not found
|
|
372
|
+
}
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function deleteRemovedManagedPaths(targetDir, oldManaged, newManaged) {
|
|
377
|
+
const newSet = new Set(newManaged.map(p => p.replace(/^\.\//, '').replace(/\\/g, '/')));
|
|
378
|
+
const deleted = [];
|
|
379
|
+
|
|
380
|
+
for (const old of oldManaged) {
|
|
381
|
+
const oldNorm = old.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
382
|
+
if (newSet.has(oldNorm)) continue;
|
|
383
|
+
|
|
384
|
+
// This path was in the old manifest but is absent from the new one — remove it
|
|
385
|
+
const fullPath = join(targetDir, old.replace(/^\.\//, ''));
|
|
386
|
+
const st = (() => { try { return statSync(fullPath); } catch { return null; } })();
|
|
387
|
+
if (!st) continue;
|
|
388
|
+
|
|
389
|
+
if (st.isFile()) {
|
|
390
|
+
try { unlinkSync(fullPath); deleted.push(fullPath); } catch {}
|
|
391
|
+
} else if (st.isDirectory()) {
|
|
392
|
+
walkDir(fullPath, (filePath) => {
|
|
393
|
+
try { unlinkSync(filePath); deleted.push(filePath); } catch {}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return deleted;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Public sync entry ───────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
export async function sync(targetDir, opts = {}) {
|
|
403
|
+
console.log(`🔄 Cadet-Agent — syncing ${targetDir}\n`);
|
|
404
|
+
|
|
405
|
+
// 1. Read existing manifest
|
|
406
|
+
const manifestPath = join(targetDir, '.cadet', 'agent', 'core', 'FrameworkManifest.json');
|
|
407
|
+
let existingManifest = null;
|
|
408
|
+
let oldVersion = 'none';
|
|
409
|
+
try {
|
|
410
|
+
existingManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
411
|
+
oldVersion = existingManifest.frameworkVersion || 'unknown';
|
|
412
|
+
console.log(` Existing install: v${normalizeVersion(oldVersion)}`);
|
|
413
|
+
} catch {
|
|
414
|
+
console.log(' No existing install found — performing full install.\n');
|
|
415
|
+
return install(targetDir, opts);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 2. Fetch release metadata
|
|
419
|
+
const release = await fetchLatestRelease(opts.sourceUrl);
|
|
420
|
+
const newVersion = normalizeVersion(release.tag_name);
|
|
421
|
+
const oldVersionNorm = normalizeVersion(oldVersion);
|
|
422
|
+
console.log(` Latest: v${newVersion} (published ${release.published_at})\n`);
|
|
423
|
+
|
|
424
|
+
if (oldVersionNorm === newVersion) {
|
|
425
|
+
console.log(`✅ Already up to date (v${oldVersionNorm}). Nothing to sync.\n`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 3. Download
|
|
430
|
+
const asset = findZipAsset(release);
|
|
431
|
+
const zipBuf = await downloadZip(asset.browser_download_url);
|
|
432
|
+
console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
|
|
433
|
+
|
|
434
|
+
// 4. Extract with manifest awareness
|
|
435
|
+
console.log('📂 Extracting (preserving local policies and plans)...');
|
|
436
|
+
const result = await extractZipWithManifest(zipBuf, targetDir, {
|
|
437
|
+
preserved: existingManifest.preservedPaths || [],
|
|
438
|
+
managed: existingManifest.managedPaths || [],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// 4b. Find new managed paths from the zip and delete any old paths that were removed
|
|
442
|
+
const newManagedPaths = findManagedPathsInZip(zipBuf);
|
|
443
|
+
const removedDeleted = deleteRemovedManagedPaths(
|
|
444
|
+
targetDir,
|
|
445
|
+
existingManifest.managedPaths || [],
|
|
446
|
+
newManagedPaths
|
|
447
|
+
);
|
|
448
|
+
if (removedDeleted.length > 0) {
|
|
449
|
+
result.deleted.push(...removedDeleted);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 5. Report
|
|
453
|
+
console.log('');
|
|
454
|
+
console.log(`✅ Cadet-Agent synced: v${oldVersionNorm} → v${newVersion}`);
|
|
455
|
+
console.log(` Updated: ${result.updated.length} files`);
|
|
456
|
+
if (result.preserved.length > 0) {
|
|
457
|
+
console.log(` Preserved: ${result.preserved.length} files (local policies/plans)`);
|
|
458
|
+
}
|
|
459
|
+
if (result.added.length > 0) {
|
|
460
|
+
console.log(` New: ${result.added.length} files`);
|
|
461
|
+
}
|
|
462
|
+
if (result.deleted.length > 0) {
|
|
463
|
+
console.log(` Removed: ${result.deleted.length} files (no longer managed)`);
|
|
464
|
+
}
|
|
465
|
+
console.log('');
|
|
466
|
+
|
|
467
|
+
// Print per-IDE next steps
|
|
468
|
+
console.log('── Next steps ──');
|
|
469
|
+
console.log(' Framework files updated. Start a fresh chat for changes to take effect:');
|
|
470
|
+
console.log(' Select "Cadet Agent" from the agent picker in Copilot Chat');
|
|
471
|
+
console.log('');
|
|
472
|
+
}
|