@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/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, digest: digest(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, { digest: built.plan.digest });
423
- return { ...built.plan, digest: receipt.digest, receipt_expires_at: receipt.expires_at };
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.digest)
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");
@@ -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
- Date.parse(existing.expires_at) >= now &&
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 { digest: receipt.digest, expires_at: receipt.expires_at };
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
- if (receipt.digest !==
298
- digest({
299
- schema_version: 1,
300
- kind: receipt.kind,
301
- scope: receipt.scope,
302
- root: receipt.root,
303
- payload: receipt.payload,
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
  }
@@ -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
- digest: string;
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
- return { plan: { ...base, digest: digest(base) }, mutations };
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
- digest: built.plan.digest,
452
- });
453
- return { ...built.plan, digest: receipt.digest, receipt_expires_at: receipt.expires_at };
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.digest !== payload.digest)
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisev/skills-opencode",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "OpenCode integration and opt-in installer for portable Agent Skills.",
5
5
  "license": "MIT",
6
6
  "repository": {