@kisev/skills-opencode 2.4.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/README.ru.md +7 -7
- package/dist/assets/agents/manager.md +9 -7
- package/dist/assets/migration-inventory.json +16 -10
- package/dist/catalog.d.ts +3 -3
- package/dist/catalog.js +2 -4
- package/dist/cli-output.d.ts +2 -0
- package/dist/cli-output.js +22 -4
- package/dist/cli.js +153 -126
- package/dist/doctor.js +0 -1
- package/dist/index.d.ts +0 -136
- package/dist/index.js +1 -95
- package/dist/installer.d.ts +0 -1
- package/dist/installer.js +15 -11
- package/dist/lifecycle.d.ts +14 -1
- package/dist/lifecycle.js +67 -24
- package/dist/reconcile.d.ts +43 -5
- package/dist/reconcile.js +539 -71
- package/dist/registry.d.ts +2 -4
- package/dist/registry.js +5 -43
- package/package.json +1 -1
- package/dist/assets/commands/agent-profiles.md +0 -9
- package/dist/assets/commands/capabilities.md +0 -9
- package/dist/assets/commands/doctor.md +0 -9
- package/dist/assets/commands/doit.md +0 -10
- package/dist/assets/commands/lsp-report.md +0 -10
- package/dist/assets/commands/reconcile.md +0 -9
package/dist/doctor.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -49,74 +49,6 @@ declare const plugin: (input: PluginInput) => Promise<{
|
|
|
49
49
|
decision?: any;
|
|
50
50
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
51
51
|
};
|
|
52
|
-
capabilities: {
|
|
53
|
-
description: string;
|
|
54
|
-
args: {};
|
|
55
|
-
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
56
|
-
};
|
|
57
|
-
doctor: {
|
|
58
|
-
description: string;
|
|
59
|
-
args: {
|
|
60
|
-
scope: import("zod").ZodDefault<import("zod").ZodEnum<{
|
|
61
|
-
global: "global";
|
|
62
|
-
project: "project";
|
|
63
|
-
}>>;
|
|
64
|
-
};
|
|
65
|
-
execute(args: {
|
|
66
|
-
scope: "global" | "project";
|
|
67
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
68
|
-
};
|
|
69
|
-
agent_profiles: {
|
|
70
|
-
description: string;
|
|
71
|
-
args: {
|
|
72
|
-
action: import("zod").ZodEnum<{
|
|
73
|
-
list: "list";
|
|
74
|
-
model_set: "model_set";
|
|
75
|
-
critic_add: "critic_add";
|
|
76
|
-
critic_remove: "critic_remove";
|
|
77
|
-
}>;
|
|
78
|
-
phase: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
79
|
-
preview: "preview";
|
|
80
|
-
apply: "apply";
|
|
81
|
-
}>>;
|
|
82
|
-
scope: import("zod").ZodEnum<{
|
|
83
|
-
global: "global";
|
|
84
|
-
project: "project";
|
|
85
|
-
}>;
|
|
86
|
-
name: import("zod").ZodOptional<import("zod").ZodString>;
|
|
87
|
-
model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
88
|
-
variant: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
|
|
89
|
-
confirmation_digest: import("zod").ZodOptional<import("zod").ZodString>;
|
|
90
|
-
};
|
|
91
|
-
execute(args: {
|
|
92
|
-
action: "list" | "model_set" | "critic_add" | "critic_remove";
|
|
93
|
-
scope: "global" | "project";
|
|
94
|
-
phase?: "preview" | "apply" | undefined;
|
|
95
|
-
name?: string | undefined;
|
|
96
|
-
model?: string | undefined;
|
|
97
|
-
variant?: string | null | undefined;
|
|
98
|
-
confirmation_digest?: string | undefined;
|
|
99
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
100
|
-
};
|
|
101
|
-
reconcile: {
|
|
102
|
-
description: string;
|
|
103
|
-
args: {
|
|
104
|
-
phase: import("zod").ZodEnum<{
|
|
105
|
-
preview: "preview";
|
|
106
|
-
apply: "apply";
|
|
107
|
-
}>;
|
|
108
|
-
scope: import("zod").ZodEnum<{
|
|
109
|
-
global: "global";
|
|
110
|
-
project: "project";
|
|
111
|
-
}>;
|
|
112
|
-
confirmation_digest: import("zod").ZodOptional<import("zod").ZodString>;
|
|
113
|
-
};
|
|
114
|
-
execute(args: {
|
|
115
|
-
phase: "preview" | "apply";
|
|
116
|
-
scope: "global" | "project";
|
|
117
|
-
confirmation_digest?: string | undefined;
|
|
118
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
119
|
-
};
|
|
120
52
|
};
|
|
121
53
|
"tool.execute.before": (input: {
|
|
122
54
|
tool: string;
|
|
@@ -173,74 +105,6 @@ export declare const server: (input: PluginInput) => Promise<{
|
|
|
173
105
|
decision?: any;
|
|
174
106
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
175
107
|
};
|
|
176
|
-
capabilities: {
|
|
177
|
-
description: string;
|
|
178
|
-
args: {};
|
|
179
|
-
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
180
|
-
};
|
|
181
|
-
doctor: {
|
|
182
|
-
description: string;
|
|
183
|
-
args: {
|
|
184
|
-
scope: import("zod").ZodDefault<import("zod").ZodEnum<{
|
|
185
|
-
global: "global";
|
|
186
|
-
project: "project";
|
|
187
|
-
}>>;
|
|
188
|
-
};
|
|
189
|
-
execute(args: {
|
|
190
|
-
scope: "global" | "project";
|
|
191
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
192
|
-
};
|
|
193
|
-
agent_profiles: {
|
|
194
|
-
description: string;
|
|
195
|
-
args: {
|
|
196
|
-
action: import("zod").ZodEnum<{
|
|
197
|
-
list: "list";
|
|
198
|
-
model_set: "model_set";
|
|
199
|
-
critic_add: "critic_add";
|
|
200
|
-
critic_remove: "critic_remove";
|
|
201
|
-
}>;
|
|
202
|
-
phase: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
203
|
-
preview: "preview";
|
|
204
|
-
apply: "apply";
|
|
205
|
-
}>>;
|
|
206
|
-
scope: import("zod").ZodEnum<{
|
|
207
|
-
global: "global";
|
|
208
|
-
project: "project";
|
|
209
|
-
}>;
|
|
210
|
-
name: import("zod").ZodOptional<import("zod").ZodString>;
|
|
211
|
-
model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
212
|
-
variant: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
|
|
213
|
-
confirmation_digest: import("zod").ZodOptional<import("zod").ZodString>;
|
|
214
|
-
};
|
|
215
|
-
execute(args: {
|
|
216
|
-
action: "list" | "model_set" | "critic_add" | "critic_remove";
|
|
217
|
-
scope: "global" | "project";
|
|
218
|
-
phase?: "preview" | "apply" | undefined;
|
|
219
|
-
name?: string | undefined;
|
|
220
|
-
model?: string | undefined;
|
|
221
|
-
variant?: string | null | undefined;
|
|
222
|
-
confirmation_digest?: string | undefined;
|
|
223
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
224
|
-
};
|
|
225
|
-
reconcile: {
|
|
226
|
-
description: string;
|
|
227
|
-
args: {
|
|
228
|
-
phase: import("zod").ZodEnum<{
|
|
229
|
-
preview: "preview";
|
|
230
|
-
apply: "apply";
|
|
231
|
-
}>;
|
|
232
|
-
scope: import("zod").ZodEnum<{
|
|
233
|
-
global: "global";
|
|
234
|
-
project: "project";
|
|
235
|
-
}>;
|
|
236
|
-
confirmation_digest: import("zod").ZodOptional<import("zod").ZodString>;
|
|
237
|
-
};
|
|
238
|
-
execute(args: {
|
|
239
|
-
phase: "preview" | "apply";
|
|
240
|
-
scope: "global" | "project";
|
|
241
|
-
confirmation_digest?: string | undefined;
|
|
242
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
243
|
-
};
|
|
244
108
|
};
|
|
245
109
|
"tool.execute.before": (input: {
|
|
246
110
|
tool: string;
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,6 @@ import { CATEGORIES, RoutingGate, } from "./routing.js";
|
|
|
3
3
|
import rulesInjector from "./plugins/rules-injector.js";
|
|
4
4
|
import rtk from "./plugins/rtk.js";
|
|
5
5
|
import zedBell from "./plugins/zed-bell.js";
|
|
6
|
-
import { applyAgentProfileChange, listAgentProfiles, previewAgentProfileChange, } from "./agent-profiles.js";
|
|
7
|
-
import { applyReconcile, previewReconcile } from "./reconcile.js";
|
|
8
|
-
import { collectDoctorFacts } from "./doctor.js";
|
|
9
|
-
import { CATALOG } from "./catalog.js";
|
|
10
6
|
import { digest } from "./lifecycle.js";
|
|
11
7
|
export { rulesInjector, rtk, zedBell };
|
|
12
8
|
const plugin = (async (input) => {
|
|
@@ -91,98 +87,8 @@ const plugin = (async (input) => {
|
|
|
91
87
|
return JSON.stringify({ decision, receipt, status: "routed" });
|
|
92
88
|
},
|
|
93
89
|
});
|
|
94
|
-
const capabilities = tool({
|
|
95
|
-
description: "Show the bundled OpenCode capability catalog without installing or changing anything.",
|
|
96
|
-
args: {},
|
|
97
|
-
async execute() {
|
|
98
|
-
return JSON.stringify({ schema_version: 1, status: "ok", ...CATALOG });
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
const doctor = tool({
|
|
102
|
-
description: "Read package health and opt-in defaults without installing or repairing anything.",
|
|
103
|
-
args: { scope: tool.schema.enum(["global", "project"]).default("project") },
|
|
104
|
-
async execute(args, context) {
|
|
105
|
-
const host = input.client
|
|
106
|
-
? {
|
|
107
|
-
config: async () => (await input.client.config.get({ query: { directory: context.directory } })).data,
|
|
108
|
-
lsp: async () => (await input.client.lsp.status({ query: { directory: context.directory } })).data,
|
|
109
|
-
}
|
|
110
|
-
: undefined;
|
|
111
|
-
return JSON.stringify(await collectDoctorFacts(args.scope ?? "project", context.directory, undefined, host));
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
const agentProfiles = tool({
|
|
115
|
-
description: "List, preview, or apply package-owned OpenCode agent profile configuration without editing opencode.json.",
|
|
116
|
-
args: {
|
|
117
|
-
action: tool.schema.enum(["list", "model_set", "critic_add", "critic_remove"]),
|
|
118
|
-
phase: tool.schema.enum(["preview", "apply"]).optional(),
|
|
119
|
-
scope: tool.schema.enum(["global", "project"]),
|
|
120
|
-
name: tool.schema.string().optional(),
|
|
121
|
-
model: tool.schema.string().optional(),
|
|
122
|
-
variant: tool.schema.string().nullable().optional(),
|
|
123
|
-
confirmation_digest: tool.schema.string().optional(),
|
|
124
|
-
},
|
|
125
|
-
async execute(args) {
|
|
126
|
-
const cwd = input.directory ?? process.cwd();
|
|
127
|
-
if (args.action === "list") {
|
|
128
|
-
if (args.phase ||
|
|
129
|
-
args.name ||
|
|
130
|
-
args.model ||
|
|
131
|
-
args.variant !== undefined ||
|
|
132
|
-
args.confirmation_digest)
|
|
133
|
-
throw new Error("agent_profiles list accepts only scope");
|
|
134
|
-
return JSON.stringify({
|
|
135
|
-
status: "ok",
|
|
136
|
-
inventory: await listAgentProfiles(args.scope, cwd),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
if (!args.phase)
|
|
140
|
-
throw new Error("agent_profiles mutation requires preview or apply phase");
|
|
141
|
-
const action = {
|
|
142
|
-
model_set: "model-set",
|
|
143
|
-
critic_add: "critic-add",
|
|
144
|
-
critic_remove: "critic-remove",
|
|
145
|
-
}[args.action];
|
|
146
|
-
const request = {
|
|
147
|
-
action,
|
|
148
|
-
...(args.name ? { name: args.name } : {}),
|
|
149
|
-
...(args.model ? { model: args.model } : {}),
|
|
150
|
-
...(args.variant !== undefined ? { variant: args.variant } : {}),
|
|
151
|
-
};
|
|
152
|
-
if (args.phase === "preview")
|
|
153
|
-
return JSON.stringify({
|
|
154
|
-
status: "ok",
|
|
155
|
-
applied: false,
|
|
156
|
-
requires_restart: false,
|
|
157
|
-
plan: await previewAgentProfileChange(request, args.scope, cwd),
|
|
158
|
-
});
|
|
159
|
-
if (!args.confirmation_digest)
|
|
160
|
-
throw new Error("agent_profiles apply requires confirmation_digest");
|
|
161
|
-
return JSON.stringify(await applyAgentProfileChange(request, args.scope, args.confirmation_digest, cwd));
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
const reconcile = tool({
|
|
165
|
-
description: "Preview or apply scope-isolated removal of retired public assets without touching unknown sources or runtime state.",
|
|
166
|
-
args: {
|
|
167
|
-
phase: tool.schema.enum(["preview", "apply"]),
|
|
168
|
-
scope: tool.schema.enum(["global", "project"]),
|
|
169
|
-
confirmation_digest: tool.schema.string().optional(),
|
|
170
|
-
},
|
|
171
|
-
async execute(args) {
|
|
172
|
-
const cwd = input.directory ?? process.cwd();
|
|
173
|
-
if (args.phase === "preview")
|
|
174
|
-
return JSON.stringify({
|
|
175
|
-
status: "ok",
|
|
176
|
-
applied: false,
|
|
177
|
-
plan: await previewReconcile(args.scope, cwd),
|
|
178
|
-
});
|
|
179
|
-
if (!args.confirmation_digest)
|
|
180
|
-
throw new Error("reconcile apply requires confirmation_digest");
|
|
181
|
-
return JSON.stringify(await applyReconcile(args.scope, args.confirmation_digest, cwd));
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
90
|
return {
|
|
185
|
-
tool: { route
|
|
91
|
+
tool: { route },
|
|
186
92
|
"tool.execute.before": async (input, output) => {
|
|
187
93
|
if (input.tool !== "task")
|
|
188
94
|
return;
|
package/dist/installer.d.ts
CHANGED
|
@@ -45,7 +45,6 @@ export type ArchiveCandidate = {
|
|
|
45
45
|
kind: "command" | "agent" | "plugin" | "state";
|
|
46
46
|
};
|
|
47
47
|
export declare const SELECTABLE_PLUGINS: SelectablePlugin[];
|
|
48
|
-
export declare const PACKAGE_COMMANDS: string[];
|
|
49
48
|
export declare const SKILL_COMMANDS: string[];
|
|
50
49
|
export declare function defaultSelection(): InstallerSelection;
|
|
51
50
|
export declare function normalizeSelection(value?: Partial<InstallerSelection>): InstallerSelection;
|
package/dist/installer.js
CHANGED
|
@@ -20,18 +20,17 @@ function retiredAssetPaths() {
|
|
|
20
20
|
]);
|
|
21
21
|
}
|
|
22
22
|
export const SELECTABLE_PLUGINS = [...CATALOG.plugins];
|
|
23
|
-
export const PACKAGE_COMMANDS = [...CATALOG.package_commands];
|
|
24
23
|
export const SKILL_COMMANDS = [...CATALOG.skills];
|
|
25
24
|
export function defaultSelection() {
|
|
26
25
|
return {
|
|
27
|
-
commands: [...SKILL_COMMANDS
|
|
26
|
+
commands: [...SKILL_COMMANDS].sort(),
|
|
28
27
|
agents: [...FIXED_AGENT_ROLES],
|
|
29
28
|
plugins: [],
|
|
30
29
|
core_activation: true,
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
32
|
export function normalizeSelection(value = {}) {
|
|
34
|
-
const allCommands = new Set(
|
|
33
|
+
const allCommands = new Set(SKILL_COMMANDS);
|
|
35
34
|
const commands = [...new Set(value.commands ?? defaultSelection().commands)].sort();
|
|
36
35
|
const agents = [...new Set(value.agents ?? FIXED_AGENT_ROLES)];
|
|
37
36
|
const plugins = [...new Set(value.plugins ?? [])];
|
|
@@ -45,8 +44,7 @@ export function normalizeSelection(value = {}) {
|
|
|
45
44
|
commands,
|
|
46
45
|
agents: agents.sort(),
|
|
47
46
|
plugins: plugins.sort(),
|
|
48
|
-
core_activation: Boolean(value.core_activation ??
|
|
49
|
-
(commands.some((name) => PACKAGE_COMMANDS.includes(name)) || agents.length)),
|
|
47
|
+
core_activation: Boolean(value.core_activation ?? agents.length),
|
|
50
48
|
};
|
|
51
49
|
}
|
|
52
50
|
export class InstallerError extends LifecycleError {
|
|
@@ -214,15 +212,20 @@ export async function archiveMutations(candidates, scope, cwd, home, sourceVersi
|
|
|
214
212
|
}
|
|
215
213
|
const known = new Set(entries.map((entry) => `${String(entry.original_path)}:${String(entry.digest)}`));
|
|
216
214
|
const mutations = [];
|
|
215
|
+
const queuedObjects = new Set();
|
|
217
216
|
const now = new Date(0).toISOString();
|
|
218
217
|
for (const candidate of candidates) {
|
|
219
218
|
const digestValue = sha256(candidate.content);
|
|
220
219
|
const key = `${candidate.path}:${digestValue}`;
|
|
221
|
-
if (known.has(key))
|
|
222
|
-
continue;
|
|
223
220
|
const objectPath = `objects/${digestValue}`;
|
|
224
221
|
const object = await readRegular(destination(root, objectPath));
|
|
225
|
-
if (
|
|
222
|
+
if (known.has(key)) {
|
|
223
|
+
if ((!object && !queuedObjects.has(digestValue)) ||
|
|
224
|
+
(object && sha256(object) !== digestValue))
|
|
225
|
+
throw new InstallerError("invalid_archive", `Archive index references an invalid object: ${digestValue}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (!object && !queuedObjects.has(digestValue)) {
|
|
226
229
|
mutations.push({
|
|
227
230
|
root,
|
|
228
231
|
path: objectPath,
|
|
@@ -231,8 +234,9 @@ export async function archiveMutations(candidates, scope, cwd, home, sourceVersi
|
|
|
231
234
|
mode: candidate.record.mode,
|
|
232
235
|
expected: { absent: true },
|
|
233
236
|
});
|
|
237
|
+
queuedObjects.add(digestValue);
|
|
234
238
|
}
|
|
235
|
-
else if (sha256(object) !== digestValue) {
|
|
239
|
+
else if (object && sha256(object) !== digestValue) {
|
|
236
240
|
throw new InstallerError("archive_collision", `Archive object collision: ${digestValue}`);
|
|
237
241
|
}
|
|
238
242
|
entries.push({
|
|
@@ -605,7 +609,7 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
|
|
|
605
609
|
const root = deploymentRoot(scope, cwd, home);
|
|
606
610
|
try {
|
|
607
611
|
return await withLifecycleLock(stateRoot, async () => {
|
|
608
|
-
if (await recoverTransaction(root, stateRoot))
|
|
612
|
+
if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
|
|
609
613
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
610
614
|
const built = await build(action, scope, cwd, home, selection);
|
|
611
615
|
const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, { plan_digest: built.plan.digest }, Date.now(), built.plan.digest);
|
|
@@ -632,7 +636,7 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
|
|
|
632
636
|
const root = deploymentRoot(scope, cwd, home);
|
|
633
637
|
try {
|
|
634
638
|
return await withLifecycleLock(stateRoot, async () => {
|
|
635
|
-
if (await recoverTransaction(root, stateRoot))
|
|
639
|
+
if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
|
|
636
640
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
637
641
|
const receipt = (await consumeReceipt(stateRoot, {
|
|
638
642
|
digest: confirmationDigest,
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -20,6 +20,18 @@ export type FileMutation = {
|
|
|
20
20
|
root?: string;
|
|
21
21
|
operation: "remove";
|
|
22
22
|
expected: FileExpectation;
|
|
23
|
+
} | {
|
|
24
|
+
path: string;
|
|
25
|
+
root?: string;
|
|
26
|
+
operation: "external-write";
|
|
27
|
+
content: Buffer;
|
|
28
|
+
mode: number;
|
|
29
|
+
expected: FileExpectation;
|
|
30
|
+
} | {
|
|
31
|
+
path: string;
|
|
32
|
+
root?: string;
|
|
33
|
+
operation: "external-remove";
|
|
34
|
+
expected: FileExpectation;
|
|
23
35
|
};
|
|
24
36
|
export type SupersededPlan = {
|
|
25
37
|
kind: string;
|
|
@@ -56,10 +68,11 @@ export declare function consumeReceipt(stateRoot: string, expected: {
|
|
|
56
68
|
}, now?: number): Promise<unknown>;
|
|
57
69
|
/** Invalidate an active receipt when a preview is blocked before a replacement exists. */
|
|
58
70
|
export declare function supersedeReceipt(stateRoot: string, now?: number): Promise<void>;
|
|
59
|
-
export declare function recoverTransaction(root: string, stateRoot: string): Promise<boolean>;
|
|
71
|
+
export declare function recoverTransaction(root: string, stateRoot: string, allowedRoots?: readonly string[]): Promise<boolean>;
|
|
60
72
|
export type TransactionOptions = {
|
|
61
73
|
beforePublish?: (index: number) => void;
|
|
62
74
|
afterPublish?: (published: number) => "continue" | "fail" | "interrupt";
|
|
75
|
+
applyExternal?: () => Promise<void>;
|
|
63
76
|
validateFinal?: () => Promise<void>;
|
|
64
77
|
};
|
|
65
78
|
export declare function applyTransaction(root: string, stateRoot: string, mutations: readonly FileMutation[], options?: TransactionOptions): Promise<void>;
|
package/dist/lifecycle.js
CHANGED
|
@@ -105,8 +105,17 @@ async function ensureDirectory(path, mode) {
|
|
|
105
105
|
throw new LifecycleError("unsafe_path", `Unsafe directory: ${current}`);
|
|
106
106
|
continue;
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
try {
|
|
109
|
+
await mkdir(current, { mode });
|
|
110
|
+
created.push(current);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error.code !== "EEXIST")
|
|
114
|
+
throw error;
|
|
115
|
+
const raced = await lstat(current);
|
|
116
|
+
if (!raced.isDirectory() || raced.isSymbolicLink())
|
|
117
|
+
throw new LifecycleError("unsafe_path", `Unsafe directory: ${current}`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
return created;
|
|
112
121
|
}
|
|
@@ -155,7 +164,8 @@ export function archiveRoot(scope, cwd = process.cwd(), home = homedir()) {
|
|
|
155
164
|
const base = process.env.XDG_DATA_HOME && home === homedir()
|
|
156
165
|
? resolve(process.env.XDG_DATA_HOME)
|
|
157
166
|
: resolve(home, ".local", "share");
|
|
158
|
-
|
|
167
|
+
const suffix = scope === "global" ? "global" : join("project", sha256(resolve(cwd)));
|
|
168
|
+
return join(base, "opencode", "skills-opencode", "archive", suffix);
|
|
159
169
|
}
|
|
160
170
|
async function processAlive(pid) {
|
|
161
171
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
@@ -476,11 +486,17 @@ async function restore(root, journal) {
|
|
|
476
486
|
const currentMode = current ? (await lstat(target)).mode & 0o777 : undefined;
|
|
477
487
|
const beforeHash = item.content === null ? undefined : sha256(Buffer.from(item.content, "base64"));
|
|
478
488
|
const beforeMode = item.mode ?? undefined;
|
|
479
|
-
const intendedHash = operation.operation === "write"
|
|
489
|
+
const intendedHash = (operation.operation === "write" || operation.operation === "external-write") &&
|
|
490
|
+
operation.content
|
|
480
491
|
? sha256(Buffer.from(operation.content, "base64"))
|
|
481
492
|
: undefined;
|
|
482
|
-
const intendedMode = operation.operation === "write"
|
|
483
|
-
|
|
493
|
+
const intendedMode = operation.operation === "write" || operation.operation === "external-write"
|
|
494
|
+
? operation.mode
|
|
495
|
+
: undefined;
|
|
496
|
+
const external = operation.operation === "external-write" || operation.operation === "external-remove";
|
|
497
|
+
const touched = external
|
|
498
|
+
? journal.external_started === true
|
|
499
|
+
: index < journal.published || index === journal.applying;
|
|
484
500
|
if (!touched) {
|
|
485
501
|
if (currentHash !== beforeHash || currentMode !== beforeMode)
|
|
486
502
|
conflicts.push(item.path);
|
|
@@ -521,7 +537,7 @@ async function restore(root, journal) {
|
|
|
521
537
|
if (conflicts.length)
|
|
522
538
|
throw new LifecycleError("recovery_conflict", `Transaction targets changed outside the lifecycle lock: ${conflicts.join(", ")}`);
|
|
523
539
|
}
|
|
524
|
-
function parseJournal(raw, expectedRoot) {
|
|
540
|
+
function parseJournal(raw, expectedRoot, allowedRoots) {
|
|
525
541
|
let value;
|
|
526
542
|
try {
|
|
527
543
|
value = JSON.parse(raw.toString("utf8"));
|
|
@@ -530,6 +546,7 @@ function parseJournal(raw, expectedRoot) {
|
|
|
530
546
|
throw new LifecycleError("invalid_journal", "Transaction journal is not valid JSON");
|
|
531
547
|
}
|
|
532
548
|
const journal = value;
|
|
549
|
+
const roots = new Set([expectedRoot, ...allowedRoots].map((value) => resolve(value)));
|
|
533
550
|
if (journal.schema_version !== 1 ||
|
|
534
551
|
journal.root !== expectedRoot ||
|
|
535
552
|
!Array.isArray(journal.operations) ||
|
|
@@ -540,6 +557,7 @@ function parseJournal(raw, expectedRoot) {
|
|
|
540
557
|
!Number.isSafeInteger(journal.published) ||
|
|
541
558
|
journal.published < 0 ||
|
|
542
559
|
journal.published > journal.operations.length ||
|
|
560
|
+
(journal.external_started !== undefined && typeof journal.external_started !== "boolean") ||
|
|
543
561
|
(journal.applying !== null &&
|
|
544
562
|
(typeof journal.applying !== "number" ||
|
|
545
563
|
!Number.isSafeInteger(journal.applying) ||
|
|
@@ -550,8 +568,8 @@ function parseJournal(raw, expectedRoot) {
|
|
|
550
568
|
}
|
|
551
569
|
for (const item of journal.snapshots) {
|
|
552
570
|
assertSafeRelative(item.path);
|
|
553
|
-
if (item.root !== undefined && !isAbsolute(item.root))
|
|
554
|
-
throw new LifecycleError("invalid_journal", "Transaction snapshot root
|
|
571
|
+
if (item.root !== undefined && (!isAbsolute(item.root) || !roots.has(resolve(item.root))))
|
|
572
|
+
throw new LifecycleError("invalid_journal", "Transaction snapshot root is not allowed");
|
|
555
573
|
}
|
|
556
574
|
for (const item of journal.snapshots) {
|
|
557
575
|
if ((item.content !== null &&
|
|
@@ -564,11 +582,14 @@ function parseJournal(raw, expectedRoot) {
|
|
|
564
582
|
}
|
|
565
583
|
for (const item of journal.operations) {
|
|
566
584
|
assertSafeRelative(item.path);
|
|
567
|
-
if (item.root !== undefined && !isAbsolute(item.root))
|
|
568
|
-
throw new LifecycleError("invalid_journal", "Transaction operation root
|
|
569
|
-
if (item.operation !== "write" &&
|
|
585
|
+
if (item.root !== undefined && (!isAbsolute(item.root) || !roots.has(resolve(item.root))))
|
|
586
|
+
throw new LifecycleError("invalid_journal", "Transaction operation root is not allowed");
|
|
587
|
+
if (item.operation !== "write" &&
|
|
588
|
+
item.operation !== "remove" &&
|
|
589
|
+
item.operation !== "external-write" &&
|
|
590
|
+
item.operation !== "external-remove")
|
|
570
591
|
throw new LifecycleError("invalid_journal", "Transaction journal contains an invalid operation");
|
|
571
|
-
if (item.operation === "write" &&
|
|
592
|
+
if ((item.operation === "write" || item.operation === "external-write") &&
|
|
572
593
|
(typeof item.content !== "string" ||
|
|
573
594
|
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(item.content) ||
|
|
574
595
|
typeof item.mode !== "number" ||
|
|
@@ -577,10 +598,17 @@ function parseJournal(raw, expectedRoot) {
|
|
|
577
598
|
item.mode > 0o777))
|
|
578
599
|
throw new LifecycleError("invalid_journal", "Transaction journal contains invalid write content");
|
|
579
600
|
}
|
|
601
|
+
for (let index = 0; index < journal.operations.length; index += 1) {
|
|
602
|
+
const operation = journal.operations[index];
|
|
603
|
+
const snapshot = journal.snapshots[index];
|
|
604
|
+
if (operation.path !== snapshot.path ||
|
|
605
|
+
resolve(operation.root ?? expectedRoot) !== resolve(snapshot.root ?? expectedRoot))
|
|
606
|
+
throw new LifecycleError("invalid_journal", "Transaction journal operation and snapshot targets differ");
|
|
607
|
+
}
|
|
580
608
|
if (!journal.created_directories.every((item) => item &&
|
|
581
609
|
typeof item.path === "string" &&
|
|
582
610
|
(inside(resolve(expectedRoot), resolve(item.path)) ||
|
|
583
|
-
|
|
611
|
+
[...roots].some((root) => inside(root, resolve(item.path)))) &&
|
|
584
612
|
Number.isSafeInteger(item.device) &&
|
|
585
613
|
item.device >= 0 &&
|
|
586
614
|
Number.isSafeInteger(item.inode) &&
|
|
@@ -592,11 +620,11 @@ function parseJournal(raw, expectedRoot) {
|
|
|
592
620
|
snapshots: journal.snapshots.map((item) => ({ ...item, root: item.root ?? expectedRoot })),
|
|
593
621
|
};
|
|
594
622
|
}
|
|
595
|
-
export async function recoverTransaction(root, stateRoot) {
|
|
623
|
+
export async function recoverTransaction(root, stateRoot, allowedRoots = []) {
|
|
596
624
|
const raw = await readRegular(journalPath(stateRoot));
|
|
597
625
|
if (!raw)
|
|
598
626
|
return false;
|
|
599
|
-
const journal = parseJournal(raw, resolve(root));
|
|
627
|
+
const journal = parseJournal(raw, resolve(root), allowedRoots);
|
|
600
628
|
await restore(root, journal);
|
|
601
629
|
await unlink(journalPath(stateRoot));
|
|
602
630
|
return true;
|
|
@@ -640,20 +668,24 @@ async function ensureTransactionDirectories(root, mutation, stateRoot, journal)
|
|
|
640
668
|
export async function applyTransaction(root, stateRoot, mutations, options = {}) {
|
|
641
669
|
if (await readRegular(journalPath(stateRoot)))
|
|
642
670
|
throw new LifecycleError("recovery_required", "An interrupted transaction must be recovered before apply");
|
|
671
|
+
const ordered = [
|
|
672
|
+
...mutations.filter((mutation) => mutation.operation !== "external-write" && mutation.operation !== "external-remove"),
|
|
673
|
+
...mutations.filter((mutation) => mutation.operation === "external-write" || mutation.operation === "external-remove"),
|
|
674
|
+
];
|
|
643
675
|
const unique = new Set();
|
|
644
|
-
for (const mutation of
|
|
676
|
+
for (const mutation of ordered) {
|
|
645
677
|
assertSafeRelative(mutation.path);
|
|
646
|
-
const key =
|
|
678
|
+
const key = destination(mutationRoot(root, mutation), mutation.path);
|
|
647
679
|
if (unique.has(key))
|
|
648
680
|
throw new LifecycleError("invalid_plan", `Duplicate transaction target: ${mutation.path}`);
|
|
649
681
|
unique.add(key);
|
|
650
682
|
await validateExpectation(root, mutation);
|
|
651
683
|
}
|
|
652
|
-
const snapshots = await Promise.all(
|
|
684
|
+
const snapshots = await Promise.all(ordered.map((mutation) => snapshot(root, mutation)));
|
|
653
685
|
const journal = {
|
|
654
686
|
schema_version: 1,
|
|
655
687
|
root: resolve(root),
|
|
656
|
-
operations:
|
|
688
|
+
operations: ordered.map((mutation) => mutation.operation === "write" || mutation.operation === "external-write"
|
|
657
689
|
? {
|
|
658
690
|
path: mutation.path,
|
|
659
691
|
...(mutation.root ? { root: resolve(mutation.root) } : {}),
|
|
@@ -672,10 +704,13 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
|
|
|
672
704
|
created_directories: [],
|
|
673
705
|
published: 0,
|
|
674
706
|
applying: null,
|
|
707
|
+
external_started: false,
|
|
675
708
|
};
|
|
676
709
|
await writeJournal(stateRoot, journal);
|
|
677
710
|
try {
|
|
678
|
-
|
|
711
|
+
const native = ordered.filter((mutation) => mutation.operation !== "external-write" && mutation.operation !== "external-remove");
|
|
712
|
+
const external = ordered.slice(native.length);
|
|
713
|
+
for (const mutation of native) {
|
|
679
714
|
journal.applying = journal.published;
|
|
680
715
|
await writeJournal(stateRoot, journal);
|
|
681
716
|
options.beforePublish?.(journal.published);
|
|
@@ -696,11 +731,19 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
|
|
|
696
731
|
if (injected === "fail")
|
|
697
732
|
throw new LifecycleError("test_failure", "Injected transaction failure");
|
|
698
733
|
}
|
|
699
|
-
|
|
734
|
+
if (external.length) {
|
|
735
|
+
for (const mutation of external)
|
|
736
|
+
await validateExpectation(root, mutation);
|
|
737
|
+
journal.external_started = true;
|
|
738
|
+
await writeJournal(stateRoot, journal);
|
|
739
|
+
await options.applyExternal?.();
|
|
740
|
+
}
|
|
741
|
+
for (const mutation of ordered) {
|
|
700
742
|
const target = destination(mutationRoot(root, mutation), mutation.path);
|
|
701
743
|
const current = await readRegular(target);
|
|
702
|
-
if ((mutation.operation === "remove"
|
|
703
|
-
|
|
744
|
+
if (((mutation.operation === "remove" || mutation.operation === "external-remove") &&
|
|
745
|
+
current) ||
|
|
746
|
+
((mutation.operation === "write" || mutation.operation === "external-write") &&
|
|
704
747
|
(!current ||
|
|
705
748
|
!current.equals(mutation.content) ||
|
|
706
749
|
((await stat(target)).mode & 0o777) !== mutation.mode))) {
|