@kisev/skills-opencode 1.1.1 → 2.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 +134 -108
- package/README.ru.md +242 -0
- package/dist/agent-profiles.d.ts +1 -1
- package/dist/agent-profiles.js +21 -10
- package/dist/assets/agents/architect.md +67 -0
- package/{assets → dist/assets}/agents/critic.md +17 -2
- package/dist/assets/agents/manager.md +36 -0
- package/{assets → dist/assets}/agents/mapper.md +2 -2
- package/dist/assets/agents/review.md +25 -0
- package/{assets → dist/assets}/agents/worker.md +7 -5
- package/dist/assets/commands/agent-profiles.md +9 -0
- package/dist/assets/commands/agents-md.md +10 -0
- package/dist/assets/commands/askme.md +10 -0
- package/dist/assets/commands/ast-grep.md +10 -0
- package/dist/assets/commands/capabilities.md +9 -0
- package/dist/assets/commands/code-explain.md +10 -0
- package/dist/assets/commands/code-review.md +10 -0
- package/dist/assets/commands/commit-msg.md +10 -0
- package/dist/assets/commands/docs-prepare.md +10 -0
- package/dist/assets/commands/docs-review.md +10 -0
- package/dist/assets/commands/doctor.md +9 -0
- package/dist/assets/commands/doit.md +10 -0
- package/dist/assets/commands/goal.md +10 -0
- package/dist/assets/commands/humanize.md +10 -0
- package/dist/assets/commands/lsp-report.md +10 -0
- package/dist/assets/commands/mattermost.md +10 -0
- package/dist/assets/commands/mr-prepare.md +10 -0
- package/dist/assets/commands/reconcile.md +9 -0
- package/dist/assets/commands/release-prepare.md +10 -0
- package/dist/assets/commands/release-review.md +10 -0
- package/dist/assets/commands/rtk.md +10 -0
- package/dist/assets/commands/skill-improve.md +10 -0
- package/dist/assets/commands/slides-prompts-prepare.md +10 -0
- package/dist/assets/commands/spec-manage.md +10 -0
- package/dist/assets/commands/stopit.md +10 -0
- package/dist/assets/commands/summary.md +10 -0
- package/dist/assets/commands/task-prepare.md +10 -0
- package/dist/assets/commands/task-review.md +10 -0
- package/dist/assets/commands/task-triage.md +10 -0
- package/dist/assets/commands/team-retro.md +10 -0
- package/dist/assets/commands/team-roadmap.md +10 -0
- package/dist/assets/commands/team-sprint-close.md +10 -0
- package/dist/assets/commands/team-sprint-start.md +10 -0
- package/dist/assets/lsp-catalog.json +30 -0
- package/dist/assets/migration-inventory.json +294 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +38 -0
- package/dist/cli-output.d.ts +7 -0
- package/dist/cli-output.js +68 -1
- package/dist/cli.js +256 -59
- package/dist/contracts.d.ts +52 -0
- package/dist/contracts.js +98 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +595 -0
- package/dist/generate-assets.js +17 -2
- package/dist/index.d.ts +84 -55
- package/dist/index.js +170 -37
- package/dist/installer.d.ts +33 -5
- package/dist/installer.js +252 -28
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.js +49 -17
- package/dist/plugins/rtk.js +4 -1
- package/dist/reconcile.d.ts +43 -0
- package/dist/reconcile.js +507 -0
- package/dist/registry.d.ts +7 -4
- package/dist/registry.js +28 -88
- package/dist/routing.d.ts +97 -3
- package/dist/routing.js +470 -18
- package/dist/runtime/state.d.ts +1 -0
- package/dist/runtime/state.js +12 -5
- package/dist/runtime/worktree.d.ts +33 -0
- package/dist/runtime/worktree.js +220 -0
- package/dist/terminal-wizard.d.ts +2 -0
- package/dist/terminal-wizard.js +125 -0
- package/package.json +10 -26
- package/assets/agents/architect.md +0 -33
- package/assets/agents/manager.md +0 -82
- package/assets/agents/review.md +0 -25
- package/assets/commands/agent-list.md +0 -9
- package/assets/commands/agent-model-set.md +0 -9
- package/assets/commands/askme.md +0 -10
- package/assets/commands/ast-grep-rewrite.md +0 -10
- package/assets/commands/ast-grep-search.md +0 -10
- package/assets/commands/attempt-cancel.md +0 -10
- package/assets/commands/attempt-list.md +0 -10
- package/assets/commands/attempt-result.md +0 -10
- package/assets/commands/attempt-show.md +0 -10
- package/assets/commands/capabilities.md +0 -9
- package/assets/commands/code-review.md +0 -10
- package/assets/commands/commit-msg.md +0 -10
- package/assets/commands/critic-add.md +0 -9
- package/assets/commands/critic-remove.md +0 -9
- package/assets/commands/docs-prepare.md +0 -10
- package/assets/commands/docs-review.md +0 -10
- package/assets/commands/doctor.md +0 -9
- package/assets/commands/doit.md +0 -10
- package/assets/commands/goal-list.md +0 -10
- package/assets/commands/goal-pause.md +0 -10
- package/assets/commands/goal-prepare.md +0 -10
- package/assets/commands/goal-remove.md +0 -10
- package/assets/commands/goal-show.md +0 -10
- package/assets/commands/goal-start.md +0 -10
- package/assets/commands/lsp-report.md +0 -10
- package/assets/commands/mattermost.md +0 -10
- package/assets/commands/mr-prepare.md +0 -10
- package/assets/commands/mr-review.md +0 -10
- package/assets/commands/multi-run-cancel.md +0 -10
- package/assets/commands/multi-run-compare.md +0 -10
- package/assets/commands/multi-run-fusion.md +0 -10
- package/assets/commands/multi-run-start.md +0 -10
- package/assets/commands/multi-run-status.md +0 -10
- package/assets/commands/overview.md +0 -10
- package/assets/commands/release-prepare.md +0 -10
- package/assets/commands/release-review.md +0 -10
- package/assets/commands/route.md +0 -9
- package/assets/commands/schedule-add.md +0 -10
- package/assets/commands/schedule-disable.md +0 -10
- package/assets/commands/schedule-enable.md +0 -10
- package/assets/commands/schedule-list.md +0 -10
- package/assets/commands/schedule-remove.md +0 -10
- package/assets/commands/schedule-status.md +0 -10
- package/assets/commands/skill-improver.md +0 -10
- package/assets/commands/spec-audit.md +0 -10
- package/assets/commands/spec-init.md +0 -10
- package/assets/commands/spec-onboard.md +0 -10
- package/assets/commands/spec-update.md +0 -10
- package/assets/commands/stopit.md +0 -10
- package/assets/commands/summary.md +0 -10
- package/assets/commands/task-prepare-batch.md +0 -10
- package/assets/commands/task-prepare.md +0 -10
- package/assets/commands/task-review.md +0 -10
- package/assets/commands/task-triage.md +0 -10
- package/assets/commands/team-planning.md +0 -10
- package/assets/commands/team-retro.md +0 -10
- package/assets/commands/team-roadmap.md +0 -10
- package/assets/commands/team-slides-prompts.md +0 -10
- package/assets/commands/team-sprint-close.md +0 -10
- package/assets/commands/team-sprint-status.md +0 -10
- package/assets/commands/walkthrough.md +0 -10
- package/assets/plugins/autonomy-policy.js +0 -3
- package/assets/plugins/background-attempts.js +0 -3
- package/assets/plugins/goal-loop.js +0 -3
- package/assets/plugins/schedule.js +0 -3
- package/assets/plugins/zed-clickable-paths.js +0 -3
- package/dist/plugins/autonomy-policy.d.ts +0 -21
- package/dist/plugins/autonomy-policy.js +0 -71
- package/dist/plugins/background-attempts.d.ts +0 -76
- package/dist/plugins/background-attempts.js +0 -113
- package/dist/plugins/goal-loop.d.ts +0 -51
- package/dist/plugins/goal-loop.js +0 -87
- package/dist/plugins/schedule.d.ts +0 -36
- package/dist/plugins/schedule.js +0 -100
- package/dist/plugins/zed-clickable-paths.d.ts +0 -11
- package/dist/plugins/zed-clickable-paths.js +0 -10
- /package/{assets → dist/assets}/plugins/rtk.js +0 -0
- /package/{assets → dist/assets}/plugins/rules-injector.js +0 -0
- /package/{assets → dist/assets}/plugins/zed-bell.js +0 -0
package/dist/installer.js
CHANGED
|
@@ -1,14 +1,52 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { lstat, readdir } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { buildAgentProfilePlan, listAgentProfiles, validateBuiltAgentProfilePlan, } from "./agent-profiles.js";
|
|
7
|
-
import { applyTransaction, consumeReceipt, deploymentRoot, destination, digest, LifecycleError, lifecycleRoot, readRegular, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
7
|
+
import { applyTransaction, archiveRoot, consumeReceipt, deploymentRoot, destination, digest, LifecycleError, lifecycleRoot, readRegular, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
8
|
+
import { CATALOG } from "./catalog.js";
|
|
9
|
+
import { FIXED_AGENT_ROLES } from "./agent-profiles.js";
|
|
8
10
|
const PACKAGE_NAME = "@kisev/skills-opencode";
|
|
9
11
|
const MANIFEST_NAME = ".skills-opencode-manifest.json";
|
|
10
12
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const assetsRoot = resolve(packageRoot, "assets");
|
|
13
|
+
const assetsRoot = resolve(packageRoot, "dist", "assets");
|
|
14
|
+
function retiredAssetPaths() {
|
|
15
|
+
const inventory = JSON.parse(readFileSync(resolve(assetsRoot, "migration-inventory.json"), "utf8"));
|
|
16
|
+
return new Set([
|
|
17
|
+
...Object.keys(inventory.retired_command_hashes ?? {}),
|
|
18
|
+
...Object.keys(inventory.retired_plugin_hashes ?? {}),
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
export const SELECTABLE_PLUGINS = [...CATALOG.plugins];
|
|
22
|
+
export const PACKAGE_COMMANDS = [...CATALOG.package_commands];
|
|
23
|
+
export const SKILL_COMMANDS = [...CATALOG.skills];
|
|
24
|
+
export function defaultSelection() {
|
|
25
|
+
return {
|
|
26
|
+
commands: [...SKILL_COMMANDS, ...PACKAGE_COMMANDS].sort(),
|
|
27
|
+
agents: [...FIXED_AGENT_ROLES],
|
|
28
|
+
plugins: [],
|
|
29
|
+
core_activation: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function normalizeSelection(value = {}) {
|
|
33
|
+
const allCommands = new Set([...SKILL_COMMANDS, ...PACKAGE_COMMANDS]);
|
|
34
|
+
const commands = [...new Set(value.commands ?? defaultSelection().commands)].sort();
|
|
35
|
+
const agents = [...new Set(value.agents ?? FIXED_AGENT_ROLES)];
|
|
36
|
+
const plugins = [...new Set(value.plugins ?? [])];
|
|
37
|
+
if (commands.some((name) => !allCommands.has(name)))
|
|
38
|
+
throw new InstallerError("invalid_selection", "Unknown command selection");
|
|
39
|
+
if (agents.some((name) => !FIXED_AGENT_ROLES.includes(name)))
|
|
40
|
+
throw new InstallerError("invalid_selection", "Unknown agent selection");
|
|
41
|
+
if (plugins.some((name) => !SELECTABLE_PLUGINS.includes(name)))
|
|
42
|
+
throw new InstallerError("invalid_selection", "Unknown plugin selection");
|
|
43
|
+
return {
|
|
44
|
+
commands,
|
|
45
|
+
agents: agents.sort(),
|
|
46
|
+
plugins: plugins.sort(),
|
|
47
|
+
core_activation: Boolean(value.core_activation ?? (commands.some((name) => PACKAGE_COMMANDS.includes(name)) || agents.length)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
12
50
|
export class InstallerError extends LifecycleError {
|
|
13
51
|
}
|
|
14
52
|
function packageVersion() {
|
|
@@ -17,14 +55,19 @@ function packageVersion() {
|
|
|
17
55
|
throw new InstallerError("invalid_package", "Package version is unavailable");
|
|
18
56
|
return metadata.version;
|
|
19
57
|
}
|
|
20
|
-
async function assets() {
|
|
58
|
+
async function assets(selection) {
|
|
21
59
|
const result = [];
|
|
22
60
|
for (const category of ["commands", "plugins"]) {
|
|
23
61
|
const directory = resolve(assetsRoot, category);
|
|
24
62
|
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
25
63
|
const extension = category === "plugins" ? ".js" : ".md";
|
|
64
|
+
const name = entry.name.slice(0, -extension.length);
|
|
26
65
|
if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith(extension))
|
|
27
66
|
throw new InstallerError("asset_error", `Asset is not a regular ${extension} file: ${entry.name}`);
|
|
67
|
+
if (category === "plugins" && !selection.plugins.includes(name))
|
|
68
|
+
continue;
|
|
69
|
+
if (category === "commands" && !selection.commands.includes(name))
|
|
70
|
+
continue;
|
|
28
71
|
const relativePath = `${category}/${entry.name}`;
|
|
29
72
|
const content = await readRegular(destination(assetsRoot, relativePath));
|
|
30
73
|
if (!content)
|
|
@@ -43,23 +86,143 @@ function parseManifest(raw, path) {
|
|
|
43
86
|
throw new InstallerError("invalid_manifest", `Ownership manifest is not valid JSON: ${path}`);
|
|
44
87
|
}
|
|
45
88
|
const manifest = value;
|
|
46
|
-
if (manifest.schema_version
|
|
89
|
+
if (manifest.schema_version === 1 &&
|
|
90
|
+
manifest.package === PACKAGE_NAME &&
|
|
91
|
+
typeof manifest.version === "string" &&
|
|
92
|
+
manifest.files &&
|
|
93
|
+
typeof manifest.files === "object" &&
|
|
94
|
+
!Array.isArray(manifest.files)) {
|
|
95
|
+
const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => [
|
|
96
|
+
relativePath,
|
|
97
|
+
{
|
|
98
|
+
sha256: record.sha256,
|
|
99
|
+
mode: 0o644,
|
|
100
|
+
kind: relativePath.startsWith("commands/")
|
|
101
|
+
? "command"
|
|
102
|
+
: relativePath.startsWith("plugins/")
|
|
103
|
+
? "plugin"
|
|
104
|
+
: relativePath.startsWith("agents/")
|
|
105
|
+
? "agent"
|
|
106
|
+
: "state",
|
|
107
|
+
},
|
|
108
|
+
]));
|
|
109
|
+
return {
|
|
110
|
+
schema_version: 1,
|
|
111
|
+
package: PACKAGE_NAME,
|
|
112
|
+
package_version: manifest.version,
|
|
113
|
+
version: manifest.version,
|
|
114
|
+
scope: "global",
|
|
115
|
+
commands: Object.keys(files).filter((path) => path.startsWith("commands/")).map((path) => path.slice(9, -3)).sort(),
|
|
116
|
+
agents: [...FIXED_AGENT_ROLES].filter((role) => `agents/${role}.md` in files),
|
|
117
|
+
plugins: Object.keys(files).filter((path) => path.startsWith("plugins/")).map((path) => path.slice(8, -3)).filter((name) => SELECTABLE_PLUGINS.includes(name)),
|
|
118
|
+
core_activation: true,
|
|
119
|
+
files,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (manifest.schema_version !== 2 || manifest.package !== PACKAGE_NAME || typeof manifest.package_version !== "string" || (manifest.scope !== "global" && manifest.scope !== "project") || !Array.isArray(manifest.commands) || !Array.isArray(manifest.agents) || !Array.isArray(manifest.plugins) || typeof manifest.core_activation !== "boolean" || !manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
|
|
47
123
|
throw new InstallerError("invalid_manifest", `Ownership manifest has an unexpected format: ${path}`);
|
|
48
124
|
}
|
|
49
|
-
|
|
125
|
+
const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => {
|
|
50
126
|
destination("/", relativePath);
|
|
51
127
|
if (!record || typeof record !== "object" || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
|
|
52
128
|
throw new InstallerError("invalid_manifest", `Ownership manifest has an invalid record: ${relativePath}`);
|
|
53
129
|
}
|
|
54
|
-
|
|
55
|
-
|
|
130
|
+
const existing = record;
|
|
131
|
+
return [relativePath, {
|
|
132
|
+
sha256: existing.sha256,
|
|
133
|
+
mode: Number.isInteger(existing.mode) ? existing.mode : 0o644,
|
|
134
|
+
kind: ["command", "agent", "plugin", "state"].includes(String(existing.kind)) ? existing.kind : relativePath.startsWith("commands/") ? "command" : relativePath.startsWith("plugins/") ? "plugin" : relativePath.startsWith("agents/") ? "agent" : "state",
|
|
135
|
+
}];
|
|
136
|
+
}));
|
|
137
|
+
return { ...manifest, files };
|
|
56
138
|
}
|
|
57
139
|
async function currentManifest(root) {
|
|
58
140
|
const path = destination(root, MANIFEST_NAME);
|
|
59
141
|
const raw = await readRegular(path);
|
|
60
142
|
return raw ? { manifest: parseManifest(raw, path), raw } : {};
|
|
61
143
|
}
|
|
62
|
-
async function
|
|
144
|
+
export async function archiveMutations(candidates, scope, cwd, home, sourceVersion) {
|
|
145
|
+
if (!candidates.length)
|
|
146
|
+
return [];
|
|
147
|
+
const root = archiveRoot(scope, cwd, home);
|
|
148
|
+
const rootInfo = await lstat(root).catch((error) => {
|
|
149
|
+
if (error.code === "ENOENT")
|
|
150
|
+
return undefined;
|
|
151
|
+
throw new InstallerError("unsafe_path", "Archive root is inaccessible");
|
|
152
|
+
});
|
|
153
|
+
if (rootInfo && (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o077) !== 0))
|
|
154
|
+
throw new InstallerError("unsafe_path", "Archive root must be a private directory");
|
|
155
|
+
const objectsInfo = await lstat(join(root, "objects")).catch((error) => {
|
|
156
|
+
if (error.code === "ENOENT")
|
|
157
|
+
return undefined;
|
|
158
|
+
throw new InstallerError("unsafe_path", "Archive object store is inaccessible");
|
|
159
|
+
});
|
|
160
|
+
if (objectsInfo && (!objectsInfo.isDirectory() || objectsInfo.isSymbolicLink() || (objectsInfo.mode & 0o077) !== 0))
|
|
161
|
+
throw new InstallerError("unsafe_path", "Archive object store must be private");
|
|
162
|
+
const indexPath = destination(root, "index.json");
|
|
163
|
+
const existingRaw = await readRegular(indexPath);
|
|
164
|
+
if (existingRaw) {
|
|
165
|
+
const indexInfo = await lstat(indexPath);
|
|
166
|
+
if ((indexInfo.mode & 0o777) !== 0o600)
|
|
167
|
+
throw new InstallerError("unsafe_path", "Archive index must be private");
|
|
168
|
+
}
|
|
169
|
+
let entries = [];
|
|
170
|
+
if (existingRaw) {
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(existingRaw.toString("utf8"));
|
|
173
|
+
if (parsed.schema_version !== 1 || !Array.isArray(parsed.entries))
|
|
174
|
+
throw new Error("invalid archive index");
|
|
175
|
+
entries = parsed.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new InstallerError("invalid_archive", "Archive index is not valid content-addressed state");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const known = new Set(entries.map((entry) => `${String(entry.original_path)}:${String(entry.digest)}`));
|
|
182
|
+
const mutations = [];
|
|
183
|
+
const now = new Date(0).toISOString();
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
const digestValue = sha256(candidate.content);
|
|
186
|
+
const key = `${candidate.path}:${digestValue}`;
|
|
187
|
+
if (known.has(key))
|
|
188
|
+
continue;
|
|
189
|
+
const objectPath = `objects/${digestValue}`;
|
|
190
|
+
const object = await readRegular(destination(root, objectPath));
|
|
191
|
+
if (!object) {
|
|
192
|
+
mutations.push({
|
|
193
|
+
root,
|
|
194
|
+
path: objectPath,
|
|
195
|
+
operation: "write",
|
|
196
|
+
content: candidate.content,
|
|
197
|
+
mode: candidate.record.mode,
|
|
198
|
+
expected: { absent: true },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (sha256(object) !== digestValue) {
|
|
202
|
+
throw new InstallerError("archive_collision", `Archive object collision: ${digestValue}`);
|
|
203
|
+
}
|
|
204
|
+
entries.push({
|
|
205
|
+
schema_version: 1,
|
|
206
|
+
digest: digestValue,
|
|
207
|
+
original_path: candidate.path,
|
|
208
|
+
scope,
|
|
209
|
+
kind: candidate.kind,
|
|
210
|
+
reason: candidate.reason,
|
|
211
|
+
package_version: packageVersion(),
|
|
212
|
+
source_version: sourceVersion,
|
|
213
|
+
original_hash: candidate.record.sha256,
|
|
214
|
+
mode: candidate.record.mode,
|
|
215
|
+
timestamp: now,
|
|
216
|
+
});
|
|
217
|
+
known.add(key);
|
|
218
|
+
}
|
|
219
|
+
const next = Buffer.from(`${stable({ schema_version: 1, version: packageVersion(), entries: entries.sort((a, b) => String(a.original_path).localeCompare(String(b.original_path)) || String(a.digest).localeCompare(String(b.digest))) })}\n`);
|
|
220
|
+
if (!existingRaw || !existingRaw.equals(next)) {
|
|
221
|
+
mutations.push({ root, path: "index.json", operation: "write", content: next, mode: 0o600, expected: existingRaw ? { sha256: sha256(existingRaw) } : { absent: true } });
|
|
222
|
+
}
|
|
223
|
+
return mutations;
|
|
224
|
+
}
|
|
225
|
+
async function validateGenericDeployment(root, action, expectedAssets, plannedOperations, expectedManifest, retiredPaths) {
|
|
63
226
|
const owned = await currentManifest(root);
|
|
64
227
|
if ((expectedManifest && (!owned.raw || !owned.raw.equals(expectedManifest))) ||
|
|
65
228
|
(!expectedManifest && owned.raw))
|
|
@@ -73,7 +236,10 @@ async function validateGenericDeployment(root, action, expectedAssets, plannedOp
|
|
|
73
236
|
throw new InstallerError("final_validation_failed", "Generic ownership manifest is not private");
|
|
74
237
|
const expected = new Map(expectedAssets.map((asset) => [asset.relativePath, asset]));
|
|
75
238
|
if (action === "install" &&
|
|
76
|
-
Object.keys(owned.manifest.files)
|
|
239
|
+
Object.keys(owned.manifest.files)
|
|
240
|
+
.filter((path) => !retiredPaths.has(path))
|
|
241
|
+
.sort()
|
|
242
|
+
.join(",") !== [...expected.keys()].sort().join(",")) {
|
|
77
243
|
throw new InstallerError("final_validation_failed", "Generic ownership inventory is incomplete");
|
|
78
244
|
}
|
|
79
245
|
for (const [relativePath, record] of Object.entries(owned.manifest.files)) {
|
|
@@ -89,28 +255,45 @@ async function validateGenericDeployment(root, action, expectedAssets, plannedOp
|
|
|
89
255
|
if (!content || (sha256(content) !== record.sha256 && !preservedDrift))
|
|
90
256
|
throw new InstallerError("final_validation_failed", `Generic managed file failed final validation: ${relativePath}`);
|
|
91
257
|
const asset = expected.get(relativePath);
|
|
92
|
-
if (action === "install" &&
|
|
258
|
+
if (action === "install" &&
|
|
259
|
+
((!asset && !retiredPaths.has(relativePath)) || (asset && asset.sha256 !== record.sha256)))
|
|
93
260
|
throw new InstallerError("final_validation_failed", `Generic manifest does not match package asset: ${relativePath}`);
|
|
94
|
-
if (action === "install" && ((await lstat(target)).mode & 0o777) !== asset.mode)
|
|
261
|
+
if (action === "install" && asset && ((await lstat(target)).mode & 0o777) !== asset.mode)
|
|
95
262
|
throw new InstallerError("final_validation_failed", `Generic managed file has an unexpected mode: ${relativePath}`);
|
|
96
263
|
}
|
|
97
264
|
}
|
|
98
265
|
function asLegacy(manifest) {
|
|
99
266
|
return manifest?.version === "1.0.0" ? manifest : undefined;
|
|
100
267
|
}
|
|
101
|
-
async function build(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
268
|
+
async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection) {
|
|
102
269
|
const root = deploymentRoot(scope, cwd, home);
|
|
103
|
-
const
|
|
270
|
+
const owned = await currentManifest(root);
|
|
271
|
+
const selection = normalizeSelection(requestedSelection ??
|
|
272
|
+
(owned.manifest
|
|
273
|
+
? {
|
|
274
|
+
commands: owned.manifest.commands,
|
|
275
|
+
agents: owned.manifest.agents,
|
|
276
|
+
plugins: owned.manifest.plugins,
|
|
277
|
+
core_activation: owned.manifest.core_activation,
|
|
278
|
+
}
|
|
279
|
+
: undefined));
|
|
280
|
+
const bundled = await assets(selection);
|
|
281
|
+
const retiredPaths = retiredAssetPaths();
|
|
104
282
|
const legacyRecord = owned.manifest && owned.raw && asLegacy(owned.manifest)
|
|
105
283
|
? { manifest: asLegacy(owned.manifest), manifestPath: destination(root, MANIFEST_NAME), manifestSha256: sha256(owned.raw) }
|
|
106
284
|
: undefined;
|
|
107
|
-
const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord);
|
|
285
|
+
const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
|
|
108
286
|
const operations = profiles.plan.operations.map((item) => ({ path: item.path, operation: item.operation, reason: item.reason }));
|
|
109
287
|
const mutations = [...profiles.mutations];
|
|
288
|
+
const archiveCandidates = [];
|
|
110
289
|
const desiredFiles = {};
|
|
111
290
|
if (action === "install") {
|
|
112
291
|
for (const asset of bundled) {
|
|
113
|
-
desiredFiles[asset.relativePath] = {
|
|
292
|
+
desiredFiles[asset.relativePath] = {
|
|
293
|
+
sha256: asset.sha256,
|
|
294
|
+
mode: asset.mode,
|
|
295
|
+
kind: asset.relativePath.startsWith("commands/") ? "command" : "plugin",
|
|
296
|
+
};
|
|
114
297
|
const current = await readRegular(destination(root, asset.relativePath));
|
|
115
298
|
const record = owned.manifest?.files[asset.relativePath];
|
|
116
299
|
if (!current) {
|
|
@@ -136,15 +319,30 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
136
319
|
for (const [relativePath, record] of Object.entries(owned.manifest?.files ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
137
320
|
if (active.has(relativePath) || profiles.legacyTransferred.includes(relativePath))
|
|
138
321
|
continue;
|
|
322
|
+
const current = await readRegular(destination(root, relativePath));
|
|
323
|
+
if (retiredPaths.has(relativePath)) {
|
|
324
|
+
if (!current) {
|
|
325
|
+
operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
|
|
326
|
+
}
|
|
327
|
+
else if (sha256(current) !== record.sha256) {
|
|
328
|
+
operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
operations.push({ path: relativePath, operation: "archive-pending", reason: "archive lifecycle is pending", sha256: record.sha256 });
|
|
332
|
+
archiveCandidates.push({ path: relativePath, record, content: current, reason: "retired managed asset", kind: record.kind });
|
|
333
|
+
mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
139
337
|
if (relativePath.startsWith("agents/")) {
|
|
140
338
|
operations.push({ path: relativePath, operation: "conflict", reason: "v1.0.0_agent_ownership_mismatch" });
|
|
141
339
|
continue;
|
|
142
340
|
}
|
|
143
|
-
const current = await readRegular(destination(root, relativePath));
|
|
144
341
|
if (!current)
|
|
145
342
|
operations.push({ path: relativePath, operation: "missing" });
|
|
146
343
|
else if (sha256(current) === record.sha256) {
|
|
147
|
-
operations.push({ path: relativePath, operation: "
|
|
344
|
+
operations.push({ path: relativePath, operation: "archive-pending", reason: "stale managed asset is archived", sha256: record.sha256 });
|
|
345
|
+
archiveCandidates.push({ path: relativePath, record, content: current, reason: "stale managed asset", kind: record.kind });
|
|
148
346
|
mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
|
|
149
347
|
}
|
|
150
348
|
else
|
|
@@ -154,10 +352,24 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
154
352
|
else {
|
|
155
353
|
for (const [relativePath, record] of Object.entries(owned.manifest?.files ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
156
354
|
const current = await readRegular(destination(root, relativePath));
|
|
157
|
-
if (
|
|
355
|
+
if (retiredPaths.has(relativePath)) {
|
|
356
|
+
if (!current) {
|
|
357
|
+
operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
|
|
358
|
+
}
|
|
359
|
+
else if (sha256(current) !== record.sha256) {
|
|
360
|
+
operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
operations.push({ path: relativePath, operation: "archive-pending", reason: "archive lifecycle is pending", sha256: record.sha256 });
|
|
364
|
+
archiveCandidates.push({ path: relativePath, record, content: current, reason: "retired managed asset", kind: record.kind });
|
|
365
|
+
mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else if (!current)
|
|
158
369
|
operations.push({ path: relativePath, operation: "missing" });
|
|
159
370
|
else if (sha256(current) === record.sha256) {
|
|
160
|
-
operations.push({ path: relativePath, operation: "
|
|
371
|
+
operations.push({ path: relativePath, operation: "archive-pending", reason: "stale managed asset is archived", sha256: record.sha256 });
|
|
372
|
+
archiveCandidates.push({ path: relativePath, record, content: current, reason: "stale managed asset", kind: record.kind });
|
|
161
373
|
mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
|
|
162
374
|
}
|
|
163
375
|
else {
|
|
@@ -166,8 +378,20 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
166
378
|
}
|
|
167
379
|
}
|
|
168
380
|
}
|
|
381
|
+
mutations.push(...await archiveMutations(archiveCandidates, scope, cwd, home, owned.manifest?.package_version ?? "1.0.0"));
|
|
169
382
|
const nextManifest = action === "install" || Object.keys(desiredFiles).length
|
|
170
|
-
? {
|
|
383
|
+
? {
|
|
384
|
+
schema_version: 2,
|
|
385
|
+
package: PACKAGE_NAME,
|
|
386
|
+
package_version: packageVersion(),
|
|
387
|
+
version: packageVersion(),
|
|
388
|
+
scope,
|
|
389
|
+
commands: selection.commands,
|
|
390
|
+
agents: selection.agents,
|
|
391
|
+
plugins: selection.plugins,
|
|
392
|
+
core_activation: selection.core_activation,
|
|
393
|
+
files: desiredFiles,
|
|
394
|
+
}
|
|
171
395
|
: undefined;
|
|
172
396
|
const manifestContent = nextManifest ? Buffer.from(`${stable(nextManifest)}\n`) : undefined;
|
|
173
397
|
if (manifestContent && (!owned.raw || !owned.raw.equals(manifestContent))) {
|
|
@@ -179,7 +403,7 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
179
403
|
mutations.push({ path: MANIFEST_NAME, operation: "remove", expected: { sha256: sha256(owned.raw) } });
|
|
180
404
|
}
|
|
181
405
|
const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
|
|
182
|
-
const base = { schema_version:
|
|
406
|
+
const base = { schema_version: 2, action, scope, root, package_version: packageVersion(), selection, operations: sorted, requires_restart: (action === "install" && owned.manifest?.package_version !== packageVersion()) || profiles.plan.requires_restart || mutations.some((item) => item.path.startsWith("agents/") || item.path.startsWith("commands/") || item.path.startsWith("plugins/")) };
|
|
183
407
|
return {
|
|
184
408
|
plan: { ...base, digest: digest(base) },
|
|
185
409
|
mutations,
|
|
@@ -187,14 +411,14 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
187
411
|
profiles,
|
|
188
412
|
};
|
|
189
413
|
}
|
|
190
|
-
export async function preview(action, scope, cwd = process.cwd(), home = homedir()) {
|
|
414
|
+
export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection) {
|
|
191
415
|
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
192
416
|
const root = deploymentRoot(scope, cwd, home);
|
|
193
417
|
try {
|
|
194
418
|
return await withLifecycleLock(stateRoot, async () => {
|
|
195
419
|
if (await recoverTransaction(root, stateRoot))
|
|
196
420
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
197
|
-
const built = await build(action, scope, cwd, home);
|
|
421
|
+
const built = await build(action, scope, cwd, home, selection);
|
|
198
422
|
const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, { digest: built.plan.digest });
|
|
199
423
|
return { ...built.plan, digest: receipt.digest, receipt_expires_at: receipt.expires_at };
|
|
200
424
|
});
|
|
@@ -207,7 +431,7 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
|
|
|
207
431
|
throw error;
|
|
208
432
|
}
|
|
209
433
|
}
|
|
210
|
-
export async function apply(action, scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}) {
|
|
434
|
+
export async function apply(action, scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}, selection) {
|
|
211
435
|
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
212
436
|
const root = deploymentRoot(scope, cwd, home);
|
|
213
437
|
try {
|
|
@@ -215,7 +439,7 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
|
|
|
215
439
|
if (await recoverTransaction(root, stateRoot))
|
|
216
440
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
217
441
|
const receipt = (await consumeReceipt(stateRoot, { digest: confirmationDigest, kind: `installer:${action}`, scope, root }));
|
|
218
|
-
const built = await build(action, scope, cwd, home);
|
|
442
|
+
const built = await build(action, scope, cwd, home, selection);
|
|
219
443
|
if (built.plan.digest !== receipt.digest)
|
|
220
444
|
throw new InstallerError("stale_plan", "Installer plan changed after preview");
|
|
221
445
|
if (built.plan.operations.some((item) => item.operation === "conflict" && (item.reason === "unmanaged_file" || item.reason === "v1.0.0_agent_ownership_mismatch" || item.reason?.includes("collision")))) {
|
|
@@ -228,11 +452,11 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
|
|
|
228
452
|
validateFinal: async () => {
|
|
229
453
|
await options.validateFinal?.();
|
|
230
454
|
await validateBuiltAgentProfilePlan(built.profiles);
|
|
231
|
-
await validateGenericDeployment(root, action, await assets(), built.plan.operations, built.expectedManifest);
|
|
455
|
+
await validateGenericDeployment(root, action, await assets(built.plan.selection), built.plan.operations, built.expectedManifest, retiredAssetPaths());
|
|
232
456
|
if (action !== "install")
|
|
233
457
|
return;
|
|
234
458
|
const inventory = await listAgentProfiles(scope, cwd, home);
|
|
235
|
-
if (inventory.collisions.length || inventory.drift.length || inventory.profiles.filter((item) => item.ownership !== "user-owned").some((item) => item.state !== "current")) {
|
|
459
|
+
if (built.plan.selection.agents.length > 0 && (inventory.collisions.length || inventory.drift.length || inventory.profiles.filter((item) => item.ownership !== "user-owned").some((item) => item.state !== "current"))) {
|
|
236
460
|
throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
|
|
237
461
|
}
|
|
238
462
|
},
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -10,12 +10,14 @@ export type FileExpectation = {
|
|
|
10
10
|
};
|
|
11
11
|
export type FileMutation = {
|
|
12
12
|
path: string;
|
|
13
|
+
root?: string;
|
|
13
14
|
operation: "write";
|
|
14
15
|
content: Buffer;
|
|
15
16
|
mode: number;
|
|
16
17
|
expected: FileExpectation;
|
|
17
18
|
} | {
|
|
18
19
|
path: string;
|
|
20
|
+
root?: string;
|
|
19
21
|
operation: "remove";
|
|
20
22
|
expected: FileExpectation;
|
|
21
23
|
};
|
|
@@ -32,6 +34,7 @@ export declare function readRegular(path: string): Promise<Buffer | undefined>;
|
|
|
32
34
|
export declare function writeAtomic(path: string, content: Buffer, mode: number): Promise<void>;
|
|
33
35
|
export declare function deploymentRoot(scope: Scope, cwd?: string, home?: string): string;
|
|
34
36
|
export declare function lifecycleRoot(scope: Scope, cwd?: string, home?: string): string;
|
|
37
|
+
export declare function archiveRoot(scope: Scope, cwd?: string, home?: string): string;
|
|
35
38
|
export declare function withLifecycleLock<T>(stateRoot: string, callback: () => Promise<T>): Promise<T>;
|
|
36
39
|
export declare function saveReceipt(stateRoot: string, kind: string, scope: Scope, root: string, payload: unknown, now?: number): Promise<{
|
|
37
40
|
digest: string;
|
package/dist/lifecycle.js
CHANGED
|
@@ -151,6 +151,12 @@ export function lifecycleRoot(scope, cwd = process.cwd(), home = homedir()) {
|
|
|
151
151
|
const suffix = scope === "global" ? "global" : join("project", sha256(resolve(cwd)));
|
|
152
152
|
return join(base, "opencode", "skills-opencode", suffix);
|
|
153
153
|
}
|
|
154
|
+
export function archiveRoot(scope, cwd = process.cwd(), home = homedir()) {
|
|
155
|
+
const base = process.env.XDG_DATA_HOME && home === homedir()
|
|
156
|
+
? resolve(process.env.XDG_DATA_HOME)
|
|
157
|
+
: resolve(home, ".local", "share");
|
|
158
|
+
return join(base, "opencode", "skills-opencode", "archive", scope);
|
|
159
|
+
}
|
|
154
160
|
async function processAlive(pid) {
|
|
155
161
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
156
162
|
return false;
|
|
@@ -331,16 +337,25 @@ export async function consumeReceipt(stateRoot, expected, now = Date.now()) {
|
|
|
331
337
|
function journalPath(stateRoot) {
|
|
332
338
|
return join(stateRoot, "transaction-journal.json");
|
|
333
339
|
}
|
|
340
|
+
function mutationRoot(root, mutation) {
|
|
341
|
+
return resolve(mutation.root ?? root);
|
|
342
|
+
}
|
|
334
343
|
async function snapshot(root, mutation) {
|
|
335
|
-
const
|
|
344
|
+
const targetRoot = mutationRoot(root, mutation);
|
|
345
|
+
const target = destination(targetRoot, mutation.path);
|
|
336
346
|
const content = await readRegular(target);
|
|
337
347
|
if (!content)
|
|
338
|
-
return { path: mutation.path, content: null, mode: null };
|
|
348
|
+
return { path: mutation.path, root: targetRoot, content: null, mode: null };
|
|
339
349
|
const metadata = await stat(target);
|
|
340
|
-
return {
|
|
350
|
+
return {
|
|
351
|
+
path: mutation.path,
|
|
352
|
+
root: targetRoot,
|
|
353
|
+
content: content.toString("base64"),
|
|
354
|
+
mode: metadata.mode & 0o777,
|
|
355
|
+
};
|
|
341
356
|
}
|
|
342
357
|
async function validateExpectation(root, mutation) {
|
|
343
|
-
const current = await readRegular(destination(root, mutation.path));
|
|
358
|
+
const current = await readRegular(destination(mutationRoot(root, mutation), mutation.path));
|
|
344
359
|
if (mutation.expected.absent) {
|
|
345
360
|
if (current)
|
|
346
361
|
throw new LifecycleError("stale_plan", `Expected an absent target: ${mutation.path}`);
|
|
@@ -358,7 +373,7 @@ async function restore(root, journal) {
|
|
|
358
373
|
for (let index = journal.snapshots.length - 1; index >= 0; index -= 1) {
|
|
359
374
|
const item = journal.snapshots[index];
|
|
360
375
|
const operation = journal.operations[index];
|
|
361
|
-
const target = destination(root, item.path);
|
|
376
|
+
const target = destination(item.root, item.path);
|
|
362
377
|
const current = await readRegular(target);
|
|
363
378
|
const currentHash = current ? sha256(current) : undefined;
|
|
364
379
|
const currentMode = current ? (await lstat(target)).mode & 0o777 : undefined;
|
|
@@ -436,8 +451,11 @@ function parseJournal(raw, expectedRoot) {
|
|
|
436
451
|
journal.applying >= journal.operations.length))) {
|
|
437
452
|
throw new LifecycleError("invalid_journal", "Transaction journal has an unsupported schema");
|
|
438
453
|
}
|
|
439
|
-
for (const item of journal.snapshots)
|
|
454
|
+
for (const item of journal.snapshots) {
|
|
440
455
|
assertSafeRelative(item.path);
|
|
456
|
+
if (item.root !== undefined && !isAbsolute(item.root))
|
|
457
|
+
throw new LifecycleError("invalid_journal", "Transaction snapshot root must be absolute");
|
|
458
|
+
}
|
|
441
459
|
for (const item of journal.snapshots) {
|
|
442
460
|
if ((item.content !== null &&
|
|
443
461
|
(typeof item.content !== "string" ||
|
|
@@ -449,6 +467,8 @@ function parseJournal(raw, expectedRoot) {
|
|
|
449
467
|
}
|
|
450
468
|
for (const item of journal.operations) {
|
|
451
469
|
assertSafeRelative(item.path);
|
|
470
|
+
if (item.root !== undefined && !isAbsolute(item.root))
|
|
471
|
+
throw new LifecycleError("invalid_journal", "Transaction operation root must be absolute");
|
|
452
472
|
if (item.operation !== "write" && item.operation !== "remove")
|
|
453
473
|
throw new LifecycleError("invalid_journal", "Transaction journal contains an invalid operation");
|
|
454
474
|
if (item.operation === "write" &&
|
|
@@ -462,14 +482,18 @@ function parseJournal(raw, expectedRoot) {
|
|
|
462
482
|
}
|
|
463
483
|
if (!journal.created_directories.every((item) => item &&
|
|
464
484
|
typeof item.path === "string" &&
|
|
465
|
-
inside(resolve(expectedRoot), resolve(item.path))
|
|
485
|
+
(inside(resolve(expectedRoot), resolve(item.path)) ||
|
|
486
|
+
(journal.operations ?? []).some((operation) => operation.root && inside(resolve(operation.root), resolve(item.path)))) &&
|
|
466
487
|
Number.isSafeInteger(item.device) &&
|
|
467
488
|
item.device >= 0 &&
|
|
468
489
|
Number.isSafeInteger(item.inode) &&
|
|
469
490
|
item.inode > 0)) {
|
|
470
491
|
throw new LifecycleError("invalid_journal", "Transaction journal contains an unsafe created directory");
|
|
471
492
|
}
|
|
472
|
-
return
|
|
493
|
+
return {
|
|
494
|
+
...journal,
|
|
495
|
+
snapshots: journal.snapshots.map((item) => ({ ...item, root: item.root ?? expectedRoot })),
|
|
496
|
+
};
|
|
473
497
|
}
|
|
474
498
|
export async function recoverTransaction(root, stateRoot) {
|
|
475
499
|
const raw = await readRegular(journalPath(stateRoot));
|
|
@@ -481,12 +505,13 @@ export async function recoverTransaction(root, stateRoot) {
|
|
|
481
505
|
return true;
|
|
482
506
|
}
|
|
483
507
|
async function ensureTransactionDirectories(root, mutation, stateRoot, journal) {
|
|
484
|
-
|
|
485
|
-
|
|
508
|
+
const targetRoot = mutationRoot(root, mutation);
|
|
509
|
+
await ensureDirectory(dirname(targetRoot), 0o700);
|
|
510
|
+
const relativeParent = relative(targetRoot, dirname(destination(targetRoot, mutation.path)));
|
|
486
511
|
const parts = relativeParent.split(sep).filter(Boolean);
|
|
487
512
|
const directories = [
|
|
488
|
-
|
|
489
|
-
...parts.map((_, index) => resolve(
|
|
513
|
+
targetRoot,
|
|
514
|
+
...parts.map((_, index) => resolve(targetRoot, ...parts.slice(0, index + 1))),
|
|
490
515
|
];
|
|
491
516
|
for (const directory of directories) {
|
|
492
517
|
const existing = await lstatSafe(directory);
|
|
@@ -521,9 +546,10 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
|
|
|
521
546
|
const unique = new Set();
|
|
522
547
|
for (const mutation of mutations) {
|
|
523
548
|
assertSafeRelative(mutation.path);
|
|
524
|
-
|
|
549
|
+
const key = `${mutationRoot(root, mutation)}:${mutation.path}`;
|
|
550
|
+
if (unique.has(key))
|
|
525
551
|
throw new LifecycleError("invalid_plan", `Duplicate transaction target: ${mutation.path}`);
|
|
526
|
-
unique.add(
|
|
552
|
+
unique.add(key);
|
|
527
553
|
await validateExpectation(root, mutation);
|
|
528
554
|
}
|
|
529
555
|
const snapshots = await Promise.all(mutations.map((mutation) => snapshot(root, mutation)));
|
|
@@ -533,12 +559,18 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
|
|
|
533
559
|
operations: mutations.map((mutation) => mutation.operation === "write"
|
|
534
560
|
? {
|
|
535
561
|
path: mutation.path,
|
|
562
|
+
...(mutation.root ? { root: resolve(mutation.root) } : {}),
|
|
536
563
|
operation: mutation.operation,
|
|
537
564
|
content: mutation.content.toString("base64"),
|
|
538
565
|
mode: mutation.mode,
|
|
539
566
|
expected: mutation.expected,
|
|
540
567
|
}
|
|
541
|
-
: {
|
|
568
|
+
: {
|
|
569
|
+
path: mutation.path,
|
|
570
|
+
...(mutation.root ? { root: resolve(mutation.root) } : {}),
|
|
571
|
+
operation: mutation.operation,
|
|
572
|
+
expected: mutation.expected,
|
|
573
|
+
}),
|
|
542
574
|
snapshots,
|
|
543
575
|
created_directories: [],
|
|
544
576
|
published: 0,
|
|
@@ -553,7 +585,7 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
|
|
|
553
585
|
if (mutation.operation === "write")
|
|
554
586
|
await ensureTransactionDirectories(root, mutation, stateRoot, journal);
|
|
555
587
|
await validateExpectation(root, mutation);
|
|
556
|
-
const target = destination(root, mutation.path);
|
|
588
|
+
const target = destination(mutationRoot(root, mutation), mutation.path);
|
|
557
589
|
if (mutation.operation === "write")
|
|
558
590
|
await writeAtomic(target, mutation.content, mutation.mode);
|
|
559
591
|
else
|
|
@@ -568,7 +600,7 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
|
|
|
568
600
|
throw new LifecycleError("test_failure", "Injected transaction failure");
|
|
569
601
|
}
|
|
570
602
|
for (const mutation of mutations) {
|
|
571
|
-
const target = destination(root, mutation.path);
|
|
603
|
+
const target = destination(mutationRoot(root, mutation), mutation.path);
|
|
572
604
|
const current = await readRegular(target);
|
|
573
605
|
if ((mutation.operation === "remove" && current) ||
|
|
574
606
|
(mutation.operation === "write" &&
|
package/dist/plugins/rtk.js
CHANGED
|
@@ -8,6 +8,8 @@ const FILTERS = [
|
|
|
8
8
|
function filter(command) {
|
|
9
9
|
if (/[|;$&\n()`]/.test(command))
|
|
10
10
|
return undefined;
|
|
11
|
+
if (/\b(--json|--format(?:=|\s)|diagnos|security|deploy|migrat|delete|remove)\b/i.test(command))
|
|
12
|
+
return undefined;
|
|
11
13
|
const values = command.trim().split(/\s+/);
|
|
12
14
|
return FILTERS.find((item) => item.prefix.every((part, index) => values[index] === part))?.filter;
|
|
13
15
|
}
|
|
@@ -40,7 +42,8 @@ export async function rtk(options = {}) {
|
|
|
40
42
|
const selected = typeof input.args?.command === "string" ? filter(input.args.command) : undefined;
|
|
41
43
|
const compressed = selected ? await run(selected, output.output) : undefined;
|
|
42
44
|
const result = compressed && compressed.length < output.output.length ? compressed : truncate(output.output);
|
|
43
|
-
|
|
45
|
+
const originalSize = output.output.length;
|
|
46
|
+
output.output = `${result}\n[rtk: compressed method=${compressed ? `rtk/${selected}` : "head+tail"}; sizes=${originalSize}->${result.length}; evidence_complete=false; loss=possible]`;
|
|
44
47
|
}
|
|
45
48
|
catch { /* RTK is fail-open: preserve original tool output on plugin failure. */ }
|
|
46
49
|
} };
|