cadet-agent 0.6.0 → 0.7.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.
- package/package.json +1 -1
- package/src/cli.mjs +78 -78
- package/src/install.mjs +337 -337
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { install, sync } from './install.mjs';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
|
|
9
|
-
function getVersion() {
|
|
10
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
11
|
-
return pkg.version;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function showHelp() {
|
|
15
|
-
console.log(`
|
|
16
|
-
██████╗ █████╗ ██████╗ ███████╗████████╗
|
|
17
|
-
██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
|
|
18
|
-
██║ ███████║██║ ██║█████╗ ██║
|
|
19
|
-
██║ ██╔══██║██║ ██║██╔══╝ ██║
|
|
20
|
-
╚██████╗██║ ██║██████╔╝███████╗ ██║
|
|
21
|
-
╚═════╝╚═╝ ╚═╝╚═════╝ ╚══════╝ ╚═╝
|
|
22
|
-
|
|
23
|
-
Cross-IDE agent framework for Unity/C# game-development
|
|
24
|
-
|
|
25
|
-
Usage:
|
|
26
|
-
npx cadet-agent@latest init Install Cadet-Agent into the current directory
|
|
27
|
-
npx cadet-agent@latest init --target <dir> Install into a specific directory
|
|
28
|
-
npx cadet-agent@latest sync Update framework, preserving local policies/plans
|
|
29
|
-
npx cadet-agent@latest sync --target <dir> Sync a specific directory
|
|
30
|
-
|
|
31
|
-
Options:
|
|
32
|
-
--target, -t Target directory (default: current working directory)
|
|
33
|
-
--source Release API URL override (for forked deployments)
|
|
34
|
-
--help, -h Show this help
|
|
35
|
-
--version, -v Show version number
|
|
36
|
-
`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function run(argv) {
|
|
40
|
-
const command = argv[2];
|
|
41
|
-
|
|
42
|
-
// Parse --target <dir> or -t <dir>
|
|
43
|
-
let targetDir = process.cwd();
|
|
44
|
-
let sourceUrl = null;
|
|
45
|
-
const targetIdx = argv.indexOf('--target');
|
|
46
|
-
const tIdx = argv.indexOf('-t');
|
|
47
|
-
const sourceIdx = argv.indexOf('--source');
|
|
48
|
-
if (targetIdx !== -1 && argv[targetIdx + 1]) {
|
|
49
|
-
targetDir = argv[targetIdx + 1];
|
|
50
|
-
} else if (tIdx !== -1 && argv[tIdx + 1]) {
|
|
51
|
-
targetDir = argv[tIdx + 1];
|
|
52
|
-
}
|
|
53
|
-
if (sourceIdx !== -1 && argv[sourceIdx + 1]) {
|
|
54
|
-
sourceUrl = argv[sourceIdx + 1];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
switch (command) {
|
|
58
|
-
case 'init':
|
|
59
|
-
await install(targetDir, { sourceUrl });
|
|
60
|
-
break;
|
|
61
|
-
case 'sync':
|
|
62
|
-
await sync(targetDir, { sourceUrl });
|
|
63
|
-
break;
|
|
64
|
-
case '--version':
|
|
65
|
-
case '-v':
|
|
66
|
-
console.log(`cadet-agent v${getVersion()}`);
|
|
67
|
-
break;
|
|
68
|
-
case '--help':
|
|
69
|
-
case '-h':
|
|
70
|
-
case undefined:
|
|
71
|
-
showHelp();
|
|
72
|
-
break;
|
|
73
|
-
default:
|
|
74
|
-
console.error(`Unknown command: ${command}`);
|
|
75
|
-
console.error('Run cadet-agent --help for usage.');
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { install, sync } from './install.mjs';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
function getVersion() {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
11
|
+
return pkg.version;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function showHelp() {
|
|
15
|
+
console.log(`
|
|
16
|
+
██████╗ █████╗ ██████╗ ███████╗████████╗
|
|
17
|
+
██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
|
|
18
|
+
██║ ███████║██║ ██║█████╗ ██║
|
|
19
|
+
██║ ██╔══██║██║ ██║██╔══╝ ██║
|
|
20
|
+
╚██████╗██║ ██║██████╔╝███████╗ ██║
|
|
21
|
+
╚═════╝╚═╝ ╚═╝╚═════╝ ╚══════╝ ╚═╝
|
|
22
|
+
|
|
23
|
+
Cross-IDE agent framework for Unity/C# game-development
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
npx cadet-agent@latest init Install Cadet-Agent into the current directory
|
|
27
|
+
npx cadet-agent@latest init --target <dir> Install into a specific directory
|
|
28
|
+
npx cadet-agent@latest sync Update framework, preserving local policies/plans
|
|
29
|
+
npx cadet-agent@latest sync --target <dir> Sync a specific directory
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--target, -t Target directory (default: current working directory)
|
|
33
|
+
--source Release API URL override (for forked deployments)
|
|
34
|
+
--help, -h Show this help
|
|
35
|
+
--version, -v Show version number
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function run(argv) {
|
|
40
|
+
const command = argv[2];
|
|
41
|
+
|
|
42
|
+
// Parse --target <dir> or -t <dir>
|
|
43
|
+
let targetDir = process.cwd();
|
|
44
|
+
let sourceUrl = null;
|
|
45
|
+
const targetIdx = argv.indexOf('--target');
|
|
46
|
+
const tIdx = argv.indexOf('-t');
|
|
47
|
+
const sourceIdx = argv.indexOf('--source');
|
|
48
|
+
if (targetIdx !== -1 && argv[targetIdx + 1]) {
|
|
49
|
+
targetDir = argv[targetIdx + 1];
|
|
50
|
+
} else if (tIdx !== -1 && argv[tIdx + 1]) {
|
|
51
|
+
targetDir = argv[tIdx + 1];
|
|
52
|
+
}
|
|
53
|
+
if (sourceIdx !== -1 && argv[sourceIdx + 1]) {
|
|
54
|
+
sourceUrl = argv[sourceIdx + 1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (command) {
|
|
58
|
+
case 'init':
|
|
59
|
+
await install(targetDir, { sourceUrl });
|
|
60
|
+
break;
|
|
61
|
+
case 'sync':
|
|
62
|
+
await sync(targetDir, { sourceUrl });
|
|
63
|
+
break;
|
|
64
|
+
case '--version':
|
|
65
|
+
case '-v':
|
|
66
|
+
console.log(`cadet-agent v${getVersion()}`);
|
|
67
|
+
break;
|
|
68
|
+
case '--help':
|
|
69
|
+
case '-h':
|
|
70
|
+
case undefined:
|
|
71
|
+
showHelp();
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
console.error(`Unknown command: ${command}`);
|
|
75
|
+
console.error('Run cadet-agent --help for usage.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/install.mjs
CHANGED
|
@@ -1,337 +1,337 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, dirname } from 'node:path';
|
|
3
|
-
import { pipeline } from 'node:stream/promises';
|
|
4
|
-
import { createGunzip } from 'node:zlib';
|
|
5
|
-
import { inflateRawSync } from 'node:zlib';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { Readable } from 'node:stream';
|
|
8
|
-
|
|
9
|
-
// ── Constants ────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
const DEFAULT_API = 'https://api.github.com/repos/naishtech/cadet-agent/releases/latest';
|
|
12
|
-
const USER_AGENT = 'cadet-agent-cli';
|
|
13
|
-
|
|
14
|
-
function resolveApiUrl(override) {
|
|
15
|
-
return override || process.env.CADET_AGENT_RELEASE_URL || DEFAULT_API;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ── ZIP parser (zero-dependency, handles store + deflate) ───────────────────
|
|
19
|
-
|
|
20
|
-
const SIG_EOCD = 0x06054b50;
|
|
21
|
-
const SIG_CD = 0x02014b50;
|
|
22
|
-
const SIG_LFH = 0x04034b50;
|
|
23
|
-
|
|
24
|
-
function read32(buf, off) { return buf.readUInt32LE(off); }
|
|
25
|
-
function read16(buf, off) { return buf.readUInt16LE(off); }
|
|
26
|
-
|
|
27
|
-
function findEocd(buf) {
|
|
28
|
-
// Search backwards from end for EOCD signature (comment is max 65535 bytes)
|
|
29
|
-
const maxStart = Math.max(0, buf.length - 65535 - 22);
|
|
30
|
-
for (let i = buf.length - 22; i >= maxStart; i--) {
|
|
31
|
-
if (read32(buf, i) === SIG_EOCD) return i;
|
|
32
|
-
}
|
|
33
|
-
throw new Error('Not a valid ZIP file: EOCD signature not found');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function* centralDirectoryEntries(buf, cdOffset, cdSize) {
|
|
37
|
-
let off = cdOffset;
|
|
38
|
-
const end = cdOffset + cdSize;
|
|
39
|
-
while (off < end) {
|
|
40
|
-
if (read32(buf, off) !== SIG_CD) break;
|
|
41
|
-
const method = read16(buf, off + 10);
|
|
42
|
-
const compressedSize = read32(buf, off + 20);
|
|
43
|
-
const uncompressedSize = read32(buf, off + 24);
|
|
44
|
-
const filenameLen = read16(buf, off + 28);
|
|
45
|
-
const extraLen = read16(buf, off + 30);
|
|
46
|
-
const commentLen = read16(buf, off + 32);
|
|
47
|
-
const localHeaderOff = read32(buf, off + 42);
|
|
48
|
-
const filename = buf.toString('utf-8', off + 46, off + 46 + filenameLen);
|
|
49
|
-
|
|
50
|
-
// Skip directory entries (trailing / in filename, or uncompressedSize == 0 with no method)
|
|
51
|
-
if (!filename.endsWith('/')) {
|
|
52
|
-
yield { filename, method, compressedSize, uncompressedSize, localHeaderOff };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
off += 46 + filenameLen + extraLen + commentLen;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function extractFile(buf, entry, targetDir) {
|
|
60
|
-
const { filename, method, compressedSize, localHeaderOff } = entry;
|
|
61
|
-
|
|
62
|
-
// Read local file header to get filename + extra lengths (they may differ from CD)
|
|
63
|
-
const lfhFilenameLen = read16(buf, localHeaderOff + 26);
|
|
64
|
-
const lfhExtraLen = read16(buf, localHeaderOff + 28);
|
|
65
|
-
|
|
66
|
-
const dataStart = localHeaderOff + 30 + lfhFilenameLen + lfhExtraLen;
|
|
67
|
-
const compressed = buf.subarray(dataStart, dataStart + compressedSize);
|
|
68
|
-
|
|
69
|
-
let data;
|
|
70
|
-
if (method === 0) {
|
|
71
|
-
// Stored — no compression
|
|
72
|
-
data = compressed;
|
|
73
|
-
} else if (method === 8) {
|
|
74
|
-
// Deflate
|
|
75
|
-
data = inflateRawSync(compressed);
|
|
76
|
-
} else {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`Unsupported compression method ${method} for ${filename}.\n` +
|
|
79
|
-
`This zip uses a compression format this CLI doesn't support.\n` +
|
|
80
|
-
`Try downloading cadet-agent.zip manually from:\n` +
|
|
81
|
-
` https://github.com/naishtech/cadet-agent/releases/latest`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const outPath = join(targetDir, filename);
|
|
86
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
87
|
-
const ws = createWriteStream(outPath);
|
|
88
|
-
Readable.from(data).pipe(ws);
|
|
89
|
-
|
|
90
|
-
return outPath;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function extractZip(buf, targetDir) {
|
|
94
|
-
const eocdOff = findEocd(buf);
|
|
95
|
-
const cdSize = read32(buf, eocdOff + 12);
|
|
96
|
-
const cdOff = read32(buf, eocdOff + 16);
|
|
97
|
-
|
|
98
|
-
// ZIP64 detection — 0xFFFFFFFF in 32-bit fields means real values are in ZIP64 extra records
|
|
99
|
-
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
102
|
-
'Try downloading cadet-agent.zip manually from:\n' +
|
|
103
|
-
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const paths = [];
|
|
108
|
-
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
109
|
-
const outPath = extractFile(buf, entry, targetDir);
|
|
110
|
-
paths.push(outPath);
|
|
111
|
-
}
|
|
112
|
-
return paths;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── GitHub release download ─────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
async function fetchLatestRelease(apiUrl) {
|
|
118
|
-
const url = resolveApiUrl(apiUrl);
|
|
119
|
-
console.log('🔍 Fetching latest Cadet-Agent release...');
|
|
120
|
-
|
|
121
|
-
const res = await fetch(url, {
|
|
122
|
-
headers: {
|
|
123
|
-
'User-Agent': USER_AGENT,
|
|
124
|
-
'Accept': 'application/vnd.github+json',
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (!res.ok) {
|
|
129
|
-
if (res.status === 403 || res.status === 429) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`GitHub API rate-limited (${res.status}). ` +
|
|
132
|
-
'Set GITHUB_TOKEN env var for higher limits, or try again later.'
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
throw new Error(`GitHub API returned ${res.status}: ${res.statusText}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return res.json();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function findZipAsset(release) {
|
|
142
|
-
const asset = release.assets?.find(a => a.name === 'cadet-agent.zip');
|
|
143
|
-
if (!asset) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
`Release ${release.tag_name} does not contain cadet-agent.zip.\n` +
|
|
146
|
-
`Available assets: ${(release.assets || []).map(a => a.name).join(', ') || 'none'}`
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
return asset;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function downloadZip(url) {
|
|
153
|
-
console.log('⬇️ Downloading cadet-agent.zip...');
|
|
154
|
-
|
|
155
|
-
const res = await fetch(url, {
|
|
156
|
-
headers: {
|
|
157
|
-
'User-Agent': USER_AGENT,
|
|
158
|
-
'Accept': 'application/octet-stream',
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (!res.ok) {
|
|
163
|
-
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const contentLength = res.headers.get('content-length');
|
|
167
|
-
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
168
|
-
|
|
169
|
-
// Stream to buffer with progress
|
|
170
|
-
const chunks = [];
|
|
171
|
-
let downloaded = 0;
|
|
172
|
-
const reader = res.body.getReader();
|
|
173
|
-
|
|
174
|
-
while (true) {
|
|
175
|
-
const { done, value } = await reader.read();
|
|
176
|
-
if (done) break;
|
|
177
|
-
chunks.push(value);
|
|
178
|
-
downloaded += value.length;
|
|
179
|
-
if (total > 0) {
|
|
180
|
-
const pct = Math.round((downloaded / total) * 100);
|
|
181
|
-
process.stdout.write(`\r ${pct}% (${(downloaded / 1024).toFixed(0)} KB / ${(total / 1024).toFixed(0)} KB)`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (total > 0) process.stdout.write('\n');
|
|
185
|
-
|
|
186
|
-
return Buffer.concat(chunks);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ── Public install entry ────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
|
-
export async function install(targetDir, opts = {}) {
|
|
192
|
-
console.log(`📦 Cadet-Agent — installing to ${targetDir}\n`);
|
|
193
|
-
|
|
194
|
-
// 1. Fetch release metadata
|
|
195
|
-
const release = await fetchLatestRelease(opts.sourceUrl);
|
|
196
|
-
console.log(` Latest: ${release.tag_name} (published ${release.published_at})\n`);
|
|
197
|
-
|
|
198
|
-
// 2. Find zip asset
|
|
199
|
-
const asset = findZipAsset(release);
|
|
200
|
-
|
|
201
|
-
// 3. Download
|
|
202
|
-
const zipBuf = await downloadZip(asset.browser_download_url);
|
|
203
|
-
console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
|
|
204
|
-
|
|
205
|
-
// 4. Extract
|
|
206
|
-
console.log('📂 Extracting...');
|
|
207
|
-
const extracted = extractZip(zipBuf, targetDir);
|
|
208
|
-
|
|
209
|
-
// 5. Report
|
|
210
|
-
console.log(`\n✅ Cadet-Agent ${release.tag_name} installed! Extracted ${extracted.length} files.\n`);
|
|
211
|
-
|
|
212
|
-
// Print per-IDE next steps
|
|
213
|
-
console.log('── Next steps ──');
|
|
214
|
-
console.log(' GitHub Copilot:');
|
|
215
|
-
console.log(' Start a chat: /cadet');
|
|
216
|
-
console.log(' Cursor:');
|
|
217
|
-
console.log(' Already active — .cursor\\rules\\cadet-agent.md loads automatically');
|
|
218
|
-
console.log(' Continue:');
|
|
219
|
-
console.log(' Already active — .continue\\rules\\cadet-agent.md loads automatically');
|
|
220
|
-
console.log(' Claude Code:');
|
|
221
|
-
console.log(' Already active — .claude\\skills\\cadet-agent.md loads as a project skill');
|
|
222
|
-
console.log('');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ── Manifest-aware extraction ───────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
function matchesPreservedPath(filename, preservedPaths) {
|
|
228
|
-
// Normalize: strip leading dot (zip paths like ".cadet/agent/policies/...")
|
|
229
|
-
const normalized = filename.replace(/^\.?\/?/, '');
|
|
230
|
-
for (const preserved of preservedPaths) {
|
|
231
|
-
const p = preserved.replace(/^\.?\/?/, '');
|
|
232
|
-
if (normalized === p || normalized.startsWith(p + '/') || normalized.startsWith(p + '\\')) {
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function extractZipWithManifest(buf, targetDir, { preserved, managed }) {
|
|
240
|
-
const eocdOff = findEocd(buf);
|
|
241
|
-
const cdSize = read32(buf, eocdOff + 12);
|
|
242
|
-
const cdOff = read32(buf, eocdOff + 16);
|
|
243
|
-
|
|
244
|
-
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
245
|
-
throw new Error(
|
|
246
|
-
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
247
|
-
'Try downloading cadet-agent.zip manually from:\n' +
|
|
248
|
-
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const updated = [];
|
|
253
|
-
const preserved_list = [];
|
|
254
|
-
const added = [];
|
|
255
|
-
|
|
256
|
-
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
257
|
-
if (matchesPreservedPath(entry.filename, preserved)) {
|
|
258
|
-
preserved_list.push(entry.filename);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
const outPath = extractFile(buf, entry, targetDir);
|
|
262
|
-
// Check if this file already existed (updated vs added)
|
|
263
|
-
// We can't easily tell without checking before extraction, so approximate:
|
|
264
|
-
// If it's under a managed path, call it updated; otherwise added
|
|
265
|
-
const isManaged = managed.some(m => {
|
|
266
|
-
const mn = m.replace(/^\.?\/?/, '');
|
|
267
|
-
const fn = entry.filename.replace(/^\.?\/?/, '');
|
|
268
|
-
return fn === mn || fn.startsWith(mn + '/') || fn.startsWith(mn + '\\');
|
|
269
|
-
});
|
|
270
|
-
if (isManaged) {
|
|
271
|
-
updated.push(outPath);
|
|
272
|
-
} else {
|
|
273
|
-
added.push(outPath);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return { updated, preserved: preserved_list, added };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ── Public sync entry ───────────────────────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
export async function sync(targetDir, opts = {}) {
|
|
283
|
-
console.log(`🔄 Cadet-Agent — syncing ${targetDir}\n`);
|
|
284
|
-
|
|
285
|
-
// 1. Read existing manifest
|
|
286
|
-
const manifestPath = join(targetDir, '.cadet', 'agent', 'core', 'FrameworkManifest.json');
|
|
287
|
-
let existingManifest = null;
|
|
288
|
-
let oldVersion = 'none';
|
|
289
|
-
try {
|
|
290
|
-
existingManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
291
|
-
oldVersion = existingManifest.frameworkVersion || 'unknown';
|
|
292
|
-
console.log(` Existing install: v${oldVersion}`);
|
|
293
|
-
} catch {
|
|
294
|
-
console.log(' No existing install found — performing full install.\n');
|
|
295
|
-
return install(targetDir, opts);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// 2. Fetch release metadata
|
|
299
|
-
const release = await fetchLatestRelease(opts.sourceUrl);
|
|
300
|
-
const newVersion = release.tag_name;
|
|
301
|
-
console.log(` Latest: ${newVersion} (published ${release.published_at})\n`);
|
|
302
|
-
|
|
303
|
-
if (oldVersion === newVersion) {
|
|
304
|
-
console.log(`✅ Already up to date (v${oldVersion}). Nothing to sync.\n`);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 3. Download
|
|
309
|
-
const asset = findZipAsset(release);
|
|
310
|
-
const zipBuf = await downloadZip(asset.browser_download_url);
|
|
311
|
-
console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
|
|
312
|
-
|
|
313
|
-
// 4. Extract with manifest awareness
|
|
314
|
-
console.log('📂 Extracting (preserving local policies and plans)...');
|
|
315
|
-
const result = extractZipWithManifest(zipBuf, targetDir, {
|
|
316
|
-
preserved: existingManifest.preservedPaths || [],
|
|
317
|
-
managed: existingManifest.managedPaths || [],
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// 5. Report
|
|
321
|
-
console.log('');
|
|
322
|
-
console.log(`✅ Cadet-Agent synced: v${oldVersion} → ${newVersion}`);
|
|
323
|
-
console.log(` Updated: ${result.updated.length} files`);
|
|
324
|
-
if (result.preserved.length > 0) {
|
|
325
|
-
console.log(` Preserved: ${result.preserved.length} files (local policies/plans)`);
|
|
326
|
-
}
|
|
327
|
-
if (result.added.length > 0) {
|
|
328
|
-
console.log(` New: ${result.added.length} files`);
|
|
329
|
-
}
|
|
330
|
-
console.log('');
|
|
331
|
-
|
|
332
|
-
// Print per-IDE next steps
|
|
333
|
-
console.log('── Next steps ──');
|
|
334
|
-
console.log(' Framework files updated. Start a fresh chat for changes to take effect:');
|
|
335
|
-
console.log(' /cadet');
|
|
336
|
-
console.log('');
|
|
337
|
-
}
|
|
1
|
+
import { createWriteStream, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import { createGunzip } from 'node:zlib';
|
|
5
|
+
import { inflateRawSync } from 'node:zlib';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
|
|
9
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const DEFAULT_API = 'https://api.github.com/repos/naishtech/cadet-agent/releases/latest';
|
|
12
|
+
const USER_AGENT = 'cadet-agent-cli';
|
|
13
|
+
|
|
14
|
+
function resolveApiUrl(override) {
|
|
15
|
+
return override || process.env.CADET_AGENT_RELEASE_URL || DEFAULT_API;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── ZIP parser (zero-dependency, handles store + deflate) ───────────────────
|
|
19
|
+
|
|
20
|
+
const SIG_EOCD = 0x06054b50;
|
|
21
|
+
const SIG_CD = 0x02014b50;
|
|
22
|
+
const SIG_LFH = 0x04034b50;
|
|
23
|
+
|
|
24
|
+
function read32(buf, off) { return buf.readUInt32LE(off); }
|
|
25
|
+
function read16(buf, off) { return buf.readUInt16LE(off); }
|
|
26
|
+
|
|
27
|
+
function findEocd(buf) {
|
|
28
|
+
// Search backwards from end for EOCD signature (comment is max 65535 bytes)
|
|
29
|
+
const maxStart = Math.max(0, buf.length - 65535 - 22);
|
|
30
|
+
for (let i = buf.length - 22; i >= maxStart; i--) {
|
|
31
|
+
if (read32(buf, i) === SIG_EOCD) return i;
|
|
32
|
+
}
|
|
33
|
+
throw new Error('Not a valid ZIP file: EOCD signature not found');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function* centralDirectoryEntries(buf, cdOffset, cdSize) {
|
|
37
|
+
let off = cdOffset;
|
|
38
|
+
const end = cdOffset + cdSize;
|
|
39
|
+
while (off < end) {
|
|
40
|
+
if (read32(buf, off) !== SIG_CD) break;
|
|
41
|
+
const method = read16(buf, off + 10);
|
|
42
|
+
const compressedSize = read32(buf, off + 20);
|
|
43
|
+
const uncompressedSize = read32(buf, off + 24);
|
|
44
|
+
const filenameLen = read16(buf, off + 28);
|
|
45
|
+
const extraLen = read16(buf, off + 30);
|
|
46
|
+
const commentLen = read16(buf, off + 32);
|
|
47
|
+
const localHeaderOff = read32(buf, off + 42);
|
|
48
|
+
const filename = buf.toString('utf-8', off + 46, off + 46 + filenameLen);
|
|
49
|
+
|
|
50
|
+
// Skip directory entries (trailing / in filename, or uncompressedSize == 0 with no method)
|
|
51
|
+
if (!filename.endsWith('/')) {
|
|
52
|
+
yield { filename, method, compressedSize, uncompressedSize, localHeaderOff };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
off += 46 + filenameLen + extraLen + commentLen;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractFile(buf, entry, targetDir) {
|
|
60
|
+
const { filename, method, compressedSize, localHeaderOff } = entry;
|
|
61
|
+
|
|
62
|
+
// Read local file header to get filename + extra lengths (they may differ from CD)
|
|
63
|
+
const lfhFilenameLen = read16(buf, localHeaderOff + 26);
|
|
64
|
+
const lfhExtraLen = read16(buf, localHeaderOff + 28);
|
|
65
|
+
|
|
66
|
+
const dataStart = localHeaderOff + 30 + lfhFilenameLen + lfhExtraLen;
|
|
67
|
+
const compressed = buf.subarray(dataStart, dataStart + compressedSize);
|
|
68
|
+
|
|
69
|
+
let data;
|
|
70
|
+
if (method === 0) {
|
|
71
|
+
// Stored — no compression
|
|
72
|
+
data = compressed;
|
|
73
|
+
} else if (method === 8) {
|
|
74
|
+
// Deflate
|
|
75
|
+
data = inflateRawSync(compressed);
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Unsupported compression method ${method} for ${filename}.\n` +
|
|
79
|
+
`This zip uses a compression format this CLI doesn't support.\n` +
|
|
80
|
+
`Try downloading cadet-agent.zip manually from:\n` +
|
|
81
|
+
` https://github.com/naishtech/cadet-agent/releases/latest`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const outPath = join(targetDir, filename);
|
|
86
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
87
|
+
const ws = createWriteStream(outPath);
|
|
88
|
+
Readable.from(data).pipe(ws);
|
|
89
|
+
|
|
90
|
+
return outPath;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractZip(buf, targetDir) {
|
|
94
|
+
const eocdOff = findEocd(buf);
|
|
95
|
+
const cdSize = read32(buf, eocdOff + 12);
|
|
96
|
+
const cdOff = read32(buf, eocdOff + 16);
|
|
97
|
+
|
|
98
|
+
// ZIP64 detection — 0xFFFFFFFF in 32-bit fields means real values are in ZIP64 extra records
|
|
99
|
+
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
102
|
+
'Try downloading cadet-agent.zip manually from:\n' +
|
|
103
|
+
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const paths = [];
|
|
108
|
+
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
109
|
+
const outPath = extractFile(buf, entry, targetDir);
|
|
110
|
+
paths.push(outPath);
|
|
111
|
+
}
|
|
112
|
+
return paths;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── GitHub release download ─────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
async function fetchLatestRelease(apiUrl) {
|
|
118
|
+
const url = resolveApiUrl(apiUrl);
|
|
119
|
+
console.log('🔍 Fetching latest Cadet-Agent release...');
|
|
120
|
+
|
|
121
|
+
const res = await fetch(url, {
|
|
122
|
+
headers: {
|
|
123
|
+
'User-Agent': USER_AGENT,
|
|
124
|
+
'Accept': 'application/vnd.github+json',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
if (res.status === 403 || res.status === 429) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`GitHub API rate-limited (${res.status}). ` +
|
|
132
|
+
'Set GITHUB_TOKEN env var for higher limits, or try again later.'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`GitHub API returned ${res.status}: ${res.statusText}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return res.json();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function findZipAsset(release) {
|
|
142
|
+
const asset = release.assets?.find(a => a.name === 'cadet-agent.zip');
|
|
143
|
+
if (!asset) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Release ${release.tag_name} does not contain cadet-agent.zip.\n` +
|
|
146
|
+
`Available assets: ${(release.assets || []).map(a => a.name).join(', ') || 'none'}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return asset;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function downloadZip(url) {
|
|
153
|
+
console.log('⬇️ Downloading cadet-agent.zip...');
|
|
154
|
+
|
|
155
|
+
const res = await fetch(url, {
|
|
156
|
+
headers: {
|
|
157
|
+
'User-Agent': USER_AGENT,
|
|
158
|
+
'Accept': 'application/octet-stream',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const contentLength = res.headers.get('content-length');
|
|
167
|
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
168
|
+
|
|
169
|
+
// Stream to buffer with progress
|
|
170
|
+
const chunks = [];
|
|
171
|
+
let downloaded = 0;
|
|
172
|
+
const reader = res.body.getReader();
|
|
173
|
+
|
|
174
|
+
while (true) {
|
|
175
|
+
const { done, value } = await reader.read();
|
|
176
|
+
if (done) break;
|
|
177
|
+
chunks.push(value);
|
|
178
|
+
downloaded += value.length;
|
|
179
|
+
if (total > 0) {
|
|
180
|
+
const pct = Math.round((downloaded / total) * 100);
|
|
181
|
+
process.stdout.write(`\r ${pct}% (${(downloaded / 1024).toFixed(0)} KB / ${(total / 1024).toFixed(0)} KB)`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (total > 0) process.stdout.write('\n');
|
|
185
|
+
|
|
186
|
+
return Buffer.concat(chunks);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Public install entry ────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export async function install(targetDir, opts = {}) {
|
|
192
|
+
console.log(`📦 Cadet-Agent — installing to ${targetDir}\n`);
|
|
193
|
+
|
|
194
|
+
// 1. Fetch release metadata
|
|
195
|
+
const release = await fetchLatestRelease(opts.sourceUrl);
|
|
196
|
+
console.log(` Latest: ${release.tag_name} (published ${release.published_at})\n`);
|
|
197
|
+
|
|
198
|
+
// 2. Find zip asset
|
|
199
|
+
const asset = findZipAsset(release);
|
|
200
|
+
|
|
201
|
+
// 3. Download
|
|
202
|
+
const zipBuf = await downloadZip(asset.browser_download_url);
|
|
203
|
+
console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
|
|
204
|
+
|
|
205
|
+
// 4. Extract
|
|
206
|
+
console.log('📂 Extracting...');
|
|
207
|
+
const extracted = extractZip(zipBuf, targetDir);
|
|
208
|
+
|
|
209
|
+
// 5. Report
|
|
210
|
+
console.log(`\n✅ Cadet-Agent ${release.tag_name} installed! Extracted ${extracted.length} files.\n`);
|
|
211
|
+
|
|
212
|
+
// Print per-IDE next steps
|
|
213
|
+
console.log('── Next steps ──');
|
|
214
|
+
console.log(' GitHub Copilot:');
|
|
215
|
+
console.log(' Start a chat: /cadet');
|
|
216
|
+
console.log(' Cursor:');
|
|
217
|
+
console.log(' Already active — .cursor\\rules\\cadet-agent.md loads automatically');
|
|
218
|
+
console.log(' Continue:');
|
|
219
|
+
console.log(' Already active — .continue\\rules\\cadet-agent.md loads automatically');
|
|
220
|
+
console.log(' Claude Code:');
|
|
221
|
+
console.log(' Already active — .claude\\skills\\cadet-agent.md loads as a project skill');
|
|
222
|
+
console.log('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Manifest-aware extraction ───────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function matchesPreservedPath(filename, preservedPaths) {
|
|
228
|
+
// Normalize: strip leading dot (zip paths like ".cadet/agent/policies/...")
|
|
229
|
+
const normalized = filename.replace(/^\.?\/?/, '');
|
|
230
|
+
for (const preserved of preservedPaths) {
|
|
231
|
+
const p = preserved.replace(/^\.?\/?/, '');
|
|
232
|
+
if (normalized === p || normalized.startsWith(p + '/') || normalized.startsWith(p + '\\')) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function extractZipWithManifest(buf, targetDir, { preserved, managed }) {
|
|
240
|
+
const eocdOff = findEocd(buf);
|
|
241
|
+
const cdSize = read32(buf, eocdOff + 12);
|
|
242
|
+
const cdOff = read32(buf, eocdOff + 16);
|
|
243
|
+
|
|
244
|
+
if (cdOff === 0xFFFFFFFF || cdSize === 0xFFFFFFFF) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
'This zip uses ZIP64 format, which is not supported.\n' +
|
|
247
|
+
'Try downloading cadet-agent.zip manually from:\n' +
|
|
248
|
+
' https://github.com/naishtech/cadet-agent/releases/latest'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const updated = [];
|
|
253
|
+
const preserved_list = [];
|
|
254
|
+
const added = [];
|
|
255
|
+
|
|
256
|
+
for (const entry of centralDirectoryEntries(buf, cdOff, cdSize)) {
|
|
257
|
+
if (matchesPreservedPath(entry.filename, preserved)) {
|
|
258
|
+
preserved_list.push(entry.filename);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const outPath = extractFile(buf, entry, targetDir);
|
|
262
|
+
// Check if this file already existed (updated vs added)
|
|
263
|
+
// We can't easily tell without checking before extraction, so approximate:
|
|
264
|
+
// If it's under a managed path, call it updated; otherwise added
|
|
265
|
+
const isManaged = managed.some(m => {
|
|
266
|
+
const mn = m.replace(/^\.?\/?/, '');
|
|
267
|
+
const fn = entry.filename.replace(/^\.?\/?/, '');
|
|
268
|
+
return fn === mn || fn.startsWith(mn + '/') || fn.startsWith(mn + '\\');
|
|
269
|
+
});
|
|
270
|
+
if (isManaged) {
|
|
271
|
+
updated.push(outPath);
|
|
272
|
+
} else {
|
|
273
|
+
added.push(outPath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { updated, preserved: preserved_list, added };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Public sync entry ───────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
export async function sync(targetDir, opts = {}) {
|
|
283
|
+
console.log(`🔄 Cadet-Agent — syncing ${targetDir}\n`);
|
|
284
|
+
|
|
285
|
+
// 1. Read existing manifest
|
|
286
|
+
const manifestPath = join(targetDir, '.cadet', 'agent', 'core', 'FrameworkManifest.json');
|
|
287
|
+
let existingManifest = null;
|
|
288
|
+
let oldVersion = 'none';
|
|
289
|
+
try {
|
|
290
|
+
existingManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
291
|
+
oldVersion = existingManifest.frameworkVersion || 'unknown';
|
|
292
|
+
console.log(` Existing install: v${oldVersion}`);
|
|
293
|
+
} catch {
|
|
294
|
+
console.log(' No existing install found — performing full install.\n');
|
|
295
|
+
return install(targetDir, opts);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 2. Fetch release metadata
|
|
299
|
+
const release = await fetchLatestRelease(opts.sourceUrl);
|
|
300
|
+
const newVersion = release.tag_name;
|
|
301
|
+
console.log(` Latest: ${newVersion} (published ${release.published_at})\n`);
|
|
302
|
+
|
|
303
|
+
if (oldVersion === newVersion) {
|
|
304
|
+
console.log(`✅ Already up to date (v${oldVersion}). Nothing to sync.\n`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 3. Download
|
|
309
|
+
const asset = findZipAsset(release);
|
|
310
|
+
const zipBuf = await downloadZip(asset.browser_download_url);
|
|
311
|
+
console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
|
|
312
|
+
|
|
313
|
+
// 4. Extract with manifest awareness
|
|
314
|
+
console.log('📂 Extracting (preserving local policies and plans)...');
|
|
315
|
+
const result = extractZipWithManifest(zipBuf, targetDir, {
|
|
316
|
+
preserved: existingManifest.preservedPaths || [],
|
|
317
|
+
managed: existingManifest.managedPaths || [],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// 5. Report
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(`✅ Cadet-Agent synced: v${oldVersion} → ${newVersion}`);
|
|
323
|
+
console.log(` Updated: ${result.updated.length} files`);
|
|
324
|
+
if (result.preserved.length > 0) {
|
|
325
|
+
console.log(` Preserved: ${result.preserved.length} files (local policies/plans)`);
|
|
326
|
+
}
|
|
327
|
+
if (result.added.length > 0) {
|
|
328
|
+
console.log(` New: ${result.added.length} files`);
|
|
329
|
+
}
|
|
330
|
+
console.log('');
|
|
331
|
+
|
|
332
|
+
// Print per-IDE next steps
|
|
333
|
+
console.log('── Next steps ──');
|
|
334
|
+
console.log(' Framework files updated. Start a fresh chat for changes to take effect:');
|
|
335
|
+
console.log(' /cadet');
|
|
336
|
+
console.log('');
|
|
337
|
+
}
|