@kisev/skills-opencode 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ npx --yes skills add kisev/skills --agent opencode --skill '*' --copy --yes
16
16
 
17
17
  Для одного skill укажите `--skill <name>`. Для воспроизводимой установки можно
18
18
  передать URL GitHub tag, например
19
- `https://github.com/kisev/skills/tree/v1.1.0`. Package никогда не устанавливает
19
+ `https://github.com/kisev/skills/tree/v1.1.1`. Package никогда не устанавливает
20
20
  и не обновляет skills. Если команда не нашла skill, она сообщает точную команду
21
21
  `npx skills add` для его установки.
22
22
 
@@ -25,16 +25,18 @@ npx --yes skills add kisev/skills --agent opencode --skill '*' --copy --yes
25
25
  Установите npm package там, где OpenCode сможет разрешить plugin:
26
26
 
27
27
  ```shell
28
- npm install @kisev/skills-opencode@1.1.0
28
+ npm install @kisev/skills-opencode@1.1.1
29
29
  ```
30
30
 
31
- Сначала покажите план installer. Эта команда не создаёт files:
31
+ Сначала покажите план installer. По умолчанию CLI выводит короткую сводку:
32
+ счётчики по группам, только изменяемые paths, conflicts, restart flag, digest и
33
+ готовую confirm-команду. Эта команда не меняет deployment:
32
34
 
33
35
  ```shell
34
36
  npm exec -- skills-opencode install --scope global --dry-run
35
37
  ```
36
38
 
37
- Проверьте exact operations и примените только показанный digest:
39
+ Проверьте сводку и примените только показанный digest:
38
40
 
39
41
  ```shell
40
42
  npm exec -- skills-opencode install --scope global --confirm <digest>
@@ -54,6 +56,14 @@ Project assets находятся в `.opencode/agents`, `.opencode/commands` и
54
56
  перезаписывает неизвестные или изменённые files и сохраняет ownership manifests
55
57
  только после confirmed apply.
56
58
 
59
+ Для automation добавьте `--json`. Этот режим сохраняет полный стабильный
60
+ machine-readable plan, включая `operations` и `requires_restart`:
61
+
62
+ ```shell
63
+ npm exec -- skills-opencode install --scope global --dry-run --json
64
+ npm exec -- skills-opencode agent list --scope global --json
65
+ ```
66
+
57
67
  Добавьте plugin в `opencode.json` вручную:
58
68
 
59
69
  ```json
@@ -95,11 +105,12 @@ npm exec -- skills-opencode critic add security --scope global \
95
105
  npm exec -- skills-opencode critic remove security --scope global --dry-run
