uniner-cli 1.0.3 → 1.0.6
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/LICENSE +4 -4
- package/README.md +181 -171
- package/bin/uniner-cli-uninstall.js +32 -129
- package/bin/uniner-cli.js +42 -42
- package/lib/SKILL.md.template +373 -371
- package/lib/paths.js +78 -78
- package/package.json +50 -47
- package/scripts/cleanup.js +123 -0
- package/scripts/postinstall-helpers.js +140 -0
- package/scripts/postinstall.js +385 -428
- package/scripts/preuninstall.js +25 -0
package/scripts/postinstall.js
CHANGED
|
@@ -1,428 +1,385 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// postinstall.js — runs after `npm install -g uniner-cli`.
|
|
3
|
-
//
|
|
4
|
-
// What it does:
|
|
5
|
-
// 1. If the binary is already in place at ~/.uniner-cli/<version>/, exit (idempotent).
|
|
6
|
-
// 2. Fetch latest.json from the OSS metadata URL to get
|
|
7
|
-
// 3. Stream-download the matching portable
|
|
8
|
-
// -
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
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
|
-
res.
|
|
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
|
-
if (res.statusCode
|
|
107
|
-
file.close(() => fs.unlink(tmp, () => {}));
|
|
108
|
-
|
|
109
|
-
return;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
log(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
log(' Flattening extracted tree...');
|
|
387
|
-
for (const item of fs.readdirSync(inner)) {
|
|
388
|
-
fs.renameSync(path.join(inner, item), path.join(targetDir, item));
|
|
389
|
-
}
|
|
390
|
-
fs.rmdirSync(inner);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (!isInstalled(version)) {
|
|
395
|
-
fail(`Extraction completed but the binary is not at ${realBinaryPath(version)}. ` +
|
|
396
|
-
`Inspect ${targetDir} to debug.`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
step('Cleaning up');
|
|
400
|
-
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
401
|
-
try { fs.rmdirSync(tmpDir); } catch (_) {}
|
|
402
|
-
log(' Removed temporary files.');
|
|
403
|
-
|
|
404
|
-
step('Installing Claude Code skill');
|
|
405
|
-
writeSkillMd(targetDir, version);
|
|
406
|
-
|
|
407
|
-
console.log('');
|
|
408
|
-
console.log('==========================================');
|
|
409
|
-
console.log(` uniner-cli ${version} installed`);
|
|
410
|
-
console.log('==========================================');
|
|
411
|
-
console.log('');
|
|
412
|
-
console.log(` Location: ${targetDir}`);
|
|
413
|
-
console.log(` Skill: ${skillFile()}`);
|
|
414
|
-
console.log('');
|
|
415
|
-
console.log(' Validate: uniner-cli --help');
|
|
416
|
-
console.log('');
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
main().catch(err => {
|
|
420
|
-
console.error('');
|
|
421
|
-
console.error(err.message || String(err));
|
|
422
|
-
console.error('');
|
|
423
|
-
console.error('Install failed. To retry:');
|
|
424
|
-
console.error(' npm uninstall -g uniner-cli');
|
|
425
|
-
console.error(' npm install -g uniner-cli');
|
|
426
|
-
console.error('');
|
|
427
|
-
process.exit(1);
|
|
428
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// postinstall.js — runs after `npm install -g uniner-cli`.
|
|
3
|
+
//
|
|
4
|
+
// What it does:
|
|
5
|
+
// 1. If the binary is already in place at ~/.uniner-cli/<version>/, exit (idempotent).
|
|
6
|
+
// 2. Fetch latest.json from the OSS metadata URL to get portable_url + sha256.
|
|
7
|
+
// 3. Stream-download the matching portable .zip into %TEMP%.
|
|
8
|
+
// - Live progress every 2 seconds.
|
|
9
|
+
// - Up to 3 retries with exponential backoff.
|
|
10
|
+
// 4. Verify SHA-256.
|
|
11
|
+
// 5. Extract via PowerShell's Expand-Archive (no native dep).
|
|
12
|
+
// 6. Render SKILL.md.template with {{INSTALL_PATH}} / {{VERSION}} substituted
|
|
13
|
+
// and write to ~/.claude/skills/uniner-cli/SKILL.md.
|
|
14
|
+
// 7. Print "ready" and how to validate.
|
|
15
|
+
//
|
|
16
|
+
// Failures throw -> non-zero exit -> npm marks install as failed (strict mode).
|
|
17
|
+
// User can then `npm install -g uniner-cli` again to retry cleanly.
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const fsp = require('fs').promises;
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const https = require('https');
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
const { spawnSync } = require('child_process');
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
pkg,
|
|
31
|
+
versionDir,
|
|
32
|
+
realBinaryPath,
|
|
33
|
+
isInstalled,
|
|
34
|
+
skillDir,
|
|
35
|
+
skillFile,
|
|
36
|
+
templatePath,
|
|
37
|
+
} = require('../lib/paths');
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
getPlatformInfo,
|
|
41
|
+
selectMetadata,
|
|
42
|
+
extractArchive,
|
|
43
|
+
getBinaryName
|
|
44
|
+
} = require('./postinstall-helpers');
|
|
45
|
+
|
|
46
|
+
// ----------------------------------------------------------------------
|
|
47
|
+
// Configuration
|
|
48
|
+
// ----------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const METADATA_URL =
|
|
51
|
+
'https://static.uniner.com/soft/editor/uninercli/latest.json';
|
|
52
|
+
|
|
53
|
+
const DOWNLOAD_RETRIES = 3;
|
|
54
|
+
const DOWNLOAD_TIMEOUT_MS = 600_000; // 10 min per attempt
|
|
55
|
+
const PROGRESS_INTERVAL_MS = 2000;
|
|
56
|
+
|
|
57
|
+
// ----------------------------------------------------------------------
|
|
58
|
+
// Logging
|
|
59
|
+
// ----------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function log(msg) { console.log(`[uniner-cli] ${msg}`); }
|
|
62
|
+
function step(msg) { console.log(`[uniner-cli] ==> ${msg}`); }
|
|
63
|
+
function fail(msg) { throw new Error(`[uniner-cli] ${msg}`); }
|
|
64
|
+
|
|
65
|
+
// ----------------------------------------------------------------------
|
|
66
|
+
// HTTP helpers
|
|
67
|
+
// ----------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function getJSON(url) {
|
|
70
|
+
// Cache-bust to dodge any CDN that ignored the no-cache header.
|
|
71
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
72
|
+
const bust = `${sep}t=${Date.now()}`;
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const req = https.get(url + bust, { timeout: 30_000 }, res => {
|
|
75
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
76
|
+
resolve(getJSON(res.headers.location));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (res.statusCode !== 200) {
|
|
80
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let buf = '';
|
|
84
|
+
res.setEncoding('utf8');
|
|
85
|
+
res.on('data', chunk => { buf += chunk; });
|
|
86
|
+
res.on('end', () => {
|
|
87
|
+
try { resolve(JSON.parse(buf)); }
|
|
88
|
+
catch (e) { reject(new Error(`Bad JSON from ${url}: ${e.message}`)); }
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
req.on('timeout', () => { req.destroy(new Error('timeout')); });
|
|
92
|
+
req.on('error', reject);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function downloadStream(url, outFile, totalBytesHint) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const tmp = outFile + '.partial';
|
|
99
|
+
const file = fs.createWriteStream(tmp);
|
|
100
|
+
let received = 0;
|
|
101
|
+
let total = totalBytesHint || 0;
|
|
102
|
+
let lastReport = Date.now();
|
|
103
|
+
let cleanup = () => {};
|
|
104
|
+
|
|
105
|
+
const req = https.get(url, { timeout: DOWNLOAD_TIMEOUT_MS }, res => {
|
|
106
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
107
|
+
file.close(() => fs.unlink(tmp, () => {}));
|
|
108
|
+
resolve(downloadStream(res.headers.location, outFile, totalBytesHint));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (res.statusCode !== 200) {
|
|
112
|
+
file.close(() => fs.unlink(tmp, () => {}));
|
|
113
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (res.headers['content-length']) {
|
|
118
|
+
total = parseInt(res.headers['content-length'], 10);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
res.on('data', chunk => {
|
|
122
|
+
received += chunk.length;
|
|
123
|
+
file.write(chunk);
|
|
124
|
+
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
if (now - lastReport >= PROGRESS_INTERVAL_MS) {
|
|
127
|
+
const mbDone = (received / 1048576).toFixed(1);
|
|
128
|
+
if (total > 0) {
|
|
129
|
+
const mbTotal = (total / 1048576).toFixed(1);
|
|
130
|
+
const pct = ((received * 100) / total).toFixed(1);
|
|
131
|
+
log(` ${mbDone}/${mbTotal} MB (${pct}%)`);
|
|
132
|
+
} else {
|
|
133
|
+
log(` ${mbDone} MB downloaded`);
|
|
134
|
+
}
|
|
135
|
+
lastReport = now;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
res.on('end', () => {
|
|
140
|
+
file.end(() => {
|
|
141
|
+
fs.rename(tmp, outFile, err => {
|
|
142
|
+
if (err) reject(err); else resolve(received);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
cleanup = () => {
|
|
148
|
+
res.destroy();
|
|
149
|
+
file.close(() => fs.unlink(tmp, () => {}));
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
req.on('timeout', () => {
|
|
154
|
+
cleanup();
|
|
155
|
+
req.destroy(new Error('download timed out'));
|
|
156
|
+
});
|
|
157
|
+
req.on('error', err => {
|
|
158
|
+
cleanup();
|
|
159
|
+
reject(err);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function downloadWithRetry(url, outFile, totalBytesHint) {
|
|
165
|
+
let lastErr;
|
|
166
|
+
for (let attempt = 1; attempt <= DOWNLOAD_RETRIES; attempt++) {
|
|
167
|
+
try {
|
|
168
|
+
log(` Download attempt ${attempt}/${DOWNLOAD_RETRIES}...`);
|
|
169
|
+
const bytes = await downloadStream(url, outFile, totalBytesHint);
|
|
170
|
+
log(` Downloaded ${bytes} bytes`);
|
|
171
|
+
return;
|
|
172
|
+
} catch (e) {
|
|
173
|
+
lastErr = e;
|
|
174
|
+
log(` Attempt ${attempt} failed: ${e.message}`);
|
|
175
|
+
try { await fsp.unlink(outFile); } catch (_) {}
|
|
176
|
+
if (attempt < DOWNLOAD_RETRIES) {
|
|
177
|
+
await new Promise(r => setTimeout(r, 5000 * attempt));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
fail(`Download failed after ${DOWNLOAD_RETRIES} attempts: ${lastErr.message}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ----------------------------------------------------------------------
|
|
185
|
+
// SHA-256
|
|
186
|
+
// ----------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function sha256OfFile(file) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const hash = crypto.createHash('sha256');
|
|
191
|
+
const s = fs.createReadStream(file);
|
|
192
|
+
s.on('data', c => hash.update(c));
|
|
193
|
+
s.on('end', () => resolve(hash.digest('hex')));
|
|
194
|
+
s.on('error', reject);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ----------------------------------------------------------------------
|
|
199
|
+
// SKILL.md template render
|
|
200
|
+
// ----------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
function writeSkillMd(installPath, version) {
|
|
203
|
+
const tplPath = templatePath();
|
|
204
|
+
if (!fs.existsSync(tplPath)) {
|
|
205
|
+
log(` [WARN] SKILL.md.template missing at ${tplPath}; skipping skill install.`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let content = fs.readFileSync(tplPath, 'utf8');
|
|
209
|
+
// PowerShell-side StringChangeEx is case-insensitive; here we keep it strict
|
|
210
|
+
// because our template uses exactly these tokens.
|
|
211
|
+
content = content.split('{{INSTALL_PATH}}').join(installPath);
|
|
212
|
+
content = content.split('{{VERSION}}').join(version);
|
|
213
|
+
|
|
214
|
+
const dir = skillDir();
|
|
215
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
216
|
+
|
|
217
|
+
// IMPORTANT: only writeFileSync the single SKILL.md file. Do NOT touch
|
|
218
|
+
// anything else under ~/.claude/skills/uniner-cli/. In particular, the
|
|
219
|
+
// scenarios/ subdirectory is the user's accumulated knowledge — if we
|
|
220
|
+
// ever rmdir or rmSync this directory we'd wipe their library on every
|
|
221
|
+
// upgrade. Single-file write is the contract.
|
|
222
|
+
fs.writeFileSync(skillFile(), content, 'utf8');
|
|
223
|
+
log(` SKILL.md written to ${skillFile()}`);
|
|
224
|
+
|
|
225
|
+
// Tell the user we kept their scenarios (if any).
|
|
226
|
+
const scenariosDir = path.join(dir, 'scenarios');
|
|
227
|
+
if (fs.existsSync(scenariosDir)) {
|
|
228
|
+
try {
|
|
229
|
+
const count = fs.readdirSync(scenariosDir).filter(n => {
|
|
230
|
+
const meta = path.join(scenariosDir, n, 'meta.json');
|
|
231
|
+
return fs.existsSync(meta);
|
|
232
|
+
}).length;
|
|
233
|
+
log(` [INFO] Preserved ${count} scenario(s) in ${scenariosDir}`);
|
|
234
|
+
} catch (_) {
|
|
235
|
+
// best-effort; never fail postinstall over a logging stat
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ----------------------------------------------------------------------
|
|
241
|
+
// Main
|
|
242
|
+
// ----------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
async function main() {
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log('==========================================');
|
|
247
|
+
console.log(' uniner-cli postinstall');
|
|
248
|
+
console.log('==========================================');
|
|
249
|
+
|
|
250
|
+
// 检查平台支持
|
|
251
|
+
const platformInfo = getPlatformInfo();
|
|
252
|
+
if (!platformInfo) {
|
|
253
|
+
fail(`不支持的平台: ${process.platform} ${process.arch}。` +
|
|
254
|
+
'当前仅支持 Windows x64 和 macOS ARM64。');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
log(`平台: ${platformInfo.platform} ${platformInfo.arch}`);
|
|
258
|
+
|
|
259
|
+
const version = pkg.version;
|
|
260
|
+
|
|
261
|
+
// Skip when a working binary is already there. This makes
|
|
262
|
+
// `npm install -g uniner-cli` idempotent and survives `npm rebuild`.
|
|
263
|
+
if (isInstalled(version)) {
|
|
264
|
+
log(`Already installed at ${realBinaryPath(version)}; skipping download.`);
|
|
265
|
+
log('To force a clean reinstall:');
|
|
266
|
+
log(' npm uninstall -g uniner-cli && npm install -g uniner-cli');
|
|
267
|
+
// Still re-render the skill — it's tiny and lets users edit the template
|
|
268
|
+
// and re-run rebuild to refresh.
|
|
269
|
+
writeSkillMd(versionDir(version), version);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
step('Reading version metadata');
|
|
274
|
+
log(` URL: ${METADATA_URL}`);
|
|
275
|
+
const meta = await getJSON(METADATA_URL);
|
|
276
|
+
|
|
277
|
+
// 根据平台选择元数据
|
|
278
|
+
const platformMeta = selectMetadata(meta);
|
|
279
|
+
log(` Latest version: ${meta.version}`);
|
|
280
|
+
log(` Package version (this npm release): ${version}`);
|
|
281
|
+
|
|
282
|
+
if (meta.version !== version) {
|
|
283
|
+
log(` [INFO] Server has ${meta.version} but this npm package is pinned ` +
|
|
284
|
+
`to ${version}. Will fetch the package's own version artifacts ` +
|
|
285
|
+
`from the OSS path that the URL implies.`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
step('Downloading portable archive');
|
|
289
|
+
const tmpDir = path.join(os.tmpdir(), 'uniner-cli-npm-install');
|
|
290
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
291
|
+
const archivePath = path.join(tmpDir, path.basename(platformMeta.portable_url));
|
|
292
|
+
|
|
293
|
+
log(` From: ${platformMeta.portable_url}`);
|
|
294
|
+
log(` To: ${archivePath}`);
|
|
295
|
+
await downloadWithRetry(platformMeta.portable_url, archivePath, platformMeta.portable_size);
|
|
296
|
+
|
|
297
|
+
step('Verifying SHA-256');
|
|
298
|
+
const actual = await sha256OfFile(archivePath);
|
|
299
|
+
if (actual.toLowerCase() !== platformMeta.portable_sha256.toLowerCase()) {
|
|
300
|
+
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
301
|
+
fail(`SHA-256 mismatch.\n expected: ${platformMeta.portable_sha256}\n got: ${actual}`);
|
|
302
|
+
}
|
|
303
|
+
log(' Checksum verified.');
|
|
304
|
+
|
|
305
|
+
step('Extracting');
|
|
306
|
+
const targetDir = versionDir(version);
|
|
307
|
+
log(` Target: ${targetDir}`);
|
|
308
|
+
// If a previous extraction left a half-written tree, blow it away.
|
|
309
|
+
if (fs.existsSync(targetDir)) {
|
|
310
|
+
log(' Removing previous half-extracted tree...');
|
|
311
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
extractArchive(archivePath, targetDir);
|
|
314
|
+
|
|
315
|
+
// 获取当前平台的二进制文件名
|
|
316
|
+
const binaryName = getBinaryName();
|
|
317
|
+
|
|
318
|
+
// build_portable.bat / build_cli_mac.sh puts everything inside an extra top-level
|
|
319
|
+
// folder. If we see exactly one subdir and the binary is one level deeper,
|
|
320
|
+
// flatten so realBinaryPath() resolves cleanly.
|
|
321
|
+
const entries = fs.readdirSync(targetDir);
|
|
322
|
+
if (entries.length === 1) {
|
|
323
|
+
const inner = path.join(targetDir, entries[0]);
|
|
324
|
+
if (fs.statSync(inner).isDirectory() &&
|
|
325
|
+
fs.existsSync(path.join(inner, 'bin', 'uniner-cli', binaryName))) {
|
|
326
|
+
log(' Flattening extracted tree...');
|
|
327
|
+
for (const item of fs.readdirSync(inner)) {
|
|
328
|
+
fs.renameSync(path.join(inner, item), path.join(targetDir, item));
|
|
329
|
+
}
|
|
330
|
+
fs.rmdirSync(inner);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!isInstalled(version)) {
|
|
335
|
+
fail(`Extraction completed but the binary is not at ${realBinaryPath(version)}. ` +
|
|
336
|
+
`Inspect ${targetDir} to debug.`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
step('Cleaning up');
|
|
340
|
+
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
341
|
+
try { fs.rmdirSync(tmpDir); } catch (_) {}
|
|
342
|
+
log(' Removed temporary files.');
|
|
343
|
+
|
|
344
|
+
step('Installing Claude Code skill');
|
|
345
|
+
writeSkillMd(targetDir, version);
|
|
346
|
+
|
|
347
|
+
console.log('');
|
|
348
|
+
console.log('==========================================');
|
|
349
|
+
console.log(` uniner-cli ${version} installed`);
|
|
350
|
+
console.log('==========================================');
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log(` Location: ${targetDir}`);
|
|
353
|
+
console.log(` Skill: ${skillFile()}`);
|
|
354
|
+
console.log('');
|
|
355
|
+
|
|
356
|
+
// macOS 首次运行提示(无代码签名)
|
|
357
|
+
if (platformInfo.platform === 'darwin') {
|
|
358
|
+
console.log(' ⚠️ macOS 首次运行提示:');
|
|
359
|
+
console.log(' 如果系统提示"无法验证开发者",请前往:');
|
|
360
|
+
console.log(' 系统设置 → 隐私与安全性 → 仍要打开');
|
|
361
|
+
console.log('');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Linux headless 环境提示
|
|
365
|
+
if (platformInfo.platform === 'linux') {
|
|
366
|
+
console.log(' ℹ️ Linux 环境提示:');
|
|
367
|
+
console.log(' 浏览器自动化(web 模块)在 headless 环境下');
|
|
368
|
+
console.log(' 可能需要 xvfb 或设置 Playwright HTTPS-only 模式');
|
|
369
|
+
console.log('');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log(' Validate: uniner-cli --help');
|
|
373
|
+
console.log('');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
main().catch(err => {
|
|
377
|
+
console.error('');
|
|
378
|
+
console.error(err.message || String(err));
|
|
379
|
+
console.error('');
|
|
380
|
+
console.error('Install failed. To retry:');
|
|
381
|
+
console.error(' npm uninstall -g uniner-cli');
|
|
382
|
+
console.error(' npm install -g uniner-cli');
|
|
383
|
+
console.error('');
|
|
384
|
+
process.exit(1);
|
|
385
|
+
});
|