opencode-plugin-flow 3.0.1 → 3.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.2.0] - 2026-06-13
6
+
7
+ Stale installs become visible: a passive update notice, the running version in `/flow-status`, and documented update steps
8
+
9
+ OpenCode caches plugin installs per spec string and never re-resolves them, so a user can keep running an old Flow version with no signal anywhere. Plugin startup now schedules a background check against the npm `latest` dist-tag and logs a one-line notice when a newer release exists, including the exact pin to set. The notice is best-effort and fire-and-forget: a 3-second timeout, fail-silent on any network or registry error, never blocking startup, and never touching the user's `opencode.json` — the pin is the user's intent and is only ever reported, not rewritten. Set `FLOW_DISABLE_UPDATE_CHECK=1` to opt out (the test suite, install smoke, and bundle sanity set it so they stay network-free).
10
+
11
+ `flow_status` now reports the running plugin version in its install check — `details.pluginVersion` plus the passing summary line — so "which version did OpenCode actually load?" has a first-class answer instead of requiring a dig through the cache directory.
12
+
13
+ The README gains an "Updating" section documenting the two-restart exact-pin bump workflow, the cache-clear step required for range pins, and the update-notice opt-out.
14
+
15
+ Not-tested: The live notice against the real npm registry from inside an OpenCode host (the check path is covered by dependency-injected tests; the registry endpoint and payload shape were verified by hand).
16
+
17
+ ## [3.1.0] - 2026-06-12
18
+
19
+ Only the v3 surface remains: five commands, seven tools, no v2 leftovers
20
+
21
+ The command surface trims from nine to five: `/flow-auto`, `/flow-plan`, `/flow-run`, `/flow-review`, and `/flow-status`. The retired four (`/flow-doctor`, `/flow-history`, `/flow-session`, `/flow-reset`) were thin wrappers over a single tool call that work just as well as a plain request — "show the flow history", "close this session as completed", "reset feature X" — and `/flow-doctor` was the same `flow_status` view as `/flow-status`. Startup sync now also cleans up: Flow-owned files that a 3.0.x install synced for the retired names are removed from `~/.config/opencode/commands/` (user-edited copies are kept untouched), and `bunx opencode-plugin-flow uninstall` does the same.
22
+
23
+ The 15 v2 tool-name redirect stubs (`flow_doctor`, `flow_run_complete_feature`, `flow_session_close`, …) are removed as scheduled — the seven canonical tools are now the entire registered surface. The session schema is still v1: v2-created `.flow/**` sessions continue to resume unchanged.
24
+
25
+ Runtime next-step hints now reference things that exist: where a suggestion used to name a retired command (`/flow-reset feature <id>`, `/flow-history`, `/flow-session activate <id>`), it now names the tool call the skills teach (`flow_feature_complete reset <id>`, `flow_session history`, `flow_session activate <id>`). The five surviving commands are still suggested as slash commands.
26
+
27
+ Restart OpenCode once after upgrading so the retired command files are cleaned up and the trimmed surface is discovered.
28
+
29
+ Not-tested: Live OpenCode UI slash-command discovery after a real npm upgrade (verified via the packed-tarball install smoke).
30
+
5
31
  ## [3.0.1] - 2026-06-12
6
32
 
7
33
  Slash commands and the reviewer agent are now synced as real files so OpenCode discovers them
