i-plane 2.0.1 → 2.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.
@@ -124,7 +124,36 @@ API endpoints; the diagnostic points to `issueId` from an earlier JSON response.
124
124
  - This is an inventory of the implemented surface, not a claim to cover every
125
125
  endpoint in every edition of Plane.
126
126
 
127
- ## Project pages
127
+ ## Project and wiki pages
128
+
129
+ `wiki list` and `wiki show|outline|read|stamp|create|set|insert|rm` use workspace
130
+ placement through the same page handlers, converters and live client. HTTP uses
131
+ `/api/v1/workspaces/{slug}/pages/`; live uses `workspace_page` and omits `projectId`.
132
+ Runtime discovery requires both wiki and API-key pages enabled. Unsupported
133
+ servers fail explicitly, with no project or session fallback. Permissions remain
134
+ server-owned and are checked for each operation.
135
+
136
+ The wiki list includes accessible nested and archived pages, preserves parent
137
+ UUIDs and follows native manual sibling order in parent-first traversal. Creation
138
+ supports `--parent`; whole-page deletion requires prior explicit archiving in
139
+ Plane. Project commands retain their original behavior. PDF and relocation are
140
+ outside the CLI surface.
141
+
142
+ `tests/wiki-commands.test.ts` covers placement routing, live scope, hierarchy,
143
+ parent resolution, deletion policy, runtime failures and shared editing. The
144
+ mutation runner requires each wiki mutation to fail its declared test and refuses
145
+ unrelated test failures. Run the shared browser acceptance script with a private
146
+ fixture containing `workspace_slug`, `api_key` and `cookie`:
147
+
148
+ ```bash
149
+ node scripts/page-key-acceptance.mjs "$ISOLATED_ORIGIN" "$PRIVATE_FIXTURE" "$OUTPUT" --wiki
150
+ ```
151
+
152
+ This creates a temporary root and child, checks both CLI/browser writers, stale
153
+ fingerprints, ancestry and deletion refusal, then explicitly archives and deletes
154
+ only those fixtures. Wiki ancestry requests canceled by snapshot refresh are
155
+ recorded separately and require a successful replacement response; other browser
156
+ errors fail the check.
128
157
 
129
158
  `pages` and `page show` automatically select API-key or native session page access. `page outline`, `page read`,
130
159
  `page stamp`, `page create`, `page set`, `page insert` and block removal use the
@@ -246,3 +275,26 @@ independent reviews of formatting and command targeting found no remaining
246
275
  confirmed issues. The complete release dry run passed 1,338 tests and twelve
247
276
  intentional mutation checks. The aggregate extension review remains separately
248
277
  tracked in PLX-12; these results do not claim production deployment or publication.
278
+
279
+
280
+ ### Wiki command acceptance (IPL-27)
281
+
282
+ The 2.1.0 release candidate passed 1,545 tests, TypeScript, formatting, built Node
283
+ smoke checks and package inventory validation. All 23 page mutations were detected;
284
+ the seven new wiki mutations each failed their declared test. Built Node checks
285
+ also confirmed explicit refusal on an HTTP server without wiki support, no page
286
+ or project requests after that refusal, and exit 2 for a surplus project argument.
287
+
288
+ On the isolated Plane 1.4.2 stack with core 0.3.3, the initial probe used the
289
+ existing CLI live transport with an API key and no project identifier. Creation,
290
+ acknowledged editing, an independent live read and saved HTML persistence passed.
291
+ No server changes were needed.
292
+
293
+ The built CLI then passed the shared browser scenario on a new wiki root and
294
+ child: parent creation, listing, both writers, stale fingerprints, insertion,
295
+ block deletion, stamping, reload and refusal to delete an unarchived page.
296
+ Temporary pages were explicitly archived and deleted; existing fixtures were
297
+ untouched. The browser had no console errors, HTTP failures or unhandled errors.
298
+ Two ancestry request cancellations had successful replacement responses, matching
299
+ the existing snapshot effect's cancellation contract. Project regression tests
300
+ passed with their existing command and deletion semantics.
package/docs/pages.md CHANGED
@@ -1,9 +1,61 @@
1
- # Project pages
1
+ # Project and wiki pages
2
2
 
3
3
  Read project knowledge as Markdown, then change a section without rewriting the
4
4
  rest of the document. Page metadata uses an automatically selected API-key or session endpoint; content
5
5
  changes use the collaborative live editor.
6
6
 
