wiki-plugin-termflow 0.1.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 +67 -0
- package/client/termflow.js +54 -0
- package/client/termflow.js.map +7 -0
- package/factory.json +5 -0
- package/package.json +43 -0
- package/pages/about-termflow-plugin +108 -0
- package/server/server.js +11 -0
package/ReadMe.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# wiki-plugin-termflow
|
|
2
|
+
|
|
3
|
+
A generic **workflow-gating** plugin for [Federated Wiki](http://fed.wiki.org).
|
|
4
|
+
A page often lists steps that must happen in order — install a tool before you
|
|
5
|
+
configure it. TermFlow turns those steps into a guided workflow: later steps stay
|
|
6
|
+
**locked** (shown as a plain, un-runnable item) until their prerequisite is met,
|
|
7
|
+
and the whole page can be run as one guarded step-through.
|
|
8
|
+
|
|
9
|
+
It is the companion to [wiki-plugin-terminal](https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-terminal):
|
|
10
|
+
Terminal supplies the runnable steps and the shell that evaluates guards;
|
|
11
|
+
TermFlow orders and gates them. The engine is generic, so other item types can
|
|
12
|
+
participate by registering an adapter (see below).
|
|
13
|
+
|
|
14
|
+
## The item
|
|
15
|
+
|
|
16
|
+
A `termflow` item's text **is** one runnable bash script — the single source of
|
|
17
|
+
truth. A preamble holds a `# ── guards ──` block of bash functions; one block per
|
|
18
|
+
step follows, keyed to the source item's id, with an optional guard:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
#!/usr/bin/env bash
|
|
22
|
+
# wiki-workflow: Codex
|
|
23
|
+
|
|
24
|
+
# ── guards ──
|
|
25
|
+
have_codex() { command -v codex >/dev/null 2>&1; }
|
|
26
|
+
|
|
27
|
+
# ── step 7d0192ae… ──
|
|
28
|
+
brew install codex
|
|
29
|
+
|
|
30
|
+
# ── step 9bc9a58f… needs: have_codex ──
|
|
31
|
+
if have_codex; then
|
|
32
|
+
codex --version && which codex
|
|
33
|
+
fi
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **`needs: <fn>`** names a guard function. A step is unlocked iff the function
|
|
37
|
+
exits 0 — the same function gates the step in the page and guards it when the
|
|
38
|
+
script is run standalone.
|
|
39
|
+
- **`tty`** marks an interactive step; the step-through pauses for it rather than
|
|
40
|
+
running it non-interactively.
|
|
41
|
+
- Press **generate** to (re)build the script from the page's runnable items,
|
|
42
|
+
**run workflow** to step through honouring guards, **copy** to take the script.
|
|
43
|
+
|
|
44
|
+
## The engine contract
|
|
45
|
+
|
|
46
|
+
TermFlow exposes registries on `window.workflow`; participant plugins register
|
|
47
|
+
adapters (idempotently, at load):
|
|
48
|
+
|
|
49
|
+
| Registry | Signature | Purpose |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `evaluators[kind]` | `(expr, ctx) => Promise<bool>` | evaluate a guard; ships `shell`, `done` |
|
|
52
|
+
| `runners[type]` | `(ctx) => Promise<{ok,exit,output}>` | run a step of an item type |
|
|
53
|
+
| `scriptOf[type]` | `(item) => string` | extract a step's body for `generate` |
|
|
54
|
+
| `setLock/getLock` | `(pageKey, id[, state])` | lock dispatch (fires a `workflow-lock` event) |
|
|
55
|
+
|
|
56
|
+
`wiki-plugin-terminal` registers `runners.terminal`, `scriptOf.terminal`, and
|
|
57
|
+
listens for `workflow-lock` to render its locked state.
|
|
58
|
+
|
|
59
|
+
## Build
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install
|
|
63
|
+
npm run build # runs tests, then bundles src/client → client/termflow.js
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Local-first only: the live engine activates on `localhost`/`.localhost`/`.fish`
|
|
67
|
+
hosts where the terminal service is reachable; elsewhere the item is display-only.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* wiki-plugin-termflow - 0.1.0 - Sat, 13 Jun 2026 12:14:37 GMT */
|
|
2
|
+
(()=>{var j=t=>String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),H=t=>t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"||t.endsWith(".localhost")||t.endsWith(".fish"),A=t=>(t.service||"http://localhost:8000").replace(/\/$/,""),I=t=>(String(t||"").match(/#\s*wiki-workflow:\s*(.+)/)||[])[1]?.trim(),K=new Set(["done","page","status"]),E=t=>{let e=String(t||"").trim();if(!e)return{kind:"shell",expr:""};let[n,...r]=e.split(/\s+/);return K.has(n)?{kind:n,expr:r.join(" ")}:{kind:"shell",expr:e}},P=/^#.*?\bstep\s+([A-Za-z0-9_-]+)\b(.*)$/,G=/[─\s]+$/,C=t=>{let e=0,n=t.length;for(;e<n&&t[e].trim()==="";)e++;for(;n>e&&t[n-1].trim()==="";)n--;return t.slice(e,n)},U=t=>{let e=C(t);if(e.length>=2&&/^if .+; then$/.test(e[0])){let n=-1;for(let r=e.length-1;r>0;r--)if(e[r].trim()==="fi"){n=r;break}if(n>0)return C(e.slice(1,n)).join(`
|
|
3
|
+
`)}return e.join(`
|
|
4
|
+
`)},S=t=>{let e=String(t||"").split(`
|
|
5
|
+
`),n=e.findIndex(l=>P.test(l));if(n===-1)return{preamble:e.join(`
|
|
6
|
+
`).replace(/\n+$/,""),steps:[]};let r=e.slice(0,n).join(`
|
|
7
|
+
`).replace(/\n+$/,""),o=[],i=null,f=[],c=()=>{i&&(i.body=U(f),o.push(i),f=[])};for(let l=n;l<e.length;l++){let h=e[l].match(P);if(h){c();let w=h[2].replace(G,""),g=w.search(/\bneeds:/),L=g>=0?w.slice(0,g):w,k=g>=0?w.slice(g).replace(/^needs:\s*/,"").trim():"";i={id:h[1]},k&&(i.guard=k),/\btty\b/.test(L)&&(i.tty=!0)}else i&&f.push(e[l])}return c(),{preamble:r,steps:o}},Y=t=>`# \u2500\u2500 step ${t.id}${t.tty?" tty":""}${t.guard?` needs: ${t.guard}`:""} \u2500\u2500`,Z=t=>t.guard&&E(t.guard).kind==="shell"?`if ${t.guard}; then
|
|
8
|
+
${t.body||""}
|
|
9
|
+
fi`:t.body||"",B=t=>`#!/usr/bin/env bash
|
|
10
|
+
# wiki-workflow: ${t||"workflow"}
|
|
11
|
+
|
|
12
|
+
# \u2500\u2500 guards \u2500\u2500
|
|
13
|
+
# define guard functions here, then reference them with: needs: <fn>`,z=(t,e)=>`${(t||"").replace(/\n+$/,"")}
|
|
14
|
+
`+e.map(n=>`
|
|
15
|
+
${Y(n)}
|
|
16
|
+
${Z(n)}
|
|
17
|
+
`).join(""),N=(t,e)=>{let n=new Map((t||[]).map(r=>[r.id,r]));return e.map(r=>{let o=n.get(r.id)||{};return{id:r.id,body:r.body,...o.guard?{guard:o.guard}:{},...o.tty?{tty:!0}:{}}})},_=(t,e)=>`${(t||"").replace(/\n+$/,"")}
|
|
18
|
+
${e}`,M=t=>({type:"edit",id:t.id,item:t});var F=(t,e)=>{for(let n of document.querySelectorAll(".page"))if($(n).data("key")===t){let r=n.querySelector(`.item[data-id="${e}"]`);if(r)return r}return document.querySelector(`.item[data-id="${e}"]`)},Q=async(t,e)=>(await(await fetch(`${t}/terminal/run`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:e})})).json()).exit,V=()=>{let t=window.workflow=window.workflow||{};return t.evaluators||={},t.runners||={},t.scriptOf||={},t.locks||=new Map,t.setLock||(t.setLock=(e,n,r)=>{let o=t.locks.get(e);o||t.locks.set(e,o=new Map),o.set(n,r);let i=F(e,n);i&&$(i).trigger("workflow-lock",[r])},t.getLock=(e,n)=>t.locks.get(e)?.get(n)),t.evaluators.shell||=async(e,n)=>{let r=_(n.preamble,e);try{let o=await fetch(`${n.base}/terminal/check`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({guards:[{id:"g",test:r}]})});if(o.ok){let i=await o.json();if(i.results&&"g"in i.results)return i.results.g===!0}}catch{}try{return await Q(n.base,r)===0}catch{return!1}},t.evaluators.done||=async(e,n)=>(e.trim()==="all"?n.priorIds:e.split(/\s+/).filter(Boolean)).every(o=>n.done.has(o)),t},b=V(),X=`
|
|
19
|
+
.termflow-item { border:1px solid #ddd; border-radius:4px; padding:6px 8px;
|
|
20
|
+
background:#fafafa }
|
|
21
|
+
.termflow-item .tf-head { font-weight:bold; margin-bottom:4px }
|
|
22
|
+
.termflow-item .tf-steps { margin:0 0 6px; padding-left:20px;
|
|
23
|
+
font-family:monospace; font-size:12px }
|
|
24
|
+
.termflow-item .tf-steps li { margin:1px 0; white-space:nowrap; overflow:hidden;
|
|
25
|
+
text-overflow:ellipsis }
|
|
26
|
+
.termflow-item .tf-steps li.locked { color:#bbb }
|
|
27
|
+
.termflow-item .tf-steps li.running { color:#1565c0 }
|
|
28
|
+
.termflow-item .tf-steps li.done { color:#0a7a32 }
|
|
29
|
+
.termflow-item .tf-steps li.failed { color:#cd3131 }
|
|
30
|
+
.termflow-item .tf-steps li .st { margin-right:4px }
|
|
31
|
+
.termflow-item .tf-steps li .g { color:#aaa }
|
|
32
|
+
.termflow-item .tf-tools button { margin-right:4px; font-size:11px }
|
|
33
|
+
.termflow-item .tf-status { font-size:11px; color:#888; margin-top:4px;
|
|
34
|
+
white-space:pre-wrap }
|
|
35
|
+
.termflow-item .tf-login { font-size:11px; margin:4px 0; padding:3px 6px;
|
|
36
|
+
border-radius:3px; display:none }
|
|
37
|
+
.termflow-item.tf-readonly .tf-login { display:block; background:#fff8e6;
|
|
38
|
+
border-left:3px solid #ffb000; color:#7a5b00 }
|
|
39
|
+
.termflow-item.tf-readonly .tf-run { opacity:.5; cursor:not-allowed }
|
|
40
|
+
`,tt=()=>{if(document.getElementById("termflow-plugin-style"))return;let t=document.createElement("style");t.id="termflow-plugin-style",t.textContent=X,document.head.appendChild(t)},D=(t,e)=>{tt(),t.append(`
|
|
41
|
+
<div class="termflow-item">
|
|
42
|
+
<div class="tf-head">\u2699 <span class="tf-title">workflow</span></div>
|
|
43
|
+
<ol class="tf-steps"></ol>
|
|
44
|
+
<div class="tf-login"></div>
|
|
45
|
+
<div class="tf-tools">
|
|
46
|
+
<button class="tf-run" title="run the workflow step by step, honouring guards">run workflow</button>
|
|
47
|
+
<button class="tf-gen" title="rebuild the script from the page's runnable items">generate \u21BB</button>
|
|
48
|
+
<button class="tf-copy" title="copy the workflow script">copy</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="tf-status"></div>
|
|
51
|
+
</div>
|
|
52
|
+
`)},et=async t=>{try{return(await fetch(`${t}/terminal/health`,{signal:AbortSignal.timeout(1500)})).ok}catch{return!1}},W=t=>(t.data("data")||{}).title||t.find("h1").first().text().trim()||"workflow",q=async(t,e)=>{if(!t)return!0;let{kind:n,expr:r}=E(t),o=b.evaluators[n];if(!o)return!1;try{return await o(r,e)}catch{return!1}},R=(t,e,n)=>t.find(`li[data-step="${e}"]`).removeClass("locked ready running done failed").addClass(n),nt=(t,e,n)=>{t.empty();for(let{s:r,pass:o}of e){let i=(r.body||"").split(`
|
|
53
|
+
`)[0],f=n.has(r.id),c=f?"done":o?"ready":"locked",l=f?"\u2713":o?r.tty?"\u2328":"\u25B6":"\u{1F512}";t.append(`<li class="${c}" data-step="${r.id}"><span class="st">${l}</span>${j(i)}${r.guard?` <span class="g">(needs ${j(r.guard)})</span>`:""}</li>`)}},rt=(t,e,n)=>{let r=t.parents(".page"),o=t.find(".termflow-item"),i=r.data("key")||"page",f=t.find(".tf-steps"),c=t.find(".tf-status"),l=new Set,h=()=>{try{return!!wiki.pageHandler&&wiki.pageHandler.useLocalStorage()===!1}catch{return!1}},w=()=>{let p=h();o.toggleClass("tf-readonly",!p),t.find(".tf-run").prop("disabled",!p),t.find(".tf-login").text(p?"":"\u{1F512} log in as the site owner to run \u2014 each step is recorded in the page history")},g=async()=>{let{preamble:p,steps:y}=S(e.text);t.find(".tf-title").text(I(e.text)||W(r));let m=[],s=[];for(let u of y){let d=await q(u.guard,{base:n,preamble:p,done:l,priorIds:[...m]});s.push({s:u,pass:d}),m.push(u.id)}for(let{s:u,pass:d}of s)b.setLock(i,u.id,{locked:!d,guard:u.guard});return nt(f,s,l),w(),y},L=()=>{let p=[];r.find(".item").each((x,v)=>{let a=$(v).data("item");if(!a||a.id===e.id||!b.runners[a.type])return;let T=b.scriptOf[a.type]?b.scriptOf[a.type](a):a.text||"";p.push({id:a.id,body:T})});let{preamble:y,steps:m}=S(e.text),s=N(m,p),u=z(y||B(I(e.text)||W(r)),s),d={...e,text:u};t.data("item",d),t.empty().off(),D(t,d),J(t,d),wiki.pageHandler.put(r,{type:"edit",id:e.id,item:d})},k=async()=>{if(!h()){w(),c.text("not logged in \u2014 log in as the site owner to run (each step is recorded in the page history)");return}let p=await g(),{preamble:y}=S(e.text),m=[];for(let s of p){let u=await q(s.guard,{base:n,preamble:y,done:l,priorIds:[...m]});if(m.push(s.id),!u){c.text(`stopped at step ${s.id}: guard \`${s.guard}\` not met`);return}if(s.tty){c.text(`step ${s.id} is interactive \u2014 run it in its own terminal, then press \u201Crun workflow\u201D again to continue`);return}let d=r.find(`.item[data-id="${s.id}"]`).get(0),x=d&&$(d).data("item"),v=x&&b.runners[x.type];if(!v){c.text(`step ${s.id}: no runner for type ${x?.type||"?"}`);return}R(f,s.id,"running");let a;try{a=await v({item:x,$item:$(d),body:s.body})}catch(T){a={ok:!1,exit:-1,output:String(T)}}if(a.item){$(d).data("item",a.item);try{wiki.pageHandler.put(r,M(a.item))}catch{}}if(l.add(s.id),R(f,s.id,a.ok?"done":"failed"),!a.ok){c.text(`step ${s.id} exited ${a.exit}; stopping`);return}await g()}c.text("workflow complete")};t.find(".tf-run").on("click",k),t.find(".tf-gen").on("click",L),t.find(".tf-copy").on("click",()=>navigator.clipboard.writeText(e.text));let O;r.off(`terminal-result.tf-${e.id}`).on(`terminal-result.tf-${e.id}`,()=>{clearTimeout(O),O=setTimeout(g,300)}),g()},J=async(t,e)=>{if(t.find(".tf-head, .tf-steps").on("dblclick",()=>wiki.textEditor(t,e)),!H(window.location.hostname))return;let n=t.find(".termflow-item");if(n.data("tf-bound"))return;n.data("tf-bound",!0);let r=A(e);if(!await et(r)){t.find(".tf-status").text("terminal service unreachable \u2014 gating disabled");return}rt(t,e,r)};typeof window<"u"&&(window.plugins.termflow={emit:D,bind:J});})();
|
|
54
|
+
//# sourceMappingURL=termflow.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/client/helpers.js", "../src/client/termflow.js"],
|
|
4
|
+
"sourcesContent": ["// wiki-plugin-termflow \u2014 pure helpers, import-free so node --test can exercise\n// them without a DOM.\n//\n// The source of truth is one runnable bash script held in the TermFlow item's\n// text: a preamble (shebang, title, a `# \u2500\u2500 guards \u2500\u2500` block of named bash\n// functions) followed by one step block per gated item:\n//\n// # \u2500\u2500 step <itemId> [tty] [needs: <guard>] \u2500\u2500\n// <body> when there is no guard\n//\n// # \u2500\u2500 step <itemId> needs: <fn> \u2500\u2500\n// if <fn>; then\n// <body>\n// fi when guarded by a shell function\n//\n// A guard is `<kind> <expr>`; the default kind is `shell` and its expr is the\n// bare name of a function from the preamble \u2014 so the same function gates the\n// step in the page and guards it when the script is run standalone. Other kinds\n// (done/page/status) are evaluated by TermFlow and are NOT wrapped in `if`.\n\nexport const expand = text =>\n String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n\n// The live engine is local-first only; elsewhere the item is display-only.\nexport const isLocalHost = hostname =>\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '::1' ||\n hostname === '[::1]' ||\n hostname.endsWith('.localhost') ||\n hostname.endsWith('.fish')\n\n// Shell guards reach the terminal service; default and override mirror Terminal.\nexport const serviceBase = item =>\n (item.service || 'http://localhost:8000').replace(/\\/$/, '')\n\nexport const workflowTitle = text =>\n (String(text || '').match(/#\\s*wiki-workflow:\\s*(.+)/) || [])[1]?.trim()\n\n// Guard kinds other than shell are evaluated in-engine and never wrapped in a\n// bash `if`. Anything else is a shell guard whose expr is a function/test.\nconst NONSHELL_KINDS = new Set(['done', 'page', 'status'])\n\nexport const guardKind = guard => {\n const g = String(guard || '').trim()\n if (!g) return { kind: 'shell', expr: '' }\n const [first, ...rest] = g.split(/\\s+/)\n if (NONSHELL_KINDS.has(first)) return { kind: first, expr: rest.join(' ') }\n return { kind: 'shell', expr: g }\n}\n\n// \u2500\u2500 parse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STEP_RE = /^#.*?\\bstep\\s+([A-Za-z0-9_-]+)\\b(.*)$/\nconst RULE_RE = /[\u2500\\s]+$/ // trailing box-drawing rule + whitespace\n\nconst trimBlank = lines => {\n let a = 0\n let b = lines.length\n while (a < b && lines[a].trim() === '') a++\n while (b > a && lines[b - 1].trim() === '') b--\n return lines.slice(a, b)\n}\n\n// Pull the body out of an `if <fn>; then \u2026 fi` wrapper (matching the LAST `fi`,\n// so a guarded body may itself contain an if/fi). Otherwise return as-is.\nconst unwrap = lines => {\n const body = trimBlank(lines)\n if (body.length >= 2 && /^if .+; then$/.test(body[0])) {\n let fi = -1\n for (let i = body.length - 1; i > 0; i--) if (body[i].trim() === 'fi') { fi = i; break }\n if (fi > 0) return trimBlank(body.slice(1, fi)).join('\\n')\n }\n return body.join('\\n')\n}\n\nexport const parseScript = text => {\n const lines = String(text || '').split('\\n')\n const first = lines.findIndex(l => STEP_RE.test(l))\n if (first === -1) return { preamble: lines.join('\\n').replace(/\\n+$/, ''), steps: [] }\n const preamble = lines.slice(0, first).join('\\n').replace(/\\n+$/, '')\n const steps = []\n let cur = null\n let buf = []\n const flush = () => { if (cur) { cur.body = unwrap(buf); steps.push(cur); buf = [] } }\n for (let i = first; i < lines.length; i++) {\n const m = lines[i].match(STEP_RE)\n if (m) {\n flush()\n const rest = m[2].replace(RULE_RE, '')\n const ni = rest.search(/\\bneeds:/)\n const head = ni >= 0 ? rest.slice(0, ni) : rest\n const guard = ni >= 0 ? rest.slice(ni).replace(/^needs:\\s*/, '').trim() : ''\n cur = { id: m[1] }\n if (guard) cur.guard = guard\n if (/\\btty\\b/.test(head)) cur.tty = true\n } else if (cur) {\n buf.push(lines[i])\n }\n }\n flush()\n return { preamble, steps }\n}\n\n// \u2500\u2500 generate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst stepMarker = s =>\n `# \u2500\u2500 step ${s.id}${s.tty ? ' tty' : ''}${s.guard ? ` needs: ${s.guard}` : ''} \u2500\u2500`\n\nconst stepBody = s => {\n if (s.guard && guardKind(s.guard).kind === 'shell')\n return `if ${s.guard}; then\\n${s.body || ''}\\nfi`\n return s.body || ''\n}\n\nexport const defaultPreamble = title =>\n `#!/usr/bin/env bash\\n# wiki-workflow: ${title || 'workflow'}\\n\\n` +\n `# \u2500\u2500 guards \u2500\u2500\\n# define guard functions here, then reference them with: needs: <fn>`\n\nexport const generateScript = (preamble, steps) =>\n `${(preamble || '').replace(/\\n+$/, '')}\\n` +\n steps.map(s => `\\n${stepMarker(s)}\\n${stepBody(s)}\\n`).join('')\n\n// Regenerating from the page keeps freshly gathered bodies but preserves the\n// authored guard/tty for each id \u2014 guards are written by editing, never inferred.\nexport const mergeSteps = (oldSteps, gathered) => {\n const prev = new Map((oldSteps || []).map(s => [s.id, s]))\n return gathered.map(g => {\n const o = prev.get(g.id) || {}\n return { id: g.id, body: g.body, ...(o.guard ? { guard: o.guard } : {}), ...(o.tty ? { tty: true } : {}) }\n })\n}\n\n// The probe sent to the shell evaluator: the preamble (function defs) plus the\n// guard expression, so the named function is in scope.\nexport const guardProbe = (preamble, expr) =>\n `${(preamble || '').replace(/\\n+$/, '')}\\n${expr}`\n\n// A workflow records progress by journaling a *native* wiki edit of the item it\n// touched \u2014 the workflow item (on generate) or a step's source item (on run, the\n// item now carrying its result). Using the native edit action is what lets the\n// wiki's history slider rewind through the workflow. The edit only reaches disk\n// when the user is the logged-in page owner; otherwise the wiki keeps it in\n// localStorage. (Hence TermFlow gates running on ownership.)\nexport const editAction = item => ({ type: 'edit', id: item.id, item })\n", "// wiki-plugin-termflow\n//\n// A generic workflow-gating coordinator. The item type `termflow` holds one\n// runnable bash script (see helpers.js) that gathers a page's steps; TermFlow\n// evaluates each step's guard and locks the steps whose prerequisite isn't met,\n// then runs the workflow as a guarded step-through.\n//\n// It owns no item rendering of its own beyond its panel: gating and running are\n// delegated to other plugins through a small shared contract on `window.workflow`:\n//\n// window.workflow.evaluators[kind] (expr, ctx) => Promise<bool> guard tests\n// window.workflow.runners[type] (ctx) => Promise<{ok,exit,output}> run a step\n// window.workflow.scriptOf[type] (item) => string a step's body\n// window.workflow.setLock(pageKey, id, {locked, guard}) lock dispatch\n// window.workflow.getLock(pageKey, id)\n//\n// TermFlow ships the `shell` (terminal service /terminal/check) and `done`\n// (prior steps completed) evaluators. wiki-plugin-terminal registers the\n// `terminal` runner + scriptOf and renders the locked state it is told about.\n\nimport { expand, isLocalHost, serviceBase, workflowTitle, guardKind,\n parseScript, generateScript, mergeSteps, guardProbe, defaultPreamble, editAction } from './helpers.js'\n\n// \u2500\u2500 shared namespace + built-in evaluators \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst findItem = (pageKey, id) => {\n for (const p of document.querySelectorAll('.page')) {\n if ($(p).data('key') === pageKey) {\n const el = p.querySelector(`.item[data-id=\"${id}\"]`)\n if (el) return el\n }\n }\n return document.querySelector(`.item[data-id=\"${id}\"]`)\n}\n\nconst runOnce = async (base, text) => {\n const res = await fetch(`${base}/terminal/run`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n })\n return (await res.json()).exit\n}\n\nconst ns = () => {\n const w = (window.workflow = window.workflow || {})\n w.evaluators ||= {}\n w.runners ||= {}\n w.scriptOf ||= {}\n w.locks ||= new Map() // pageKey -> Map(itemId -> {locked, guard})\n if (!w.setLock) {\n w.setLock = (pageKey, id, state) => {\n let m = w.locks.get(pageKey)\n if (!m) w.locks.set(pageKey, (m = new Map()))\n m.set(id, state)\n const el = findItem(pageKey, id)\n if (el) $(el).trigger('workflow-lock', [state])\n }\n w.getLock = (pageKey, id) => w.locks.get(pageKey)?.get(id)\n }\n // shell \u2014 a guard expression (a function from the preamble, or any test) run\n // by the terminal service; exit 0 = unlocked. Falls back to /run.\n w.evaluators.shell ||= async (expr, ctx) => {\n const test = guardProbe(ctx.preamble, expr)\n try {\n const res = await fetch(`${ctx.base}/terminal/check`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ guards: [{ id: 'g', test }] }),\n })\n if (res.ok) {\n const d = await res.json()\n if (d.results && 'g' in d.results) return d.results.g === true\n }\n } catch { /* fall through */ }\n try { return (await runOnce(ctx.base, test)) === 0 } catch { return false }\n }\n // done \u2014 workflow-internal: `done <id> [<id>\u2026]` or `done all`.\n w.evaluators.done ||= async (expr, ctx) => {\n const ids = expr.trim() === 'all' ? ctx.priorIds : expr.split(/\\s+/).filter(Boolean)\n return ids.every(id => ctx.done.has(id))\n }\n return w\n}\n\nconst wf = ns()\n\n// \u2500\u2500 styles + panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STYLE = `\n .termflow-item { border:1px solid #ddd; border-radius:4px; padding:6px 8px;\n background:#fafafa }\n .termflow-item .tf-head { font-weight:bold; margin-bottom:4px }\n .termflow-item .tf-steps { margin:0 0 6px; padding-left:20px;\n font-family:monospace; font-size:12px }\n .termflow-item .tf-steps li { margin:1px 0; white-space:nowrap; overflow:hidden;\n text-overflow:ellipsis }\n .termflow-item .tf-steps li.locked { color:#bbb }\n .termflow-item .tf-steps li.running { color:#1565c0 }\n .termflow-item .tf-steps li.done { color:#0a7a32 }\n .termflow-item .tf-steps li.failed { color:#cd3131 }\n .termflow-item .tf-steps li .st { margin-right:4px }\n .termflow-item .tf-steps li .g { color:#aaa }\n .termflow-item .tf-tools button { margin-right:4px; font-size:11px }\n .termflow-item .tf-status { font-size:11px; color:#888; margin-top:4px;\n white-space:pre-wrap }\n .termflow-item .tf-login { font-size:11px; margin:4px 0; padding:3px 6px;\n border-radius:3px; display:none }\n .termflow-item.tf-readonly .tf-login { display:block; background:#fff8e6;\n border-left:3px solid #ffb000; color:#7a5b00 }\n .termflow-item.tf-readonly .tf-run { opacity:.5; cursor:not-allowed }\n`\n\nconst ensureAssets = () => {\n if (document.getElementById('termflow-plugin-style')) return\n const style = document.createElement('style')\n style.id = 'termflow-plugin-style'\n style.textContent = STYLE\n document.head.appendChild(style)\n}\n\nconst emit = ($item, item) => {\n ensureAssets()\n $item.append(`\n <div class=\"termflow-item\">\n <div class=\"tf-head\">\u2699 <span class=\"tf-title\">workflow</span></div>\n <ol class=\"tf-steps\"></ol>\n <div class=\"tf-login\"></div>\n <div class=\"tf-tools\">\n <button class=\"tf-run\" title=\"run the workflow step by step, honouring guards\">run workflow</button>\n <button class=\"tf-gen\" title=\"rebuild the script from the page's runnable items\">generate \u21BB</button>\n <button class=\"tf-copy\" title=\"copy the workflow script\">copy</button>\n </div>\n <div class=\"tf-status\"></div>\n </div>\n `)\n}\n\n// \u2500\u2500 helpers used by bind \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst healthy = async base => {\n try {\n const res = await fetch(`${base}/terminal/health`, { signal: AbortSignal.timeout(1500) })\n return res.ok\n } catch {\n return false\n }\n}\n\nconst pageTitle = $page =>\n ($page.data('data') || {}).title || $page.find('h1').first().text().trim() || 'workflow'\n\nconst evaluateGuard = async (guard, ctx) => {\n if (!guard) return true\n const { kind, expr } = guardKind(guard)\n const ev = wf.evaluators[kind]\n if (!ev) return false // unknown kind \u2192 stay locked\n try { return await ev(expr, ctx) } catch { return false }\n}\n\nconst setStepState = ($steps, id, state) =>\n $steps.find(`li[data-step=\"${id}\"]`).removeClass('locked ready running done failed').addClass(state)\n\nconst renderSteps = ($steps, verdicts, done) => {\n $steps.empty()\n for (const { s, pass } of verdicts) {\n const first = (s.body || '').split('\\n')[0]\n const isDone = done.has(s.id)\n const state = isDone ? 'done' : pass ? 'ready' : 'locked'\n const icon = isDone ? '\u2713' : !pass ? '\uD83D\uDD12' : s.tty ? '\u2328' : '\u25B6'\n $steps.append(`<li class=\"${state}\" data-step=\"${s.id}\"><span class=\"st\">${icon}</span>` +\n `${expand(first)}${s.guard ? ` <span class=\"g\">(needs ${expand(s.guard)})</span>` : ''}</li>`)\n }\n}\n\n// \u2500\u2500 bind \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst bindWorkflow = ($item, item, base) => {\n const $page = $item.parents('.page')\n const $box = $item.find('.termflow-item')\n const pageKey = $page.data('key') || 'page'\n const $steps = $item.find('.tf-steps')\n const $status = $item.find('.tf-status')\n const done = new Set()\n\n // Running records each step in the page journal (a native wiki edit), which\n // only reaches disk for the logged-in page owner \u2014 the wiki's own signal is\n // pageHandler.useLocalStorage() (false \u21D2 owner). So gate running on ownership\n // and say so, rather than silently dropping the history into localStorage.\n const canPersist = () => {\n try { return !!wiki.pageHandler && wiki.pageHandler.useLocalStorage() === false }\n catch { return false }\n }\n const refreshLogin = () => {\n const ok = canPersist()\n $box.toggleClass('tf-readonly', !ok)\n $item.find('.tf-run').prop('disabled', !ok)\n $item.find('.tf-login').text(ok ? ''\n : '\uD83D\uDD12 log in as the site owner to run \u2014 each step is recorded in the page history')\n }\n\n // Evaluate every guard in order, publish lock verdicts, redraw the list.\n const evaluate = async () => {\n const { preamble, steps } = parseScript(item.text)\n $item.find('.tf-title').text(workflowTitle(item.text) || pageTitle($page))\n const priorIds = []\n const verdicts = []\n for (const s of steps) {\n const pass = await evaluateGuard(s.guard, { base, preamble, done, priorIds: [...priorIds] })\n verdicts.push({ s, pass })\n priorIds.push(s.id)\n }\n for (const { s, pass } of verdicts) wf.setLock(pageKey, s.id, { locked: !pass, guard: s.guard })\n renderSteps($steps, verdicts, done)\n refreshLogin()\n return steps\n }\n\n // Rebuild the script from the page's runnable items, preserving the preamble\n // (guard functions) and authored guards/flags by id, and persist it back here.\n const generate = () => {\n const gathered = []\n $page.find('.item').each((_i, el) => {\n const it = $(el).data('item')\n if (!it || it.id === item.id || !wf.runners[it.type]) return\n const body = wf.scriptOf[it.type] ? wf.scriptOf[it.type](it) : (it.text || '')\n gathered.push({ id: it.id, body })\n })\n const { preamble, steps } = parseScript(item.text)\n const merged = mergeSteps(steps, gathered)\n const text = generateScript(preamble || defaultPreamble(workflowTitle(item.text) || pageTitle($page)), merged)\n const newItem = { ...item, text }\n $item.data('item', newItem)\n $item.empty().off()\n emit($item, newItem)\n bind($item, newItem)\n wiki.pageHandler.put($page, { type: 'edit', id: item.id, item: newItem })\n }\n\n // Guarded step-through: re-check each guard, run unlocked non-tty steps via the\n // target type's runner (output rendered by that plugin), stop on a failed guard,\n // a non-zero exit, or an interactive step. Re-evaluate after each so later\n // steps unlock as earlier ones take effect.\n const runWorkflow = async () => {\n if (!canPersist()) {\n refreshLogin()\n $status.text('not logged in \u2014 log in as the site owner to run (each step is recorded in the page history)')\n return\n }\n const steps = await evaluate()\n const { preamble } = parseScript(item.text)\n const priorIds = []\n for (const s of steps) {\n const pass = await evaluateGuard(s.guard, { base, preamble, done, priorIds: [...priorIds] })\n priorIds.push(s.id)\n if (!pass) { $status.text(`stopped at step ${s.id}: guard \\`${s.guard}\\` not met`); return }\n if (s.tty) {\n $status.text(`step ${s.id} is interactive \u2014 run it in its own terminal, ` +\n `then press \u201Crun workflow\u201D again to continue`)\n return\n }\n const el = $page.find(`.item[data-id=\"${s.id}\"]`).get(0)\n const target = el && $(el).data('item')\n const runner = target && wf.runners[target.type]\n if (!runner) { $status.text(`step ${s.id}: no runner for type ${target?.type || '?'}`); return }\n setStepState($steps, s.id, 'running')\n let res\n try { res = await runner({ item: target, $item: $(el), body: s.body }) }\n catch (err) { res = { ok: false, exit: -1, output: String(err) } }\n // Record the step in the page history: journal a native edit of the source\n // item, now carrying its result, so the wiki can rewind through the run.\n if (res.item) {\n $(el).data('item', res.item)\n try { wiki.pageHandler.put($page, editAction(res.item)) } catch { /* ignore */ }\n }\n done.add(s.id)\n setStepState($steps, s.id, res.ok ? 'done' : 'failed')\n if (!res.ok) { $status.text(`step ${s.id} exited ${res.exit}; stopping`); return }\n await evaluate()\n }\n $status.text('workflow complete')\n }\n\n $item.find('.tf-run').on('click', runWorkflow)\n $item.find('.tf-gen').on('click', generate)\n $item.find('.tf-copy').on('click', () => navigator.clipboard.writeText(item.text))\n\n // A step run by hand (a completed command in a live terminal) may change system\n // state, so re-evaluate guards when one reports a result. Debounced.\n let pending\n $page.off(`terminal-result.tf-${item.id}`).on(`terminal-result.tf-${item.id}`, () => {\n clearTimeout(pending)\n pending = setTimeout(evaluate, 300)\n })\n\n evaluate()\n}\n\nconst bind = async ($item, item) => {\n $item.find('.tf-head, .tf-steps').on('dblclick', () => wiki.textEditor($item, item))\n if (!isLocalHost(window.location.hostname)) return // display-only off localhost\n\n const $box = $item.find('.termflow-item')\n if ($box.data('tf-bound')) return\n $box.data('tf-bound', true)\n\n const base = serviceBase(item)\n if (!(await healthy(base))) {\n $item.find('.tf-status').text('terminal service unreachable \u2014 gating disabled')\n return\n }\n bindWorkflow($item, item, base)\n}\n\nif (typeof window !== 'undefined') window.plugins.termflow = { emit, bind }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["expand", "text", "isLocalHost", "hostname", "serviceBase", "item", "workflowTitle", "NONSHELL_KINDS", "guardKind", "guard", "g", "first", "rest", "STEP_RE", "RULE_RE", "trimBlank", "lines", "a", "b", "unwrap", "body", "fi", "i", "parseScript", "preamble", "steps", "cur", "buf", "flush", "m", "ni", "head", "stepMarker", "s", "stepBody", "defaultPreamble", "title", "generateScript", "mergeSteps", "oldSteps", "gathered", "prev", "guardProbe", "expr", "editAction", "findItem", "pageKey", "id", "p", "el", "runOnce", "base", "text", "ns", "w", "state", "m", "expr", "ctx", "test", "guardProbe", "res", "d", "wf", "STYLE", "ensureAssets", "style", "emit", "$item", "item", "healthy", "pageTitle", "$page", "evaluateGuard", "guard", "kind", "guardKind", "ev", "setStepState", "$steps", "renderSteps", "verdicts", "done", "s", "pass", "first", "isDone", "icon", "expand", "bindWorkflow", "$box", "$status", "canPersist", "refreshLogin", "ok", "evaluate", "preamble", "steps", "parseScript", "workflowTitle", "priorIds", "generate", "gathered", "_i", "it", "body", "merged", "mergeSteps", "generateScript", "defaultPreamble", "newItem", "bind", "runWorkflow", "target", "runner", "err", "editAction", "pending", "isLocalHost", "serviceBase"]
|
|
7
|
+
}
|
package/factory.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wiki-plugin-termflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Federated Wiki plugin — generic workflow gating: guarded, ordered steps across a page; companion to wiki-plugin-terminal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wiki",
|
|
7
|
+
"federated wiki",
|
|
8
|
+
"plugin",
|
|
9
|
+
"workflow",
|
|
10
|
+
"terminal",
|
|
11
|
+
"gating"
|
|
12
|
+
],
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "David Bovill",
|
|
15
|
+
"url": "https://github.com/Hitchhikers-Guide-to-the-Galaxy"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"client",
|
|
21
|
+
"server",
|
|
22
|
+
"pages",
|
|
23
|
+
"factory.json"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-termflow.git"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.x"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "npm run clean && npm run test && node --no-warnings scripts/build-client.js",
|
|
34
|
+
"dev": "node --no-warnings scripts/build-client.js --watch",
|
|
35
|
+
"about": "wiki -p 3010 -d . --security_legacy",
|
|
36
|
+
"clean": "rm -f client/termflow.js client/termflow.js.map",
|
|
37
|
+
"test": "node --test",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"esbuild": "^0.25.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "About TermFlow Plugin",
|
|
3
|
+
"story": [
|
|
4
|
+
{
|
|
5
|
+
"type": "markdown",
|
|
6
|
+
"id": "4cb5a0e2eb4f09fe",
|
|
7
|
+
"text": "**TermFlow** registers the `termflow` item type — a generic workflow that gathers a page's steps, **locks** each one until its prerequisite is met, and runs them as a guarded step-through. It is the companion to the [[Terminal Plugin]]: Terminal supplies runnable steps and the shell that tests guards, TermFlow orders and gates them. Full design at [[TermFlow Plan]] on plugin.fedwiki.club."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"type": "markdown",
|
|
11
|
+
"id": "7b11289b973852bd",
|
|
12
|
+
"text": "## Repositories\n\n- GitHub — https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-termflow\n- npm — https://www.npmjs.com/package/wiki-plugin-termflow"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "markdown",
|
|
16
|
+
"id": "79efeebc03e9ad9e",
|
|
17
|
+
"text": "## The workflow item\n\nThe item's text **is** one runnable bash script — the single source of truth. A preamble holds a `# ── guards ──` block of bash functions; one block per step follows, keyed to the source item's id, with an optional guard:"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "code",
|
|
21
|
+
"id": "0f566013cdd32847",
|
|
22
|
+
"text": "#!/usr/bin/env bash\n# wiki-workflow: Codex\n\n# ── guards ──\nhave_codex() { command -v codex >/dev/null 2>&1; }\n\n# ── step 7d0192ae… ──\nbrew install codex\n\n# ── step 9bc9a58f… needs: have_codex ──\nif have_codex; then\n codex --version && which codex\nfi"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "markdown",
|
|
26
|
+
"id": "ef57171e1bed81e8",
|
|
27
|
+
"text": "`needs: <fn>` names a guard function. A step is unlocked iff the function exits 0 — the *same* function gates the step in the page and guards it when the script is run standalone. `tty` marks an interactive step the step-through pauses for. Press **generate** to (re)build the script from the page's runnable items, **run workflow** to step through honouring guards, **copy** to take the script."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "markdown",
|
|
31
|
+
"id": "4412450cf7233238",
|
|
32
|
+
"text": "## History & login\n\nRunning a workflow records each step in the **page journal** as a native wiki edit of its source item, carrying the run result — so the wiki's history slider can rewind through the run, and a reload re-renders each step's output. Because the journal only reaches disk for the logged-in page owner, **running is gated on being logged in**; otherwise the run button is disabled with a notice. (Executing commands needs no login — only recording history to the page does.)"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "markdown",
|
|
36
|
+
"id": "dfa7fc48eedac282",
|
|
37
|
+
"text": "## Engine contract\n\nTermFlow is generic. It exposes registries on `window.workflow` that other plugins extend at load:\n\n- `evaluators[kind]` — guard tests; ships `shell` (a bash function, via the terminal service) and `done` (a prior step completed)\n- `runners[type]` — how to run a step of a given item type (Terminal registers `terminal`)\n- `scriptOf[type]` — extract a step's body for **generate**\n- `setLock`/`getLock` — lock dispatch, via a `workflow-lock` event each plugin renders its own way"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "markdown",
|
|
41
|
+
"id": "16d16072663c2ddf",
|
|
42
|
+
"text": "## Requirements\n\nShell guards and terminal steps use the local-first pty service from [[Terminal Plugin]] (`service/terminal_service.py`, with the `/terminal/check` route) bound to `127.0.0.1`. Local-first only: the engine activates on `localhost`/`.localhost`/`.fish` hosts; elsewhere the item is display-only."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "markdown",
|
|
46
|
+
"id": "985cf812e2cccc33",
|
|
47
|
+
"text": "## See\n\n- [[Terminal Plugin]] · [[TermFlow Plan]] · [[About Terminal Plugin]] · [[Shell Plugin]]"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"plugin": "termflow",
|
|
51
|
+
"journal": [
|
|
52
|
+
{
|
|
53
|
+
"type": "create",
|
|
54
|
+
"item": {
|
|
55
|
+
"title": "About TermFlow Plugin",
|
|
56
|
+
"story": [
|
|
57
|
+
{
|
|
58
|
+
"type": "markdown",
|
|
59
|
+
"id": "4cb5a0e2eb4f09fe",
|
|
60
|
+
"text": "**TermFlow** registers the `termflow` item type — a generic workflow that gathers a page's steps, **locks** each one until its prerequisite is met, and runs them as a guarded step-through. It is the companion to the [[Terminal Plugin]]: Terminal supplies runnable steps and the shell that tests guards, TermFlow orders and gates them. Full design at [[TermFlow Plan]] on plugin.fedwiki.club."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"type": "markdown",
|
|
64
|
+
"id": "7b11289b973852bd",
|
|
65
|
+
"text": "## Repositories\n\n- GitHub — https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-termflow\n- npm — https://www.npmjs.com/package/wiki-plugin-termflow"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "markdown",
|
|
69
|
+
"id": "79efeebc03e9ad9e",
|
|
70
|
+
"text": "## The workflow item\n\nThe item's text **is** one runnable bash script — the single source of truth. A preamble holds a `# ── guards ──` block of bash functions; one block per step follows, keyed to the source item's id, with an optional guard:"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "code",
|
|
74
|
+
"id": "0f566013cdd32847",
|
|
75
|
+
"text": "#!/usr/bin/env bash\n# wiki-workflow: Codex\n\n# ── guards ──\nhave_codex() { command -v codex >/dev/null 2>&1; }\n\n# ── step 7d0192ae… ──\nbrew install codex\n\n# ── step 9bc9a58f… needs: have_codex ──\nif have_codex; then\n codex --version && which codex\nfi"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "markdown",
|
|
79
|
+
"id": "ef57171e1bed81e8",
|
|
80
|
+
"text": "`needs: <fn>` names a guard function. A step is unlocked iff the function exits 0 — the *same* function gates the step in the page and guards it when the script is run standalone. `tty` marks an interactive step the step-through pauses for. Press **generate** to (re)build the script from the page's runnable items, **run workflow** to step through honouring guards, **copy** to take the script."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"type": "markdown",
|
|
84
|
+
"id": "4412450cf7233238",
|
|
85
|
+
"text": "## History & login\n\nRunning a workflow records each step in the **page journal** as a native wiki edit of its source item, carrying the run result — so the wiki's history slider can rewind through the run, and a reload re-renders each step's output. Because the journal only reaches disk for the logged-in page owner, **running is gated on being logged in**; otherwise the run button is disabled with a notice. (Executing commands needs no login — only recording history to the page does.)"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "markdown",
|
|
89
|
+
"id": "dfa7fc48eedac282",
|
|
90
|
+
"text": "## Engine contract\n\nTermFlow is generic. It exposes registries on `window.workflow` that other plugins extend at load:\n\n- `evaluators[kind]` — guard tests; ships `shell` (a bash function, via the terminal service) and `done` (a prior step completed)\n- `runners[type]` — how to run a step of a given item type (Terminal registers `terminal`)\n- `scriptOf[type]` — extract a step's body for **generate**\n- `setLock`/`getLock` — lock dispatch, via a `workflow-lock` event each plugin renders its own way"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"type": "markdown",
|
|
94
|
+
"id": "16d16072663c2ddf",
|
|
95
|
+
"text": "## Requirements\n\nShell guards and terminal steps use the local-first pty service from [[Terminal Plugin]] (`service/terminal_service.py`, with the `/terminal/check` route) bound to `127.0.0.1`. Local-first only: the engine activates on `localhost`/`.localhost`/`.fish` hosts; elsewhere the item is display-only."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "markdown",
|
|
99
|
+
"id": "985cf812e2cccc33",
|
|
100
|
+
"text": "## See\n\n- [[Terminal Plugin]] · [[TermFlow Plan]] · [[About Terminal Plugin]] · [[Shell Plugin]]"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"date": 1781352795901,
|
|
105
|
+
"certificate": "from marvin"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
package/server/server.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// wiki-plugin-termflow — server-side component
|
|
2
|
+
//
|
|
3
|
+
// Intentionally a no-op. TermFlow is a client-side coordinator: it gates and
|
|
4
|
+
// runs steps owned by other plugins, and evaluates shell guards through the
|
|
5
|
+
// local-first terminal service (wiki-plugin-terminal's FastAPI /terminal/check).
|
|
6
|
+
// There is nothing to register on the wiki node server; on public servers the
|
|
7
|
+
// workflow item degrades to a plain display.
|
|
8
|
+
|
|
9
|
+
const startServer = params => {}
|
|
10
|
+
|
|
11
|
+
export { startServer }
|