@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 +2 -3
- package/README.ru.md +2 -3
- package/dist/cli-output.js +1 -3
- package/dist/cli.js +1 -1
- package/dist/reconcile.d.ts +2 -40
- package/dist/reconcile.js +3 -528
- package/package.json +1 -1
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
|
|
18
|
-
|
|
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
|
-
|
|
20
|
-
удалить помеченный устаревший навык из OpenCode и Codex.
|
|
18
|
+
Пакет не содержит, не устанавливает, не обновляет, не проверяет и не удаляет
|
|
19
|
+
переносимые навыки. Их жизненным циклом управляет CLI `skills`.
|
|
21
20
|
|
|
22
21
|
## Требования
|
|
23
22
|
|
package/dist/cli-output.js
CHANGED
|
@@ -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)}
|
|
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
|
-
"
|
|
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"]),
|
package/dist/reconcile.d.ts
CHANGED
|
@@ -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" | "
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
}
|