7
+ ## Workspace wiki
8
+
9
+ Use `wiki` in place of `page`, omitting the project argument. The selected
10
+ workspace comes from the usual configuration or `--workspace`:
11
+
12
+ ```bash
13
+ i-plane wiki list
14
+ i-plane wiki show "Release notes"
15
+ i-plane wiki create --name "Release notes" --file release.md
16
+ i-plane wiki create --name "Deployment" --parent "Release notes"
17
+ i-plane wiki outline "Deployment"
18
+ i-plane wiki stamp "Deployment"
19
+ i-plane wiki read "Deployment" --block a1b2c3d4 --json
20
+ i-plane wiki set "Deployment" --block a1b2c3d4 --if-match "$FINGERPRINT" --text "Updated paragraph"
21
+ i-plane wiki insert "Deployment" --at-end --file appendix.md
22
+ i-plane wiki rm "Deployment" --block a1b2c3d4 --yes
23
+ ```
24
+
25
+ All editing flags have the same meaning as for project pages: `--if-match`,
26
+ `--force`, `--allow-loss`, `--yes`, `--file`, `--text` and `--json`. Both placements
27
+ share the Markdown/Yjs conversion, block editing, reader preservation checks and
28
+ delivery semantics below. Project command syntax and behavior are unchanged.
29
+
30
+ Wiki requires an API key and enabled `workspace-wiki` and `api-key-pages` modules.
31
+ Commands check runtime support before accessing pages and refuse unsupported
32
+ servers with exit code 1. Missing key configuration is a usage error (exit 2).
33
+ Wiki commands do not attempt session sign-in or project routes. Live uses
34
+ `workspace_page` without `projectId`. Reader capabilities reuse the common cache
35
+ implementation with a separate wiki entry, so a cached project session cannot
36
+ change the wiki identity. Runtime support is rechecked on every invocation;
37
+ permissions are never cached. `--refresh-pages` bypasses retained capabilities,
38
+ and `config --refresh-pages` clears both placement entries.
39
+
40
+ `wiki list` includes all accessible pages, including nested and archived pages;
41
+ project `pages` also includes archived pages. Text shows UUID, name, parent UUID
42
+ and modification date. JSON retains the server metadata and makes `parent`
43
+ explicit as a UUID or `null`. No parent name or invented parent is supplied.
44
+ Parents precede children; siblings sort by native manual `sort_order`, then
45
+ `created_at` and UUID. Missing parents appear among roots while keeping their
46
+ returned parent UUID. Legacy cycles appear once, in deterministic order.
47
+
48
+ Names resolve by exact match or unique prefix across that complete list;
49
+ ambiguous matches report candidate names and UUIDs. `wiki create --parent`
50
+ resolves its parent before creating the page. Omit the flag to create a root.
51
+ Reordering and moving existing pages are outside this command set.
52
+
53
+ Whole-page `wiki rm <page> --yes` sends deletion to the server **without archiving
54
+ automatically**. Archive the page in Plane first. The server permits deletion only
55
+ to the owner or a workspace administrator; refusals retain their explanation.
56
+ Block deletion uses live and does not archive or delete the page itself.
57
+ PDF export is not provided by either command family.
58
+
7
59
  ## Connect
8
60
 
9
61
  Configure the same settings used for work items:
@@ -150,14 +202,21 @@ supports `--json`. Run `i-plane page set --help` for flags and examples.
150
202
  ## Knowledge review marks
151
203
 
152
204
  Writing a product node requires confirmation that the server preserves it.
153
- Until the reader-capability endpoint is available, `knowledgeReview` writes are
154
- refused with exit code **1**, before conversion, page creation or document changes.
155
- This applies to create, replace, insert and edits that would retain an existing
156
- review node. `--allow-loss` and `--force` cannot override it. Ordinary Markdown
157
- and explicit removal of the last review node remain supported. Existing review
158
- content can still be read. Reader discovery will use the existing per-instance
159
- capability mechanism and `--refresh-pages`; transport support alone is not proof
160
- of document-schema support.
205
+ Core 0.2.1 or newer reports retained node readers through
206
+ `GET /api/extensions/node-readers/`, independently of the wiki interface.
207
+ The CLI recognizes format version 1 and checks that the inventory matches the
208
+ current core, runtime release and reader fingerprint. Results share the existing
209
+ per-instance capability cache and `--refresh-pages`. Every custom-node write
210
+ rechecks the runtime identity, so a rollback cannot reuse a previous positive claim.
211
+
212
+ An absent endpoint means refusal with exit code **1**, before conversion, page
213
+ creation or document changes. `401`/`403` remain access errors; malformed inventory,
214
+ service errors and disagreement between runtimes do not mean an absent feature.
215
+ The rule applies to create, replace, insert and edits retaining existing review
216
+ nodes. `--allow-loss` and `--force` cannot override it. Ordinary Markdown and
217
+ explicit removal of the last review remain supported; existing review content
218
+ can still be read. A converter response that loses confirmed review nodes or
219
+ attributes is also rejected before a document write.
161
220
 
162
221
  Saved detail responses from the matching extension include canonical JSON for