96
106
  ```
97
107
 
98
- Для любой mutation замените `--dry-run` на `--confirm <digest>` и повторите те же
99
- аргументы. Plan содержит TLDR operations без полного diff. Digest связан с
100
- одноразовым private receipt, действует 10 минут и повторно не применяется.
101
- Успешный machine-readable result содержит `requires_restart`; после `true`
102
- полностью перезапустите OpenCode.
108
+ Для любой mutation используйте готовую confirm-команду из preview либо замените
109
+ `--dry-run` на `--confirm <digest>` и повторите те же аргументы. Человекочитаемый
110
+ plan не печатает полный JSON и сворачивает длинные группы paths. Digest связан с
111
+ одноразовым private receipt, действует 10 минут и повторно не применяется. Для
112
+ machine-readable result добавьте `--json`; поле `requires_restart` сообщает о
113
+ необходимости полностью перезапустить OpenCode.
103
114
 
104
115
  В global scope profile configuration хранится в
105
116
  `~/.config/opencode/.skills-opencode/agent-profiles.json`, а semantic deployment
@@ -0,0 +1,11 @@
1
+ import type { AgentInventory, AgentProfilePlan } from "./agent-profiles.js";
2
+ import type { Plan as InstallerPlan } from "./installer.js";
3
+ type DisplayPlan = InstallerPlan | AgentProfilePlan;
4
+ export declare function terminalSafe(value: string): string;
5
+ export declare function renderPlan(plan: DisplayPlan, options: {
6
+ applied: boolean;
7
+ confirmationCommand?: string;
8
+ }): string;
9
+ export declare function renderInventory(inventory: AgentInventory): string;
10
+ export declare function shellCommand(arguments_: readonly string[]): string;
11
+ export {};
@@ -0,0 +1,149 @@
1
+ const GROUPS = ["Agents", "Commands", "Plugins", "State"];
2
+ const OPERATIONS = ["create", "update", "remove", "conflict", "missing", "unchanged"];
3
+ const UNSAFE_TERMINAL_CHARACTER = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
4
+ export function terminalSafe(value) {
5
+ return [...value]
6
+ .map((character) => {
7
+ const code = character.codePointAt(0);
8
+ const unsafe = character === "\\" || UNSAFE_TERMINAL_CHARACTER.test(character);
9
+ if (!unsafe)
10
+ return character;
11
+ if (character === "\\")
12
+ return "\\\\";
13
+ if (character === "\n")
14
+ return "\\n";
15
+ if (character === "\r")
16
+ return "\\r";
17
+ if (character === "\t")
18
+ return "\\t";
19
+ if (code <= 0xff)
20
+ return `\\x${code.toString(16).padStart(2, "0")}`;
21
+ if (code <= 0xffff)
22
+ return `\\u${code.toString(16).padStart(4, "0")}`;
23
+ return `\\u{${code.toString(16)}}`;
24
+ })
25
+ .join("");
26
+ }
27
+ function groupFor(path) {
28
+ if (path.startsWith("agents/"))
29
+ return "Agents";
30
+ if (path.startsWith("commands/"))
31
+ return "Commands";
32
+ if (path.startsWith("plugins/"))
33
+ return "Plugins";
34
+ return "State";
35
+ }
36
+ function actionName(action) {
37
+ return {
38
+ install: "Install",
39
+ uninstall: "Uninstall",
40
+ "model-set": "Configure agent model",
41
+ "critic-add": "Add critic",
42
+ "critic-remove": "Remove critic",
43
+ reconcile: "Reconcile agents",
44
+ }[action];
45
+ }
46
+ function operationSummary(operations) {
47
+ return OPERATIONS.map((operation) => {
48
+ const count = operations.filter((item) => item.operation === operation).length;
49
+ return count ? `${operation} ${count}` : undefined;
50
+ })
51
+ .filter(Boolean)
52
+ .join(", ");
53
+ }
54
+ function shortPath(path, group) {
55
+ if (group === "State")
56
+ return terminalSafe(path);
57
+ const value = path.slice(path.indexOf("/") + 1);
58
+ return terminalSafe(value.endsWith(".md") || value.endsWith(".js") ? value.slice(0, -3) : value);
59
+ }
60
+ function detailLines(operations) {
61
+ const lines = [];
62
+ for (const group of GROUPS) {
63
+ const grouped = operations.filter((item) => groupFor(item.path) === group && item.operation !== "unchanged");
64
+ for (const operation of OPERATIONS.filter((value) => value !== "unchanged" && grouped.some((item) => item.operation === value))) {
65
+ const values = grouped
66
+ .filter((item) => item.operation === operation)
67
+ .map((item) => operation === "conflict"
68
+ ? `${shortPath(item.path, group)} (${terminalSafe(item.reason ?? "conflict")})`
69
+ : shortPath(item.path, group));
70
+ const visible = operation === "conflict" ? values : values.slice(0, 8);
71
+ const rest = values.length - visible.length;
72
+ lines.push(` ${group}/${operation}: ${visible.join(", ")}${rest ? ` (+${rest} more)` : ""}`);
73
+ }
74
+ }
75
+ return lines;
76
+ }
77
+ function migrationSummary(operations) {
78
+ const count = operations.filter((item) => item.reason === "v1.0.0 ownership transfer").length;
79
+ return count
80
+ ? ` Ownership migration: ${count} agent${count === 1 ? "" : "s"} from v1.0.0`
81
+ : undefined;
82
+ }
83
+ export function renderPlan(plan, options) {
84
+ const version = "package_version" in plan ? ` @kisev/skills-opencode ${plan.package_version}` : "";
85
+ const lines = [
86
+ `${actionName(plan.action)}${version} (${plan.scope})`,
87
+ `Target: ${terminalSafe(plan.root)}`,
88
+ "",
89
+ options.applied ? "Applied changes:" : "Planned changes:",
90
+ ];
91
+ for (const group of GROUPS) {
92
+ const operations = plan.operations.filter((item) => groupFor(item.path) === group);
93
+ if (operations.length)
94
+ lines.push(` ${group}: ${operationSummary(operations)}`);
95
+ }
96
+ const migration = migrationSummary(plan.operations);
97
+ if (migration)
98
+ lines.push(migration);
99
+ const details = detailLines(plan.operations);
100
+ if (details.length)
101
+ lines.push("", "Details:", ...details);
102
+ const conflicts = plan.operations.filter((item) => item.operation === "conflict").length;
103
+ lines.push("", `Conflicts: ${conflicts || "none"}`);
104
+ lines.push(options.applied
105
+ ? `Restart required: ${plan.requires_restart ? "yes" : "no"}`
106
+ : `Restart after apply: ${plan.requires_restart ? "yes" : "no"}`);
107
+ if (!options.applied) {
108
+ if (plan.receipt_expires_at)
109
+ lines.push(`Confirmation expires: ${plan.receipt_expires_at}`);
110
+ lines.push(`Digest: ${plan.digest}`);
111
+ if (options.confirmationCommand)
112
+ lines.push("", "Apply:", ` ${options.confirmationCommand}`);
113
+ }
114
+ return `${lines.join("\n")}\n`;
115
+ }
116
+ function table(rows) {
117
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column]?.length ?? 0)));
118
+ return rows.map((row) => row
119
+ .map((value, column) => value.padEnd(widths[column]))
120
+ .join(" ")
121
+ .trimEnd());
122
+ }
123
+ export function renderInventory(inventory) {
124
+ const rows = [
125
+ ["NAME", "MODEL", "VARIANT", "OWNER", "STATE"],
126
+ ...inventory.profiles.map((profile) => [
127
+ terminalSafe(profile.name),
128
+ terminalSafe(profile.model ?? "default"),
129
+ terminalSafe(profile.variant ?? "-"),
130
+ profile.ownership,
131
+ profile.state,
132
+ ]),
133
+ ];
134
+ return `${[
135
+ `OpenCode agents (${inventory.scope})`,
136
+ `Target: ${terminalSafe(inventory.root)}`,
137
+ "",
138
+ ...table(rows),
139
+ "",
140
+ `Critic pool: ${inventory.critic_pool.map(terminalSafe).join(", ")}`,
141
+ `Collisions: ${inventory.collisions.length ? inventory.collisions.map(terminalSafe).join(", ") : "none"}`,
142
+ `Drift: ${inventory.drift.length ? inventory.drift.map(terminalSafe).join(", ") : "none"}`,
143
+ ].join("\n")}\n`;
144
+ }
145
+ export function shellCommand(arguments_) {
146
+ return ["npm", "exec", "--", "skills-opencode", ...arguments_]
147
+ .map((value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`)
148
+ .join(" ");
149
+ }
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { AgentProfileError, applyAgentProfileChange, availableModels, availableModelVariants, FIXED_AGENT_ROLES, listAgentProfiles, previewAgentProfileChange, validateAgentName, validateModel, validateVariant, } from "./agent-profiles.js";
4
- import { apply, InstallerError, preview, result } from "./installer.js";
4
+ import { renderInventory, renderPlan, shellCommand, terminalSafe } from "./cli-output.js";
5
+ import { apply, InstallerError, preview } from "./installer.js";
5
6
  import { LifecycleError } from "./lifecycle.js";
