opencode-plugin-flow 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +19 -2
- package/dist/cli.js +9 -9
- package/dist/index.js +47 -81
- package/dist/index.js.map +6 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.2.1] - 2026-06-13
|
|
6
|
+
|
|
7
|
+
A missing Flow backend is now a stop condition in the skills, not a silent downgrade
|
|
8
|
+
|
|
9
|
+
A real `/flow-auto` run in an environment where the plugin had not loaded showed the gap: the slash commands and skills are markdown files OpenCode discovers from disk, so they keep working even when the `flow_*` tools are absent — and with no instruction for that case, the model improvised an ad-hoc unrecorded review that looked like success while recording no session, plan, evidence, or decision.
|
|
10
|
+
|
|
11
|
+
All four skills now treat missing `flow_*` tools as a stop-and-tell-the-user condition: the `flow` driving loop gets it as the first entry in its stop list (including the hint that a project-local `plugin` array overrides the global one), and `flow-plan`, `flow-run`, and `flow-review` each get a matching line at the point where they would first need their tool — each can be entered directly through its command without the driving loop. The instruction is explicit that substituting an unrecorded workflow is never acceptable.
|
|
12
|
+
|
|
13
|
+
Skill content only; no plugin code changes. Bump the pin and restart twice so the re-synced skills are picked up.
|
|
14
|
+
|
|
15
|
+
## [3.2.0] - 2026-06-13
|
|
16
|
+
|
|
17
|
+
Stale installs become visible: a passive update notice, the running version in `/flow-status`, and documented update steps
|
|
18
|
+
|
|
19
|
+
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).
|
|
20
|
+
|
|
21
|
+
`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.
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
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).
|
|
26
|
+
|
|
5
27
|
## [3.1.0] - 2026-06-12
|
|
6
28
|
|
|
7
29
|
Only the v3 surface remains: five commands, seven tools, no v2 leftovers
|
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.1"]
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
OpenCode installs the package from npm on startup. Pin
|
|
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 (same form as the install snippet above).
|
|
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:
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{homedir as ue}from"node:os";import{readdir as ae,readFile as
|
|
2
|
+
import{homedir as ue}from"node:os";import{readdir as ae,readFile as oe,rm as u,rmdir as se}from"node:fs/promises";import{dirname as ie,join as d,normalize as ne,sep as re}from"node:path";var $={fast:"low",balanced:"medium",deep:"high"},j={"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"}}},O={"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 k=w(".config","opencode","skills"),m=w(".config","opencode","commands"),x=w(".config","opencode","agents"),v=".flow-skill-version",E="SKILL.md.backup",A=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
|
|
3
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
|
-
`),
|
|
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=",
|
|
6
|
-
`)){if(!
|
|
7
|
-
`)){let r=n.indexOf("=");if(r===-1)continue;a.set(n.slice(0,r),n.slice(r+1))}let
|
|
8
|
-
`)){let n=
|
|
4
|
+
`),o=t.flatMap((p,Z)=>p.startsWith(M)?[Z]:[]);if(o.length===0)return{kind:"not_generated"};if(o.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let a=o[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 i=s.match(C);if(!i)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,c]=i;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=",W="=sha256:";function g(e){let t=new Map;for(let o of e.split(`
|
|
6
|
+
`)){if(!o.startsWith(N))continue;let a=o.slice(N.length),s=a.lastIndexOf(W);if(s===-1)continue;let i=a.slice(0,s),n=a.slice(s+W.length);if(i.length>0&&/^[a-f0-9]{64}$/.test(n))t.set(i,n)}return t}function b(e,t,o){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"),i=a.get("hash");if(a.get("plugin")!==z||a.get("kind")!==t||a.get("name")!==o||!s||!i?.startsWith("sha256:"))return null;return{kind:t,name:o,version:s,hash:i.slice(7)}}function T(e){let t=new Map;for(let i of e.split(`
|
|
8
|
+
`)){let n=i.indexOf("=");if(n===-1)continue;t.set(i.slice(0,n),i.slice(n+1))}let o=t.get("plugin"),a=t.get("version");if(o!==z||!a)return null;let s=t.get("hash");return{plugin:o,version:a,hash:s?.startsWith("sha256:")?s.slice(7):null}}import{mkdir as Ye,readFile as D,rm as F,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}
|
|
@@ -13,7 +13,7 @@ ${e.template}
|
|
|
13
13
|
`)}
|
|
14
14
|
|
|
15
15
|
${e.prompt}
|
|
16
|
-
`}function G(){return new Map(Object.entries(
|
|
16
|
+
`}function G(){return new Map(Object.entries(O).map(([e,t])=>[e,P(t)]))}function Y(){return new Map(Object.entries(j).map(([e,t])=>[e,ee(t)]))}async function H(e){let t=[],o=[];for(let a of e.names){let s=V(e.root,`${a}.md`),i=V(e.root,`.${a}.flow-version`),n=await L(i),r=n===null?null:b(n,e.kind,a);if(r===null)continue;let c=await L(s);if(c!==null&&h(c)!==r.hash){o.push(s);continue}if(!e.dryRun)await F(s,{force:!0}),await F(i,{force:!0}),await F(`${s}.backup`,{force:!0});t.push(s)}return{removed:t,keptUserEdited:o}}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[o,a]of Object.entries(e)){if(typeof a==="string"){t.push(` ${JSON.stringify(o)}: ${JSON.stringify(a)}`);continue}if(a&&typeof a==="object"){t.push(` ${JSON.stringify(o)}:`);for(let[s,i]of Object.entries(a))t.push(` ${JSON.stringify(s)}: ${JSON.stringify(i)}`)}}return t}async function Q({homeDir:e,dryRun:t=!1,logger:o}){let a={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},s=d(e,k);for(let c of await le(s)){if(c!=="flow"&&!c.startsWith("flow-"))continue;let l=d(s,c),p=await ce(l);if(p==="foreign")continue;if(p==="user_edited"){a.keptUserEditedSkills.push(l),o?.(`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),o?.(`${t?"Would remove":"Removed"} Flow skill at ${l}.`)}let i=await H({kind:"command",root:d(e,m),names:_,dryRun:t});for(let c of i.removed)a.removedCommands.push(c),o?.(`${t?"Would remove":"Removed"} retired Flow command at ${c}.`);for(let c of i.keptUserEdited)a.keptUserEditedCommands.push(c),o?.(`Kept user-edited Flow command at ${c}; remove it manually if it is no longer needed.`);await U({homeDir:e,dryRun:t,logger:o,kind:"command",root:d(e,m),files:G(),removed:a.removedCommands,keptUserEdited:a.keptUserEditedCommands}),await U({homeDir:e,dryRun:t,logger:o,kind:"agent",root:d(e,x),files:Y(),removed:a.removedAgents,keptUserEdited:a.keptUserEditedAgents});let n=d(e,A),r=await f(n);if(r!==null)if(r.startsWith(R)){if(!t)await u(n,{force:!0});a.removedPreNpmPlugin=n,o?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${n}.`)}else a.keptForeignPreNpmPlugin=n,o?.(`Kept ${n}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return o?.('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"),o=await f(t),a=await f(d(e,v)),s=a===null?null:T(a);if(s!==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(o===null)return"pristine";if(s.hash!==null&&h(o)===s.hash)return"pristine";return y(o).kind==="valid_generated"?"pristine":"user_edited"}if(o===null)return"foreign";let i=y(o);if(i.kind==="valid_generated")return"pristine";if(i.kind==="invalid_generated")return"user_edited";return"foreign"}function X(e,t){let o=ne(d(e,...t.split("/")));if(o!==e&&o.startsWith(`${e}${re}`))return o;return null}async function de(e){let t=await f(d(e,v)),o=new Set;if(t!==null)for(let a of g(t).keys()){let s=X(e,a);if(s===null)continue;await u(s,{force:!0}),await u(`${s}.backup`,{force:!0});let i=ie(s);if(i!==e)o.add(i)}await u(d(e,"SKILL.md"),{force:!0}),await u(d(e,v),{force:!0}),await u(d(e,E),{force:!0});for(let a of[...o].sort((s,i)=>i.length-s.length))await I(a);await I(e)}async function I(e){try{await se(e)}catch(t){let o=t.code;if(o!=="ENOENT"&&o!=="ENOTEMPTY")throw t}}async function le(e){try{return(await ae(e,{withFileTypes:!0})).filter((o)=>o.isDirectory()).map((o)=>o.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function f(e){try{return await oe(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function U(e){for(let[t,o]of e.files){let a=d(e.root,`${t}.md`),s=d(e.root,`.${t}.flow-version`),i=await f(a),n=await f(s),r=n===null?null:b(n,e.kind,t);if(i===null&&r===null)continue;if(!(r!==null||i===o))continue;if(i!==null&&r!==null&&h(i)!==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(s,{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 I(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]
|
|
@@ -28,8 +28,8 @@ Options:
|
|
|
28
28
|
--dry-run Show what would be removed without deleting anything
|
|
29
29
|
--help Show this message`;function J(e){process.stdout.write(`${e}
|
|
30
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
|
|
31
|
+
`)}async function pe(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return J(q),0;let o=t.shift();if(o!=="uninstall")return S(`Unknown command: ${o}
|
|
32
32
|
|
|
33
|
-
${q}`),1;let a=!1;for(let
|
|
33
|
+
${q}`),1;let a=!1;for(let s of t){if(s==="--dry-run"){a=!0;continue}return S(`Unknown argument: ${s}
|
|
34
34
|
|
|
35
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}
|