@kisev/skills-opencode 3.1.1 → 3.2.1

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 CHANGED
@@ -14,9 +14,8 @@ update, and removal lifecycle.
14
14
  - Capability routing plus direct CLI diagnostics, reconciliation, and profiles.
15
15
  - Optional `rules-injector`, `rtk`, and `zed-bell` plugin wrappers.
16
16
 
17
- The package does not contain, install, or update portable skills. Confirmed
18
- reconcile can invoke the pinned `skills` CLI directly to remove a marked retired
19
- skill from OpenCode and Codex.
17
+ The package does not contain, install, update, inspect, or remove portable skills.
18
+ Their lifecycle is owned by the `skills` CLI.
20
19
 
21
20
  ## Requirements
22
21
 
package/README.ru.md CHANGED
@@ -15,9 +15,8 @@
15
15
  управления профилями агентов.
16
16
  - Необязательные обертки плагинов `rules-injector`, `rtk` и `zed-bell`.
17
17
 
18
- Пакет не содержит, не устанавливает и не обновляет переносимые навыки. После
19
- подтверждения `reconcile` может напрямую вызвать закреплённую версию `skills` и
20
- удалить помеченный устаревший навык из OpenCode и Codex.
18
+ Пакет не содержит, не устанавливает, не обновляет, не проверяет и не удаляет
19
+ переносимые навыки. Их жизненным циклом управляет CLI `skills`.
21
20
 
22
21
  ## Требования
23
22
 