6
7
  function parseOptions(values) {
7
- const options = { dryRun: false };
8
+ const options = { dryRun: false, json: false };
8
9
  for (let index = 0; index < values.length; index += 1) {
9
10
  const value = values[index];
10
11
  if (!value.startsWith("--")) {
@@ -52,6 +53,11 @@ function parseOptions(values) {
52
53
  throw new InstallerError("invalid_input", "Use only one variant option");
53
54
  options.variant = null;
54
55
  }
56
+ else if (value === "--json") {
57
+ if (options.json)
58
+ throw new InstallerError("invalid_input", "--json may be supplied once");
59
+ options.json = true;
60
+ }
55
61
  else {
56
62
  throw new InstallerError("invalid_input", `Unknown argument: ${value}`);
57
63
  }
@@ -133,12 +139,33 @@ async function runProfile(request, options) {
133
139
  requireConfirmationMode(options);
134
140
  if (options.dryRun) {
135
141
  const plan = await previewAgentProfileChange(request, options.scope);
136
- process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, requires_restart: false, plan }, null, 2)}\n`);
142
+ if (options.json)
143
+ process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, requires_restart: false, plan }, null, 2)}\n`);
144
+ else
145
+ process.stdout.write(renderPlan(plan, { applied: false, confirmationCommand: shellCommand(profileConfirmationArguments(request, options.scope, plan.digest)) }));
137
146
  }
138
147
  else {
139
- process.stdout.write(`${JSON.stringify(await applyAgentProfileChange(request, options.scope, options.confirm), null, 2)}\n`);
148
+ const applied = await applyAgentProfileChange(request, options.scope, options.confirm);
149
+ if (options.json)
150
+ process.stdout.write(`${JSON.stringify(applied, null, 2)}\n`);
151
+ else
152
+ process.stdout.write(renderPlan(applied.plan, { applied: true }));
140
153
  }
141
154
  }
