@kisev/skills-opencode 2.0.2 → 2.0.4
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 +158 -135
- package/README.ru.md +162 -173
- package/dist/agent-profiles.d.ts +4 -1
- package/dist/agent-profiles.js +13 -4
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/cli-output.js +28 -8
- package/dist/cli.js +11 -3
- package/dist/installer.d.ts +4 -1
- package/dist/installer.js +12 -4
- package/dist/lifecycle.d.ts +11 -1
- package/dist/lifecycle.js +58 -20
- package/dist/reconcile.d.ts +6 -2
- package/dist/reconcile.js +24 -6
- package/package.json +1 -1
package/dist/installer.js
CHANGED
|
@@ -404,8 +404,9 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
404
404
|
}
|
|
405
405
|
const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
|
|
406
406
|
const base = { schema_version: 2, action, scope, root, package_version: packageVersion(), selection, operations: sorted, requires_restart: (action === "install" && owned.manifest?.package_version !== packageVersion()) || profiles.plan.requires_restart || mutations.some((item) => item.path.startsWith("agents/") || item.path.startsWith("commands/") || item.path.startsWith("plugins/")) };
|
|
407
|
+
const planDigest = digest(base);
|
|
407
408
|
return {
|
|
408
|
-
plan: { ...base,
|
|
409
|
+
plan: { ...base, plan_digest: planDigest, digest: planDigest },
|
|
409
410
|
mutations,
|
|
410
411
|
expectedManifest: manifestContent,
|
|
411
412
|
profiles,
|
|
@@ -419,8 +420,15 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
|
|
|
419
420
|
if (await recoverTransaction(root, stateRoot))
|
|
420
421
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
421
422
|
const built = await build(action, scope, cwd, home, selection);
|
|
422
|
-
const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, {
|
|
423
|
-
return {
|
|
423
|
+
const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, { plan_digest: built.plan.digest }, Date.now(), built.plan.digest);
|
|
424
|
+
return {
|
|
425
|
+
...built.plan,
|
|
426
|
+
plan_digest: built.plan.digest,
|
|
427
|
+
confirmation_digest: receipt.digest,
|
|
428
|
+
digest: receipt.digest,
|
|
429
|
+
receipt_expires_at: receipt.expires_at,
|
|
430
|
+
...(receipt.superseded_plan ? { superseded_plan: receipt.superseded_plan } : {}),
|
|
431
|
+
};
|
|
424
432
|
});
|
|
425
433
|
}
|
|
426
434
|
catch (error) {
|
|
@@ -440,7 +448,7 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
|
|
|
440
448
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
441
449
|
const receipt = (await consumeReceipt(stateRoot, { digest: confirmationDigest, kind: `installer:${action}`, scope, root }));
|
|
442
450
|
const built = await build(action, scope, cwd, home, selection);
|
|
443
|
-
if (built.plan.digest !== receipt.
|
|
451
|
+
if (built.plan.digest !== receipt.plan_digest)
|
|
444
452
|
throw new InstallerError("stale_plan", "Installer plan changed after preview");
|
|
445
453
|
if (built.plan.operations.some((item) => item.operation === "conflict" && (item.reason === "unmanaged_file" || item.reason === "v1.0.0_agent_ownership_mismatch" || item.reason?.includes("collision")))) {
|
|
446
454
|
throw new InstallerError("conflict", "Installer plan contains an exact-name ownership conflict");
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export type FileMutation = {
|
|
|
21
21
|
operation: "remove";
|
|
22
22
|
expected: FileExpectation;
|
|
23
23
|
};
|
|
24
|
+
export type SupersededPlan = {
|
|
25
|
+
kind: string;
|
|
26
|
+
confirmation_digest: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
expires_at: string;
|
|
29
|
+
};
|
|
24
30
|
export declare function stable(value: unknown): string;
|
|
25
31
|
export declare function sha256(value: Buffer | string): string;
|
|
26
32
|
export declare function digest(value: unknown): string;
|
|
@@ -36,9 +42,11 @@ export declare function deploymentRoot(scope: Scope, cwd?: string, home?: string
|
|
|
36
42
|
export declare function lifecycleRoot(scope: Scope, cwd?: string, home?: string): string;
|
|
37
43
|
export declare function archiveRoot(scope: Scope, cwd?: string, home?: string): string;
|
|
38
44
|
export declare function withLifecycleLock<T>(stateRoot: string, callback: () => Promise<T>): Promise<T>;
|
|
39
|
-
export declare function saveReceipt(stateRoot: string, kind: string, scope: Scope, root: string, payload: unknown, now?: number): Promise<{
|
|
45
|
+
export declare function saveReceipt(stateRoot: string, kind: string, scope: Scope, root: string, payload: unknown, now?: number, planDigest?: string): Promise<{
|
|
40
46
|
digest: string;
|
|
47
|
+
plan_digest: string;
|
|
41
48
|
expires_at: string;
|
|
49
|
+
superseded_plan?: SupersededPlan;
|
|
42
50
|
}>;
|
|
43
51
|
export declare function consumeReceipt(stateRoot: string, expected: {
|
|
44
52
|
digest: string;
|
|
@@ -46,6 +54,8 @@ export declare function consumeReceipt(stateRoot: string, expected: {
|
|
|
46
54
|
scope: Scope;
|
|
47
55
|
root: string;
|
|
48
56
|
}, now?: number): Promise<unknown>;
|
|
57
|
+
/** Invalidate an active receipt when a preview is blocked before a replacement exists. */
|
|
58
|
+
export declare function supersedeReceipt(stateRoot: string, now?: number): Promise<void>;
|
|
49
59
|
export declare function recoverTransaction(root: string, stateRoot: string): Promise<boolean>;
|
|
50
60
|
export type TransactionOptions = {
|
|
51
61
|
beforePublish?: (index: number) => void;
|
package/dist/lifecycle.js
CHANGED
|
@@ -240,37 +240,52 @@ export async function withLifecycleLock(stateRoot, callback) {
|
|
|
240
240
|
function receiptPath(stateRoot) {
|
|
241
241
|
return join(stateRoot, "receipt.json");
|
|
242
242
|
}
|
|
243
|
-
export async function saveReceipt(stateRoot, kind, scope, root, payload, now = Date.now()) {
|
|
244
|
-
const confirmationDigest = digest({ schema_version: 1, kind, scope, root, payload });
|
|
243
|
+
export async function saveReceipt(stateRoot, kind, scope, root, payload, now = Date.now(), planDigest = digest(payload)) {
|
|
245
244
|
const existingRaw = await readRegular(receiptPath(stateRoot));
|
|
245
|
+
let superseded;
|
|
246
246
|
if (existingRaw) {
|
|
247
247
|
const existing = parseReceipt(existingRaw);
|
|
248
|
-
if (!existing.consumed &&
|
|
249
|
-
|
|
250
|
-
existing.digest !== confirmationDigest) {
|
|
251
|
-
throw new LifecycleError("active_receipt", "An unconsumed agent or installer plan is still active");
|
|
252
|
-
}
|
|
253
|
-
if (!existing.consumed &&
|
|
254
|
-
Date.parse(existing.expires_at) >= now &&
|
|
255
|
-
existing.digest === confirmationDigest) {
|
|
256
|
-
return { digest: existing.digest, expires_at: existing.expires_at };
|
|
248
|
+
if (!existing.consumed && Date.parse(existing.expires_at) >= now) {
|
|
249
|
+
superseded = existing;
|
|
257
250
|
}
|
|
258
251
|
}
|
|
252
|
+
const confirmationDigest = sha256(randomBytes(32));
|
|
259
253
|
const receipt = {
|
|
260
254
|
schema_version: 1,
|
|
261
255
|
digest: confirmationDigest,
|
|
256
|
+
plan_digest: planDigest,
|
|
262
257
|
nonce: randomBytes(32).toString("base64url"),
|
|
258
|
+
created_at: new Date(now).toISOString(),
|
|
263
259
|
expires_at: new Date(now + RECEIPT_TTL_MS).toISOString(),
|
|
264
260
|
consumed: false,
|
|
265
261
|
kind,
|
|
266
262
|
scope,
|
|
267
263
|
root,
|
|
268
264
|
payload,
|
|
265
|
+
...(superseded
|
|
266
|
+
? {
|
|
267
|
+
superseded_digests: [superseded.digest, ...(superseded.superseded_digests ?? [])],
|
|
268
|
+
}
|
|
269
|
+
: {}),
|
|
269
270
|
integrity: "",
|
|
270
271
|
};
|
|
271
272
|
receipt.integrity = receiptIntegrity(receipt);
|
|
272
273
|
await writeAtomic(receiptPath(stateRoot), Buffer.from(`${stable(receipt)}\n`), 0o600);
|
|
273
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
digest: receipt.digest,
|
|
276
|
+
plan_digest: receipt.plan_digest,
|
|
277
|
+
expires_at: receipt.expires_at,
|
|
278
|
+
...(superseded
|
|
279
|
+
? {
|
|
280
|
+
superseded_plan: {
|
|
281
|
+
kind: superseded.kind,
|
|
282
|
+
confirmation_digest: superseded.digest.slice(0, 12),
|
|
283
|
+
created_at: superseded.created_at,
|
|
284
|
+
expires_at: superseded.expires_at,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
: {}),
|
|
288
|
+
};
|
|
274
289
|
}
|
|
275
290
|
function parseReceipt(raw) {
|
|
276
291
|
let value;
|
|
@@ -285,23 +300,29 @@ function parseReceipt(raw) {
|
|
|
285
300
|
receipt.schema_version !== 1 ||
|
|
286
301
|
typeof receipt.digest !== "string" ||
|
|
287
302
|
!/^[a-f0-9]{64}$/.test(receipt.digest) ||
|
|
303
|
+
typeof receipt.plan_digest !== "string" ||
|
|
304
|
+
!/^[a-f0-9]{64}$/.test(receipt.plan_digest) ||
|
|
288
305
|
typeof receipt.nonce !== "string" ||
|
|
306
|
+
typeof receipt.created_at !== "string" ||
|
|
289
307
|
typeof receipt.expires_at !== "string" ||
|
|
290
308
|
typeof receipt.consumed !== "boolean" ||
|
|
309
|
+
(receipt.superseded !== undefined && typeof receipt.superseded !== "boolean") ||
|
|
291
310
|
typeof receipt.kind !== "string" ||
|
|
292
311
|
(receipt.scope !== "global" && receipt.scope !== "project") ||
|
|
293
312
|
typeof receipt.root !== "string" ||
|
|
313
|
+
(receipt.superseded_digests !== undefined &&
|
|
314
|
+
(!Array.isArray(receipt.superseded_digests) ||
|
|
315
|
+
!receipt.superseded_digests.every((value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value)))) ||
|
|
294
316
|
typeof receipt.integrity !== "string") {
|
|
295
317
|
throw new LifecycleError("invalid_receipt", "Receipt has an unsupported schema");
|
|
296
318
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
})) {
|
|
319
|
+
const payloadRecord = receipt.payload;
|
|
320
|
+
const payloadPlanDigest = payloadRecord &&
|
|
321
|
+
typeof payloadRecord === "object" &&
|
|
322
|
+
typeof payloadRecord.plan_digest === "string"
|
|
323
|
+
? payloadRecord.plan_digest
|
|
324
|
+
: digest(receipt.payload);
|
|
325
|
+
if (receipt.plan_digest !== payloadPlanDigest) {
|
|
305
326
|
throw new LifecycleError("invalid_receipt", "Receipt digest does not match its payload");
|
|
306
327
|
}
|
|
307
328
|
if (receipt.integrity !== receiptIntegrity(receipt))
|
|
@@ -323,8 +344,12 @@ export async function consumeReceipt(stateRoot, expected, now = Date.now()) {
|
|
|
323
344
|
receipt.kind !== expected.kind ||
|
|
324
345
|
receipt.scope !== expected.scope ||
|
|
325
346
|
receipt.root !== expected.root) {
|
|
347
|
+
if (receipt.superseded_digests?.includes(expected.digest))
|
|
348
|
+
throw new LifecycleError("superseded_plan", "Confirmation receipt was superseded by a newer plan");
|
|
326
349
|
throw new LifecycleError("confirmation_unknown", "Confirmation does not match the saved plan");
|
|
327
350
|
}
|
|
351
|
+
if (receipt.superseded)
|
|
352
|
+
throw new LifecycleError("superseded_plan", "Confirmation receipt was superseded by a newer plan");
|
|
328
353
|
if (receipt.consumed)
|
|
329
354
|
throw new LifecycleError("confirmation_consumed", "Confirmation receipt was already consumed");
|
|
330
355
|
if (Date.parse(receipt.expires_at) < now)
|
|
@@ -334,6 +359,19 @@ export async function consumeReceipt(stateRoot, expected, now = Date.now()) {
|
|
|
334
359
|
await writeAtomic(receiptPath(stateRoot), Buffer.from(`${stable(consumed)}\n`), 0o600);
|
|
335
360
|
return receipt.payload;
|
|
336
361
|
}
|
|
362
|
+
/** Invalidate an active receipt when a preview is blocked before a replacement exists. */
|
|
363
|
+
export async function supersedeReceipt(stateRoot, now = Date.now()) {
|
|
364
|
+
const path = receiptPath(stateRoot);
|
|
365
|
+
const raw = await readRegular(path);
|
|
366
|
+
if (!raw)
|
|
367
|
+
return;
|
|
368
|
+
const receipt = parseReceipt(raw);
|
|
369
|
+
if (receipt.consumed || receipt.superseded || Date.parse(receipt.expires_at) < now)
|
|
370
|
+
return;
|
|
371
|
+
const superseded = { ...receipt, consumed: true, superseded: true, integrity: "" };
|
|
372
|
+
superseded.integrity = receiptIntegrity(superseded);
|
|
373
|
+
await writeAtomic(path, Buffer.from(`${stable(superseded)}\n`), 0o600);
|
|
374
|
+
}
|
|
337
375
|
function journalPath(stateRoot) {
|
|
338
376
|
return join(stateRoot, "transaction-journal.json");
|
|
339
377
|
}
|
package/dist/reconcile.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LifecycleError, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
1
|
+
import { LifecycleError, type SupersededPlan, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
2
2
|
export type ReconcileStatus = "current" | "retired" | "archive-pending" | "renamed" | "modified-managed" | "user-owned" | "unknown" | "conflict" | "diagnostic-state-only";
|
|
3
3
|
export type ReconcileItem = {
|
|
4
4
|
path: string;
|
|
@@ -27,7 +27,11 @@ export type ReconcilePlan = {
|
|
|
27
27
|
operation: "remove" | "write";
|
|
28
28
|
sha256?: string;
|
|
29
29
|
}>;
|
|
30
|
-
|
|
30
|
+
plan_digest: string;
|
|
31
|
+
confirmation_digest?: string;
|
|
32
|
+
superseded_plan?: SupersededPlan;
|
|
33
|
+
confirmable: boolean;
|
|
34
|
+
digest?: string;
|
|
31
35
|
receipt_expires_at?: string;
|
|
32
36
|
};
|
|
33
37
|
export type ReconcileResult = {
|
package/dist/reconcile.js
CHANGED
|
@@ -3,7 +3,7 @@ import { lstat, readdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { applyTransaction, assertSafePath, consumeReceipt, destination, digest, LifecycleError, lifecycleRoot, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
6
|
+
import { applyTransaction, assertSafePath, consumeReceipt, destination, digest, LifecycleError, lifecycleRoot, recoverTransaction, saveReceipt, supersedeReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
7
7
|
import { archiveMutations } from "./installer.js";
|
|
8
8
|
const PACKAGE_NAME = "@kisev/skills-opencode";
|
|
9
9
|
const GENERIC_MANIFEST = ".skills-opencode-manifest.json";
|
|
@@ -437,7 +437,11 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
|
|
|
437
437
|
diagnostic_state_only: diagnostic(scope, cwd, home),
|
|
438
438
|
operations: operations.sort((left, right) => left.path.localeCompare(right.path)),
|
|
439
439
|
};
|
|
440
|
-
|
|
440
|
+
const planDigest = digest(base);
|
|
441
|
+
return {
|
|
442
|
+
plan: { ...base, plan_digest: planDigest, confirmable: true, digest: planDigest },
|
|
443
|
+
mutations,
|
|
444
|
+
};
|
|
441
445
|
}
|
|
442
446
|
export async function previewReconcile(scope, cwd = process.cwd(), home = homedir()) {
|
|
443
447
|
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
@@ -447,10 +451,24 @@ export async function previewReconcile(scope, cwd = process.cwd(), home = homedi
|
|
|
447
451
|
if (await recoverTransaction(root, stateRoot))
|
|
448
452
|
throw new ReconcileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
449
453
|
const built = await build(scope, cwd, home);
|
|
454
|
+
const blocked = built.plan.modified_managed.length > 0 || built.plan.conflicts.length > 0;
|
|
455
|
+
if (blocked) {
|
|
456
|
+
await supersedeReceipt(stateRoot);
|
|
457
|
+
const { digest: _digest, ...withoutReceipt } = built.plan;
|
|
458
|
+
return { ...withoutReceipt, confirmable: false };
|
|
459
|
+
}
|
|
450
460
|
const receipt = await saveReceipt(stateRoot, "reconcile", scope, root, {
|
|
451
|
-
|
|
452
|
-
});
|
|
453
|
-
return {
|
|
461
|
+
plan_digest: built.plan.plan_digest,
|
|
462
|
+
}, Date.now(), built.plan.plan_digest);
|
|
463
|
+
return {
|
|
464
|
+
...built.plan,
|
|
465
|
+
plan_digest: built.plan.plan_digest,
|
|
466
|
+
confirmation_digest: receipt.digest,
|
|
467
|
+
digest: receipt.digest,
|
|
468
|
+
confirmable: true,
|
|
469
|
+
receipt_expires_at: receipt.expires_at,
|
|
470
|
+
...(receipt.superseded_plan ? { superseded_plan: receipt.superseded_plan } : {}),
|
|
471
|
+
};
|
|
454
472
|
});
|
|
455
473
|
}
|
|
456
474
|
catch (error) {
|
|
@@ -479,7 +497,7 @@ export async function applyReconcile(scope, confirmationDigest, cwd = process.cw
|
|
|
479
497
|
root,
|
|
480
498
|
}));
|
|
481
499
|
const built = await build(scope, cwd, home);
|
|
482
|
-
if (built.plan.
|
|
500
|
+
if (built.plan.plan_digest !== payload.plan_digest)
|
|
483
501
|
throw new ReconcileError("stale_plan", "Reconcile inventory changed after preview");
|
|
484
502
|
if (built.plan.conflicts.length || built.plan.modified_managed.length)
|
|
485
503
|
throw new ReconcileError("conflict", "Reconcile contains unsafe ownership conflicts");
|