@@ -198,9 +198,7 @@ export function renderReconcile(plan, options) {
198
198
  if (plan.unknown.length)
199
199
  lines.push("", "Unknown (preserved):", ...plan.unknown.map((entry) => ` ${terminalSafe(entry.path)} (${terminalSafe(entry.reason)})`));
200
200
  if (plan.operations.length)
201
- lines.push("", "Operations:", ...plan.operations.map((entry) => ` ${entry.operation}: ${terminalSafe(entry.path)}${entry.via ? ` via ${entry.via}` : ""}`));
202
- if (plan.portable_cleanup)
203
- lines.push("", "Direct portable cleanup after confirmation:", ` ${commandLine(plan.portable_cleanup.command)}`);
201
+ lines.push("", "Operations:", ...plan.operations.map((entry) => ` ${entry.operation}: ${terminalSafe(entry.path)}`));
204
202
  if (plan.conflicts.length)
205
203
  lines.push("", "Conflicts:", ...plan.conflicts.map((entry) => ` ${terminalSafe(entry.path)} (${terminalSafe(entry.reason)})`));
206
204
  if (plan.modified_managed.length)
package/dist/cli.js CHANGED
@@ -310,7 +310,7 @@ function contextualHelp(arguments_) {
310
310
  ], [
311
311
  "Preview is read-only and blocks Apply on modified managed files or ownership conflicts.",
312
312
  "Confirmed reconcile archives exact-owned retired package assets.",
313
- "Marked retired portable skills are removed by the pinned skills CLI.",
313
+ "Portable skills are managed separately by the skills CLI.",
314
314
  ], [
315
315
  shellCommand(["reconcile", "--global", "--dry-run"]),
316
316
  shellCommand(["reconcile", "--dry-run", "--json"]),
@@ -1,5 +1,4 @@
1
1
  import { LifecycleError, type SupersededPlan, type Scope, type TransactionOptions } from "./lifecycle.js";
2
- declare const PORTABLE_SOURCE: "https://kisev.github.io/skills";
3
2
  export type ReconcileStatus = "current" | "retired" | "renamed" | "modified-managed" | "user-owned" | "unknown" | "conflict" | "diagnostic-state-only";
4
3
  export type ReconcileItem = {
5
4
  path: string;
@@ -24,33 +23,9 @@ export type ReconcilePlan = {
24
23
  diagnostic_state_only: ReconcileItem[];
25
24
  operations: Array<{
26
25
  path: string;
27
- operation: "archive" | "execute" | "remove" | "write";
26
+ operation: "archive" | "remove" | "write";
28
27
  sha256?: string;
29
- via?: "skills-cli" | "transaction";
30
28
  }>;
31
- portable_cleanup?: {
32
- source: typeof PORTABLE_SOURCE;
33
- names: string[];
34
- command: string[];
35
- cwd: string;
36
- environment: {
37
- HOME: string;
38
- XDG_CONFIG_HOME: string;
39
- XDG_STATE_HOME?: string;
40
- CODEX_HOME: string;
41
- };
42
- locks: Array<{
43
- path: string;
44
- before_sha256: string;
45
- after_sha256: string;
46
- }>;
47
- trees: Array<{
48
- name: string;
49
- path: string;
50
- tree_sha256: string;
51
- files: number;
52
- }>;
53
- };
54
29
  plan_digest: string;
55
30
  confirmation_digest?: string;
56
31
  superseded_plan?: SupersededPlan;
@@ -62,24 +37,11 @@ export type ReconcileResult = {
62
37
  status: "ok";
63
38
  applied: true;
64
39
  plan: ReconcilePlan;
65
- portable_remove?: PortableRemoveResult;
66
- };
67
- export type PortableRemoveResult = {
68
- code: number | null;
69
- signal: NodeJS.Signals | null;
70
- stdout: string;
71
- stderr: string;
72
- stdout_truncated: boolean;
73
- stderr_truncated: boolean;
74
- timed_out?: boolean;
75
- };
76
- export type ReconcileOptions = TransactionOptions & {
77
- runPortableRemove?: (command: readonly string[], cwd: string, environment: NodeJS.ProcessEnv) => Promise<PortableRemoveResult>;
78
40
  };
41
+ export type ReconcileOptions = TransactionOptions;
79
42
  export declare class ReconcileError extends LifecycleError {
80
43
  }
81
44
  export declare function previewReconcile(scope: Scope, cwd?: string, home?: string): Promise<ReconcilePlan>;
82
45
  /** Build the ownership classification without receipts, locks, or recovery. */
83
46
  export declare function inspectReconcile(scope: Scope, cwd?: string, home?: string): Promise<ReconcilePlan>;
84
47
  export declare function applyReconcile(scope: Scope, confirmationDigest: string, cwd?: string, home?: string, options?: ReconcileOptions): Promise<ReconcileResult>;
85
- export {};
package/dist/reconcile.js CHANGED
@@ -1,23 +1,13 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { spawn } from "node:child_process";
3
- import { lstat, opendir, readdir, readFile } from "node:fs/promises";
2
+ import { lstat, readdir, readFile } from "node:fs/promises";
4
3
  import { homedir } from "node:os";
5
4
  import { dirname, join, relative, resolve, sep } from "node:path";
6
5
  import { fileURLToPath } from "node:url";
7
6
  import { applyTransaction, archiveRoot, assertSafePath, consumeReceipt, destination, digest, LifecycleError, lifecycleRoot, recoverTransaction, saveReceipt, supersedeReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
8
7
  import { archiveMutations } from "./installer.js";
9
- import { skillsInstallerSpec } from "./package-metadata.js";
10
8
  const PACKAGE_NAME = "@kisev/skills-opencode";
11
9
  const GENERIC_MANIFEST = ".skills-opencode-manifest.json";
12
10
  const SEMANTIC_MANIFEST = ".skills-opencode/agent-profiles.manifest.json";
13
- const PORTABLE_SOURCE = "https://kisev.github.io/skills";
14
- const PROCESS_OUTPUT_LIMIT = 64 * 1024;
15
- const PROCESS_TIMEOUT_MS = 120_000;
16
- const PORTABLE_FILE_LIMIT = 4 * 1024 * 1024;
17
- const PORTABLE_TREE_FILE_LIMIT = 256;
18
- const PORTABLE_TREE_BYTE_LIMIT = 16 * 1024 * 1024;
19
- const PORTABLE_TREE_ENTRY_LIMIT = 1_024;
20
- const PORTABLE_TREE_DEPTH_LIMIT = 32;
21
11
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
22
12
  const assetsRoot = resolve(packageRoot, "dist", "assets");
23
13
  const inventoryPath = resolve(assetsRoot, "migration-inventory.json");
@@ -27,14 +17,6 @@ function loadInventory() {
27
17
  const value = JSON.parse(readFileSync(inventoryPath, "utf8"));
28
18
  if (value.schema_version !== 2 ||
29
19
  typeof value.inventory_version !== "string" ||
30
- !Array.isArray(value.active_portable_skills) ||
31
- !Array.isArray(value.removed) ||
32
- !value.renamed ||
33
- typeof value.renamed !== "object" ||
34
- Array.isArray(value.renamed) ||
35
- !value.replacements ||
36
- typeof value.replacements !== "object" ||
37
- Array.isArray(value.replacements) ||
38
20
  !Array.isArray(value.records)) {
39
21
  throw new ReconcileError("invalid_inventory", "Migration inventory has an unsupported schema");
40
22
  }
@@ -43,46 +25,8 @@ function loadInventory() {
43
25
  function scopeRoot(scope, cwd, home) {
44
26
  return scope === "global" ? resolve(home) : resolve(cwd);
45
27
  }
46
- function environmentRoot(name, home, fallback) {
47
- const configured = home === homedir() ? process.env[name]?.trim() : undefined;
48
- return resolve(configured || resolve(home, fallback));
49
- }
50
- function portableEnvironment(home) {
51
- const xdgState = home === homedir() ? process.env.XDG_STATE_HOME?.trim() : undefined;
52
- return {
53
- HOME: resolve(home),
54
- XDG_CONFIG_HOME: environmentRoot("XDG_CONFIG_HOME", home, ".config"),
55
- ...(xdgState ? { XDG_STATE_HOME: resolve(xdgState) } : {}),
56
- CODEX_HOME: environmentRoot("CODEX_HOME", home, ".codex"),
57
- };
58
- }
59
- function portableRoots(scope, cwd, home) {
60
- if (scope === "project")
61
- return [resolve(cwd, ".agents", "skills")];
62
- const environment = portableEnvironment(home);
63
- return [
64
- resolve(home, ".agents", "skills"),
65
- resolve(environment.XDG_CONFIG_HOME, "opencode", "skills"),
66
- resolve(environment.CODEX_HOME, "skills"),
67
- ].filter((value, index, values) => values.indexOf(value) === index);
68
- }
69
28
  function reconcileAllowedRoots(scope, cwd, home) {
70
- const environment = portableEnvironment(home);
71
- const lock = scope === "global"
72
- ? environment.XDG_STATE_HOME
73
- ? resolve(environment.XDG_STATE_HOME, "skills")
74
- : resolve(home, ".agents")
75
- : resolve(cwd);
76
- return [archiveRoot(scope, cwd, home), ...portableRoots(scope, cwd, home), lock];
77
- }
78
- function displayPath(root, target) {
79
- const value = relative(resolve(root), resolve(target));
80
- return !value.startsWith(`..${sep}`) && value !== ".." && !value.startsWith(sep)
81
- ? value.split(sep).join("/")
82
- : resolve(target);
83
- }
84
- function safePortableName(name) {
85
- return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
29
+ return [archiveRoot(scope, cwd, home)];
86
30
  }
87
31
  function relativePath(root, target) {
88
32
  const value = relative(resolve(root), resolve(target));
@@ -126,157 +70,6 @@ async function regular(path) {
126
70
  return { unsafe: true };
127
71
  return { content: await readFile(path), mode: Number(info.mode) & 0o777 };
128
72
  }
129
- async function filesUnder(root) {
130
- const info = await metadata(root);
131
- if (!info)
132
- return [];
133
- if (info.isSymbolicLink() || !info.isDirectory())
134
- return [{ relative: "", absolute: root, unsafe: true }];
135
- const result = [];
136
- let fileCount = 0;
137
- let totalBytes = 0;
138
- let entryCount = 0;
139
- let exceeded = false;
140
- const exceed = (absolute) => {
141
- if (!exceeded)
142
- result.push({ relative: relativePath(root, absolute), absolute, unsafe: true });
143
- exceeded = true;
144
- };
145
- async function visit(directory, depth) {
146
- if (exceeded)
147
- return;
148
- if (depth > PORTABLE_TREE_DEPTH_LIMIT) {
149
- exceed(directory);
150
- return;
151
- }
152
- const entries = [];
153
- const handle = await opendir(directory);
154
- for await (const entry of handle) {
155
- entryCount += 1;
156
- if (entryCount > PORTABLE_TREE_ENTRY_LIMIT) {
157
- exceed(directory);
158
- return;
159
- }
160
- entries.push(entry);
161
- }
162
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
163
- if (exceeded)
164
- return;
165
- const absolute = join(directory, entry.name);
166
- const child = await metadata(absolute);
167
- if (!child || child.isSymbolicLink()) {
168
- result.push({ relative: relativePath(root, absolute), absolute, unsafe: true });
169
- }
170
- else if (child.isDirectory()) {
171
- await visit(absolute, depth + 1);
172
- }
173
- else if (child.isFile() && child.nlink === 1) {
174
- fileCount += 1;
175
- totalBytes += Number(child.size);
176
- if (fileCount > PORTABLE_TREE_FILE_LIMIT ||
177
- Number(child.size) > PORTABLE_FILE_LIMIT ||
178
- totalBytes > PORTABLE_TREE_BYTE_LIMIT) {
179
- exceed(absolute);
180
- return;
181
- }
182
- result.push({
183
- relative: relativePath(root, absolute),
184
- absolute,
185
- content: await readFile(absolute),
186
- mode: Number(child.mode) & 0o777,
187
- });
188
- }
189
- else {
190
- result.push({ relative: relativePath(root, absolute), absolute, unsafe: true });
191
- }
192
- }
193
- }
194
- await visit(root, 0);
195
- return result;
196
- }
197
- function portableIdentity(content) {
198
- const text = content.toString("utf8");
199
- if (!text.startsWith("---\n"))
200
- return undefined;
201
- const end = text.indexOf("\n---\n", 4);
202
- if (end < 0)
203
- return undefined;
204
- const lines = text.slice(4, end).split("\n");
205
- const scalar = (value) => {
206
- const trimmed = value.trim();
207
- if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("{"))
208
- return undefined;
209
- const first = trimmed[0];
210
- const last = trimmed.at(-1);
211
- if (first === "'" || first === '"') {
212
- if (last !== first || trimmed.length < 2)
213
- return undefined;
214
- const inner = trimmed.slice(1, -1);
215
- return inner.includes(first) ? undefined : inner;
216
- }
217
- return last === "'" || last === '"' ? undefined : trimmed;
218
- };
219
- const allowed = new Set([
220
- "name",
221
- "description",
222
- "license",
223
- "compatibility",
224
- "metadata",
225
- "allowed-tools",
226
- ]);
227
- const fields = new Map();
228
- const metadata = new Map();
229
- let current = "";
230
- let block = false;
231
- for (const line of lines) {
232
- if (!line)
233
- continue;
234
- if (!line.startsWith(" ")) {
235
- const match = /^([a-z][a-z0-9-]*):\s*(.*)$/.exec(line);
236
- if (!match || !allowed.has(match[1]) || fields.has(match[1]))
237
- return undefined;
238
- current = match[1];
239
- const value = match[2];
240
- block = value === ">-" || value === "|" || value === "|-";
241
- if (current === "metadata") {
242
- if (value || block)
243
- return undefined;
244
- }
245
- else if (!block && scalar(value) === undefined)
246
- return undefined;
247
- fields.set(current, value);
248
- continue;
249
- }
250
- if (current === "metadata") {
251
- const match = /^ ([a-z][a-z0-9-]*):\s*(.*)$/.exec(line);
252
- if (!match || metadata.has(match[1]))
253
- return undefined;
254
- const value = scalar(match[2]);
255
- if (value === undefined)
256
- return undefined;
257
- metadata.set(match[1], value);
258
- continue;
259
- }
260
- if (!block || !/^ \S/.test(line))
261
- return undefined;
262
- }
263
- if (!["name", "description", "license", "metadata"].every((key) => fields.has(key)))
264
- return undefined;
265
- const name = scalar(fields.get("name"));
266
- const source = metadata.get("source");
267
- return name ? { name, ...(source ? { source } : {}) } : undefined;
268
- }
269
- function treeDigest(files) {
270
- return digest({
271
- schema_version: 1,
272
- files: files.map((file) => ({
273
- path: file.relative,
274
- mode: file.mode,
275
- size: file.content?.length,
276
- sha256: file.content ? sha256(file.content) : undefined,
277
- })),
278
- });
279
- }
280
73
  function item(path, status, reason, content, replacement) {
281
74
  return {
282
75
  path,
@@ -350,119 +143,10 @@ async function diagnostic(home) {
350
143
  }
351
144
  return result;
352
145
  }
353
- function portableRemoveCommand(scope, names) {
354
- return [
355
- "npx",
356
- "--yes",
357
- skillsInstallerSpec(),
358
- "remove",
359
- ...names,
360
- "--agent",
361
- "opencode",
362
- "--agent",
363
- "codex",
364
- ...(scope === "global" ? ["--global"] : []),
365
- "--yes",
366
- ];
367
- }
368
- function boundedAppend(current, chunk) {
369
- if (current.length >= PROCESS_OUTPUT_LIMIT)
370
- return { value: current, truncated: true };
371
- const remaining = PROCESS_OUTPUT_LIMIT - current.length;
372
- return {
373
- value: Buffer.concat([current, chunk.subarray(0, remaining)]),
374
- truncated: chunk.length > remaining,
375
- };
376
- }
377
- async function runPortableRemove(command, cwd, environment) {
378
- return new Promise((resolvePromise, reject) => {
379
- const windows = process.platform === "win32";
380
- if (windows && command.some((value) => !/^[A-Za-z0-9_./:@=-]+$/.test(value))) {
381
- reject(new ReconcileError("invalid_plan", "Portable remover has an unsafe Windows argument"));
382
- return;
383
- }
384
- const executable = windows ? (process.env.ComSpec ?? "cmd.exe") : command[0];
385
- const arguments_ = windows
386
- ? ["/d", "/s", "/c", `npx.cmd ${command.slice(1).join(" ")}`]
387
- : command.slice(1);
388
- const child = spawn(executable, arguments_, {
389
- cwd,
390
- env: environment,
391
- shell: false,
392
- detached: !windows,
393
- stdio: ["ignore", "pipe", "pipe"],
394
- });
395
- let stdout = Buffer.alloc(0);
396
- let stderr = Buffer.alloc(0);
397
- let stdoutTruncated = false;
398
- let stderrTruncated = false;
399
- let timedOut = false;
400
- let forceTimeout;
401
- const terminate = (signal) => {
402
- if (!windows && child.pid) {
403
- try {
404
- process.kill(-child.pid, signal);
405
- return;
406
- }
407
- catch { }
408
- }
409
- if (windows && child.pid) {
410
- const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
411
- stdio: "ignore",
412
- windowsHide: true,
413
- });
414
- killer.on("error", () => undefined);
415
- }
416
- child.kill(signal);
417
- };
418
- const timeout = setTimeout(() => {
419
- timedOut = true;
420
- terminate("SIGTERM");
421
- forceTimeout = setTimeout(() => terminate("SIGKILL"), 5_000);
422
- }, PROCESS_TIMEOUT_MS);
423
- child.stdout.on("data", (chunk) => {
424
- const next = boundedAppend(stdout, chunk);
425
- stdout = next.value;
426
- stdoutTruncated ||= next.truncated;
427
- });
428
- child.stderr.on("data", (chunk) => {
429
- const next = boundedAppend(stderr, chunk);
430
- stderr = next.value;
431
- stderrTruncated ||= next.truncated;
432
- });
433
- child.once("error", (error) => {
434
- clearTimeout(timeout);
435
- if (forceTimeout)
436
- clearTimeout(forceTimeout);
437
- reject(error);
438
- });
439
- child.once("close", (code, signal) => {
440
- clearTimeout(timeout);
441
- if (forceTimeout)
442
- clearTimeout(forceTimeout);
443
- resolvePromise({
444
- code,
445
- signal,
446
- stdout: stdout.toString("utf8"),
447
- stderr: stderr.toString("utf8"),
448
- stdout_truncated: stdoutTruncated,
449
- stderr_truncated: stderrTruncated,
450
- timed_out: timedOut,
451
- });
452
- });
453
- });
454
- }
455
146
  async function build(scope, cwd = process.cwd(), home = homedir()) {
456
147
  const inventory = loadInventory();
457
- const retiredPortableNames = new Set([
458
- ...inventory.removed,
459
- ...Object.keys(inventory.renamed),
460
- ...Object.keys(inventory.replacements),
461
- ]);
462
148
  const root = scopeRoot(scope, cwd, home);
463
149
  const deployment = resolve(root, scope === "global" ? ".config/opencode" : ".opencode");
464
- const portableLocations = portableRoots(scope, cwd, home);
465
- const portableEnv = portableEnvironment(home);
466
150
  const currentAssets = await packageAssets();
467
151
  const retired = retiredRecords(inventory, scope);
468
152
  const groups = {
@@ -476,9 +160,7 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
476
160
  "diagnostic-state-only": [],
477
161
  };
478
162
  const mutations = [];
479
- const portableMutationTargets = new Set();
480
163
  const archiveCandidates = [];
481
- const portableTrees = [];
482
164
  const managed = new Map();
483
165
  const manifestPath = join(deployment, GENERIC_MANIFEST);
484
166
  const manifestValue = await regular(manifestPath);
@@ -644,153 +326,6 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
644
326
  add(groups, item(relativePath(root, target), "user-owned", "agent has no semantic ownership record", value.content));
645
327
  }
646
328
  }
647
- const installed = new Map();
648
- for (const portableRoot of portableLocations) {
649
- const portableInfo = await metadata(portableRoot);
650
- if (portableInfo?.isSymbolicLink() || (portableInfo && !portableInfo.isDirectory())) {
651
- add(groups, item(displayPath(root, portableRoot), "conflict", "portable skill root is a symlink or non-directory"));
652
- continue;
653
- }
654
- for (const entry of portableInfo?.isDirectory() ? (await readdir(portableRoot)).sort() : []) {
655
- const directory = resolve(portableRoot, entry);
656
- const directoryInfo = await metadata(directory);
657
- if (!directoryInfo?.isDirectory() || directoryInfo.isSymbolicLink()) {
658
- add(groups, item(displayPath(root, directory), "unknown", "portable skill entry is an external symlink or non-directory"));
659
- continue;
660
- }
661
- const values = installed.get(entry) ?? [];
662
- values.push({ directory, portableRoot });
663
- installed.set(entry, values);
664
- }
665
- }
666
- for (const [entry, locations] of [...installed].sort(([left], [right]) => left.localeCompare(right))) {
667
- if (inventory.active_portable_skills.includes(entry)) {
668
- for (const { directory } of locations)
669
- add(groups, item(displayPath(root, directory), "current", "current portable skill source"));
670
- continue;
671
- }
672
- if (!safePortableName(entry)) {
673
- for (const { directory } of locations)
674
- add(groups, item(displayPath(root, directory), "conflict", "portable skill name is unsafe for delegated removal"));
675
- continue;
676
- }
677
- const candidates = [];
678
- let blocked = false;
679
- for (const { directory, portableRoot } of locations) {
680
- const path = displayPath(root, directory);
681
- const skill = await regular(resolve(directory, "SKILL.md"));
682
- if (!skill || "unsafe" in skill) {
683
- add(groups, item(path, "unknown", "portable skill has no safe SKILL.md ownership marker"));
684
- blocked = true;
685
- continue;
686
- }
687
- const identity = portableIdentity(skill.content);
688
- if (identity?.name !== entry || identity.source !== PORTABLE_SOURCE) {
689
- add(groups, item(path, "unknown", "portable skill is outside the marked project inventory"));
690
- blocked = true;
691
- continue;
692
- }
693
- const files = await filesUnder(directory);
694
- if (!files.length ||
695
- files.some((file) => file.unsafe || !file.content || file.mode === undefined)) {
696
- add(groups, item(path, "conflict", "marked portable skill contains an unsafe file"));
697
- blocked = true;
698
- continue;
699
- }
700
- candidates.push({ directory, portableRoot, files });
701
- }
702
- if (blocked)
703
- continue;
704
- if (!retiredPortableNames.has(entry)) {
705
- for (const candidate of candidates)
706
- add(groups, item(displayPath(root, candidate.directory), "unknown", "marked portable skill is not declared retired by this package inventory"));
707
- continue;
708
- }
709
- const replacement = inventory.renamed[entry];
710
- const status = replacement ? "renamed" : "retired";
711
- for (const candidate of candidates) {
712
- const path = displayPath(root, candidate.directory);
713
- add(groups, item(path, status, replacement
714
- ? "project source marker proves renamed portable ownership"
715
- : "project source marker proves retired portable ownership", undefined, replacement));
716
- portableTrees.push({
717
- name: entry,
718
- path,
719
- root: candidate.portableRoot,
720
- tree_sha256: treeDigest(candidate.files),
721
- files: candidate.files.length,
722
- });
723
- for (const file of candidate.files) {
724
- const target = `${entry}/${file.relative}`;
725
- archiveCandidates.push({
726
- path: displayPath(root, file.absolute),
727
- record: {
728
- sha256: sha256(file.content),
729
- mode: file.mode,
730
- kind: "state",
731
- },
732
- content: file.content,
733
- reason: `${status} portable skill`,
734
- kind: "state",
735
- });
736
- mutations.push({
737
- root: candidate.portableRoot,
738
- path: target,
739
- operation: "external-remove",
740
- expected: { sha256: sha256(file.content) },
741
- });
742
- portableMutationTargets.add(file.absolute);
743
- }
744
- }
745
- }
746
- const portableNames = [...new Set(portableTrees.map((tree) => tree.name))].sort();
747
- const locks = [];
748
- if (portableNames.length) {
749
- const lockPath = scope === "global"
750
- ? portableEnv.XDG_STATE_HOME
751
- ? resolve(portableEnv.XDG_STATE_HOME, "skills", ".skill-lock.json")
752
- : resolve(home, ".agents", ".skill-lock.json")
753
- : resolve(root, "skills-lock.json");
754
- const lock = await regular(lockPath);
755
- if (lock && "unsafe" in lock) {
756
- add(groups, item(lockPath, "conflict", "portable installer lock is unsafe"));
757
- }
758
- else if (lock) {
759
- try {
760
- const parsed = JSON.parse(lock.content.toString("utf8"));
761
- if (typeof parsed.version !== "number" || !parsed.skills || Array.isArray(parsed.skills))
762
- throw new Error("unsupported lock");
763
- if ((scope === "global" && parsed.version < 3) ||
764
- (scope === "project" && parsed.version < 1))
765
- throw new Error("unsupported lock version");
766
- const tracked = portableNames.filter((name) => Object.hasOwn(parsed.skills, name));
767
- if (tracked.length) {
768
- const remaining = Object.fromEntries(Object.entries(parsed.skills).filter(([name]) => !portableNames.includes(name)));
769
- const next = Buffer.from(scope === "global"
770
- ? JSON.stringify({ ...parsed, skills: remaining }, null, 2)
771
- : `${JSON.stringify({ version: parsed.version, skills: Object.fromEntries(Object.entries(remaining).sort(([left], [right]) => left.localeCompare(right))) }, null, 2)}\n`);
772
- const lockRoot = resolve(dirname(lockPath));
773
- mutations.push({
774
- root: lockRoot,
775
- path: lockPath.slice(lockRoot.length + 1),
776
- operation: "external-write",
777
- content: next,
778
- mode: (await lstat(lockPath)).mode & 0o777,
779
- expected: { sha256: sha256(lock.content) },
780
- });
781
- portableMutationTargets.add(lockPath);
782
- locks.push({
783
- path: lockPath,
784
- before_sha256: sha256(lock.content),
785
- after_sha256: sha256(next),
786
- });
787
- }
788
- }
789
- catch {
790
- add(groups, item(lockPath, "conflict", "portable installer lock is not valid JSON"));
791
- }
792
- }
793
- }
794
329
  if (archiveCandidates.length && manifestRaw) {
795
330
  for (const candidate of archiveCandidates) {
796
331
  delete manifestFiles[candidate.path];
@@ -808,10 +343,7 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
808
343
  });
809
344
  }
810
345
  mutations.push(...(await archiveMutations(archiveCandidates, scope, cwd, home, inventory.inventory_version)));
811
- const command = portableRemoveCommand(scope, portableNames);
812
- const operations = mutations
813
- .filter((mutation) => !portableMutationTargets.has(destination(resolve(mutation.root ?? root), mutation.path)))
814
- .map((mutation) => ({
346
+ const operations = mutations.map((mutation) => ({
815
347
  path: mutation.root ? resolve(mutation.root, mutation.path) : mutation.path,
816
348
  operation: mutation.root
817
349
  ? "archive"
@@ -824,19 +356,6 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
824
356
  ? { sha256: sha256(mutation.content) }
825
357
  : {}),
826
358
  }));
827
- for (const tree of portableTrees) {
828
- operations.push({ path: tree.path, operation: "archive", sha256: tree.tree_sha256 });
829
- operations.push({ path: tree.path, operation: "remove", via: "skills-cli" });
830
- }
831
- for (const lock of locks)
832
- operations.push({
833
- path: lock.path,
834
- operation: "write",
835
- sha256: lock.after_sha256,
836
- via: "skills-cli",
837
- });
838
- if (portableNames.length)
839
- operations.push({ path: command[0], operation: "execute", via: "skills-cli" });
840
359
  const base = {
841
360
  schema_version: 2,
842
361
  domain: "reconcile",
@@ -852,21 +371,6 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
852
371
  conflicts: groups.conflict.sort((left, right) => left.path.localeCompare(right.path)),
853
372
  diagnostic_state_only: await diagnostic(home),
854
373
  operations: operations.sort((left, right) => left.path.localeCompare(right.path)),
855
- ...(portableNames.length
856
- ? {
857
- portable_cleanup: {
858
- source: PORTABLE_SOURCE,
859
- names: portableNames,
860
- command,
861
- cwd: root,
862
- environment: portableEnv,
863
- locks,
864
- trees: portableTrees
865
- .map(({ root: _root, ...tree }) => tree)
866
- .sort((left, right) => left.path.localeCompare(right.path)),
867
- },
868
- }
869
- : {}),
870
374
  };
871
375
  const planDigest = digest(base);
872
376
  const confirmable = operations.length > 0 &&
@@ -936,38 +440,10 @@ export async function applyReconcile(scope, confirmationDigest, cwd = process.cw
936
440
  throw new ReconcileError("stale_plan", "Reconcile inventory changed after preview");
937
441
  if (built.plan.conflicts.length || built.plan.modified_managed.length)
938
442
  throw new ReconcileError("conflict", "Reconcile contains unsafe ownership conflicts");
939
- const cleanup = built.plan.portable_cleanup;
940
- let portableRemove;
941
443
  await applyTransaction(root, stateRoot, built.mutations, {
942
444
  ...options,
943
- applyExternal: async () => {
944
- await options.applyExternal?.();
945
- if (!cleanup)
946
- return;
947
- const environment = {
948
- ...process.env,
949
- ...cleanup.environment,
950
- DO_NOT_TRACK: "1",
951
- };
952
- if (!cleanup.environment.XDG_STATE_HOME)
953
- delete environment.XDG_STATE_HOME;
954
- const result = await (options.runPortableRemove ?? runPortableRemove)(cleanup.command, cleanup.cwd, environment);
955
- portableRemove = result;
956
- if (result.code !== 0 || result.signal || result.timed_out)
957
- throw new ReconcileError("portable_remove_failed", `skills remove failed with ${result.signal ?? `exit ${result.code}`}`);
958
- },
959
445
  validateFinal: async () => {
960
446
  await options.validateFinal?.();
961
- if (cleanup) {
962
- for (const tree of cleanup.trees)
963
- if (await metadata(resolve(root, tree.path)))
964
- throw new ReconcileError("final_validation_failed", `skills remove retained portable skill: ${tree.path}`);
965
- for (const expected of cleanup.locks) {
966
- const lock = await regular(expected.path);
967
- if (!lock || "unsafe" in lock || sha256(lock.content) !== expected.after_sha256)
968
- throw new ReconcileError("final_validation_failed", `skills remove left an invalid lock: ${expected.path}`);
969
- }
970
- }
971
447
  const final = await build(scope, cwd, home);
972
448
  if (final.plan.retired.length ||
973
449
  final.plan.renamed.length ||
@@ -979,7 +455,6 @@ export async function applyReconcile(scope, confirmationDigest, cwd = process.cw
979
455
  status: "ok",
980
456
  applied: true,
981
457
  plan: { ...built.plan, digest: confirmationDigest },
982
- ...(portableRemove ? { portable_remove: portableRemove } : {}),
983
458
  };
984
459
  });
985
460
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisev/skills-opencode",
3
- "version": "3.1.1",
3
+ "version": "3.2.1",
4
4
  "skillsInstallerVersion": "1.5.23",
5
5
  "description": "OpenCode integration and opt-in installer for portable Agent Skills.",
6
6
  "license": "MIT",