155
+ function profileConfirmationArguments(request, scope, digest) {
156
+ if (request.action === "reconcile")
157
+ return ["agent", "reconcile", "--scope", scope, "--confirm", digest];
158
+ if (request.action === "critic-remove")
159
+ return ["critic", "remove", request.name, "--scope", scope, "--confirm", digest];
160
+ const command = request.action === "critic-add" ? ["critic", "add", request.name] : ["agent", "model-set", request.name];
161
+ command.push("--scope", scope, "--model", request.model);
162
+ if (request.variant)
163
+ command.push("--variant", request.variant);
164
+ else if (request.action === "model-set")
165
+ command.push("--clear-variant");
166
+ command.push("--confirm", digest);
167
+ return command;
168
+ }
142
169
  async function run(arguments_) {
143
170
  const [domain, operation, ...rest] = arguments_;
144
171
  if (domain === "install" || domain === "uninstall") {
@@ -151,14 +178,31 @@ async function run(arguments_) {
151
178
  throw new InstallerError("invalid_input", "Installer accepts only scope and confirmation options");
152
179
  requireConfirmationMode(options);
153
180
  const action = domain;
154
- process.stdout.write(`${result(options.dryRun ? await preview(action, options.scope) : await apply(action, options.scope, options.confirm), !options.dryRun)}\n`);
181
+ if (options.dryRun) {
182
+ const plan = await preview(action, options.scope);
183
+ if (options.json)
184
+ process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, requires_restart: false, plan }, null, 2)}\n`);
185
+ else
186
+ process.stdout.write(renderPlan(plan, { applied: false, confirmationCommand: shellCommand([action, "--scope", options.scope, "--confirm", plan.digest]) }));
187
+ }
188
+ else {
189
+ const plan = await apply(action, options.scope, options.confirm);
190
+ if (options.json)
191
+ process.stdout.write(`${JSON.stringify({ status: "ok", applied: true, requires_restart: plan.requires_restart, plan }, null, 2)}\n`);
192
+ else
193
+ process.stdout.write(renderPlan(plan, { applied: true }));
194
+ }
155
195
  return;
156
196
  }
157
197
  if (domain === "agent" && operation === "list") {
158
198
  const options = parseOptions(rest);
159
199
  if (options.dryRun || options.confirm || options.name || options.provider || options.model || options.variant !== undefined)
160
200
  throw new InstallerError("invalid_input", "agent list accepts only --scope");
161
- process.stdout.write(`${JSON.stringify({ status: "ok", inventory: await listAgentProfiles(options.scope) }, null, 2)}\n`);
201
+ const inventory = await listAgentProfiles(options.scope);
202
+ if (options.json)
203
+ process.stdout.write(`${JSON.stringify({ status: "ok", inventory }, null, 2)}\n`);
204
+ else
205
+ process.stdout.write(renderInventory(inventory));
162
206
  return;
163
207
  }
164
208
  if (domain === "agent" && operation === "configure") {
@@ -212,7 +256,10 @@ async function main() {
212
256
  }
213
257
  catch (error) {
214
258
  const known = error instanceof LifecycleError ? error : new InstallerError("internal_error", error instanceof Error ? error.message : String(error));
215
- process.stdout.write(`${JSON.stringify({ status: "error", error: { code: known.code, message: known.message } })}\n`);
259
+ if (process.argv.includes("--json"))
260
+ process.stdout.write(`${JSON.stringify({ status: "error", error: { code: known.code, message: known.message } })}\n`);
261
+ else
262
+ process.stderr.write(`Error [${known.code}]: ${terminalSafe(known.message)}\n`);
216
263
  process.exitCode = 2;
217
264
  }
218
265
  }
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ const CATALOG = {
17
17
  skills: ["attempt", "goal", "schedule", "multi-run", "usage", "overview", "lsp-report"],
18
18
  plugins: ["background-attempts", "goal-loop", "schedule", "autonomy-policy", "rules-injector", "rtk", "zed-bell", "zed-clickable-paths"],
19
19
  replacements: ["capabilities", "route", "doctor", "agent_profiles"],
20
- version: "1.1.0",
20
+ version: "1.1.1",
21
21
  };
22
22
  const plugin = (async (input) => {
23
23
  const gate = new RoutingGate();
package/dist/installer.js CHANGED
@@ -179,7 +179,7 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
179
179
  mutations.push({ path: MANIFEST_NAME, operation: "remove", expected: { sha256: sha256(owned.raw) } });
180
180
  }
181
181
  const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
182
- const base = { schema_version: 1, action, scope, root, package_version: packageVersion(), operations: sorted, requires_restart: profiles.plan.requires_restart || sorted.some((item) => item.path.startsWith("commands/") || item.path.startsWith("plugins/")) };
182
+ const base = { schema_version: 1, action, scope, root, package_version: packageVersion(), operations: sorted, requires_restart: (action === "install" && owned.manifest?.version !== packageVersion()) || profiles.plan.requires_restart || mutations.some((item) => item.path.startsWith("agents/") || item.path.startsWith("commands/") || item.path.startsWith("plugins/")) };
183
183
  return {
184
184
  plan: { ...base, digest: digest(base) },
185
185
  mutations,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisev/skills-opencode",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "OpenCode integration and opt-in installer for portable Agent Skills.",
5
5
  "license": "MIT",
6
6
  "repository": {