package/README.md CHANGED
@@ -29,11 +29,11 @@ Add Flow to the `plugin` array in your `opencode.json` (global `~/.config/openco
29
29
 
30
30
  ```json
31
31
  {
32
- "plugin": ["opencode-plugin-flow@3"]
32
+ "plugin": ["opencode-plugin-flow@3.2.0"]
33
33
  }
34
34
  ```
35
35
 
36
- OpenCode installs the package from npm on startup. Pin the major version you install (currently `@3`) so restarts pick up fixes without crossing a breaking release.
36
+ OpenCode installs the package from npm on the first startup and caches it per spec string under `~/.cache/opencode/packages/<spec>/` — it does **not** re-resolve the version on later startups. Pin an exact version and bump the pin to upgrade: a changed spec string installs fresh. See [Updating](#updating) below.
37
37
 
38
38
  On startup the plugin syncs its global skills, commands, and review agent into OpenCode's normal discovery paths:
39
39
 
@@ -50,6 +50,23 @@ On startup the plugin syncs its global skills, commands, and review agent into O
50
50
 
51
51
  Sync is ownership-aware: each Flow-owned skill folder carries a `.flow-skill-version` marker with a sha256 line per shipped file, and each synced command/agent file has a sidecar `.flow-version` marker. Folders or files without Flow markers are never touched, and if you edit a Flow-owned file by hand the previous content is backed up next to it before an update replaces it.
52
52
 
53
+ ### Updating
54
+
55
+ OpenCode never auto-updates plugins: the cached install for a given spec string is reused as-is on every startup. Flow therefore checks npm in the background after startup and logs a one-line notice when a newer release exists — it only notifies and never edits your `opencode.json`. Set `FLOW_DISABLE_UPDATE_CHECK=1` to turn the check off.
56
+
57
+ To update with an exact pin (recommended):
58
+
59
+ 1. Change the pin in `opencode.json` to the new version, e.g. `"opencode-plugin-flow@3.2.0"`.
60
+ 2. Restart OpenCode once to install the new version, and a second time so the freshly re-synced skills, commands, and agents are discovered.
61
+
62
+ If you pinned a range like `@3` instead, the spec string never changes, so the cache entry must be cleared by hand before restarting:
63
+
64
+ ```bash
65
+ rm -rf ~/.cache/opencode/packages/opencode-plugin-flow@3
66
+ ```
67
+
68
+ `/flow-status` shows the running plugin version in its install check, so you can always confirm which version OpenCode actually loaded.
69
+
53
70
  ### Per-project skill overrides
54
71
 
55
72
  Skills are plain markdown and deliberately overridable. To customize Flow's behavior for one project — for example a team-specific planning or review rubric — place a project-local skill at:
@@ -87,7 +104,7 @@ Deeper methodology (worked plan examples, validation and review rubrics) lives i
87
104
 
88
105
  ### Commands
89
106
 
90
- Commands are thin pointers into the skills — the skill content is the real instruction surface. Command names are stable across the v2 → v3 upgrade.
107
+ Commands are thin pointers into the skills — the skill content is the real instruction surface.
91
108
 
92
109
  | Command | Use it when... |
93
110
  | --- | --- |
@@ -96,10 +113,8 @@ Commands are thin pointers into the skills — the skill content is the real ins
96
113
  | `/flow-run [feature-id]` | You want to execute exactly one approved feature. |
97
114
  | `/flow-review <goal>` | You want a read-only review and findings report (runs in the fresh-context `flow-reviewer` subagent). |
98
115
  | `/flow-status` | You want session state, readiness checks, and the suggested next step. |
99
- | `/flow-doctor` | You want the workspace readiness checks with remediation steps (same `flow_status` view, doctor framing). |
100
- | `/flow-history` | You want to list saved sessions. |
101
- | `/flow-session activate <id>` / `close <completed\|deferred\|abandoned>` / `show <id>` | You want to switch, close, or inspect sessions. |
102
- | `/flow-reset <feature-id>` | You want to reset a feature back to pending (convenience over `flow_feature_complete` with `reset: true`). |
116
+
117
+ These five are the whole command surface since v3.1. The v2/v3.0 convenience commands (`/flow-doctor`, `/flow-history`, `/flow-session`, `/flow-reset`) were retired: each was a thin wrapper over a single tool call that works as a plain request — "show the flow history", "close this session as completed", "reset feature X" — and `/flow-doctor` duplicated `/flow-status`. Startup sync removes the retired command files from earlier installs (user-edited copies are kept).
103
118
 
104
119
  ### Tools
105
120
 
@@ -115,7 +130,7 @@ The plugin registers a small tool surface (7 tools) that owns all `.flow/**` mut
115
130
  | `flow_review_record` | Record a reviewer decision (`scope: feature` or `final`). |
116
131
  | `flow_session` | Activate or close a session, list history, or show a stored session. |
117
132
 
118
- These seven tools are the whole canonical surface. For one minor cycle, the retired v2 tool names (for example `flow_run_complete_feature` or `flow_session_close`) remain registered as hidden redirect stubs: calling one returns an error naming its replacement and the key arguments, so resumed v2 sessions and old transcripts degrade gracefully instead of failing on an unknown tool. The stubs are scheduled for removal in v3.1. Existing v2 sessions migrate seamlessly (the session schema is unchanged).
133
+ These seven tools are the whole registered surface the v2 tool-name redirect stubs that shipped in 3.0 were removed in v3.1 as scheduled. Existing v2 sessions still migrate seamlessly (the session schema is unchanged).
119
134
 
120
135
  ### Agents
121
136
 
@@ -153,10 +168,10 @@ Flow refuses to write session state at filesystem roots or directly in `$HOME`,
153
168
  ## Troubleshooting
154
169
 
155
170
  ```text
156
- /flow-doctor
171
+ /flow-status
157
172
  ```
158
173
 
159
- shows the workspace readiness checks (skills in sync, no stale pre-npm copy, workspace writable) with remediation steps; `/flow-status` adds the active session state, the current blocker, and the suggested next step.
174
+ shows the workspace readiness checks (skills in sync, no stale pre-npm copy, workspace writable) with remediation steps, plus the active session state, the current blocker, and the suggested next step.
160
175
 
161
176
  ## OpenCode references
162
177
 
package/dist/cli.js CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import{homedir as oe}from"node:os";import{readdir as H,readFile as M,rm as l,rmdir as D}from"node:fs/promises";import{dirname as P,join as c,normalize as ee,sep as te}from"node:path";import{createHash as Q}from"node:crypto";import{join as w}from"node:path";var g=w(".config","opencode","skills"),b=w(".config","opencode","commands"),x=w(".config","opencode","agents"),f=".flow-skill-version",F="SKILL.md.backup",A=w(".config","opencode","plugins","flow.js"),_=`// Managed by flow-opencode install/uninstall
3
- `,W="flow-opencode-generated-skill",X=`<!-- ${W} `,J=new RegExp(`^<!-- ${W} name=([a-z0-9]+(?:-[a-z0-9]+)*) version=([0-9]+) hash=sha256:([a-f0-9]{64}) -->$`,"u");function h(e){return Q("sha256").update(e,"utf8").digest("hex")}function m(e){let t=e.split(`
4
- `),i=t.flatMap((v,U)=>v.startsWith(X)?[U]:[]);if(i.length===0)return{kind:"not_generated"};if(i.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let a=i[0];if(a===void 0)return{kind:"not_generated"};let s=t[a];if(s===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let o=s.match(J);if(!o)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,d]=o;if(n===void 0||r===void 0||d===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let u=[...t.slice(0,a),...t.slice(a+1)].join(`
5
- `);if(h(u)!==d)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:n,version:r,hash:d}}}var z="opencode-plugin-flow",j="file=",B="=sha256:";function y(e){let t=new Map;for(let i of e.split(`
6
- `)){if(!i.startsWith(j))continue;let a=i.slice(j.length),s=a.lastIndexOf(B);if(s===-1)continue;let o=a.slice(0,s),n=a.slice(s+B.length);if(o.length>0&&/^[a-f0-9]{64}$/.test(n))t.set(o,n)}return t}function k(e,t,i){let a=new Map;for(let n of e.split(`
7
- `)){let r=n.indexOf("=");if(r===-1)continue;a.set(n.slice(0,r),n.slice(r+1))}let s=a.get("version"),o=a.get("hash");if(a.get("plugin")!==z||a.get("kind")!==t||a.get("name")!==i||!s||!o?.startsWith("sha256:"))return null;return{kind:t,name:i,version:s,hash:o.slice(7)}}function R(e){let t=new Map;for(let o of e.split(`
8
- `)){let n=o.indexOf("=");if(n===-1)continue;t.set(o.slice(0,n),o.slice(n+1))}let i=t.get("plugin"),a=t.get("version");if(i!==z||!a)return null;let s=t.get("hash");return{plugin:i,version:a,hash:s?.startsWith("sha256:")?s.slice(7):null}}var Z={fast:"low",balanced:"medium",deep:"high"},O={"flow-reviewer":{mode:"all",description:"Review Flow work read-only and record a reviewer decision.",prompt:"You are the Flow reviewer. Load the `flow-review` skill, review the requested work read-only, then record your decision with flow_review_record.",reasoningEffort:Z.deep,permission:{edit:"deny",bash:"deny",task:{"*":"deny"},"flow_*":"deny",flow_status:"allow",flow_review_record:"allow"}}},S={"flow-plan":{description:"Create, update, or approve a Flow plan",template:"Load the `flow-plan` skill and plan: $ARGUMENTS"},"flow-run":{description:"Run one approved Flow feature",template:"Load the `flow-run` skill and execute the next approved Flow feature. $ARGUMENTS"},"flow-auto":{description:"Drive the Flow loop autonomously until completion or a real blocker",template:"Load the `flow` skill and drive the Flow loop (status, plan, run, review) until completion or a real blocker: $ARGUMENTS"},"flow-status":{description:"Inspect the active Flow session and workspace readiness",template:"Call flow_status (detailed) and report session state, readiness checks, and the suggested next step."},"flow-doctor":{description:"Check Flow readiness for the current workspace",template:"Call flow_status (detailed) and report the readiness checks with any remediation steps."},"flow-history":{description:"Inspect stored Flow session history",template:"Call flow_session with action 'history' and summarize the sessions."},"flow-session":{description:"Activate, close, list, or show a Flow session",template:"Call flow_session with the requested action (activate, close, history, or show): $ARGUMENTS"},"flow-reset":{description:"Reset a Flow feature to pending",template:"Call flow_feature_complete with reset=true for feature: $ARGUMENTS"},"flow-review":{description:"Run a read-only Flow review with a fresh context",agent:"flow-reviewer",subtask:!0,template:"Load the `flow-review` skill and review: $ARGUMENTS"}};function $(e){return`${["---",`description: ${JSON.stringify(e.description)}`,...e.agent?[`agent: ${JSON.stringify(e.agent)}`]:[],...e.subtask===void 0?[]:[`subtask: ${e.subtask}`],"---"].join(`
2
+ import{homedir as ue}from"node:os";import{readdir as ae,readFile as se,rm as u,rmdir as ie}from"node:fs/promises";import{dirname as oe,join as d,normalize as ne,sep as re}from"node:path";var $={fast:"low",balanced:"medium",deep:"high"},W={"flow-reviewer":{mode:"all",description:"Review Flow work read-only and record a reviewer decision.",prompt:"You are the Flow reviewer. Load the `flow-review` skill, review the requested work read-only, then record your decision with flow_review_record.",reasoningEffort:$.deep,permission:{edit:"deny",bash:"deny",task:{"*":"deny"},"flow_*":"deny",flow_status:"allow",flow_review_record:"allow"}}},j={"flow-plan":{description:"Create, update, or approve a Flow plan",template:"Load the `flow-plan` skill and plan: $ARGUMENTS"},"flow-run":{description:"Run one approved Flow feature",template:"Load the `flow-run` skill and execute the next approved Flow feature. $ARGUMENTS"},"flow-auto":{description:"Drive the Flow loop autonomously until completion or a real blocker",template:"Load the `flow` skill and drive the Flow loop (status, plan, run, review) until completion or a real blocker: $ARGUMENTS"},"flow-status":{description:"Inspect the active Flow session and workspace readiness",template:"Call flow_status (detailed) and report session state, readiness checks, and the suggested next step."},"flow-review":{description:"Run a read-only Flow review with a fresh context",agent:"flow-reviewer",subtask:!0,template:"Load the `flow-review` skill and review: $ARGUMENTS"}},_=["flow-doctor","flow-history","flow-reset","flow-session"];import{createHash as K}from"node:crypto";import{join as w}from"node:path";var x=w(".config","opencode","skills"),m=w(".config","opencode","commands"),A=w(".config","opencode","agents"),v=".flow-skill-version",O="SKILL.md.backup",k=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
3
+ `,B="flow-opencode-generated-skill",M=`<!-- ${B} `,C=new RegExp(`^<!-- ${B} name=([a-z0-9]+(?:-[a-z0-9]+)*) version=([0-9]+) hash=sha256:([a-f0-9]{64}) -->$`,"u");function h(e){return K("sha256").update(e,"utf8").digest("hex")}function y(e){let t=e.split(`
4
+ `),s=t.flatMap((p,Z)=>p.startsWith(M)?[Z]:[]);if(s.length===0)return{kind:"not_generated"};if(s.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let a=s[0];if(a===void 0)return{kind:"not_generated"};let i=t[a];if(i===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let o=i.match(C);if(!o)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,c]=o;if(n===void 0||r===void 0||c===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let l=[...t.slice(0,a),...t.slice(a+1)].join(`
5
+ `);if(h(l)!==c)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:n,version:r,hash:c}}}var z="opencode-plugin-flow",N="file=",E="=sha256:";function g(e){let t=new Map;for(let s of e.split(`
6
+ `)){if(!s.startsWith(N))continue;let a=s.slice(N.length),i=a.lastIndexOf(E);if(i===-1)continue;let o=a.slice(0,i),n=a.slice(i+E.length);if(o.length>0&&/^[a-f0-9]{64}$/.test(n))t.set(o,n)}return t}function b(e,t,s){let a=new Map;for(let n of e.split(`
7
+ `)){let r=n.indexOf("=");if(r===-1)continue;a.set(n.slice(0,r),n.slice(r+1))}let i=a.get("version"),o=a.get("hash");if(a.get("plugin")!==z||a.get("kind")!==t||a.get("name")!==s||!i||!o?.startsWith("sha256:"))return null;return{kind:t,name:s,version:i,hash:o.slice(7)}}function T(e){let t=new Map;for(let o of e.split(`
8
+ `)){let n=o.indexOf("=");if(n===-1)continue;t.set(o.slice(0,n),o.slice(n+1))}let s=t.get("plugin"),a=t.get("version");if(s!==z||!a)return null;let i=t.get("hash");return{plugin:s,version:a,hash:i?.startsWith("sha256:")?i.slice(7):null}}import{mkdir as Ye,readFile as D,rm as I,writeFile as He}from"node:fs/promises";import{dirname as Qe,join as V,sep as Xe}from"node:path";function P(e){return`${["---",`description: ${JSON.stringify(e.description)}`,...e.agent?[`agent: ${JSON.stringify(e.agent)}`]:[],...e.subtask===void 0?[]:[`subtask: ${e.subtask}`],"---"].join(`
9
9
  `)}
10
10
 
11
11
  ${e.template}
12
- `}function K(e){return`${["---",`description: ${JSON.stringify(e.description)}`,`mode: ${e.mode}`,...e.reasoningEffort?[`reasoningEffort: ${e.reasoningEffort}`]:[],...e.permission?C(e.permission):[],"---"].join(`
12
+ `}function ee(e){return`${["---",`description: ${JSON.stringify(e.description)}`,`mode: ${e.mode}`,...e.reasoningEffort?[`reasoningEffort: ${e.reasoningEffort}`]:[],...e.permission?te(e.permission):[],"---"].join(`
13
13
  `)}
14
14
 
15
15
  ${e.prompt}
16
- `}function N(){return new Map(Object.entries(S).map(([e,t])=>[e,$(t)]))}function V(){return new Map(Object.entries(O).map(([e,t])=>[e,K(t)]))}function C(e){if(!e)return[];let t=["permission:"];for(let[i,a]of Object.entries(e)){if(typeof a==="string"){t.push(` ${JSON.stringify(i)}: ${JSON.stringify(a)}`);continue}if(a&&typeof a==="object"){t.push(` ${JSON.stringify(i)}:`);for(let[s,o]of Object.entries(a))t.push(` ${JSON.stringify(s)}: ${JSON.stringify(o)}`)}}return t}async function Y({homeDir:e,dryRun:t=!1,logger:i}){let a={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},s=c(e,g);for(let r of await se(s)){if(r!=="flow"&&!r.startsWith("flow-"))continue;let d=c(s,r),u=await ae(d);if(u==="foreign")continue;if(u==="user_edited"){a.keptUserEditedSkills.push(d),i?.(`Kept user-edited Flow skill at ${d}; remove it manually if it is no longer needed.`);continue}if(!t)await ie(d);a.removedSkills.push(d),i?.(`${t?"Would remove":"Removed"} Flow skill at ${d}.`)}await E({homeDir:e,dryRun:t,logger:i,kind:"command",root:c(e,b),files:N(),removed:a.removedCommands,keptUserEdited:a.keptUserEditedCommands}),await E({homeDir:e,dryRun:t,logger:i,kind:"agent",root:c(e,x),files:V(),removed:a.removedAgents,keptUserEdited:a.keptUserEditedAgents});let o=c(e,A),n=await p(o);if(n!==null)if(n.startsWith(_)){if(!t)await l(o,{force:!0});a.removedPreNpmPlugin=o,i?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${o}.`)}else a.keptForeignPreNpmPlugin=o,i?.(`Kept ${o}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return i?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),a}async function ae(e){let t=c(e,"SKILL.md"),i=await p(t),a=await p(c(e,f)),s=a===null?null:R(a);if(s!==null&&a!==null){for(let[r,d]of y(a)){if(r==="SKILL.md")continue;let u=G(e,r);if(u===null)continue;let v=await p(u);if(v!==null&&h(v)!==d)return"user_edited"}if(i===null)return"pristine";if(s.hash!==null&&h(i)===s.hash)return"pristine";return m(i).kind==="valid_generated"?"pristine":"user_edited"}if(i===null)return"foreign";let o=m(i);if(o.kind==="valid_generated")return"pristine";if(o.kind==="invalid_generated")return"user_edited";return"foreign"}function G(e,t){let i=ee(c(e,...t.split("/")));if(i!==e&&i.startsWith(`${e}${te}`))return i;return null}async function ie(e){let t=await p(c(e,f)),i=new Set;if(t!==null)for(let a of y(t).keys()){let s=G(e,a);if(s===null)continue;await l(s,{force:!0}),await l(`${s}.backup`,{force:!0});let o=P(s);if(o!==e)i.add(o)}await l(c(e,"SKILL.md"),{force:!0}),await l(c(e,f),{force:!0}),await l(c(e,F),{force:!0});for(let a of[...i].sort((s,o)=>o.length-s.length))await T(a);await T(e)}async function T(e){try{await D(e)}catch(t){let i=t.code;if(i!=="ENOENT"&&i!=="ENOTEMPTY")throw t}}async function se(e){try{return(await H(e,{withFileTypes:!0})).filter((i)=>i.isDirectory()).map((i)=>i.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function p(e){try{return await M(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function E(e){for(let[t,i]of e.files){let a=c(e.root,`${t}.md`),s=c(e.root,`.${t}.flow-version`),o=await p(a),n=await p(s),r=n===null?null:k(n,e.kind,t);if(o===null&&r===null)continue;if(!(r!==null||o===i))continue;if(o!==null&&r!==null&&h(o)!==r.hash){e.keptUserEdited.push(a),e.logger?.(`Kept user-edited Flow ${e.kind} at ${a}; remove it manually if it is no longer needed.`);continue}if(!e.dryRun)await l(a,{force:!0}),await l(s,{force:!0}),await l(`${a}.backup`,{force:!0});e.removed.push(a),e.logger?.(`${e.dryRun?"Would remove":"Removed"} Flow ${e.kind} at ${a}.`)}if(!e.dryRun)await T(e.root)}var I=`opencode-plugin-flow — Flow plugin lifecycle commands
16
+ `}function G(){return new Map(Object.entries(j).map(([e,t])=>[e,P(t)]))}function Y(){return new Map(Object.entries(W).map(([e,t])=>[e,ee(t)]))}async function H(e){let t=[],s=[];for(let a of e.names){let i=V(e.root,`${a}.md`),o=V(e.root,`.${a}.flow-version`),n=await L(o),r=n===null?null:b(n,e.kind,a);if(r===null)continue;let c=await L(i);if(c!==null&&h(c)!==r.hash){s.push(i);continue}if(!e.dryRun)await I(i,{force:!0}),await I(o,{force:!0}),await I(`${i}.backup`,{force:!0});t.push(i)}return{removed:t,keptUserEdited:s}}async function L(e){try{return await D(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}function te(e){if(!e)return[];let t=["permission:"];for(let[s,a]of Object.entries(e)){if(typeof a==="string"){t.push(` ${JSON.stringify(s)}: ${JSON.stringify(a)}`);continue}if(a&&typeof a==="object"){t.push(` ${JSON.stringify(s)}:`);for(let[i,o]of Object.entries(a))t.push(` ${JSON.stringify(i)}: ${JSON.stringify(o)}`)}}return t}async function Q({homeDir:e,dryRun:t=!1,logger:s}){let a={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},i=d(e,x);for(let c of await le(i)){if(c!=="flow"&&!c.startsWith("flow-"))continue;let l=d(i,c),p=await ce(l);if(p==="foreign")continue;if(p==="user_edited"){a.keptUserEditedSkills.push(l),s?.(`Kept user-edited Flow skill at ${l}; remove it manually if it is no longer needed.`);continue}if(!t)await de(l);a.removedSkills.push(l),s?.(`${t?"Would remove":"Removed"} Flow skill at ${l}.`)}let o=await H({kind:"command",root:d(e,m),names:_,dryRun:t});for(let c of o.removed)a.removedCommands.push(c),s?.(`${t?"Would remove":"Removed"} retired Flow command at ${c}.`);for(let c of o.keptUserEdited)a.keptUserEditedCommands.push(c),s?.(`Kept user-edited Flow command at ${c}; remove it manually if it is no longer needed.`);await U({homeDir:e,dryRun:t,logger:s,kind:"command",root:d(e,m),files:G(),removed:a.removedCommands,keptUserEdited:a.keptUserEditedCommands}),await U({homeDir:e,dryRun:t,logger:s,kind:"agent",root:d(e,A),files:Y(),removed:a.removedAgents,keptUserEdited:a.keptUserEditedAgents});let n=d(e,k),r=await f(n);if(r!==null)if(r.startsWith(R)){if(!t)await u(n,{force:!0});a.removedPreNpmPlugin=n,s?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${n}.`)}else a.keptForeignPreNpmPlugin=n,s?.(`Kept ${n}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return s?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),a}async function ce(e){let t=d(e,"SKILL.md"),s=await f(t),a=await f(d(e,v)),i=a===null?null:T(a);if(i!==null&&a!==null){for(let[r,c]of g(a)){if(r==="SKILL.md")continue;let l=X(e,r);if(l===null)continue;let p=await f(l);if(p!==null&&h(p)!==c)return"user_edited"}if(s===null)return"pristine";if(i.hash!==null&&h(s)===i.hash)return"pristine";return y(s).kind==="valid_generated"?"pristine":"user_edited"}if(s===null)return"foreign";let o=y(s);if(o.kind==="valid_generated")return"pristine";if(o.kind==="invalid_generated")return"user_edited";return"foreign"}function X(e,t){let s=ne(d(e,...t.split("/")));if(s!==e&&s.startsWith(`${e}${re}`))return s;return null}async function de(e){let t=await f(d(e,v)),s=new Set;if(t!==null)for(let a of g(t).keys()){let i=X(e,a);if(i===null)continue;await u(i,{force:!0}),await u(`${i}.backup`,{force:!0});let o=oe(i);if(o!==e)s.add(o)}await u(d(e,"SKILL.md"),{force:!0}),await u(d(e,v),{force:!0}),await u(d(e,O),{force:!0});for(let a of[...s].sort((i,o)=>o.length-i.length))await F(a);await F(e)}async function F(e){try{await ie(e)}catch(t){let s=t.code;if(s!=="ENOENT"&&s!=="ENOTEMPTY")throw t}}async function le(e){try{return(await ae(e,{withFileTypes:!0})).filter((s)=>s.isDirectory()).map((s)=>s.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function f(e){try{return await se(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function U(e){for(let[t,s]of e.files){let a=d(e.root,`${t}.md`),i=d(e.root,`.${t}.flow-version`),o=await f(a),n=await f(i),r=n===null?null:b(n,e.kind,t);if(o===null&&r===null)continue;if(!(r!==null||o===s))continue;if(o!==null&&r!==null&&h(o)!==r.hash){e.keptUserEdited.push(a),e.logger?.(`Kept user-edited Flow ${e.kind} at ${a}; remove it manually if it is no longer needed.`);continue}if(!e.dryRun)await u(a,{force:!0}),await u(i,{force:!0}),await u(`${a}.backup`,{force:!0});e.removed.push(a),e.logger?.(`${e.dryRun?"Would remove":"Removed"} Flow ${e.kind} at ${a}.`)}if(!e.dryRun)await F(e.root)}var q=`opencode-plugin-flow — Flow plugin lifecycle commands
17
17
 
18
18
  Usage:
19
19
  bunx opencode-plugin-flow uninstall [--dry-run]
@@ -26,10 +26,10 @@ Commands:
26
26
 
27
27
  Options:
28
28
  --dry-run Show what would be removed without deleting anything
29
- --help Show this message`;function L(e){process.stdout.write(`${e}
30
- `)}function q(e){process.stderr.write(`${e}
31
- `)}async function ne(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return L(I),0;let i=t.shift();if(i!=="uninstall")return q(`Unknown command: ${i}
29
+ --help Show this message`;function J(e){process.stdout.write(`${e}
30
+ `)}function S(e){process.stderr.write(`${e}
31
+ `)}async function pe(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return J(q),0;let s=t.shift();if(s!=="uninstall")return S(`Unknown command: ${s}
32
32
 
33
- ${I}`),1;let a=!1;for(let s of t){if(s==="--dry-run"){a=!0;continue}return q(`Unknown argument: ${s}
33
+ ${q}`),1;let a=!1;for(let i of t){if(i==="--dry-run"){a=!0;continue}return S(`Unknown argument: ${i}
34
34
 
35
- ${I}`),1}return await Y({homeDir:process.env.HOME??oe(),dryRun:a,logger:L}),0}try{process.exitCode=await ne(process.argv.slice(2))}catch(e){q(e instanceof Error?e.message:String(e)),process.exitCode=1}
35
+ ${q}`),1}return await Q({homeDir:process.env.HOME??ue(),dryRun:a,logger:J}),0}try{process.exitCode=await pe(process.argv.slice(2))}catch(e){S(e instanceof Error?e.message:String(e)),process.exitCode=1}