163
222
  review restoration. If older HTML lost review attributes, `page show` can recover
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i-plane",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Compact Plane CLI for coding agents: tasks, projects, live pages, cycles, modules and intake.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,2 +0,0 @@
1
- import{A as ge,B as z,C as Se,D as Z,F as Ye,G as ee,K as P,l as le,n as N,o as O,p as F,x as de,y as fe,z as me}from"./cli-hgk20taa.js";import{N as xe,S as d}from"./cli-tdf7t5es.js";import{aa as h,ba as U,ca as C}from"./cli-2sf0pmt1.js";import{fa as Ue,ga as Re,ka as q,ma as k,na as E}from"./cli-5v4ee99d.js";var se=Re(function(et,ae){var W=!0,ie=!1,oe=!1;ae.exports=function(e,t){if(t)W=!t.enabled,ie=!!t.label,oe=!!t.labelAfter;e.core.ruler.after("inline","github-task-lists",function(r){var n=r.tokens;for(var i=2;i<n.length;i++)if(Me(n,i))Ce(n[i],r.Token),ne(n[i-2],"class","task-list-item"+(!W?" enabled":"")),ne(n[Xe(n,i-2)],"class","contains-task-list")})};function ne(e,t,r){var n=e.attrIndex(t),i=[t,r];if(n<0)e.attrPush(i);else e.attrs[n]=i}function Xe(e,t){var r=e[t].level-1;for(var n=t-1;n>=0;n--)if(e[n].level===r)return n;return-1}function Me(e,t){return je(e[t])&&qe(e[t-1])&&Oe(e[t-2])&&Fe(e[t])}function Ce(e,t){if(e.children.unshift(Te(e,t)),e.children[1].content=e.children[1].content.slice(3),e.content=e.content.slice(3),ie)if(oe){e.children.pop();var r="task-item-"+Math.ceil(Math.random()*1e7-1000);e.children[0].content=e.children[0].content.slice(0,-1)+' id="'+r+'">',e.children.push(De(e.content,r,t))}else e.children.unshift(Ne(t)),e.children.push(Be(t))}function Te(e,t){var r=new t("html_inline","",0),n=W?' disabled="" ':"";if(e.content.indexOf("[ ] ")===0)r.content='<input class="task-list-item-checkbox"'+n+'type="checkbox">';else if(e.content.indexOf("[x] ")===0||e.content.indexOf("[X] ")===0)r.content='<input class="task-list-item-checkbox" checked=""'+n+'type="checkbox">';return r}function Ne(e){var t=new e("html_inline","",0);return t.content="<label>",t}function Be(e){var t=new e("html_inline","",0);return t.content="</label>",t}function De(e,t,r){var n=new r("html_inline","",0);return n.content='<label class="task-list-item-label" for="'+t+'">'+e+"</label>",n.attrs=[{for:t}],n}function je(e){return e.type==="inline"}function qe(e){return e.type==="paragraph_open"}function Oe(e){return e.type==="list_item_open"}function Fe(e){return e.content.indexOf("[ ] ")===0||e.content.indexOf("[x] ")===0||e.content.indexOf("[X] ")===0}});import{readFile as Qe}from"node:fs/promises";import{createHash as Ie,randomUUID as _e}from"node:crypto";var ue=Ue(se(),1);import*as m from"yjs";var X=(e)=>{let t=new Set,r=(n)=>{if(!(n instanceof m.XmlElement))return;if(n.nodeName.toLowerCase()==="knowledgereview")t.add("knowledgeReview");for(let i of n.toArray())r(i)};return e.forEach(r),[...t]},L=(e)=>String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),x=(e,t)=>t==null||t===""?"":` ${e}="${L(t)}"`,T=(e)=>{if(Array.isArray(e))return e.map(T);if(e&&typeof e==="object")return Object.fromEntries(Object.entries(e).sort(([t],[r])=>t.localeCompare(r)).map(([t,r])=>[t,T(r)]));return e},He=new Set(["doc","paragraph","heading","bulletlist","orderedlist","listitem","tasklist","taskitem","blockquote","codeblock","horizontalrule","hardbreak","table","tablerow","tableheader","tablecell","imagecomponent","image","knowledgereview"]),pe=(e,t=!0)=>e instanceof m.XmlText?{text:T(e.toDelta())}:{kind:e.nodeName,attributes:T(Object.fromEntries(Object.entries(e.getAttributes()).filter(([r])=>r!=="id"||!(t||He.has(e.nodeName.toLowerCase()))))),children:e.toArray().map((r)=>pe(r,!1))},J=(e)=>Ie("sha256").update(JSON.stringify(pe(e))).digest("hex"),R=(e)=>e instanceof m.XmlElement&&e.nodeName==="knowledgeReview"?`Reviewed ${String(e.getAttribute("reviewedAt")??"")}: ${String(e.getAttribute("source")??"")}`:e instanceof m.XmlText?e.toDelta().map((t)=>typeof t.insert==="string"?t.insert:JSON.stringify(t.insert)).join(""):e.toArray().map((t)=>R(t)).join(" "),he=(e)=>e.toArray().map((t,r)=>{let n=t instanceof m.XmlElement?String(t.getAttribute("id")??"")||null:null;return{index:r,anchor:n,shortAnchor:n?k(n).slice(0,8):null,kind:t instanceof m.XmlElement?t.nodeName:"text",preview:k(R(t)).replace(/\s+/g," ").trim().slice(0,40)}}),B=(e,t)=>{let r=e.toArray().flatMap((i,a)=>{if(!(i instanceof m.XmlElement))return[];let l=String(i.getAttribute("id")??"");return l.length>0&&l.toLowerCase().startsWith(t.toLowerCase())?[{node:i,index:a,anchor:l}]:[]});if(r.length===0)throw new d(`No top-level block matches "${t}". Run page outline to see current anchors.`,404);if(r.length!==1)throw new h(`Ambiguous block anchor "${t}": ${r.map((i)=>`${i.anchor} [${i.index}]`).join(", ")}`);let n=r[0];if(!n)throw new d("Block disappeared.");return n},K=(e)=>{let t=0;for(let r of e.toArray())if(r instanceof m.XmlElement&&!r.getAttribute("id"))r.setAttribute("id",_e()),t++;return t},D=(e,t,r=!1)=>{if(e instanceof m.XmlText){let o=R(e),s=r?k(o):o;if(s!==o)return t.add("Sensitive text was redacted; the printed block is not a lossless copy"),L(s);return e.toDelta().map((f)=>{let c=L(typeof f.insert==="string"?f.insert:JSON.stringify(f.insert));for(let[p,w]of Object.entries(f.attributes??{})){let y=Object.fromEntries(Object.entries(w&&typeof w==="object"?w:{}).map(([A,b])=>[A,r&&typeof b==="string"?k(b):b]));if(p==="bold")c=`<strong>${c}</strong>`;else if(p==="italic")c=`<em>${c}</em>`;else if(p==="strike")c=`<s>${c}</s>`;else if(p==="code")c=`<code>${c}</code>`;else if(p==="link")c=`<a${x("href",y.href)}${x("title",y.title)}>${c}</a>`;else if(t.add(`Mark ${p} is not representable by this Markdown reader`),p==="underline")c=`&lt;u&gt;${c}&lt;/u&gt;`}return c}).join("")}let n=e.nodeName.toLowerCase(),i=Object.fromEntries(Object.entries(e.getAttributes()).map(([o,s])=>[o,r&&typeof s==="string"?k(s):s])),a=()=>e.toArray().map((o)=>D(o,t,r)).join("");if(i.textAlign&&i.textAlign!=="left")t.add("Text alignment is not representable in Markdown");if(n==="knowledgereview"){let o=String(i.reviewedAt??""),s=String(i.source??"");if(typeof i.reviewedAt!=="string"||typeof i.source!=="string"||!fe(o)||!s.trim())t.add("Review metadata is invalid");if(Object.keys(i).some((f)=>!["id","reviewedAt","source"].includes(f))||e.length)t.add("Additional review attributes or content are not represented in Markdown");if(r&&(i.reviewedAt!==e.getAttribute("reviewedAt")||i.source!==e.getAttribute("source")))t.add("Sensitive review metadata was redacted; the printed block is not a lossless copy");return z({date:o,source:s})}if(n==="doc")return a();if(n==="paragraph")return`<p>${a()}</p>`;if(n==="heading"){let o=Math.min(6,Math.max(1,Number(i.level)||1));return`<h${o}>${a()}</h${o}>`}if(n==="bulletlist")return`<ul>${a()}</ul>`;if(n==="orderedlist")return`<ol${x("start",i.start)}>${a()}</ol>`;if(n==="listitem")return`<li>${a()}</li>`;if(n==="tasklist")return`<ul>${a()}</ul>`;if(n==="taskitem"){let o=`<input type="checkbox"${i.checked===!0||i.checked==="true"?" checked":""}>`,s=a().replace(/^<p>([\s\S]*?)<\/p>/,"$1");return`<li>${o}${s}</li>`}if(n==="blockquote")return`<blockquote>${a()}</blockquote>`;if(n==="codeblock"&&i.language==="knowledge-review")t.add("The knowledge-review fence language is reserved for review metadata");if(n==="codeblock")return`<pre><code${i.language?x("class",`language-${i.language}`):""}>${L(r?k(R(e)):R(e))}</code></pre>`;if(n==="horizontalrule")return"<hr>";if(n==="hardbreak")return"<br>";if(n==="table")return`<table>${a()}</table>`;if(n==="tablerow")return`<tr>${a()}</tr>`;if(n==="tablecell"||n==="tableheader"){let o=e.toArray().filter((c)=>c instanceof m.XmlElement),s=(c)=>c.nodeName.toLowerCase()==="hardbreak"||c.toArray().some((p)=>p instanceof m.XmlElement&&s(p));if(o.length>1||o.some((c)=>c.nodeName.toLowerCase()!=="paragraph"||s(c)))t.add("Multiple paragraphs, line breaks or block structure inside table cells are flattened in Markdown");if(Number(i.colspan??1)!==1||Number(i.rowspan??1)!==1)t.add("Merged table cells are not representable in Markdown");if(i.background||i.colwidth)t.add("Table styling is not representable in Markdown");let f=n==="tablecell"?"td":"th";return`<${f}>${a()}</${f}>`}if(n==="imagecomponent"||n==="image"){if(i.width||i.height||i.alignment)t.add("Image dimensions or alignment are not representable in Markdown");return`<img${x("src",i.src)}${x("alt",i.alt)}${x("title",i.title)}>`}t.add(`Unsupported node ${e.nodeName}; preserved as XML text`);let l=e.toString();return`<pre><code class="language-xml">${L(r?de(l):l)}</code></pre>`},We=(e)=>{let t=new Set,r=e.toArray().map((n)=>D(n,t)).join("");return{markdown:N(r),losses:[...t]}},we=(e,t)=>{let{node:r,anchor:n}=B(e,t),i=new Set;return{anchor:n,fingerprint:J(r),markdown:N(D(r,i,!0)),losses:[...i]}},ke=(e)=>{let t=new Set;for(let r of e)D(r,t);return[...t]},M=new le({html:!1}).use(ue.default),ze=M.renderer.rules.fence;M.renderer.rules.fence=(e,t,r,n,i)=>{let a=e[t];if(/^knowledge-review\s/.test(a?.info.trim()??""))throw new h("A knowledge-review fence does not accept additional info fields.");if(a?.info.trim()==="knowledge-review"){let l=me(a.content);if(!n?.reviewAsCode&&n?.requiredNodes instanceof Set)n.requiredNodes.add("knowledgeReview");return n?.reviewAsCode?z(l):ge(l)}return ze?.(e,t,r,n,i)??i.renderToken(e,t,r)};M.core.ruler.before("github-task-lists","empty-task-items",(e)=>{for(let t=2;t<e.tokens.length;t++){let r=e.tokens[t],n=r?.children?.[0];if(r&&n&&/^\[[ xX]\]$/.test(r.content)&&e.tokens[t-1]?.tag==="p"&&e.tokens[t-2]?.tag==="li")r.content+=" ",n.content+=" "}});var be=(e)=>e.toArray().flatMap((t)=>t instanceof m.XmlElement?[...t.nodeName.toLowerCase()==="codeblock"?[t]:[],...be(t)]:[]),ce=(e,t=!1,r)=>M.render(e,{reviewAsCode:t,requiredNodes:r}).trim().replace(/(<li class="task-list-item">)\s*<p>([\s\S]*?)<\/p>/g,"$1$2").replace(/(<input class="task-list-item-checkbox"[^>]*>) /g,"$1"),ye=async(e,t)=>{let r=new Set,n=ce(t,!1,r);if(r.size)P([...r],await e.nodeReaders?.()??[]);if(!n){let o=new m.Doc;return{doc:o,fragment:o.getXmlFragment("default"),losses:[]}}let i=n.replace(/<img([^>]*?)\s*\/?>/g,"<image-component$1></image-component>").replace(/<ul class="contains-task-list">/g,'<ul data-type="taskList">').replace(/<li class="task-list-item">(\s*<p>)?<input\b([^>]*)>/g,(o,s,f)=>`<li data-type="taskItem" data-checked="${/\bchecked=/.test(f)}">${s??""}`),a=await e.request("/live/convert-document",{method:"POST",body:{description_html:i,variant:"rich"}}),l=new m.Doc;try{if(typeof a.description_binary!=="string"||!a.description_binary)throw Error("Missing document binary");m.applyUpdate(l,Buffer.from(a.description_binary,"base64"));let o=l.getXmlFragment("default"),s=M.parse(t,{}).filter((w)=>w.block&&w.tag==="code"&&w.info.trim()!=="knowledge-review").map((w)=>w.info.trim().split(/\s+/)[0]||null),f=be(o),c=[];if(s.length!==f.length)c.push("The converter changed the number of code blocks");else l.transact(()=>f.forEach((w,S)=>{let y=s[S];if(y)w.setAttribute("language",y);else w.removeAttribute("language")}));if(We(o).markdown!==N(ce(t,!0)))c.push("The converter changed Markdown content or formatting");return l.transact(()=>K(o)),{doc:l,fragment:o,losses:c}}catch(o){if(l.destroy(),o instanceof d)throw o;throw new d(`Cannot decode the converted document: ${o instanceof Error?o.message:String(o)}`)}};import*as j from"yjs";var V=(e,t=!1)=>{if(e.length&&!t)throw new h(`This edit can lose content or formatting: ${e.join("; ")}. Inspect the document, then pass --allow-loss to accept these losses.`)},ve=(e,t,r)=>{let n=t.block?B(e,t.block):void 0;if(n&&t.ifMatch&&!t.force&&J(n.node)!==t.ifMatch)throw new d("Block content changed since page read. Read it again and use its new --if-match fingerprint, or pass --force to overwrite the concurrent edit.",409);let i=r?.fragment.length??0;if(t.kind!=="rm"&&!r)throw new h("Expected Markdown content.");if((t.kind==="insert"||n&&t.kind==="set")&&i===0)throw new h("Empty block content. Use page rm --block to remove a block.");let a=t.kind==="insert"?[]:n?[n.node]:e.toArray(),l=[...new Set([...r?.losses??[],...ke(a)])];if(t.kind!=="rm")V(l,t.allowLoss);let o=n?.index??0;if(t.kind==="insert")o=t.after?B(e,t.after).index+1:e.length;let s=r?.fragment.toArray().map((c)=>{if(!(c instanceof j.XmlElement)&&!(c instanceof j.XmlText))throw new d("Unsupported document block; no changes were made.");return c.clone()})??[],f=e.toArray();if(t.kind!=="insert")f.splice(o,n?1:f.length);if(P(X([...f,...t.kind==="rm"?[]:r?.fragment.toArray()??[]]),t.nodeReaders??[]),t.kind!=="insert")e.delete(o,n?1:e.length);if(t.kind!=="rm"&&s.length){if(e.insert(o,s),n&&s[0]&&"setAttribute"in s[0])s[0].setAttribute("id",n.anchor)}return t.kind==="rm"?[]:l};import{HocuspocusProvider as Je,HocuspocusProviderWebsocket as Ke}from"@hocuspocus/provider";import{HttpsProxyAgent as Ve}from"https-proxy-agent";import Ge from"ws";import*as $e from"yjs";class G extends d{}class Ae{doc=new $e.Doc;fragment=this.doc.getXmlFragment("default");socket;provider;agent;pending;failure;writing=!1;acknowledged=!1;disposed=!1;deadline;constructor(e,t,r,n=20000){this.deadline=n,q(r);let i=xe(e);if(i)q(i),this.agent=new Ve(i);let a=this.agent;class l extends Ge{constructor(o){super(o,{agent:a,handshakeTimeout:n})}}this.socket=new Ke({url:e,WebSocketPolyfill:l,autoConnect:!1,maxAttempts:1,onClose:()=>this.fail(new d("Live connection closed."))}),this.socket.on("maxAttemptsFailed",()=>this.fail(new d("Cannot connect to the live server."))),this.provider=new Je({websocketProvider:this.socket,name:t,document:this.doc,awareness:null,token:r,forceSyncInterval:!1,flushDelay:!1,onClose:()=>this.fail(new d("Live document connection closed.")),onSynced:()=>this.progress(),onUnsyncedChanges:()=>this.progress(),onAuthenticationFailed:()=>this.fail(new G("Live authentication was refused."))}),this.provider.attach()}progress(){if(this.disposed||this.failure||!this.provider?.isSynced||this.provider.hasUnsyncedChanges)return;if(this.writing)this.acknowledged=!0;this.pending?.resolve()}fail(e){if(this.disposed||this.failure)return;if(this.writing&&!this.acknowledged)e=new d("Live connection failed after sending changes; delivery is uncertain. Inspect the live document before retrying. The command was not replayed.");this.failure=e,this.socket.shouldConnect=!1,this.pending?.reject(e)}async wait(e){if(this.failure)throw this.failure;let t;try{await new Promise((r,n)=>{this.pending={resolve:r,reject:n},t=setTimeout(()=>this.fail(new d("Live synchronization timed out.")),this.deadline),e(),this.progress()})}finally{clearTimeout(t),this.pending=void 0}}async ready(){await this.wait(()=>{this.socket.connect().catch(()=>this.fail(new d("Cannot connect to the live server.")))})}async write(e){if(this.failure)throw this.failure;if(!this.provider.isSynced)throw new d("Live document is not synchronized.");if(this.provider.authorizedScope!=="read-write")throw new d("The connection does not allow writing this page.");let t=!1,r=(n,i)=>{if(i===this)t=!0};this.doc.on("update",r);try{return await this.wait(()=>{this.writing=!0,this.doc.transact(()=>e(this.fragment),this)}),{delivery:t?"acknowledged":"unchanged",persistence:"asynchronous"}}finally{this.doc.off("update",r)}}destroy(){if(this.disposed)return;this.disposed=!0,this.provider.destroy(),this.socket.destroy(),this.agent?.destroy(),this.doc.destroy()}}var Pe=async(e,t,r,n={})=>{await e.preparePages?.(t);let i=!!n.writable,a=await e.liveCredentials?.(i);if(!a){q(e.config.token.value);let l=await e.request("/api/extensions/configuration/").catch((o)=>{if(o instanceof d&&o.status===404)throw new d("Page commands require the for-plane API-key pages extension.",404);throw o});if(typeof l.release!=="string"||!l.release)throw new d("The for-plane runtime has no active extension release.");a={release:l.release,token:JSON.stringify({apiKey:e.config.token.value,readOnly:!i})}}for(let l=0;l<2;l++){let o=new URL(`${e.config.url.value}/live/collaboration`);if(o.protocol=o.protocol==="https:"?"wss:":"ws:",o.searchParams.set("documentType","project_page"),o.searchParams.set("projectId",t),o.searchParams.set("workspaceSlug",e.config.workspace.value),a.release)o.searchParams.set("forPlaneRelease",a.release);let s=new Ae(o.href,r,a.token);try{return await s.ready(),s}catch(f){if(s.destroy(),l===0&&f instanceof G){let c=await e.liveCredentials?.(i,!0);if(c){a=c;continue}}throw new d(k(f instanceof Error?f.message:String(f)))}}throw new d("Live session authentication failed after one refresh.")};var Ze=async(e,t=!1)=>{let r=U(e,"file"),n=U(e,"text");if(r!==void 0&&n!==void 0)throw new h("--file and --text are mutually exclusive.");if(r!==void 0)try{return await Qe(r,"utf8")}catch{throw new h(`Cannot read Markdown file "${r}".`)}if(n!==void 0||t)return n;throw new h("Expected --file <path> or --text <markdown>.")},Ee=(e)=>{if(e.length)console.error(k(`Warning: ${e.join("; ")}`))},Q=(e)=>`${e.id} ${e.deleted?"deleted":e.delivery??"created"}${e.stamped!==void 0?`; stamped ${e.stamped} blocks`:""}${e.delivery?"; database persistence is asynchronous":""}`,Ct=async(e,t,r,n)=>{let i=O(r.positionals[0],"a project"),a=e==="page create",l=e==="page rm",o=a?F(r,"name"):O(r.positionals[1],"a page UUID or name"),s=r.flags.has("block")?F(r,"block"):void 0,f=r.flags.has("after")?F(r,"after"):void 0,c=C(r,"at-end"),p=U(r,"if-match"),w=C(r,"force"),S=C(r,"allow-loss");if(l&&!C(r,"yes"))throw new h("Deletion requires --yes.");if((p!==void 0||w)&&!s)throw new h("--if-match and --force require --block.");if(p!==void 0&&!/^[a-f0-9]{64}$/i.test(p))throw new h("--if-match expects the SHA-256 fingerprint from page read --json.");if(e==="page read"&&!s)throw new h("Expected --block <anchor>.");if(e==="page insert"&&Boolean(f)===c)throw new h("Choose exactly one of --after <anchor> or --at-end.");let y=a||e==="page set"||e==="page insert"?await Ze(r,a):void 0,A=await Se(t,i),b,I;try{if(y!==void 0)b=await ye(t,y),V(b.losses,S);let g=a?await t.request(Z(A.id),{method:"POST",body:{name:o,access:0}}):await Ye(t,A.id,o);if(a)I=g;if(a&&!b){E({id:g.id,name:g.name},n,Q);return}if(l&&!s){ee(g,!0);let v=`${Z(A.id)}${g.id}/`;if(!g.archived_at)try{await t.request(`${v}archive/`,{method:"POST"})}catch(u){throw new d(`Archiving page ${g.id} was not confirmed: ${u instanceof Error?u.message:String(u)}. Inspect this UUID before retrying; deletion was not attempted.`)}try{await t.request(v,{method:"DELETE"})}catch(u){throw new d(`Page ${g.id} was archived, but deletion was not confirmed: ${u instanceof Error?u.message:String(u)}. Inspect it by UUID before retrying.`)}E({id:g.id,deleted:!0},n,Q);return}let te=e==="page outline"||e==="page read";if(!te)ee(g);let Y=await Pe(t,A.id,g.id,{writable:!te});try{if(e==="page outline")E(he(Y.fragment),n,(v)=>v.map((u)=>`${u.index} ${u.shortAnchor??"(no anchor)"} ${u.kind} ${u.preview}`).join(`
2
- `));else if(e==="page read"){let v=we(Y.fragment,O(s,"--block <anchor>"));Ee(v.losses),E(v,n,(u)=>u.markdown)}else{let u=X([...Y.fragment.toArray(),...b?.fragment.toArray()??[]]).length?await t.nodeReaders?.()??[]:[],re,_=[];t.markPageMutation?.();let Le=await Y.write((H)=>{if(e==="page stamp")P(X(H.toArray()),u),re=K(H);else _=ve(H,{kind:l?"rm":e==="page insert"?"insert":"set",block:s,after:f,atEnd:c,ifMatch:p?.toLowerCase(),force:w,allowLoss:S,nodeReaders:u},b)});Ee(_),E({id:g.id,...Le,stamped:re,losses:_},n,Q)}}finally{Y.destroy()}}catch(g){if(I)throw new d(`Page ${I.id} was created, but its content operation did not complete: ${g instanceof Error?g.message:String(g)}. Inspect this page before retrying; do not create a duplicate.`);throw g}finally{b?.doc.destroy()}};export{Ct as runPageContent};
@@ -1 +0,0 @@
1
- import{O,P as R,Q as b,S as a}from"./cli-tdf7t5es.js";import{ka as u,ma as S}from"./cli-5v4ee99d.js";import{createHash as T,randomUUID as D}from"node:crypto";import{constants as y}from"node:fs";import{chmod as N,lstat as _,mkdir as q,open as k,rename as E,stat as F,unlink as g}from"node:fs/promises";import{join as L}from"node:path";import{setTimeout as j}from"node:timers/promises";var H=20000,x=(e)=>(u(e.sessionId),u(e.csrf),u(P(e)),e),P=(e)=>`session-id=${e.sessionId}; csrftoken=${e.csrf}`,J=(e,i,t)=>L(t,`${T("sha256").update(JSON.stringify([e,i])).digest("hex")}.json`),C=async(e)=>{try{let i=await k(e,y.O_RDONLY|y.O_NOFOLLOW|y.O_NONBLOCK),t;try{let r=await i.stat();if((r.mode&63)!==0||!r.isFile()||r.size>16384)return;t=JSON.parse(await i.readFile("utf8"))}finally{await i.close()}if(t.version!==1||typeof t.userId!=="string"||!t.userId||typeof t.sessionId!=="string"||!t.sessionId||typeof t.csrf!=="string"||!t.csrf||/[;\r\n]/.test(t.sessionId+t.csrf)||typeof t.expiresAt!=="number"||!Number.isFinite(t.expiresAt))return;return x(t),t.expiresAt>Date.now()?t:void 0}catch{return}};class U{config;cachePath;current;flight;constructor(e){if(this.config=e,this.cachePath=J(e.url.value,e.login.value,e.cacheDirectory),u(e.password.value),e.token.value)u(e.token.value)}async raw(e,i={},t=H){let r=`${this.config.url.value}${e}`,o=new AbortController,n;try{let s=await b(r)??fetch,c=new Promise((d,f)=>{n=setTimeout(()=>{o.abort(),f(new a(`Session request timed out after ${t/1000}s: ${e}`))},t)});return await Promise.race([(async()=>{let d=await s(r,{...i,redirect:"manual",signal:o.signal});return{status:d.status,headers:d.headers,text:await d.text()}})(),c])}catch(s){if(s instanceof a)throw s;let c=R(s)?`Connection ${O(r)} denied by the operating system; check sandbox network permissions.`:s instanceof Error?s.message:String(s);throw new a(S(`Session request failed: ${c}`))}finally{if(n!==void 0)clearTimeout(n)}}parse(e,i){if(e.status<200||e.status>=300){let t=e.status===403?" Check page permissions, lock state and CSRF credentials.":"";throw new a(S(`Plane session API answered ${e.status} for ${i}.${t} ${e.text}`).slice(0,450),e.status)}if(!e.text||e.status===204)return;try{return JSON.parse(e.text)}catch{throw new a(`Expected JSON from session API: ${i}`)}}async authenticate(){let e=new Map,i=Date.now()+86400000,t=(w)=>{for(let h of w.getSetCookie()){let[m=""]=h.split(";"),p=m.indexOf("="),I=m.slice(0,p).trim(),l=m.slice(p+1).trim();if(p<1||!["session-id","csrftoken"].includes(I))continue;if(/[;\r\n]/.test(l))throw new a("Invalid session cookie received.");if(l)u(l);e.set(I,l);let A=/(?:^|;)\s*max-age=(-?\d+)/i.exec(h)?.[1];if(A!==void 0)i=Math.min(i,Date.now()+Number(A)*1000)}},r=await this.raw("/auth/get-csrf-token/");t(r.headers);let o=this.parse(r,"/auth/get-csrf-token/").csrf_token??e.get("csrftoken");if(!o)throw new a("Plane did not provide a CSRF token.");u(o);let n=[...e].map(([w,h])=>`${w}=${h}`).join("; ");if(n)u(n);let s=await this.raw("/auth/sign-in/",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Referer:`${this.config.url.value}/`,Cookie:n},body:new URLSearchParams({csrfmiddlewaretoken:o,email:this.config.login.value,password:this.config.password.value}).toString()});t(s.headers);let c=e.get("session-id"),d=e.get("csrftoken");if(![200,302,303].includes(s.status)||!c||!d)throw new a("Plane sign-in failed. Check PLANE_LOGIN and PLANE_PASSWORD; this authentication flow requires password sign-in.",s.status);let f=x({userId:"",sessionId:c,csrf:d,expiresAt:i}),v=this.parse(await this.raw("/api/users/me/",{headers:{Cookie:P(f)}}),"/api/users/me/");if(!v.id)throw new a("Session was not authenticated: no user identifier returned.");return x({...f,userId:v.id})}async session(e=!1){if(!e&&this.current&&this.current.expiresAt>Date.now())return this.current;if(this.flight)return this.flight;let i=this.current;this.flight=(async()=>{let t=await C(this.cachePath);if(t&&(!e||t.sessionId!==i?.sessionId))return t;try{await q(this.config.cacheDirectory,{recursive:!0,mode:448});let s=await _(this.config.cacheDirectory);if(!s.isDirectory()||s.isSymbolicLink())throw Error("Invalid session cache directory");await N(this.config.cacheDirectory,448)}catch{throw new a(`Cannot write session cache in ${this.config.cacheDirectory}. Set PLANE_SESSION_CACHE or --session-cache to a private writable directory.`)}let r=`${this.cachePath}.lock`,o=Date.now(),n;while(!n)try{n=await k(r,"wx",384)}catch(s){if(s.code!=="EEXIST")throw new a("Cannot lock the session cache.");let c=await F(r).catch(()=>{return});if(c&&Date.now()-c.mtimeMs>90000){await g(r).catch(()=>{});continue}if(Date.now()-o>1e4)throw new a("Another command is refreshing the session; retry shortly.");await j(100)}try{let s=await C(this.cachePath);if(s&&(!e||s.sessionId!==i?.sessionId))return s;let c=await this.authenticate(),d=`${this.cachePath}.${D()}.tmp`;try{let f=await k(d,"wx",384);try{await f.writeFile(JSON.stringify({version:1,...c}))}finally{await f.close()}await E(d,this.cachePath)}finally{await g(d).catch(()=>{})}return c}finally{await n.close(),await g(r).catch(()=>{})}})();try{return this.current=await this.flight,this.current}finally{this.flight=void 0}}async request(e,i={}){let t=e.startsWith("/")?e:`/api/workspaces/${encodeURIComponent(this.config.workspace.value)}/${e}`,r=new URLSearchParams;for(let[n,s]of Object.entries(i.query??{}))if(s!==void 0)r.set(n,String(s));let o=r.size?`${t}?${r}`:t;for(let n=0;n<2;n++){let s=await this.session(n===1),c=await this.raw(o,{method:i.method??"GET",headers:{Cookie:P(s),"X-CSRFToken":s.csrf,Referer:`${this.config.url.value}/`,Accept:"application/json",...i.body===void 0?{}:{"Content-Type":"application/json"}},body:i.body===void 0?void 0:JSON.stringify(i.body)},i.timeoutMs);if(c.status===401&&n===0)continue;return this.parse(c,t)}throw new a("Session authentication failed after one refresh.",401)}async listAll(e,i={}){let t=[],r=new Set,o;do{let n=await this.request(e,{...i,query:{...i.query,...o?{cursor:o}:{}}});if(Array.isArray(n))return[...t,...n];if(!n||typeof n!=="object"||!("results"in n)||!Array.isArray(n.results))throw new a(`Unexpected list response from session API: ${e}`);t.push(...n.results);let s=n;if(o=s.next_page_results?s.next_cursor:void 0,o){if(r.has(o))throw new a("Session API repeated a pagination cursor.");r.add(o)}}while(o);return t}}export{U as SessionClient,J as cacheFile,P as cookieHeader};