@kinlab/kin 0.3.5 → 0.5.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/README.md +12 -1
- package/lib/provision.mjs +352 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,11 @@ npx -y @kinlab/kin --version
|
|
|
16
16
|
npx -y @kinlab/kin setup --intent agent --no-interactive
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Native Windows x86_64 can install and run repository-free CLI diagnostics, but repository admission is currently unavailable: kin init fails closed, so graph, lexical, daemon, repository setup, MCP, and review workflows are unsupported. Use WSL2 for usable Kin repositories.
|
|
20
|
+
No native Windows ARM64 archive is published. An x64 Node process under Windows
|
|
21
|
+
emulation can provision the x86_64 archive for diagnostics; use WSL2 for the
|
|
22
|
+
repository workflow documented below.
|
|
23
|
+
|
|
19
24
|
## What it does
|
|
20
25
|
|
|
21
26
|
`@kinlab/kin` ships two thin launchers, not a JavaScript reimplementation:
|
|
@@ -31,7 +36,7 @@ The launcher and the shell installer (`scripts/install.sh`) share the same insta
|
|
|
31
36
|
contract (`$KIN_HOME`, default `~/.kin`): either lane satisfies the other, and neither
|
|
32
37
|
silently downgrades an install the other made.
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
On macOS, Linux, or WSL2, run `kin setup --intent agent` after provisioning. Setup writes the Kin MCP server into
|
|
35
40
|
detected AI clients with the `agent-default` tool profile, adds the managed bin directory
|
|
36
41
|
to your shell profile, installs the shell/session hook, and records the install ledger used
|
|
37
42
|
by `kin setup status`, `kin doctor --fix`, and `kin setup uninstall`.
|
|
@@ -75,5 +80,11 @@ Any MCP client can also use the included server manually:
|
|
|
75
80
|
}
|
|
76
81
|
```
|
|
77
82
|
|
|
83
|
+
`kin setup status` and `kin doctor` recognize this exact canonical wrapper topology instead
|
|
84
|
+
of flagging it for repair. Do not shorten `command` to a bare `kin`: agent clients do not
|
|
85
|
+
reliably inherit your shell `PATH`. Codex and Antigravity bindings additionally require
|
|
86
|
+
`"--repo", "/absolute/path/to/repository"` at the end of the argument vector; an Antigravity
|
|
87
|
+
workspace entry also uses that path as `cwd`.
|
|
88
|
+
|
|
78
89
|
`@kinlab/kin-mcp` remains published for existing configurations; new setups should use
|
|
79
90
|
this package.
|
package/lib/provision.mjs
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
writeLauncherStamp,
|
|
30
30
|
} from './resolve.mjs';
|
|
31
31
|
|
|
32
|
+
/** Parse the boolean vocabulary promised by Kin's generated env contract. */
|
|
33
|
+
export function isTruthyEnv(value) {
|
|
34
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
/**
|
|
33
38
|
* Release-artifact file name for a host. Follows the release workflow's
|
|
34
39
|
* naming (kin-{macos|linux|windows}-{x86_64|aarch64}); Windows ships as .zip.
|
|
@@ -79,14 +84,129 @@ export function sha256Hex(buf) {
|
|
|
79
84
|
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
/** Compact, stable byte count for first-install progress. */
|
|
88
|
+
export function formatByteCount(bytes) {
|
|
89
|
+
const value = Math.max(0, Number(bytes) || 0);
|
|
90
|
+
const units = ['B', 'KiB', 'MiB', 'GiB'];
|
|
91
|
+
let amount = value;
|
|
92
|
+
let unit = 0;
|
|
93
|
+
while (amount >= 1024 && unit < units.length - 1) {
|
|
94
|
+
amount /= 1024;
|
|
95
|
+
unit += 1;
|
|
96
|
+
}
|
|
97
|
+
const digits = unit === 0 ? 0 : 1;
|
|
98
|
+
return `${amount.toFixed(digits)} ${units[unit]}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Pure rendering helper so progress wording remains testable without a TTY. */
|
|
102
|
+
export function formatDownloadProgress(file, received, total = null) {
|
|
103
|
+
const transferred = formatByteCount(received);
|
|
104
|
+
if (Number.isFinite(total) && total > 0) {
|
|
105
|
+
const percent = Math.min(100, Math.floor((received * 100) / total));
|
|
106
|
+
return `kin: downloading ${file}: ${percent}% (${transferred} / ${formatByteCount(total)})`;
|
|
107
|
+
}
|
|
108
|
+
return `kin: downloading ${file}: ${transferred} received`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createDownloadProgress(file, stream = process.stderr) {
|
|
112
|
+
let lastAt = 0;
|
|
113
|
+
let lastBytes = 0;
|
|
114
|
+
let lastPercent = -1;
|
|
115
|
+
let active = false;
|
|
116
|
+
return ({ received, total, done = false, failed = false }) => {
|
|
117
|
+
if (failed) {
|
|
118
|
+
if (active) stream.write('\r\x1b[2K\n');
|
|
119
|
+
active = false;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const percent = Number.isFinite(total) && total > 0 ? Math.floor((received * 100) / total) : null;
|
|
124
|
+
if (!done) {
|
|
125
|
+
if (percent !== null && percent === lastPercent && now - lastAt < 250) return;
|
|
126
|
+
if (percent === null && received - lastBytes < 1024 * 1024 && now - lastAt < 250) return;
|
|
127
|
+
}
|
|
128
|
+
stream.write(`\r\x1b[2K${formatDownloadProgress(file, received, total)}${done ? '\n' : ''}`);
|
|
129
|
+
active = !done;
|
|
130
|
+
lastAt = now;
|
|
131
|
+
lastBytes = received;
|
|
132
|
+
lastPercent = percent;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
82
136
|
async function fetchBuffer(url, fetchImpl) {
|
|
83
137
|
const res = await fetchImpl(url);
|
|
84
138
|
if (!res.ok) {
|
|
85
139
|
throw new Error(`download failed (${res.status}) for ${url}`);
|
|
86
140
|
}
|
|
141
|
+
|
|
87
142
|
return Buffer.from(await res.arrayBuffer());
|
|
88
143
|
}
|
|
89
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Download one response directly into a staging file while hashing it. The
|
|
147
|
+
* streaming path consumes and writes one chunk before requesting the next, so
|
|
148
|
+
* archive-sized data is never accumulated in JavaScript memory. Lightweight
|
|
149
|
+
* injected fetches that expose only arrayBuffer() retain their existing one-
|
|
150
|
+
* buffer contract.
|
|
151
|
+
*/
|
|
152
|
+
export async function downloadToFile(url, destination, fetchImpl, onProgress = null) {
|
|
153
|
+
let received = 0;
|
|
154
|
+
let total = null;
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetchImpl(url);
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
throw new Error(`download failed (${res.status}) for ${url}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const contentLength = Number(res.headers?.get?.('content-length'));
|
|
162
|
+
total = Number.isFinite(contentLength) && contentLength > 0 ? contentLength : null;
|
|
163
|
+
const hash = crypto.createHash('sha256');
|
|
164
|
+
|
|
165
|
+
if (res.body && typeof res.body[Symbol.asyncIterator] === 'function') {
|
|
166
|
+
const fd = fs.openSync(destination, 'w', 0o600);
|
|
167
|
+
try {
|
|
168
|
+
for await (const chunk of res.body) {
|
|
169
|
+
const bytes = ArrayBuffer.isView(chunk)
|
|
170
|
+
? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
|
|
171
|
+
: Buffer.from(chunk);
|
|
172
|
+
let offset = 0;
|
|
173
|
+
while (offset < bytes.byteLength) {
|
|
174
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
175
|
+
if (written <= 0) throw new Error(`archive staging made no progress for ${url}`);
|
|
176
|
+
offset += written;
|
|
177
|
+
}
|
|
178
|
+
hash.update(bytes);
|
|
179
|
+
received += bytes.byteLength;
|
|
180
|
+
onProgress?.({ received, total, done: false });
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
fs.closeSync(fd);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Preserve offline/injected fetch compatibility without adding a second
|
|
187
|
+
// archive-sized allocation: Buffer.from(ArrayBuffer) is a shared view.
|
|
188
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
189
|
+
fs.writeFileSync(destination, bytes, { mode: 0o600 });
|
|
190
|
+
hash.update(bytes);
|
|
191
|
+
received = bytes.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (total !== null && received !== total) {
|
|
195
|
+
throw new Error(`download truncated for ${url}: expected ${total} bytes, received ${received}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
onProgress?.({ received, total: total ?? received, done: true });
|
|
199
|
+
return { bytes: received, sha256: hash.digest('hex') };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
try {
|
|
202
|
+
onProgress?.({ received, total, done: true, failed: true });
|
|
203
|
+
} catch {
|
|
204
|
+
// Progress cleanup is best-effort and must not hide the download error.
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
90
210
|
/**
|
|
91
211
|
* Locate the extraction root: release archives contain a kin-* subdirectory;
|
|
92
212
|
* tolerate a flat archive the way install.sh does.
|
|
@@ -106,6 +226,185 @@ function installFile(src, dest, mode) {
|
|
|
106
226
|
fs.chmodSync(dest, mode);
|
|
107
227
|
}
|
|
108
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Copy a directory tree, refusing anything that is not a directory or a regular
|
|
231
|
+
* file. Written out rather than delegating to fs.cpSync so a symlink inside an
|
|
232
|
+
* extracted archive stops the copy instead of being reproduced under $KIN_HOME,
|
|
233
|
+
* and so the executable bit is carried across deliberately.
|
|
234
|
+
*/
|
|
235
|
+
function copyTree(src, dest) {
|
|
236
|
+
const stat = fs.lstatSync(src);
|
|
237
|
+
if (stat.isSymbolicLink()) {
|
|
238
|
+
throw new Error(`refusing to copy symlink from the release archive: ${src}`);
|
|
239
|
+
}
|
|
240
|
+
if (stat.isDirectory()) {
|
|
241
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
242
|
+
for (const entry of fs.readdirSync(src)) {
|
|
243
|
+
copyTree(path.join(src, entry), path.join(dest, entry));
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!stat.isFile()) {
|
|
248
|
+
throw new Error(`refusing to copy a non-regular archive entry: ${src}`);
|
|
249
|
+
}
|
|
250
|
+
fs.copyFileSync(src, dest);
|
|
251
|
+
fs.chmodSync(dest, stat.mode & 0o111 ? 0o755 : 0o644);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Read-only counterpart to copyTree, used before any live install mutation. */
|
|
255
|
+
function validateTree(src) {
|
|
256
|
+
const stat = fs.lstatSync(src);
|
|
257
|
+
if (stat.isSymbolicLink()) {
|
|
258
|
+
throw new Error(`refusing symlink in the release archive: ${src}`);
|
|
259
|
+
}
|
|
260
|
+
if (stat.isDirectory()) {
|
|
261
|
+
for (const entry of fs.readdirSync(src)) {
|
|
262
|
+
validateTree(path.join(src, entry));
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!stat.isFile()) {
|
|
267
|
+
throw new Error(`refusing a non-regular archive entry: ${src}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Install the macOS notification bundle from an extracted release archive.
|
|
273
|
+
*
|
|
274
|
+
* macOS reads a notification's sender name, icon, and grouping from the posting
|
|
275
|
+
* process's bundle; a CLI has none, so without this every Kin notification is
|
|
276
|
+
* credited to Script Editor. That is a silent downgrade rather than a visible
|
|
277
|
+
* failure, which is why its absence is reported here rather than passed over.
|
|
278
|
+
*
|
|
279
|
+
* Replaced whole rather than merged, for the same reason install.sh does: a
|
|
280
|
+
* stale executable left inside a newer bundle breaks the signature seal macOS
|
|
281
|
+
* checks over the bundle as a unit.
|
|
282
|
+
*
|
|
283
|
+
* Returns the validated bundle source, or null on a platform with no bundle.
|
|
284
|
+
*/
|
|
285
|
+
function preflightNotifierBundle(root, platform) {
|
|
286
|
+
if (platform !== 'darwin') return null;
|
|
287
|
+
const src = path.join(root, 'KinNotifier.app');
|
|
288
|
+
if (!fs.existsSync(src)) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
'this macOS release archive carries no KinNotifier.app; refusing to replace binaries or ' +
|
|
291
|
+
'stamp the release with a missing notification identity',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const rootStat = fs.lstatSync(src);
|
|
295
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
296
|
+
throw new Error('KinNotifier.app in the macOS release archive is not a real directory');
|
|
297
|
+
}
|
|
298
|
+
validateTree(src);
|
|
299
|
+
for (const required of ['Contents/MacOS/KinNotifier', 'Contents/Info.plist']) {
|
|
300
|
+
const member = path.join(src, required);
|
|
301
|
+
if (!fs.existsSync(member)) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`this macOS release archive's KinNotifier.app is missing ${required}; refusing to ` +
|
|
304
|
+
'replace binaries or stamp the release',
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const stat = fs.lstatSync(member);
|
|
308
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
309
|
+
throw new Error(`KinNotifier.app/${required} is not a regular file`);
|
|
310
|
+
}
|
|
311
|
+
if (stat.size === 0) {
|
|
312
|
+
throw new Error(`KinNotifier.app/${required} is empty`);
|
|
313
|
+
}
|
|
314
|
+
if (required.endsWith('/KinNotifier') && (stat.mode & 0o111) === 0) {
|
|
315
|
+
throw new Error(`KinNotifier.app/${required} is not executable`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return src;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function installNotifierBundleSource(src, env) {
|
|
322
|
+
const libDir = path.join(kinHome(env), 'lib');
|
|
323
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
324
|
+
const dest = path.join(libDir, 'KinNotifier.app');
|
|
325
|
+
// Build the incoming tree beside the live one and swap, rather than removing
|
|
326
|
+
// the live one and copying into the gap it leaves. copyTree refuses a symlink
|
|
327
|
+
// or a non-regular entry part-way down, and a bundle interrupted there has a
|
|
328
|
+
// launchable executable with no `Info.plist` behind it, which every freshness
|
|
329
|
+
// check reads as an installed bundle. The updater stages and renames for the
|
|
330
|
+
// same reason.
|
|
331
|
+
const staged = path.join(libDir, '.KinNotifier.app.incoming');
|
|
332
|
+
try {
|
|
333
|
+
fs.rmSync(staged, { recursive: true, force: true });
|
|
334
|
+
copyTree(src, staged);
|
|
335
|
+
fs.chmodSync(path.join(staged, 'Contents', 'MacOS', 'KinNotifier'), 0o755);
|
|
336
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
337
|
+
fs.renameSync(staged, dest);
|
|
338
|
+
} finally {
|
|
339
|
+
fs.rmSync(staged, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Registering with LaunchServices is what lets the notification daemon
|
|
343
|
+
// validate the bundle; an unregistered app is refused outright. Authorization
|
|
344
|
+
// itself is NOT requested here: an unanswered prompt is recorded as a
|
|
345
|
+
// permanent denial, so it must be raised interactively by `kin setup`.
|
|
346
|
+
const lsregister =
|
|
347
|
+
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
|
|
348
|
+
if (fs.existsSync(lsregister)) {
|
|
349
|
+
spawnSync(lsregister, ['-f', dest], { stdio: 'ignore' });
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Validate and install the macOS bundle as one tree. */
|
|
355
|
+
export function installNotifierBundle(root, env, platform, log) {
|
|
356
|
+
void log;
|
|
357
|
+
const src = preflightNotifierBundle(root, platform);
|
|
358
|
+
return src ? installNotifierBundleSource(src, env) : false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function mergeEnvironment(base, overrides) {
|
|
362
|
+
const merged = { ...base };
|
|
363
|
+
for (const [name, value] of Object.entries(overrides || {})) {
|
|
364
|
+
for (const inherited of Object.keys(merged)) {
|
|
365
|
+
if (inherited.toLowerCase() === name.toLowerCase()) {
|
|
366
|
+
delete merged[inherited];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
merged[name] = value;
|
|
370
|
+
}
|
|
371
|
+
return merged;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function environmentValue(env, name) {
|
|
375
|
+
const key = Object.keys(env).find(
|
|
376
|
+
(candidate) => candidate.toLowerCase() === name.toLowerCase(),
|
|
377
|
+
);
|
|
378
|
+
return key === undefined ? undefined : env[key];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function windowsSystemTarPath(env) {
|
|
382
|
+
const systemRoot = environmentValue(env, 'SystemRoot');
|
|
383
|
+
if (!systemRoot) {
|
|
384
|
+
throw new Error('native Windows ZIP extraction requires SystemRoot');
|
|
385
|
+
}
|
|
386
|
+
return path.win32.join(systemRoot, 'System32', 'tar.exe');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function archiveExtraction(platform, env, file) {
|
|
390
|
+
if (platform === 'win32') {
|
|
391
|
+
if (process.platform === 'win32') {
|
|
392
|
+
return {
|
|
393
|
+
executable: windowsSystemTarPath(env),
|
|
394
|
+
args: ['-xf', file, '-C', '.'],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// Cross-target tests on Unix exercise genuine ZIP bytes with the host's
|
|
398
|
+
// deterministic system unzip. Production never installs Windows assets
|
|
399
|
+
// on a Unix host.
|
|
400
|
+
return {
|
|
401
|
+
executable: '/usr/bin/unzip',
|
|
402
|
+
args: ['-q', file, '-d', '.'],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return { executable: 'tar', args: ['-xf', file, '-C', '.'] };
|
|
406
|
+
}
|
|
407
|
+
|
|
109
408
|
/**
|
|
110
409
|
* Download, verify, and install the pinned Kin release. Returns the installed
|
|
111
410
|
* managed `kin` path. Mirrors scripts/install.sh: kin + kin-daemon are
|
|
@@ -118,34 +417,57 @@ export async function provision(version, opts = {}) {
|
|
|
118
417
|
arch = process.arch,
|
|
119
418
|
fetchImpl = fetch,
|
|
120
419
|
log = (line) => process.stderr.write(`${line}\n`),
|
|
420
|
+
onProgress,
|
|
121
421
|
} = opts;
|
|
122
422
|
|
|
123
423
|
const file = artifactName(platform, arch);
|
|
124
424
|
const url = releaseDownloadUrl(version, file);
|
|
125
425
|
log(`kin: provisioning managed kin ${version} (${file})...`);
|
|
126
426
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
throw new Error(
|
|
135
|
-
`SHA-256 mismatch for ${file}: expected ${expected}, got ${actual}. ` +
|
|
136
|
-
'The download may be corrupted or tampered with; refusing to install.',
|
|
137
|
-
);
|
|
138
|
-
}
|
|
427
|
+
// Interactive first runs show live bytes/percent; redirected/non-TTY npm
|
|
428
|
+
// invocations stay line-oriented. An injected callback keeps streaming fully
|
|
429
|
+
// testable without coupling fake fetch implementations to terminal behavior.
|
|
430
|
+
const archiveProgress =
|
|
431
|
+
onProgress === undefined && fetchImpl === globalThis.fetch && process.stderr.isTTY
|
|
432
|
+
? createDownloadProgress(file)
|
|
433
|
+
: onProgress;
|
|
139
434
|
|
|
140
435
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kinlab-kin-'));
|
|
141
436
|
try {
|
|
142
437
|
const archivePath = path.join(tmp, file);
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
438
|
+
// Wait for both concurrent fetches to settle before removing the staging
|
|
439
|
+
// directory. Promise.all would race cleanup against an archive stream when
|
|
440
|
+
// the small checksum request fails first.
|
|
441
|
+
const [archiveResult, checksumResult] = await Promise.allSettled([
|
|
442
|
+
downloadToFile(url, archivePath, fetchImpl, archiveProgress),
|
|
443
|
+
fetchBuffer(`${url}.sha256`, fetchImpl),
|
|
444
|
+
]);
|
|
445
|
+
if (archiveResult.status === 'rejected') throw archiveResult.reason;
|
|
446
|
+
if (checksumResult.status === 'rejected') throw checksumResult.reason;
|
|
447
|
+
|
|
448
|
+
const expected = parseSha256File(checksumResult.value.toString('utf8'));
|
|
449
|
+
const actual = archiveResult.value.sha256;
|
|
450
|
+
if (actual !== expected) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`SHA-256 mismatch for ${file}: expected ${expected}, got ${actual}. ` +
|
|
453
|
+
'The download may be corrupted or tampered with; refusing to install.',
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const toolEnv = mergeEnvironment(process.env, env);
|
|
458
|
+
const extraction = archiveExtraction(platform, toolEnv, file);
|
|
459
|
+
// Windows authority is the absolute System32 bsdtar, never a Git/MSYS or
|
|
460
|
+
// user-provided `tar` found through PATH. Relative operands also avoid the
|
|
461
|
+
// GNU remote-host interpretation of `C:\\...` paths.
|
|
462
|
+
const extracted = spawnSync(extraction.executable, extraction.args, {
|
|
463
|
+
cwd: tmp,
|
|
464
|
+
encoding: 'utf8',
|
|
465
|
+
env: toolEnv,
|
|
466
|
+
});
|
|
467
|
+
if (extracted.status !== 0) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`archive extraction failed: ${extracted.stderr || extracted.error?.message || 'extractor exited non-zero'}`,
|
|
470
|
+
);
|
|
149
471
|
}
|
|
150
472
|
|
|
151
473
|
const root = extractionRoot(tmp);
|
|
@@ -158,6 +480,12 @@ export async function provision(version, opts = {}) {
|
|
|
158
480
|
);
|
|
159
481
|
}
|
|
160
482
|
|
|
483
|
+
// A macOS bundle is part of the release contract, not an optional extra.
|
|
484
|
+
// Validate it before creating or replacing anything under KIN_HOME so a
|
|
485
|
+
// malformed upgrade cannot stamp new binaries while retaining a stale
|
|
486
|
+
// live notifier from the previous release.
|
|
487
|
+
const notifierSrc = preflightNotifierBundle(root, platform);
|
|
488
|
+
|
|
161
489
|
const binDir = path.join(kinHome(env), 'bin');
|
|
162
490
|
const libDir = path.join(kinHome(env), 'lib');
|
|
163
491
|
fs.mkdirSync(binDir, { recursive: true });
|
|
@@ -177,6 +505,10 @@ export async function provision(version, opts = {}) {
|
|
|
177
505
|
}
|
|
178
506
|
}
|
|
179
507
|
|
|
508
|
+
if (notifierSrc && installNotifierBundleSource(notifierSrc, env)) {
|
|
509
|
+
log('kin: notification identity installed (KinNotifier.app)');
|
|
510
|
+
}
|
|
511
|
+
|
|
180
512
|
writeLauncherStamp(version, env);
|
|
181
513
|
log(`kin: managed kin ${version} installed at ${binDir}`);
|
|
182
514
|
return path.join(binDir, binaryName('kin', platform));
|
|
@@ -234,13 +566,13 @@ export async function ensureProvisioned(opts = {}) {
|
|
|
234
566
|
return resolveManagedBinary('kin', env, platform);
|
|
235
567
|
}
|
|
236
568
|
const existing = resolveManagedBinary('kin', env, platform);
|
|
237
|
-
if (env.KIN_NO_PROVISION
|
|
569
|
+
if (isTruthyEnv(env.KIN_NO_PROVISION)) {
|
|
238
570
|
return existing;
|
|
239
571
|
}
|
|
240
572
|
|
|
241
573
|
const doProvision = () => provision(target, { env, platform, arch, fetchImpl, log });
|
|
242
574
|
|
|
243
|
-
if (env.KIN_LAUNCHER_ADOPT
|
|
575
|
+
if (isTruthyEnv(env.KIN_LAUNCHER_ADOPT)) {
|
|
244
576
|
return doProvision();
|
|
245
577
|
}
|
|
246
578
|
if (!existing) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinlab/kin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Canonical installer and launcher for Kin, the system of record for AI-written software. Provisions and runs the managed kin + kin-daemon release. MCP is one included mode (kin mcp start).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|