dockito 0.0.1-beta.13 → 0.0.1-beta.15

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.
Files changed (2) hide show
  1. package/dockito.js +64 -37
  2. package/package.json +1 -1
package/dockito.js CHANGED
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as an,spawnSync as bt}from"node:child_process";import{createHash as cn}from"node:crypto";import{existsSync as Tt,readFileSync as xe,statSync as ln,writeFileSync as $t}from"node:fs";import{hostname as dn}from"node:os";import{basename as Be,dirname as un,isAbsolute as pn,resolve as Ot}from"node:path";import{parseArgs as hn}from"node:util";var I=e=>e.level===null||e.level==="publish"||e.level==="everything",H=(e,t)=>e.projects.find(n=>n.slug===t)??e.projects.find(n=>n.name.toLowerCase()===t.toLowerCase())??null;function q(e){let t=new Map((e?.folders??[]).map(s=>[s.id,s])),n=s=>{let i=t.get(s);return[...i.parentId?n(i.parentId):[],i.name]},o=new Map((e?.folders??[]).map(s=>[n(s.id).join("/").toLowerCase(),s.id]));return{idOf:s=>s.length?o.get(s.join("/").toLowerCase()):null,pathOf:s=>s&&t.has(s)?n(s):[],add:(s,i)=>{o.set(s.join("/").toLowerCase(),i)}}}var Et=e=>e.fileName??`${e.title}.${e.kind==="sheet"?"csv":e.kind}`,oe=(e,t,n)=>e.filter(o=>(o.folderId??null)===t&&Et(o).toLowerCase()===n.toLowerCase());function it(e){let t=e.trim();if(/^https?:\/\//.test(t))try{t=new URL(t).pathname}catch{return null}let n=(t.split("/").filter(Boolean).pop()??"").replace(/\.[a-z]+$/,"");return/^[a-z0-9]{12}$/.test(n)&&/\d/.test(n)?n:null}var x=class extends Error{constructor(n,o,s={}){super(n);this.code=n;this.status=o;this.detail=s}code;status;detail};function at(e){let t=e;if(!t?.number)return"";let n=t.at?new Date(t.at).toISOString().slice(0,10):"";return`The oldest is entry ${t.number}${t.note?` ("${t.note}")`:""}${n?`, saved ${n}`:""}.`}function fe(e){let t=e.url.replace(/\/+$/,"");async function n(a,c,d={}){let l=new URL(`${t}/api${c}`);for(let[k,v]of Object.entries(d.query??{}))v!=null&&v!==""&&l.searchParams.set(k,String(v));let m={},f=d.token===void 0?e.token:d.token;f&&(m.authorization=`Bearer ${f}`),e.userAgent&&(m["user-agent"]=e.userAgent);let w;d.json!==void 0?(m["content-type"]="application/json",w=JSON.stringify(d.json)):d.body&&(m["content-type"]="application/octet-stream",w=d.body);let O;try{O=e.fetch?await e.fetch(new Request(l,{method:a,headers:m,body:w})):await fetch(l,{method:a,headers:m,body:w})}catch{throw new x("offline",0)}return O}async function o(a){let c=await a.json().catch(()=>({}));if(!a.ok){let{error:d,...l}=c;throw new x(typeof d=="string"?d:`http_${a.status}`,a.status,l)}return c}async function s(a){let c=await a.json().catch(()=>({}));if(typeof c.error=="string"||typeof c.text!="string"||a.status>=500){let{error:d,...l}=c;throw new x(typeof d=="string"?d:`http_${a.status}`,a.status,l)}return{ok:a.ok,status:String(c.status),version:c.version,seq:c.seq,text:c.text}}let i=encodeURIComponent;return{url:t,linkOf:a=>`${t}/${a}`,startLogin:async(a,c)=>o(await n("POST","/device/code",{json:{client:a,device:c}})),pollLogin:async a=>{let c=await n("POST","/device/token",{json:{device_code:a},token:null}),d=await c.json().catch(()=>({}));if(c.ok)return d;if(d.error==="authorization_pending"||d.error==="slow_down")return null;throw new x(d.error??`http_${c.status}`,c.status)},logout:async()=>o(await n("DELETE","/tokens/current")),whoami:async()=>o(await n("GET","/whoami")),tree:async()=>o(await n("GET","/tree")),projects:async a=>(await o(await n("GET",`/workspaces/${i(a)}/projects`))).projects,docs:async(a,c={})=>(await o(await n("GET",`/workspaces/${i(a)}/docs`,{query:c}))).docs,files:async(a,c)=>(await o(await n("GET",`/workspaces/${i(a)}/files`,{query:{project:c}}))).docs,createProject:async(a,c)=>o(await n("POST",`/workspaces/${i(a)}/projects`,{json:{name:c}})),createFolder:async(a,c,d)=>o(await n("POST",`/projects/${i(a)}/folders`,{json:{name:c,parentId:d}})),recent:async a=>(await o(await n("GET",`/workspaces/${i(a)}/docs/recent`))).docs,doc:async a=>o(await n("GET",`/docs/by-code/${i(a)}`)),content:async(a,c,d={})=>{let l=await n("GET",`/docs/${i(a)}/content`,{query:{v:c,outline:d.outline?1:null,lines:d.lines,text:d.text?1:null}});l.ok||await o(l);let m=l.headers.get("x-doc-lines");return{bytes:new Uint8Array(await l.arrayBuffer()),entry:Number(l.headers.get("x-doc-version")),lines:m?Number(m):null,shown:l.headers.get("x-doc-shown")}},create:async(a,c,d,l={})=>o(await n("POST",`/workspaces/${i(a)}/docs`,{query:{name:c,title:l.title,project:l.project,folder:l.folder,public:l.public?1:null,source:e.source},body:d})),update:async(a,c,d,l={})=>o(await n("POST",`/docs/${i(a)}/versions`,{query:{name:c,note:l.note,commit:l.commit,base:l.base,source:e.source},body:d})),restore:async(a,c,d)=>o(await n("POST",`/docs/${i(a)}/history/${c}/restore`,{query:{base:d}})),save:async(a,c,d)=>o(await n("PUT",`/docs/${i(a)}/history/${c}/saved`,{json:{note:d}})),makePrivate:async a=>o(await n("PATCH",`/docs/${i(a)}`,{json:{public:!1}})),rename:async(a,c)=>o(await n("PATCH",`/docs/${i(a)}`,{json:{title:c}})),readSheet:async(a,c)=>s(await n("POST",`/docs/${i(a)}/sheet`,{query:{source:e.source},json:c})),changeSheet:async(a,c)=>s(await n("POST",`/docs/${i(a)}/sheet/change`,{query:{source:e.source},json:c}))}}import{emitKeypressEvents as _t}from"node:readline";import{createInterface as Rt}from"node:readline/promises";var It=!process.env.NO_COLOR&&process.stdout.isTTY,z=e=>t=>It?`\x1B[${e}m${t}\x1B[0m`:t,g=z("2"),S=z("1"),b=z("32"),_=z("31"),R=z("33"),Ee=z("36"),h=(e="")=>process.stdout.write(e+`
2
+ import{spawn as ao,spawnSync as bt}from"node:child_process";import{createHash as co}from"node:crypto";import{existsSync as Tt,readFileSync as Oe,statSync as lo,writeFileSync as $t}from"node:fs";import{hostname as uo}from"node:os";import{basename as Je,dirname as po,isAbsolute as ho,resolve as Ot}from"node:path";import{parseArgs as go}from"node:util";var R=e=>e.level===null||e.level==="publish"||e.level==="everything",U=(e,t)=>e.projects.find(o=>o.slug===t)??e.projects.find(o=>o.name.toLowerCase()===t.toLowerCase())??null;function G(e){let t=new Map((e?.folders??[]).map(s=>[s.id,s])),o=s=>{let i=t.get(s);return[...i.parentId?o(i.parentId):[],i.name]},n=new Map((e?.folders??[]).map(s=>[o(s.id).join("/").toLowerCase(),s.id]));return{idOf:s=>s.length?n.get(s.join("/").toLowerCase()):null,pathOf:s=>s&&t.has(s)?o(s):[],add:(s,i)=>{n.set(s.join("/").toLowerCase(),i)}}}var Et=e=>e.fileName??`${e.title}.${e.kind==="sheet"?"csv":e.kind}`,ie=(e,t,o)=>e.filter(n=>(n.folderId??null)===t&&Et(n).toLowerCase()===o.toLowerCase());function _e(e){let t=e.trim();if(/^https?:\/\//.test(t))try{t=new URL(t).pathname}catch{return null}let o=(t.split("/").filter(Boolean).pop()??"").replace(/\.[a-z]+$/,"");return/^[a-z0-9]{12}$/.test(o)&&/\d/.test(o)?o:null}var O=class extends Error{constructor(o,n,s={}){super(o);this.code=o;this.status=n;this.detail=s}code;status;detail};function at(e){let t=e;if(!t?.number)return"";let o=t.at?new Date(t.at).toISOString().slice(0,10):"";return`The oldest is entry ${t.number}${t.note?` ("${t.note}")`:""}${o?`, saved ${o}`:""}.`}function ye(e){let t=e.url.replace(/\/+$/,"");async function o(r,c,d={}){let l=new URL(`${t}/api${c}`);for(let[b,j]of Object.entries(d.query??{}))j!=null&&j!==""&&l.searchParams.set(b,String(j));let f={},m=d.token===void 0?e.token:d.token;m&&(f.authorization=`Bearer ${m}`),e.userAgent&&(f["user-agent"]=e.userAgent);let w;d.json!==void 0?(f["content-type"]="application/json",w=JSON.stringify(d.json)):d.body&&(f["content-type"]="application/octet-stream",w=d.body);let v;try{v=e.fetch?await e.fetch(new Request(l,{method:r,headers:f,body:w})):await fetch(l,{method:r,headers:f,body:w})}catch{throw new O("offline",0)}return v}async function n(r){let c=await r.json().catch(()=>({}));if(!r.ok){let{error:d,...l}=c;throw new O(typeof d=="string"?d:`http_${r.status}`,r.status,l)}return c}async function s(r){let c=await r.json().catch(()=>({}));if(typeof c.error=="string"||typeof c.text!="string"||r.status>=500){let{error:d,...l}=c;throw new O(typeof d=="string"?d:`http_${r.status}`,r.status,l)}return{ok:r.ok,status:String(c.status),version:c.version,seq:c.seq,text:c.text}}let i=encodeURIComponent;return{url:t,linkOf:r=>`${t}/${r}`,startLogin:async(r,c)=>n(await o("POST","/device/code",{json:{client:r,device:c}})),pollLogin:async r=>{let c=await o("POST","/device/token",{json:{device_code:r},token:null}),d=await c.json().catch(()=>({}));if(c.ok)return d;if(d.error==="authorization_pending"||d.error==="slow_down")return null;throw new O(d.error??`http_${c.status}`,c.status)},logout:async()=>n(await o("DELETE","/tokens/current")),whoami:async()=>n(await o("GET","/whoami")),tree:async()=>n(await o("GET","/tree")),projects:async r=>(await n(await o("GET",`/workspaces/${i(r)}/projects`))).projects,docs:async(r,c={})=>(await n(await o("GET",`/workspaces/${i(r)}/docs`,{query:c}))).docs,files:async(r,c)=>(await n(await o("GET",`/workspaces/${i(r)}/files`,{query:{project:c}}))).docs,createProject:async(r,c)=>n(await o("POST",`/workspaces/${i(r)}/projects`,{json:{name:c}})),createFolder:async(r,c,d)=>n(await o("POST",`/projects/${i(r)}/folders`,{json:{name:c,parentId:d}})),moveFolder:async(r,c)=>n(await o("PATCH",`/folders/${i(r)}`,{json:{parentId:c}})),deleteFolder:async r=>n(await o("DELETE",`/folders/${i(r)}`)),recent:async r=>(await n(await o("GET",`/workspaces/${i(r)}/docs/recent`))).docs,doc:async r=>n(await o("GET",`/docs/by-code/${i(r)}`)),content:async(r,c,d={})=>{let l=await o("GET",`/docs/${i(r)}/content`,{query:{v:c,outline:d.outline?1:null,lines:d.lines,text:d.text?1:null}});l.ok||await n(l);let f=l.headers.get("x-doc-lines");return{bytes:new Uint8Array(await l.arrayBuffer()),entry:Number(l.headers.get("x-doc-version")),lines:f?Number(f):null,shown:l.headers.get("x-doc-shown")}},create:async(r,c,d,l={})=>n(await o("POST",`/workspaces/${i(r)}/docs`,{query:{name:c,title:l.title,project:l.project,folder:l.folder,public:l.public?1:null,source:e.source},body:d})),update:async(r,c,d,l={})=>n(await o("POST",`/docs/${i(r)}/versions`,{query:{name:c,note:l.note,commit:l.commit,base:l.base,source:e.source},body:d})),restore:async(r,c,d)=>n(await o("POST",`/docs/${i(r)}/history/${c}/restore`,{query:{base:d}})),save:async(r,c,d)=>n(await o("PUT",`/docs/${i(r)}/history/${c}/saved`,{json:{note:d}})),restoreSheet:async(r,c)=>n(await o("POST",`/docs/${i(r)}/history/${c}/restore`,{query:{source:e.source}})),saveSheetNow:async(r,c)=>n(await o("PUT",`/docs/${i(r)}/history/now/saved`,{json:{note:c}})),makePrivate:async r=>n(await o("PATCH",`/docs/${i(r)}`,{json:{public:!1}})),rename:async(r,c)=>n(await o("PATCH",`/docs/${i(r)}`,{json:{title:c}})),moveDoc:async(r,c)=>n(await o("PATCH",`/docs/${i(r)}`,{json:c})),readSheet:async(r,c)=>s(await o("POST",`/docs/${i(r)}/sheet`,{query:{source:e.source},json:c})),changeSheet:async(r,c)=>s(await o("POST",`/docs/${i(r)}/sheet/change`,{query:{source:e.source},json:c}))}}import{emitKeypressEvents as _t}from"node:readline";import{createInterface as Lt}from"node:readline/promises";var It=!process.env.NO_COLOR&&process.stdout.isTTY,Y=e=>t=>It?`\x1B[${e}m${t}\x1B[0m`:t,g=Y("2"),A=Y("1"),k=Y("32"),_=Y("31"),E=Y("33"),Le=Y("36"),u=(e="")=>process.stdout.write(e+`
3
3
  `),$=(e="")=>process.stderr.write(e+`
4
- `),J=class extends Error{},p=e=>{throw new J(e)},G={view:"View",publish:"Publish",everything:"Everything"};function me(e){let t=Math.round((Date.now()-new Date(e).getTime())/1e3);if(t<60)return"just now";let n=Math.round(t/60);if(n<60)return`${n} min ago`;let o=Math.round(n/60);if(o<24)return`${o} h ago`;let s=Math.round(o/24);return s<30?s===1?"yesterday":`${s} days ago`:new Date(e).toLocaleDateString(void 0,{day:"numeric",month:"short",year:"numeric"})}function se(e){let t=o=>o.replace(/\x1b\[[0-9;]*m/g,""),n=e[0]?.map((o,s)=>Math.max(...e.map(i=>t(i[s]??"").length)))??[];for(let o of e)h(o.map((s,i)=>i===o.length-1?s:s+" ".repeat(n[i]-t(s).length)).join(" ").trimEnd())}function re(e,t){switch(e.code){case"offline":return`Couldn't reach Dockito at ${t}.`;case"invalid_token":return"Your login has ended or was signed out. Run `dockito login`.";case"token_level":return`This needs ${G[e.detail.needs]??e.detail.needs} access; this login has ${G[e.detail.has]??e.detail.has}. Run \`dockito login --update\`.`;case"not_for_tokens":return"That can only be done in the browser.";case"not_found":return"Not found, or this login has no access to it. `dockito whoami` lists what it can reach.";case"forbidden":return"You can open this but not change it. Ask its owner or a workspace admin for Edit.";case"public_off":return"This workspace doesn't allow public links. Its owner can turn them on in Workspace settings.";case"deleted":return"That doc was deleted.";case"conflict":return"Someone published a newer version since yours. Get it with `dockito get`, or publish anyway with --force.";case"rate_limited":return"Too many requests. Try again in a minute.";case"too_large":return`The file is too large (limit ${Math.round(Number(e.detail.limit??0)/1048576)} MB).`;case"out_of_space":return"The workspace is out of storage.";case"too_many_saved":return`This doc already keeps ${e.detail.limit} saved points, the most it can, so nothing changed. ${at(e.detail.oldest)} In the app, use Don't keep on one in History, then save again.`;case"unsupported":return"Dockito takes .md and .html files.";case"other_kind":return"That doc is a different kind of file (Markdown and HTML can't replace each other).";case"empty":return"The file is empty.";case"not_text":return"The file isn't text.";case"too_deep":return"Folders go at most 2 deep inside a project.";case"invalid_name":return"Names need 1 to 80 characters.";case"folder_gone":return"That project or folder no longer exists.";default:return typeof e.detail.message=="string"?e.detail.message:`Dockito said: ${e.code}`}}var Ie=()=>!!process.stdin.isTTY&&!!process.stdout.isTTY&&!process.env.CI,Y=e=>process.stdout.write(e);function ct(e,t,n=0){return new Promise((o,s)=>{let i=n,a=l=>{l||Y(`\x1B[${t.length}A`);for(let[m,f]of t.entries())Y(`\x1B[2K${m===i?Ee("\u276F "+f.label):" "+f.label}${f.hint?g(" "+f.hint):""}
5
- `)};Y(`${b("?")} ${S(e)} ${g("(\u2191\u2193, Enter)")}
6
- \x1B[?25l`),a(!0),_t(process.stdin),process.stdin.setRawMode(!0),process.stdin.resume();let c=()=>{process.stdin.off("keypress",d),process.stdin.setRawMode(!1),process.stdin.pause(),Y("\x1B[?25h")},d=(l,m)=>{if(m.ctrl&&m.name==="c")return c(),Y(`
7
- `),s(new ie);if(m.name==="up"||m.name==="k")i=(i-1+t.length)%t.length;else if(m.name==="down"||m.name==="j")i=(i+1)%t.length;else return m.name==="return"?(c(),Y(`\x1B[${t.length+1}A\x1B[0J${b("?")} ${S(e)} ${Ee(t[i].label)}
8
- `),o(i)):void 0;a(!1)};process.stdin.on("keypress",d)})}async function we(e,t=""){let n=Rt({input:process.stdin,output:process.stdout});try{return(await n.question(`${b("?")} ${S(e)}${t?g(` (${t})`):""} `)).trim()||t}finally{n.close()}}async function _e(e,t=!0){let n=(await we(`${e} ${g(t?"[Y/n]":"[y/N]")}`)).toLowerCase();return n?n.startsWith("y"):t}var ie=class extends Error{};import{homedir as Lt}from"node:os";import{join as Re}from"node:path";var Ft="https://dockito-app.dockito.workers.dev";function lt(){return process.env.DOCKITO_CONFIG_DIR?process.env.DOCKITO_CONFIG_DIR:process.platform==="win32"&&process.env.APPDATA?Re(process.env.APPDATA,"dockito"):Re(process.env.XDG_CONFIG_HOME||Re(Lt(),".config"),"dockito")}var dt=()=>(process.env.DOCKITO_URL||Ft).replace(/\/+$/,""),ut=e=>new URL(e).host;import{spawnSync as Mt}from"node:child_process";import{chmodSync as Kt,existsSync as Le,mkdirSync as Ht,readFileSync as Ut,rmSync as pt,writeFileSync as Wt}from"node:fs";import{dirname as Bt,join as qt}from"node:path";var V="dockito";function X(e,t,n){try{let o=Mt(e,t,{input:n,encoding:"utf8",stdio:["pipe","pipe","pipe"]});return o.error?null:o}catch{return null}}var ae=e=>qt(lt(),"tokens",`${e.replace(/[^a-z0-9.-]/gi,"_")}`);function Jt(e,t){if(process.platform==="darwin"){let n=X("security",["-i"],`add-generic-password -U -s ${V} -a ${e} -l Dockito -w ${t}
9
- `);return!!n&&n.status===0&&!/error/i.test(n.stderr??"")}if(process.platform==="linux"){let n=X("secret-tool",["store","--label=Dockito","service",V,"account",e],t);return!!n&&n.status===0}return!1}function Gt(e){let t=process.platform==="darwin"?X("security",["find-generic-password","-s",V,"-a",e,"-w"]):process.platform==="linux"?X("secret-tool",["lookup","service",V,"account",e]):null;return(t?.status===0?t.stdout.trim():"")||null}function zt(e){process.platform==="darwin"?X("security",["delete-generic-password","-s",V,"-a",e]):process.platform==="linux"&&X("secret-tool",["clear","service",V,"account",e])}function ht(e,t){if(Jt(e,t))return Le(ae(e))&&pt(ae(e)),"keychain";let n=ae(e);return Ht(Bt(n),{recursive:!0,mode:448}),Wt(n,t,{mode:384}),Kt(n,384),"file"}function ye(e){if(process.env.DOCKITO_TOKEN)return process.env.DOCKITO_TOKEN.trim();let t=Gt(e);if(t)return t;let n=ae(e);return Le(n)&&Ut(n,"utf8").trim()||null}function gt(e){zt(e);let t=ae(e);Le(t)&&pt(t)}var N=class extends Error{constructor(n,o,s={}){super(o);this.code=n;this.data=s}code;data};function ke(e,t){let n=e.workspaces;if(t)return n.find(s=>s.slug===t)??p(`This login can't reach workspace "${t}". \`dockito tree\` lists the ones it can.`);let o=n.length===1?n[0]:n.find(s=>s.kind==="personal"&&s.role==="owner");if(o)return o;throw new N("needs_workspace",`Pick a workspace with -w: ${n.map(s=>s.slug).join(", ")}`,{workspaces:n.map(s=>s.slug)})}function Fe(e,t){if(!t)return{project:null,folder:null,label:`${e.slug} Docs`};let[n,...o]=t.split("/").filter(Boolean),s=H(e,n);if(!s)return p(`No project "${n}" in ${e.slug}. \`dockito tree\` lists them; \`dockito mkdir ${t}\` makes it.`);let i=q(s).idOf(o);return i===void 0?p(`No folder "${o.join("/")}" in ${s.slug}. \`dockito mkdir ${s.slug}/${o.join("/")}\` makes it.`):{project:s.id,folder:i,label:[e.slug,s.slug,...o].join("/")}}import{createHash as Yt}from"node:crypto";import{spawnSync as Vt}from"node:child_process";import{existsSync as le,readFileSync as Xt,readdirSync as Qt,statSync as wt}from"node:fs";import{basename as ft,join as L,relative as Zt,resolve as Me,sep as en}from"node:path";var tn=4,ce=e=>e<1024*1024?`${e?Math.max(1,Math.round(e/1024)):0} KB`:`${(e/1048576).toFixed(1)} MB`,nn=e=>Yt("sha256").update(e).digest("hex"),yt=e=>e.split(en).join("/"),Q=(e,t)=>`${e} ${t}${e===1?"":"s"}`,on=(e,t)=>{let n=/\.[^./]+$/.exec(t.toLowerCase())?.[0]??"";return e.find(o=>o.extensions.includes(n))?.id??null};function $e(e,t){let n=Vt("git",e,{cwd:t,encoding:"utf8",maxBuffer:67108864});return n.error||n.status!==0?null:n.stdout}function sn(e,t){let n=$e(["ls-files","-z","--cached","--others","--exclude-standard","--","."],e);return(n!==null?n.split("\0").filter(Boolean):(function s(i){return Qt(L(e,i),{withFileTypes:!0}).flatMap(a=>a.name.startsWith(".")||a.name==="node_modules"?[]:a.isDirectory()?s(L(i,a.name)):[yt(L(i,a.name))])})("")).filter(s=>t(s)&&le(L(e,s))&&wt(L(e,s)).isFile()).sort()}function rn(e,t,n){let o=[...e.matchAll(/\b(?:src|href)=["']([^"'#?]+)/gi)].map(s=>s[1]);return[...new Set(o.filter(s=>!/^([a-z]+:|\/\/|\/)/i.test(s)&&!n(s)&&le(L(t,s))))]}function mt(e,t){let n=(i,a,c="")=>`dockito sync ${t} -w ${i} --to ${a}${c}`,o=e.workspaces.filter(I).flatMap(i=>[...i.projects.map(a=>({workspace:i.slug,project:a.slug,name:a.name,docs:a.docs,command:n(i.slug,a.slug)})),{workspace:i.slug,project:null,name:"a new project",docs:0,command:n(i.slug,"<name>"," --create")}]),s=Math.max(...o.map(i=>i.command.length));throw new N("needs_place",[`Which project should ${t==="."?"this folder":t+"/"} go to? Ask the user, then run one of:`,...o.map(i=>` ${i.command.padEnd(s)} ${g(i.project?`${i.name} \xB7 ${Q(i.docs,"doc")}`:i.name)}`),g("Subfolders become folders. `dockito tree` shows every place.")].join(`
10
- `),{choices:o})}async function kt(e,t,n){let o=Ie()&&!n.json,s=await e.tree(),i=r=>!!on(s.kinds,r);s.workspaces.some(I)||p("This login can't publish anywhere. Run `dockito login --update`.");let a=t;a||(o||mt(s,le("docs")?"docs":"."),a=await we("Folder to sync",le("docs")?"docs":"."));let c=Me(a);(!le(c)||!wt(c).isDirectory())&&p(`${a} isn't a folder.`);let d=yt(Zt(process.cwd(),c))||".",l;n.workspace&&(l=s.workspaces.find(r=>r.slug===n.workspace)??p(`This login can't reach workspace "${n.workspace}".`));let m=n.to;if(!m){o||mt(s,d);let r=(l?[l]:s.workspaces.filter(I)).flatMap(y=>[...y.projects.map(j=>({w:y,p:j.slug,label:j.name,hint:`${y.slug} \xB7 ${Q(j.docs,"doc")}`})),{w:y,p:null,label:"New project",hint:y.slug}]),u=r[await ct("Project",r)];l=u.w,m=u.p??await we("Project name",ft(c===process.cwd()?c:Me(c,".."))),n.create||=!u.p}if(!l){let r=s.workspaces.filter(y=>I(y)&&H(y,m.split("/")[0])),u=s.workspaces.find(y=>y.kind==="personal"&&y.role==="owner");if(l=r.length===1?r[0]:s.workspaces.length===1?s.workspaces[0]:!r.length&&u?u:void 0,!l)throw new N("needs_workspace",`Which workspace? Run again with -w: ${(r.length?r:s.workspaces).map(y=>y.slug).join(", ")}`)}let f=l;I(f)||p(`This login has View access in ${f.slug}; syncing needs Publish. Run \`dockito login --update\`.`);let[w,...O]=m.split("/").filter(Boolean);w||p("--to takes a project, or project/folder."),O.length>2&&p(`Folders go at most ${2} deep inside a project.`);let k=H(f,w);if(!k&&!n.create)throw new N("no_project",`No project "${w}" in ${f.slug}. Projects: ${f.projects.map(r=>r.slug).join(", ")||"none"}.
11
- To make it, run again with --create.`,{workspace:f.slug,projects:f.projects.map(r=>r.slug)});let v=sn(c,i);v.length||p(`No ${s.kinds.flatMap(r=>r.extensions).join(", ")} files in ${d}.`);let U=2-O.length,D=v.filter(r=>r.split("/").length-1>U);if(D.length&&!n.fold){let r=y=>y.split("/").slice(0,U).join("/")||"the project's top",u=`${Q(D.length,"file")} ${D.length===1?"is":"are"} more than ${2} folders deep inside the project. With --fold they go in ${[...new Set(D.map(r))].join(", ")}; where two would share a name, the folders left out go in front (plans/archive/grok/README.md \u2192 grok-README.md).`;if(!o)throw new N("too_deep",`${u}
12
- Ask the user, then run again with --fold.`,{files:D});if($(u),!await _e("Fold them like that?"))return $(g("Nothing changed."));n.fold=!0}let ze=r=>{let u=r.split("/");return{folder:[...O,...u.slice(0,-1).slice(0,U)],dropped:u.slice(0,-1).slice(U),file:u[u.length-1]}},Oe=new Map;for(let r of v){let u=ze(r),y=[...u.folder,u.file].join("/").toLowerCase();Oe.set(y,(Oe.get(y)??0)+1)}let Ye=k?await e.files(f.id,k.id):[],te=q(k),K=v.map(r=>{let{folder:u,dropped:y,file:j}=ze(r),F=y.length&&Oe.get([...u,j].join("/").toLowerCase())>1?[...y,j].join("-"):j,P=new Uint8Array(Xt(L(c,r))),B={rel:r,name:F,folder:u,bytes:P,hash:nn(P),action:"new",doc:null,others:[],assets:[]};/\.html?$/i.test(r)&&(B.assets=rn(new TextDecoder().decode(P),L(c,r,".."),i));let rt=te.idOf(u);if(rt===void 0)return B;let ne=oe(Ye,rt,F);return ne.length>1?{...B,action:"ambiguous",others:ne}:ne.length?{...B,doc:ne[0],action:ne[0].sha256===B.hash?"same":"update"}:B}),Ve=K.filter(r=>r.bytes.length>f.maxFile);Ve.length&&p(`Over the ${ce(f.maxFile)} limit per file in ${f.slug}:
13
- ${Ve.map(r=>` ${r.rel} ${ce(r.bytes.length)}`).join(`
14
- `)}`);let Se=new Map;for(let r of K){let u=[...r.folder,r.name].join("/").toLowerCase();Se.set(u,[...Se.get(u)??[],r.rel])}let Xe=[...Se.values()].filter(r=>r.length>1);Xe.length&&p(`These files would be the same doc; rename one of each:
15
- ${Xe.map(r=>" "+r.join(" and ")).join(`
16
- `)}`);let E=[],Qe=new Set;for(let r of K)for(let u=1;u<=r.folder.length;u++){let y=r.folder.slice(0,u),j=y.join("/").toLowerCase();te.idOf(y)===void 0&&!Qe.has(j)&&(Qe.add(j),E.push(y))}E.sort((r,u)=>r.length-u.length);let A=K.filter(r=>r.action==="new"||r.action==="update"),Ze=K.filter(r=>r.action==="ambiguous"),Ae=K.filter(r=>r.action==="same").length,Dt=new Set(K.flatMap(r=>r.doc?[r.doc.id]:r.others.map(u=>u.id))),De=Ye.filter(r=>!Dt.has(r.id)).length,et=`${f.slug}/${k?.slug??w}${O.length?"/"+O.join("/"):""}`,ue={workspace:f.slug,to:et,create:{project:k?null:w,folders:E.map(r=>r.join("/"))},docs:A.map(r=>({file:r.rel,action:r.action,folder:r.folder.join("/")||null,name:r.name,link:r.doc?e.linkOf(r.doc.slug):null})),unchanged:Ae,ambiguous:Ze.map(r=>({file:r.rel,docs:r.others.map(u=>e.linkOf(u.slug))})),notHere:De};if(!n.json){h(`${S(d==="."?"./":d+"/")} \u2192 ${S(et)}`),k||h(` ${b("+ project")} ${w}`);for(let u of E)h(` ${b("+ folder ")} ${u.join("/")}`);if(A.length>15)for(let[u,y]of[["new",b("+ new ")],["update",R("~ update ")]]){let j=new Map;for(let F of A.filter(P=>P.action===u))j.set(F.folder.join("/"),[...j.get(F.folder.join("/"))??[],F.name]);for(let[F,P]of j)h(` ${y} ${P.length} in ${F||"the project's top"}: ${g(P.slice(0,4).join(", ")+(P.length>4?`, ${P.length-4} more`:""))}`)}else for(let u of A){let y=u.name!==ft(u.rel)?g(` \u2192 ${[...u.folder,u.name].join("/")}`):"";h(` ${u.action==="new"?b("+ new "):R("~ update ")} ${u.rel}${y}`)}Ae&&h(g(` ${Ae} unchanged`));for(let u of Ze)h(` ${_("? skip ")} ${u.rel} ${g(`${u.others.length} docs there have this name: ${u.others.map(y=>e.linkOf(y.slug)).join(", ")}`)}`);De&&h(g(` ${Q(De,"doc")} in the project with no file here, left as they are`)),A.length&&h(g(` ${Q(A.length,"file")} to send, ${ce(A.reduce((u,y)=>u+y.bytes.length,0))}`));let r=A.filter(u=>u.assets.length);r.length&&h(R(` ! ${r.length===1?r[0].rel:`${r.length} HTML files`} load ${[...new Set(r.flatMap(u=>u.assets))].join(", ")} from next to ${r.length===1?"it":"them"}; only HTML files are published`))}if(!A.length&&k&&!E.length){n.json?h(JSON.stringify({...ue,applied:!0},null,2)):h(`${b("\u2713")} Up to date`);return}if(n["dry-run"]){n.json&&h(JSON.stringify({...ue,applied:!1},null,2));return}if((!k||E.length)&&!n.yes){let r=["dockito sync",d,`-w ${f.slug}`,`--to ${m}`,n.create&&!k?"--create":"",n.fold?"--fold":"","--yes"].filter(Boolean).join(" ");if(!o){if(n.json)return h(JSON.stringify({...ue,applied:!1,next:r},null,2));throw new N("confirm",`
17
- Nothing changed yet. Show the user this plan, then run: ${r}`,{next:r})}if(!await _e("Apply?"))return $(g("Nothing changed."))}if(!k){let r=await e.createProject(f.id,w);k={id:r.id,slug:r.slug,name:w,docs:0,folders:[]},n.json||h(`${b("\u2713")} Project ${w}`)}let Ne=k;for(let r of E){let u=await e.createFolder(Ne.id,r[r.length-1],r.length>1?te.idOf(r.slice(0,-1)):null);te.add(r,u.id)}E.length&&!n.json&&h(`${b("\u2713")} ${Q(E.length,"folder")}`);let pe=$e(["rev-parse","--show-toplevel"],c)?.trim()??null,tt=pe?$e(["rev-parse","HEAD"],pe)?.trim()??null:null,Nt=new Set(pe?($e(["status","--porcelain","-z","--","."],c)??"").split("\0").filter(Boolean).map(r=>Me(pe,r.slice(3))):[]),W=[],he=null,Pe=[...A],ge=!n.json&&Ie(),Pt=A.reduce((r,u)=>r+u.bytes.length,0),nt=0,ot=()=>ge&&process.stdout.write(`\r\x1B[2K${g(`Sending ${W.length} of ${A.length} \xB7 ${ce(nt)} of ${ce(Pt)}`)}`);async function Ct(r){let u=tt&&!Nt.has(L(c,r.rel))?tt:null;for(;;)try{let y=r.doc?await e.update(r.doc.id,r.name,r.bytes,{commit:u}):await e.create(f.id,r.name,r.bytes,{project:Ne.id,folder:te.idOf(r.folder)??null});return{file:r.rel,action:r.action,link:e.linkOf(y.slug)}}catch(y){if(y instanceof x&&y.code==="rate_limited"){he||(n.json||$(g("\rUpload limit reached; carrying on in a minute\u2026")),he=new Promise(j=>setTimeout(j,3e4)).then(()=>{he=null})),await he;continue}return{file:r.rel,action:r.action,error:y instanceof x?re(y,e.url):String(y)}}}ot(),await Promise.all(Array.from({length:Math.min(tn,Pe.length)},async()=>{for(let r=Pe.shift();r;r=Pe.shift()){let u=await Ct(r);W.push(u),nt+=r.bytes.length,ge&&process.stdout.write("\r\x1B[2K"),u.error&&!n.json?h(`${_("\u2717")} ${u.file} ${_(u.error)}`):!n.json&&!ge&&h(`${b("\u2713")} ${u.file} ${g(u.link)}`),ot()}})),ge&&process.stdout.write("\r\x1B[2K");let Ce=W.filter(r=>r.error),st=`${e.url}/${f.slug}/project/${Ne.slug}`;n.json?h(JSON.stringify({...ue,applied:!0,link:st,results:W},null,2)):h(`${Ce.length?R(`${W.length-Ce.length} of ${W.length} sent`):b("\u2713 Synced")} ${g(st)}`),Ce.length&&(process.exitCode=1)}var Ke=`Dockito hosts Markdown and HTML docs, and live sheets, at a short link,
4
+ `),z=class extends Error{},h=e=>{throw new z(e)},V={view:"View",publish:"Publish",everything:"Everything"};function ae(e){let t=Math.round((Date.now()-new Date(e).getTime())/1e3);if(t<60)return"just now";let o=Math.round(t/60);if(o<60)return`${o} min ago`;let n=Math.round(o/60);if(n<24)return`${n} h ago`;let s=Math.round(n/24);return s<30?s===1?"yesterday":`${s} days ago`:new Date(e).toLocaleDateString(void 0,{day:"numeric",month:"short",year:"numeric"})}function X(e){let t=n=>n.replace(/\x1b\[[0-9;]*m/g,""),o=e[0]?.map((n,s)=>Math.max(...e.map(i=>t(i[s]??"").length)))??[];for(let n of e)u(n.map((s,i)=>i===n.length-1?s:s+" ".repeat(o[i]-t(s).length)).join(" ").trimEnd())}function Rt(e){let t=Array.isArray(e)?e.map(String):[];return t.length<=2?t.join(" and ")||"more people":t.length<=3?`${t.slice(0,-1).join(", ")} and ${t[t.length-1]}`:`${t.slice(0,2).join(", ")} and ${t.length-2} others`}function ce(e,t){switch(e.code){case"offline":return`Couldn't reach Dockito at ${t}.`;case"invalid_token":return"Your login has ended or was signed out. Run `dockito login`.";case"token_level":return`This needs ${V[e.detail.needs]??e.detail.needs} access; this login has ${V[e.detail.has]??e.detail.has}. Run \`dockito login --update\`.`;case"not_for_tokens":return"That can only be done in the browser.";case"not_found":return"Not found, or this login has no access to it. `dockito whoami` lists what it can reach.";case"forbidden":return"You can open this but not change it. Ask its owner or a workspace admin for Edit.";case"public_off":return"This workspace doesn't allow public links. Its owner can turn them on in Workspace settings.";case"deleted":return"That doc was deleted.";case"conflict":return"Someone published a newer version since yours. Get it with `dockito get`, or publish anyway with --force.";case"rate_limited":return"Too many requests. Try again in a minute.";case"too_large":return`The file is too large (limit ${Math.round(Number(e.detail.limit??0)/1048576)} MB).`;case"out_of_space":return"The workspace is out of storage.";case"too_many_saved":return`This doc already keeps ${e.detail.limit} saved points, the most it can, so nothing changed. ${at(e.detail.oldest)} In the app, use Don't keep on one in History, then save again.`;case"unsupported":return"Dockito takes .md and .html files.";case"other_kind":return"That doc is a different kind of file (Markdown and HTML can't replace each other).";case"empty":return"The file is empty.";case"not_text":return"The file isn't text.";case"too_deep":return"Folders go at most 2 deep inside a project.";case"invalid_name":return"Names need 1 to 80 characters.";case"folder_gone":return"That project or folder no longer exists.";case"widens_access":return`Moving it there would let ${Rt(e.detail.people)} open it or do more with it, so nothing moved. Move it in the app, which asks you first.`;case"not_empty":return"That folder has docs or folders in it, so it stays. Nothing was removed.";case"invalid_move":return"A folder can't go inside itself or one of its own folders.";default:return typeof e.detail.message=="string"?e.detail.message:`Dockito said: ${e.code}`}}var Fe=()=>!!process.stdin.isTTY&&!!process.stdout.isTTY&&!process.env.CI,Q=e=>process.stdout.write(e);function ct(e,t,o=0){return new Promise((n,s)=>{let i=o,r=l=>{l||Q(`\x1B[${t.length}A`);for(let[f,m]of t.entries())Q(`\x1B[2K${f===i?Le("\u276F "+m.label):" "+m.label}${m.hint?g(" "+m.hint):""}
5
+ `)};Q(`${k("?")} ${A(e)} ${g("(\u2191\u2193, Enter)")}
6
+ \x1B[?25l`),r(!0),_t(process.stdin),process.stdin.setRawMode(!0),process.stdin.resume();let c=()=>{process.stdin.off("keypress",d),process.stdin.setRawMode(!1),process.stdin.pause(),Q("\x1B[?25h")},d=(l,f)=>{if(f.ctrl&&f.name==="c")return c(),Q(`
7
+ `),s(new le);if(f.name==="up"||f.name==="k")i=(i-1+t.length)%t.length;else if(f.name==="down"||f.name==="j")i=(i+1)%t.length;else return f.name==="return"?(c(),Q(`\x1B[${t.length+1}A\x1B[0J${k("?")} ${A(e)} ${Le(t[i].label)}
8
+ `),n(i)):void 0;r(!1)};process.stdin.on("keypress",d)})}async function ke(e,t=""){let o=Lt({input:process.stdin,output:process.stdout});try{return(await o.question(`${k("?")} ${A(e)}${t?g(` (${t})`):""} `)).trim()||t}finally{o.close()}}async function Me(e,t=!0){let o=(await ke(`${e} ${g(t?"[Y/n]":"[y/N]")}`)).toLowerCase();return o?o.startsWith("y"):t}var le=class extends Error{};import{homedir as Ft}from"node:os";import{join as Ke}from"node:path";var Mt="https://dockito-app.dockito.workers.dev";function lt(){return process.env.DOCKITO_CONFIG_DIR?process.env.DOCKITO_CONFIG_DIR:process.platform==="win32"&&process.env.APPDATA?Ke(process.env.APPDATA,"dockito"):Ke(process.env.XDG_CONFIG_HOME||Ke(Ft(),".config"),"dockito")}var dt=()=>(process.env.DOCKITO_URL||Mt).replace(/\/+$/,""),ut=e=>new URL(e).host;import{spawnSync as Kt}from"node:child_process";import{chmodSync as Ut,existsSync as Ue,mkdirSync as Wt,readFileSync as Ht,rmSync as pt,writeFileSync as Bt}from"node:fs";import{dirname as qt,join as Jt}from"node:path";var Z="dockito";function ee(e,t,o){try{let n=Kt(e,t,{input:o,encoding:"utf8",stdio:["pipe","pipe","pipe"]});return n.error?null:n}catch{return null}}var de=e=>Jt(lt(),"tokens",`${e.replace(/[^a-z0-9.-]/gi,"_")}`);function Gt(e,t){if(process.platform==="darwin"){let o=ee("security",["-i"],`add-generic-password -U -s ${Z} -a ${e} -l Dockito -w ${t}
9
+ `);return!!o&&o.status===0&&!/error/i.test(o.stderr??"")}if(process.platform==="linux"){let o=ee("secret-tool",["store","--label=Dockito","service",Z,"account",e],t);return!!o&&o.status===0}return!1}function zt(e){let t=process.platform==="darwin"?ee("security",["find-generic-password","-s",Z,"-a",e,"-w"]):process.platform==="linux"?ee("secret-tool",["lookup","service",Z,"account",e]):null;return(t?.status===0?t.stdout.trim():"")||null}function Vt(e){process.platform==="darwin"?ee("security",["delete-generic-password","-s",Z,"-a",e]):process.platform==="linux"&&ee("secret-tool",["clear","service",Z,"account",e])}function ht(e,t){if(Gt(e,t))return Ue(de(e))&&pt(de(e)),"keychain";let o=de(e);return Wt(qt(o),{recursive:!0,mode:448}),Bt(o,t,{mode:384}),Ut(o,384),"file"}function be(e){if(process.env.DOCKITO_TOKEN)return process.env.DOCKITO_TOKEN.trim();let t=zt(e);if(t)return t;let o=de(e);return Ue(o)&&Ht(o,"utf8").trim()||null}function gt(e){Vt(e);let t=de(e);Ue(t)&&pt(t)}var N=class extends Error{constructor(o,n,s={}){super(n);this.code=o;this.data=s}code;data};function W(e,t){let o=e.workspaces;if(t)return o.find(s=>s.slug===t)??h(`This login can't reach workspace "${t}". \`dockito tree\` lists the ones it can.`);let n=o.length===1?o[0]:o.find(s=>s.kind==="personal"&&s.role==="owner");if(n)return n;throw new N("needs_workspace",`Pick a workspace with -w: ${o.map(s=>s.slug).join(", ")}`,{workspaces:o.map(s=>s.slug)})}function H(e,t){if(!t)return{project:null,folder:null,label:`${e.slug} Docs`};let[o,...n]=t.split("/").filter(Boolean),s=U(e,o);if(!s)return h(`No project "${o}" in ${e.slug}. \`dockito tree\` lists them; \`dockito mkdir ${t}\` makes it.`);let i=G(s).idOf(n);return i===void 0?h(`No folder "${n.join("/")}" in ${s.slug}. \`dockito mkdir ${s.slug}/${n.join("/")}\` makes it.`):{project:s.id,folder:i,label:[e.slug,s.slug,...n].join("/")}}import{createHash as Yt}from"node:crypto";import{spawnSync as Xt}from"node:child_process";import{existsSync as pe,readFileSync as Qt,readdirSync as Zt,statSync as wt}from"node:fs";import{basename as ft,join as L,relative as eo,resolve as We,sep as to}from"node:path";var oo=4,ue=e=>e<1024*1024?`${e?Math.max(1,Math.round(e/1024)):0} KB`:`${(e/1048576).toFixed(1)} MB`,no=e=>Yt("sha256").update(e).digest("hex"),yt=e=>e.split(to).join("/"),te=(e,t)=>`${e} ${t}${e===1?"":"s"}`,so=(e,t)=>{let o=/\.[^./]+$/.exec(t.toLowerCase())?.[0]??"";return e.find(n=>n.extensions.includes(o))?.id??null};function ve(e,t){let o=Xt("git",e,{cwd:t,encoding:"utf8",maxBuffer:67108864});return o.error||o.status!==0?null:o.stdout}function ro(e,t){let o=ve(["ls-files","-z","--cached","--others","--exclude-standard","--","."],e);return(o!==null?o.split("\0").filter(Boolean):(function s(i){return Zt(L(e,i),{withFileTypes:!0}).flatMap(r=>r.name.startsWith(".")||r.name==="node_modules"?[]:r.isDirectory()?s(L(i,r.name)):[yt(L(i,r.name))])})("")).filter(s=>t(s)&&pe(L(e,s))&&wt(L(e,s)).isFile()).sort()}function io(e,t,o){let n=[...e.matchAll(/\b(?:src|href)=["']([^"'#?]+)/gi)].map(s=>s[1]);return[...new Set(n.filter(s=>!/^([a-z]+:|\/\/|\/)/i.test(s)&&!o(s)&&pe(L(t,s))))]}function mt(e,t){let o=(i,r,c="")=>`dockito sync ${t} -w ${i} --to ${r}${c}`,n=e.workspaces.filter(R).flatMap(i=>[...i.projects.map(r=>({workspace:i.slug,project:r.slug,name:r.name,docs:r.docs,command:o(i.slug,r.slug)})),{workspace:i.slug,project:null,name:"a new project",docs:0,command:o(i.slug,"<name>"," --create")}]),s=Math.max(...n.map(i=>i.command.length));throw new N("needs_place",[`Which project should ${t==="."?"this folder":t+"/"} go to? Ask the user, then run one of:`,...n.map(i=>` ${i.command.padEnd(s)} ${g(i.project?`${i.name} \xB7 ${te(i.docs,"doc")}`:i.name)}`),g("Subfolders become folders. `dockito tree` shows every place.")].join(`
10
+ `),{choices:n})}async function kt(e,t,o){let n=Fe()&&!o.json,s=await e.tree(),i=a=>!!so(s.kinds,a);s.workspaces.some(R)||h("This login can't publish anywhere. Run `dockito login --update`.");let r=t;r||(n||mt(s,pe("docs")?"docs":"."),r=await ke("Folder to sync",pe("docs")?"docs":"."));let c=We(r);(!pe(c)||!wt(c).isDirectory())&&h(`${r} isn't a folder.`);let d=yt(eo(process.cwd(),c))||".",l;o.workspace&&(l=s.workspaces.find(a=>a.slug===o.workspace)??h(`This login can't reach workspace "${o.workspace}".`));let f=o.to;if(!f){n||mt(s,d);let a=(l?[l]:s.workspaces.filter(R)).flatMap(y=>[...y.projects.map(T=>({w:y,p:T.slug,label:T.name,hint:`${y.slug} \xB7 ${te(T.docs,"doc")}`})),{w:y,p:null,label:"New project",hint:y.slug}]),p=a[await ct("Project",a)];l=p.w,f=p.p??await ke("Project name",ft(c===process.cwd()?c:We(c,".."))),o.create||=!p.p}if(!l){let a=s.workspaces.filter(y=>R(y)&&U(y,f.split("/")[0])),p=s.workspaces.find(y=>y.kind==="personal"&&y.role==="owner");if(l=a.length===1?a[0]:s.workspaces.length===1?s.workspaces[0]:!a.length&&p?p:void 0,!l)throw new N("needs_workspace",`Which workspace? Run again with -w: ${(a.length?a:s.workspaces).map(y=>y.slug).join(", ")}`)}let m=l;R(m)||h(`This login has View access in ${m.slug}; syncing needs Publish. Run \`dockito login --update\`.`);let[w,...v]=f.split("/").filter(Boolean);w||h("--to takes a project, or project/folder."),v.length>2&&h(`Folders go at most ${2} deep inside a project.`);let b=U(m,w);if(!b&&!o.create)throw new N("no_project",`No project "${w}" in ${m.slug}. Projects: ${m.projects.map(a=>a.slug).join(", ")||"none"}.
11
+ To make it, run again with --create.`,{workspace:m.slug,projects:m.projects.map(a=>a.slug)});let j=ro(c,i);j.length||h(`No ${s.kinds.flatMap(a=>a.extensions).join(", ")} files in ${d}.`);let B=2-v.length,D=j.filter(a=>a.split("/").length-1>B);if(D.length&&!o.fold){let a=y=>y.split("/").slice(0,B).join("/")||"the project's top",p=`${te(D.length,"file")} ${D.length===1?"is":"are"} more than ${2} folders deep inside the project. With --fold they go in ${[...new Set(D.map(a))].join(", ")}; where two would share a name, the folders left out go in front (plans/archive/grok/README.md \u2192 grok-README.md).`;if(!n)throw new N("too_deep",`${p}
12
+ Ask the user, then run again with --fold.`,{files:D});if($(p),!await Me("Fold them like that?"))return $(g("Nothing changed."));o.fold=!0}let Ve=a=>{let p=a.split("/");return{folder:[...v,...p.slice(0,-1).slice(0,B)],dropped:p.slice(0,-1).slice(B),file:p[p.length-1]}},De=new Map;for(let a of j){let p=Ve(a),y=[...p.folder,p.file].join("/").toLowerCase();De.set(y,(De.get(y)??0)+1)}let Ye=b?await e.files(m.id,b.id):[],se=G(b),K=j.map(a=>{let{folder:p,dropped:y,file:T}=Ve(a),F=y.length&&De.get([...p,T].join("/").toLowerCase())>1?[...y,T].join("-"):T,C=new Uint8Array(Qt(L(c,a))),J={rel:a,name:F,folder:p,bytes:C,hash:no(C),action:"new",doc:null,others:[],assets:[]};/\.html?$/i.test(a)&&(J.assets=io(new TextDecoder().decode(C),L(c,a,".."),i));let it=se.idOf(p);if(it===void 0)return J;let re=ie(Ye,it,F);return re.length>1?{...J,action:"ambiguous",others:re}:re.length?{...J,doc:re[0],action:re[0].sha256===J.hash?"same":"update"}:J}),Xe=K.filter(a=>a.bytes.length>m.maxFile);Xe.length&&h(`Over the ${ue(m.maxFile)} limit per file in ${m.slug}:
13
+ ${Xe.map(a=>` ${a.rel} ${ue(a.bytes.length)}`).join(`
14
+ `)}`);let Ne=new Map;for(let a of K){let p=[...a.folder,a.name].join("/").toLowerCase();Ne.set(p,[...Ne.get(p)??[],a.rel])}let Qe=[...Ne.values()].filter(a=>a.length>1);Qe.length&&h(`These files would be the same doc; rename one of each:
15
+ ${Qe.map(a=>" "+a.join(" and ")).join(`
16
+ `)}`);let I=[],Ze=new Set;for(let a of K)for(let p=1;p<=a.folder.length;p++){let y=a.folder.slice(0,p),T=y.join("/").toLowerCase();se.idOf(y)===void 0&&!Ze.has(T)&&(Ze.add(T),I.push(y))}I.sort((a,p)=>a.length-p.length);let S=K.filter(a=>a.action==="new"||a.action==="update"),et=K.filter(a=>a.action==="ambiguous"),Pe=K.filter(a=>a.action==="same").length,Dt=new Set(K.flatMap(a=>a.doc?[a.doc.id]:a.others.map(p=>p.id))),Ce=Ye.filter(a=>!Dt.has(a.id)).length,tt=`${m.slug}/${b?.slug??w}${v.length?"/"+v.join("/"):""}`,ge={workspace:m.slug,to:tt,create:{project:b?null:w,folders:I.map(a=>a.join("/"))},docs:S.map(a=>({file:a.rel,action:a.action,folder:a.folder.join("/")||null,name:a.name,link:a.doc?e.linkOf(a.doc.slug):null})),unchanged:Pe,ambiguous:et.map(a=>({file:a.rel,docs:a.others.map(p=>e.linkOf(p.slug))})),notHere:Ce};if(!o.json){u(`${A(d==="."?"./":d+"/")} \u2192 ${A(tt)}`),b||u(` ${k("+ project")} ${w}`);for(let p of I)u(` ${k("+ folder ")} ${p.join("/")}`);if(S.length>15)for(let[p,y]of[["new",k("+ new ")],["update",E("~ update ")]]){let T=new Map;for(let F of S.filter(C=>C.action===p))T.set(F.folder.join("/"),[...T.get(F.folder.join("/"))??[],F.name]);for(let[F,C]of T)u(` ${y} ${C.length} in ${F||"the project's top"}: ${g(C.slice(0,4).join(", ")+(C.length>4?`, ${C.length-4} more`:""))}`)}else for(let p of S){let y=p.name!==ft(p.rel)?g(` \u2192 ${[...p.folder,p.name].join("/")}`):"";u(` ${p.action==="new"?k("+ new "):E("~ update ")} ${p.rel}${y}`)}Pe&&u(g(` ${Pe} unchanged`));for(let p of et)u(` ${_("? skip ")} ${p.rel} ${g(`${p.others.length} docs there have this name: ${p.others.map(y=>e.linkOf(y.slug)).join(", ")}`)}`);Ce&&u(g(` ${te(Ce,"doc")} in the project with no file here, left as they are`)),S.length&&u(g(` ${te(S.length,"file")} to send, ${ue(S.reduce((p,y)=>p+y.bytes.length,0))}`));let a=S.filter(p=>p.assets.length);a.length&&u(E(` ! ${a.length===1?a[0].rel:`${a.length} HTML files`} load ${[...new Set(a.flatMap(p=>p.assets))].join(", ")} from next to ${a.length===1?"it":"them"}; only HTML files are published`))}if(!S.length&&b&&!I.length){o.json?u(JSON.stringify({...ge,applied:!0},null,2)):u(`${k("\u2713")} Up to date`);return}if(o["dry-run"]){o.json&&u(JSON.stringify({...ge,applied:!1},null,2));return}if((!b||I.length)&&!o.yes){let a=["dockito sync",d,`-w ${m.slug}`,`--to ${f}`,o.create&&!b?"--create":"",o.fold?"--fold":"","--yes"].filter(Boolean).join(" ");if(!n){if(o.json)return u(JSON.stringify({...ge,applied:!1,next:a},null,2));throw new N("confirm",`
17
+ Nothing changed yet. Show the user this plan, then run: ${a}`,{next:a})}if(!await Me("Apply?"))return $(g("Nothing changed."))}if(!b){let a=await e.createProject(m.id,w);b={id:a.id,slug:a.slug,name:w,role:"edit",docs:0,folders:[]},o.json||u(`${k("\u2713")} Project ${w}`)}let Ee=b;for(let a of I){let p=await e.createFolder(Ee.id,a[a.length-1],a.length>1?se.idOf(a.slice(0,-1)):null);se.add(a,p.id)}I.length&&!o.json&&u(`${k("\u2713")} ${te(I.length,"folder")}`);let fe=ve(["rev-parse","--show-toplevel"],c)?.trim()??null,ot=fe?ve(["rev-parse","HEAD"],fe)?.trim()??null:null,Nt=new Set(fe?(ve(["status","--porcelain","-z","--","."],c)??"").split("\0").filter(Boolean).map(a=>We(fe,a.slice(3))):[]),q=[],me=null,Ie=[...S],we=!o.json&&Fe(),Pt=S.reduce((a,p)=>a+p.bytes.length,0),nt=0,st=()=>we&&process.stdout.write(`\r\x1B[2K${g(`Sending ${q.length} of ${S.length} \xB7 ${ue(nt)} of ${ue(Pt)}`)}`);async function Ct(a){let p=ot&&!Nt.has(L(c,a.rel))?ot:null;for(;;)try{let y=a.doc?await e.update(a.doc.id,a.name,a.bytes,{commit:p}):await e.create(m.id,a.name,a.bytes,{project:Ee.id,folder:se.idOf(a.folder)??null});return{file:a.rel,action:a.action,link:e.linkOf(y.slug)}}catch(y){if(y instanceof O&&y.code==="rate_limited"){me||(o.json||$(g("\rUpload limit reached; carrying on in a minute\u2026")),me=new Promise(T=>setTimeout(T,3e4)).then(()=>{me=null})),await me;continue}return{file:a.rel,action:a.action,error:y instanceof O?ce(y,e.url):String(y)}}}st(),await Promise.all(Array.from({length:Math.min(oo,Ie.length)},async()=>{for(let a=Ie.shift();a;a=Ie.shift()){let p=await Ct(a);q.push(p),nt+=a.bytes.length,we&&process.stdout.write("\r\x1B[2K"),p.error&&!o.json?u(`${_("\u2717")} ${p.file} ${_(p.error)}`):!o.json&&!we&&u(`${k("\u2713")} ${p.file} ${g(p.link)}`),st()}})),we&&process.stdout.write("\r\x1B[2K");let Re=q.filter(a=>a.error),rt=`${e.url}/${m.slug}/project/${Ee.slug}`;o.json?u(JSON.stringify({...ge,applied:!0,link:rt,results:q},null,2)):u(`${Re.length?E(`${q.length-Re.length} of ${q.length} sent`):k("\u2713 Synced")} ${g(rt)}`),Re.length&&(process.exitCode=1)}var He=`Dockito hosts Markdown and HTML docs, and live sheets, at a short link,
18
18
  with History.
19
19
 
20
20
  workspace a person's or a team's space; its slug is in every link
@@ -36,6 +36,9 @@ For AI agents:
36
36
  asked for a public link. If they didn't say, keep it private.
37
37
  - A folder of docs: \`dockito sync <folder> --to <project>\`. It shows a
38
38
  plan; show it to the user before running with --yes.
39
+ - \`dockito move\` puts a doc or folder in another place; its link stays.
40
+ A move that would let more people open it is refused: the user moves
41
+ it in the app. \`tree\` and \`ls\` mark places you can only view.
39
42
  - \`dockito <command> --help\` explains each command. Use --json; on
40
43
  failure, read "error" and the commands it suggests.
41
44
 
@@ -49,7 +52,7 @@ makes a new one.
49
52
  changed the same cells meanwhile, nothing applies and it says who
50
53
  (exit 1): read again, then apply on the new version.
51
54
  - Change rows by what's in them, not by row number: \`update\` with a
52
- filter ("ID = T-104"), and "max" when you mean several rows.`,ve={sync:{use:"sync <folder> --to <project>",about:"Mirror a folder into a project",page:`Mirrors a local folder into a project: subfolders become folders, and each
55
+ filter ("ID = T-104"), and "max" when you mean several rows.`,je={sync:{use:"sync <folder> --to <project>",about:"Mirror a folder into a project",page:`Mirrors a local folder into a project: subfolders become folders, and each
53
56
  file updates the doc with the same file name in the same folder, or becomes
54
57
  a new doc there. Only files whose content changed are sent. Nothing is moved
55
58
  or deleted in Dockito. The CLI remembers nothing: pass --to every time.
@@ -98,14 +101,32 @@ An existing doc is made public only from Share in the app; a new doc can be
98
101
  published public with \`dockito publish <file> --public\` when the user asks.
99
102
 
100
103
  dockito share <link or code>
101
- dockito share <link or code> --private`},tree:{use:"tree",about:"Every workspace, project and folder you can use",page:"Lists every workspace this login can reach, its projects and their folders,\nwith doc counts, each written as the --to it takes. Docs themselves: `dockito ls`."},ls:{use:"ls [project/folder]",about:"What's in a place",page:`Lists the docs (with their codes), projects and folders in a place: your
104
+ dockito share <link or code> --private`},tree:{use:"tree",about:"Every workspace, project and folder you can use",page:`Lists every workspace this login can reach, its projects and their folders,
105
+ with doc counts, each written as the --to it takes. Places where you can only
106
+ view or comment are marked: publishing there is refused. Docs themselves:
107
+ \`dockito ls\`.`},ls:{use:"ls [project/folder]",about:"What's in a place",page:`Lists the docs (with their codes), projects and folders in a place: your
102
108
  workspace's top level, a project, or a folder. The place can also be given
103
109
  as --to, like publish. -w for another workspace.
104
110
 
105
111
  dockito ls handbook/specs
106
112
  dockito ls --to handbook/specs`},mkdir:{use:"mkdir <project/folder>",about:"Make a project or folders",page:`Makes whatever is missing: the project, then up to 2 levels of folders.
107
113
 
108
- dockito mkdir handbook/specs/drafts`},open:{use:"open <doc>",about:"Open in the browser",page:"Opens a doc (link or code) in the browser."},get:{use:"get <doc> [-o file]",about:"Download a doc, or read it in parts",page:`Prints a doc's file, or writes it with -o. Big docs can be read in parts.
114
+ dockito mkdir handbook/specs/drafts`},move:{use:"move <doc or project/folder> [place]",about:"Move a doc or folder",page:`Puts a doc anywhere in its workspace (no place: the workspace's Docs), or
115
+ a folder elsewhere in its project. The link stays the same.
116
+
117
+ Who can open it follows where it sits. A move that would let more people
118
+ open it, or do more with it, is refused and names them: move it in the app,
119
+ which asks first. A move that shuts people out says who.
120
+
121
+ --to <project[/folder]> The place, as publish takes it
122
+ -w, --workspace <slug> Another workspace (for a folder)
123
+
124
+ dockito move 5jyx8jjx6kc5 handbook/specs
125
+ dockito move 5jyx8jjx6kc5
126
+ dockito move handbook/drafts handbook/specs`},rmdir:{use:"rmdir <project/folder>",about:"Remove an empty folder",page:`Removes a folder with nothing in it. A folder with docs or folders in it
127
+ stays, and nothing is deleted with it. Projects are removed in the app.
128
+
129
+ dockito rmdir handbook/specs/old`},open:{use:"open <doc>",about:"Open in the browser",page:"Opens a doc (link or code) in the browser."},get:{use:"get <doc> [-o file]",about:"Download a doc, or read it in parts",page:`Prints a doc's file, or writes it with -o. Big docs can be read in parts.
109
130
  A sheet prints its outline: each sheet's columns, what's in them, and the
110
131
  version to apply changes at; \`dockito read\` gives its rows.
111
132
 
@@ -146,12 +167,15 @@ gives a chart's points instead.
146
167
  dockito read <doc> --chart 2`},apply:{use:"apply <doc> <changes.json>",about:"Change a sheet at the version you read",page:`Changes a sheet, read at --base: the version (v N) your last get, read or
147
168
  changes showed. Addresses, rows and headers mean what they meant then,
148
169
  wherever they moved since; so do chart and format numbers (#N, by order in
149
- the outline). Every change applies, or none: a cell someone
170
+ the outline; a chart an earlier change here makes takes the next number) and
171
+ sheet names (after a rename here, either works). Every change applies, or
172
+ none: a cell someone
150
173
  changed since is refused, naming who (exit 1). Read again and apply on the
151
174
  new version. Prints each cell old \u2192 new and the new version.
152
175
 
153
176
  The file (or - for stdin) is a list of changes, or {"base": N, "ops": [...],
154
- "expect": {...}}. "sheet" can be left out when there's one. Each change:
177
+ "expect": {...}} (cells that must hold these values before the ops, as at
178
+ base). "sheet" can be left out when there's one. Each change:
155
179
  {"set": "Tasks!E17", "value": "Done"}
156
180
  {"set": "Tasks!B2:C3", "values": [["Tile grout", "Kitchen"], ["Vent", "Bath"]]}
157
181
  {"update": {"sheet": "Tasks", "where": "ID = T-104", "set": {"Status": "Done"}}}
@@ -159,8 +183,8 @@ The file (or - for stdin) is a list of changes, or {"base": N, "ops": [...],
159
183
  "set": {"Status": "Done"}, "max": 50}} max is 1 unless set; more
160
184
  matches than max are refused
161
185
  {"append": {"sheet": "Tasks", "rows": [{"ID": "T-214", "Status": "Open"}]}}
162
- a column's formula in the rows above carries
163
- into new rows, as in Excel
186
+ a column's formula and the formats of the row
187
+ above carry into new rows, as in Excel
164
188
  {"insertRows": {"sheet": "Tasks", "after": 40, "count": 2}}, deleteRows
165
189
  {"rows": "41:42"} or a list, "9, 16, 41:42"; moveRows {"rows": "5:6",
166
190
  "after": 10}; insertCols, deleteCols, moveCols the same with letters
@@ -211,8 +235,8 @@ The file (or - for stdin) is a list of changes, or {"base": N, "ops": [...],
211
235
  what was left and why; over 100,000 cells changes nothing
212
236
  {"chart": {"range": "Tasks!A1:C40", "type": "column", "title": "Costs"}}:
213
237
  the outline lists charts as chart #N. "header" (first row names the
214
- series) and "labels" (first column labels the points) are guessed when
215
- left out. Type: column, bar, line, area, pie, donut, scatter (first
238
+ series) and "labels" (first column labels the points; a scatter's x)
239
+ are guessed when left out. Type: column, bar, line, area, pie, donut, scatter (first
216
240
  column: x), or "stacked column", "stacked bar", "stacked area",
217
241
  "doughnut", "horizontal bar"; "stacked": true for column, bar, area.
218
242
  "at": "E2", "below", "beside", "below chart #2" or "beside chart #2";
@@ -224,8 +248,8 @@ The file (or - for stdin) is a list of changes, or {"base": N, "ops": [...],
224
248
  label column, "Kitchen and Bath by Month"), "x title", "y title";
225
249
  "legend": right, bottom, top, none; "colors": ["#4285f4", null] per
226
250
  series. The answer says what it draws and warns about a total row or
227
- column, or a column with no numbers. {"chart": 2, ...} changes chart 2;
228
- {"chart": 2, "remove": true} removes it
251
+ column, or a column with no numbers. {"chart": {"chart": 2, ...}}
252
+ changes chart 2; {"chart": {"chart": 2, "remove": true}} removes it
229
253
  Values: a number, true, false, null (empty), a string ("=\u2026" is a formula),
230
254
  {"text": "=as typed"} or {"date": "2026-10-19"}.
231
255
 
@@ -238,8 +262,9 @@ Values: a number, true, false, null (empty), a string ("=\u2026" is a formula),
238
262
 
239
263
  dockito apply <doc> changes.json --base 412 -m "close finished tasks"
240
264
  echo '[{"set": "E17", "value": "Done"}]' | dockito apply <doc> - --base 412`},check:{use:"check <doc>",about:"Problems in a sheet",page:`Finds problems in a sheet, by rules: formula errors, a formula that breaks
241
- its column's pattern, a value typed over a column's formulas, numbers or
242
- dates typed as text, totals that stop before their column does.
265
+ its column's pattern, a value typed over a column's formulas or a cell left
266
+ empty among them, numbers or dates typed as text, totals that stop before
267
+ their column does. Exits 1 when it finds any.
243
268
 
244
269
  --sheet <name> One sheet (default: every sheet)`},changes:{use:"changes <doc> --since <v>",about:"What changed in a sheet",page:`Lists what changed after a version, or since a time in UTC, oldest first:
245
270
  who, when, through what, with each cell old \u2192 new, named where it is now.
@@ -247,24 +272,26 @@ Ends at the current version, to apply changes at.
247
272
 
248
273
  --since <v|time> 412, 2026-10-13 or "2026-10-13 09:00"
249
274
  --max <n> At most this many changes (default 50, at most 200)`},history:{use:"history <doc>",about:"History entries",page:`Lists a doc's History: entry, when, who, from where (upload, edit, cli,
250
- mcp), saved points with their notes, and the git commit.`},restore:{use:"restore <doc> <entry>",about:"Make a History entry current again",page:"Makes an older entry the current one, as a new entry. Nothing is lost."},save:{use:"save <doc> [entry] -m <note>",about:"Keep an entry as a saved point",page:`Keeps an entry (default: the current one) as a saved point, so History
251
- never drops it, with an optional note. A doc keeps up to 10.`},login:{use:"login [--update]",about:"Sign in through the browser",page:`Opens the browser, where you check a code and choose this login's access
275
+ mcp), saved points with their notes, and the git commit. A sheet's entries
276
+ are how each editing session left it: its version and everyone who changed it.`},restore:{use:"restore <doc> <entry>",about:"Make a History entry current again",page:"Makes an older entry the current one, as a new entry. Nothing is lost.\nA sheet is made as it was then in one change, which `dockito apply <doc>\n--undo <version>` takes back."},save:{use:"save <doc> [entry] -m <note>",about:"Keep an entry as a saved point",page:`Keeps an entry (default: the current one; a sheet: as it is now) as a
277
+ saved point, so History never drops it, with an optional note. A doc keeps
278
+ up to 10.`},login:{use:"login [--update]",about:"Sign in through the browser",page:`Opens the browser, where you check a code and choose this login's access
252
279
  in each workspace: View, Publish or Everything. --update changes that access.
253
- --no-browser prints the link instead.`},logout:{use:"logout",about:"Sign out this machine",page:"Signs this machine out and removes its token."},whoami:{use:"whoami",about:"Who you are and what this login can reach",page:"Your name, the login's expiry, and its access in each workspace."},agent:{use:"agent",about:"What an AI agent needs to know",page:"Prints what Dockito is and how an AI agent should use this CLI."}};var qe="0.0.1-beta.13",C=dt(),de=ut(C),Je=`dockito-cli/${qe} (${process.platform})`;function T(){let e=ye(de);return e||p("Not logged in. Run `dockito login`."),fe({url:C,token:e,source:"cli",userAgent:Je})}function St(e){let[t,n]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["cmd",["/c","start","",e]]:["xdg-open",[e]];try{let o=an(t,n,{stdio:"ignore",detached:!0});return o.on("error",()=>{}),o.unref(),!0}catch{return!1}}var gn=()=>!process.env.SSH_CONNECTION&&!process.env.CI&&(process.platform!=="linux"||!!process.env.DISPLAY||!!process.env.WAYLAND_DISPLAY),fn=e=>new Promise(t=>setTimeout(t,e)),mn=()=>dn().replace(/\.local$/,"").slice(0,60)||"Unknown device";function M(e){return it(e)??p(`"${e}" isn't a doc link or code. \`dockito ls\` lists docs with their codes.`)}function vt(e){let t=un(Ot(e)),n=bt("git",["rev-parse","HEAD"],{cwd:t,encoding:"utf8"});if(n.error||n.status!==0)return null;let o=bt("git",["status","--porcelain","--",Be(e)],{cwd:t,encoding:"utf8"});return o.status!==0||o.stdout.trim()?null:n.stdout.trim()}var At=e=>cn("sha256").update(e).digest("hex");async function wn(e){let t=ye(de);e.update&&!t&&p("Not logged in. Run `dockito login` first."),process.env.DOCKITO_TOKEN&&$(R("DOCKITO_TOKEN is set, so commands will keep using it instead of this login."));let n=fe({url:C,token:e.update?t:null,userAgent:Je}),o=await n.startLogin("Dockito CLI",mn()),s=!e["no-browser"]&&gn()&&St(o.verification_uri_complete);h(s?`Opening ${o.verification_uri_complete}`:`Open ${S(o.verification_uri)}`),h(`Code: ${S(o.user_code)}`),$(g("Waiting for approval\u2026"));let i=o.interval*1e3,a=Date.now()+o.expires_in*1e3;for(;Date.now()<a;){await fn(i);try{let c=await n.pollLogin(o.device_code);if(!c)continue;let d=ht(de,c.token);h(`${b("\u2713")} Logged in as ${c.user.name}${c.user.username?g(` (${c.user.username})`):""}`);for(let l of c.workspaces)h(` ${l.name} ${g(`${l.slug} \xB7 ${G[l.level]}`)}`);d==="file"&&$(R("No keychain found, so the token is in a file only you can read, in ~/.config/dockito."));return}catch(c){if(c instanceof x&&c.code==="access_denied"&&p("Denied in the browser."),c instanceof x&&c.code==="expired_token"&&p("The code expired. Run `dockito login` again."),c instanceof x&&c.code==="offline"){i=Math.min(i*2,3e4);continue}throw c}}p("The code expired. Run `dockito login` again.")}async function yn(){let e=ye(de);if(!e)return h("Not logged in.");process.env.DOCKITO_TOKEN&&p("DOCKITO_TOKEN is set; unset it or revoke that token in Account \u2192 Tokens.");try{await fe({url:C,token:e,userAgent:Je}).logout()}catch(t){if(!(t instanceof x)||t.code!=="invalid_token"&&t.code!=="not_found")throw t}gt(de),h(`${b("\u2713")} Logged out`)}async function kn(e){let t=await T().whoami();if(e.json)return h(JSON.stringify(t,null,2));h(`${t.user.name} ${g(`${t.user.username} \xB7 ${t.user.email}`)}`),t.token&&h(g(`${t.token.name} \xB7 ${t.token.source==="cli"?"ends after 90 days without use":`expires ${new Date(t.token.expiresAt).toLocaleDateString()}`}`)),h(),se(t.workspaces.map(n=>[n.slug,n.name,g(n.level?G[n.level]:n.role)])),C!=="https://dockito-app.dockito.workers.dev"&&$(g(`
254
- ${C}`))}async function bn(e,t){e.length||p("Name a file: `dockito publish notes.md --to <project/folder>`"),t.doc&&e.length>1&&p("--doc takes one file."),t.title!==void 0&&e.length>1&&p("--title takes one file."),t.title!==void 0&&!t.title.trim()&&p("--title needs a title."),t.public&&t.doc&&p("--public is only for a new doc; an existing one can be made public from Share in the app.");for(let l of e)(!Tt(l)||!ln(l).isFile())&&p(`${l} isn't a file${pn(l)?"":` (looked for ${Ot(l)})`}.`);let n=e.filter(He);n.length&&t.doc&&p("A .csv makes a new sheet; it can't replace a doc. Leave out --doc, or change a sheet with `dockito apply`."),n.length&&t.message&&p("Sheets don't keep saved points yet: publish the .csv without -m.");let o=T(),s=[];if(t.doc){let l=await o.doc(M(t.doc)),m=new Uint8Array(xe(e[0])),f=await o.update(l.doc.id,Be(e[0]),m,{note:t.message,commit:vt(e[0])});return t.json?h(JSON.stringify([{file:e[0],...f,link:o.linkOf(f.slug)}],null,2)):h(f.unchanged?`${g("=")} ${e[0]} ${g("unchanged")} ${o.linkOf(f.slug)}`:`${b("\u2713")} ${e[0]} \u2192 ${o.linkOf(f.slug)}`)}let i=await o.tree(),a=ke(i,t.workspace);I(a)||p(`This login has View access in ${a.slug}. Run \`dockito login --update\`.`);let c=Fe(a,t.to),d=await o.files(a.id,c.project);for(let l of e){let m=new Uint8Array(xe(l)),f=Be(l),w=t.new?[]:oe(d,c.folder,f),O=w.filter(D=>D.kind==="sheet");O.length&&p(`${f} in ${c.label} is ${O.length>1?`${O.length} live sheets`:"a live sheet"} (${O.map(D=>o.linkOf(D.slug)).join(", ")}): change ${O.length>1?"one":"it"} with \`dockito apply\`. --new makes a new sheet from the file instead.`),w.length>1&&p(`${w.length} docs in ${c.label} are named ${f}: ${w.map(D=>o.linkOf(D.slug)).join(", ")}. Pick one with --doc, or publish a new one with --new.`);let k=w[0];if(k&&t.public&&!(await o.doc(k.slug)).doc.public&&p(`${f} is already a private doc in ${c.label} (${o.linkOf(k.slug)}). Only Share in the app can make it public; \`--new --public\` publishes a new public doc.`),k&&k.sha256===At(m)&&!t.message){s.push({file:l,id:k.id,slug:k.slug,version:k.version,unchanged:!0,link:o.linkOf(k.slug)}),t.json||h(`${g("=")} ${l} ${g("unchanged")} ${o.linkOf(k.slug)}`);continue}let v=k?await o.update(k.id,f,m,{note:t.message,commit:vt(l)}):await o.create(a.id,f,m,{project:c.project,folder:c.folder,public:t.public,title:t.title});!k&&t.message&&await o.save(v.id,v.version,t.message);let U="public"in v&&v.public===!0;s.push({file:l,...v,...He(l)?{type:"sheet"}:{},link:o.linkOf(v.slug),new:!k}),t.json||h(`${b("\u2713")} ${l} \u2192 ${o.linkOf(v.slug)} ${g(k?v.unchanged?"unchanged":"updated":`new ${He(l)?"sheet ":""}in ${c.label}`)}${U?` ${b("\xB7 public")}`:""}${t.message?g(` \xB7 saved: ${t.message}`):""}`)}t.json?h(JSON.stringify(s,null,2)):!t.to&&s.some(l=>l.new)&&$(g("In the workspace's Docs. To put docs in a project: --to <project/folder>; `dockito tree` lists them."))}async function $n(e){let t=await T().tree();if(e.json)return h(JSON.stringify(t,null,2));let n=o=>g(`${o} doc${o===1?"":"s"}`);for(let o of t.workspaces){h(`${S(o.slug)} ${o.name} ${g(`\xB7 ${o.level?G[o.level]:o.role}`)}`);let s=[[g("Docs"),"",n(o.docs)]];for(let i of o.projects){s.push([i.slug,i.name,n(i.docs)]);let a=(c,d)=>{for(let l of i.folders.filter(m=>m.parentId===c))s.push([`${d}/${l.name}`,"",n(l.docs)]),a(l.id,`${d}/${l.name}`)};a(null,i.slug)}se(s.map(i=>[" ",...i])),h()}$(g("Publish into a place: dockito publish <file> -w <workspace> --to <project/folder>"))}async function vn(e,t){e&&t.to&&e!==t.to&&p(`Two places: ${e} and --to ${t.to}. Give one.`);let n=e??t.to,o=T(),s=await o.tree(),i=ke(s,t.workspace),a=Fe(i,n),c=await o.docs(i.id,{project:a.project,folder:a.folder}),d=a.project?i.projects.find(w=>w.id===a.project):null,l=n?[]:i.projects,m=d?d.folders.filter(w=>w.parentId===a.folder):[];if(t.json)return h(JSON.stringify({workspace:i.slug,place:n??null,projects:l,folders:m,docs:c},null,2));let f=[];for(let w of l)f.push([S(`${w.slug}/`),w.name,g(`${w.docs} doc${w.docs===1?"":"s"}`),""]);for(let w of m)f.push([S(`${w.name}/`),"",g(`${w.docs} doc${w.docs===1?"":"s"}`),""]);for(let w of c)f.push([w.slug,w.title+g(w.kind==="html"?" .html":w.kind==="sheet"?" sheet":" .md"),g(me(w.updatedAt)),g(w.updatedByName??"")]);if(!f.length)return $(g(`Nothing in ${a.label} yet.`));se(f)}async function jn(e,t){let[n,...o]=(e??"").split("/").filter(Boolean);n||p("`dockito mkdir <project>[/folder[/subfolder]]`"),o.length>2&&p("Folders go at most 2 deep inside a project.");let s=T(),i=ke(await s.tree(),t.workspace),a=H(i,n),c=a?.id??(await s.createProject(i.id,n)).id,d=!a;a||h(`${b("\u2713")} Project ${n}`);let l=q(a),m=null;for(let f=0;f<o.length;f++){let w=l.idOf(o.slice(0,f+1));if(w){m=w;continue}m=(await s.createFolder(c,o[f],m)).id,l.add(o.slice(0,f+1),m),d=!0,h(`${b("\u2713")} Folder ${o.slice(0,f+1).join("/")}`)}d||$(g("Already there."))}async function xn(e){e||p("Name a doc: `dockito open <link or code>`");let t=`${C}/${M(e)}`;St(t)||h(t)}async function Tn(e,t){e||p("Name a doc: `dockito get <link or code> [-o file]`"),t.outline&&(t.lines!==void 0||t.text)&&p("--outline is the headings only; leave out --lines and --text.");let n=T(),o=await n.doc(M(e));if(o.doc.kind==="sheet"){let d=["entry","outline","lines","text"].filter(m=>t[m]!==void 0);d.length&&p(`A sheet has no file to read by ${d.map(m=>`--${m}`).join(" or ")}: \`dockito read\` gives its rows, a range or totals.`);let l=await n.readSheet(o.doc.id,{outline:!0});return t.output?($t(t.output,`${l.text.trimEnd()}
255
- `),$(`${b("\u2713")} ${t.output} ${g(`\u2190 ${n.linkOf(o.doc.slug)} outline`)}`)):ee(l)}let s=t.entry?Number(t.entry):null;t.entry&&(!Number.isInteger(s)||s<1)&&p("--entry takes an entry number from `dockito history`.");let i=await n.content(o.doc.id,s,{outline:t.outline,lines:t.lines,text:t.text}),a=i.bytes.length&&i.lines!==null&&!t.output&&i.bytes[i.bytes.length-1]!==10?new Uint8Array([...i.bytes,10]):i.bytes;if(!t.output)return void process.stdout.write(a);$t(t.output,a);let c=t.outline?" outline":i.shown?` lines ${i.shown} of ${i.lines}`:t.text?" as text":"";$(`${b("\u2713")} ${t.output} ${g(`\u2190 ${n.linkOf(o.doc.slug)}${s?` entry ${s}`:""}${c}`)}`)}async function On(e,t){e||p("Name a doc: `dockito history <link or code>`");let n=await T().doc(M(e));if(n.doc.kind==="sheet"&&p(Ge),t.json)return h(JSON.stringify(n.versions,null,2));h(`${S(n.doc.title)} ${g(`${C}/${n.doc.slug}`)}`),se(n.versions.map((o,s)=>[String(o.number),s===0?b("current"):g(me(o.createdAt)),s===0?g(me(o.createdAt)):"",o.byName??"",g(o.source),o.savedAt?`${R("saved")}${o.note?` ${o.note}`:""}`:"",o.gitCommit?g(o.gitCommit.slice(0,7)):""]))}async function Sn(e,t){let n=Number(t);(!e||!Number.isInteger(n)||n<1)&&p("`dockito restore <doc> <entry>`; entries are in `dockito history`.");let o=T(),s=await o.doc(M(e));s.doc.kind==="sheet"&&p(`${Ge} \`dockito apply <doc> --undo <version>\` takes back a change of yours.`);let i=await o.restore(s.doc.id,n,s.versions[0]?.number??null);h(`${b("\u2713")} Restored entry ${n} as entry ${i.version} ${g(o.linkOf(i.slug))}`)}async function An(e,t,n){e||p('`dockito save <doc> [entry] -m "note"`');let o=T(),s=await o.doc(M(e));s.doc.kind==="sheet"&&p(Ge);let i=t?Number(t):s.versions[0]?.number;(!Number.isInteger(i)||i<1)&&p("Entries are in `dockito history`."),await o.save(s.doc.id,i,n.message??null),h(`${b("\u2713")} Saved entry ${i}${n.message?g(` \xB7 ${n.message}`):""}`)}async function Dn(e,t){e||p("`dockito share <link or code> [--private]`"),t.public&&p("An existing doc can be made public only from Share in the app. `dockito publish <file> --public` makes a new doc public.");let n=T(),o=await n.doc(M(e)),s=n.linkOf(o.doc.slug);if(t.private)return o.doc.public&&await n.makePrivate(o.doc.id),t.json?h(JSON.stringify({public:!1,link:s},null,2)):h(`${b("\u2713")} Private: only people in ${o.workspace.name} can open it ${g(s)}`);if(t.json)return h(JSON.stringify({public:o.doc.public,link:s},null,2));h(o.doc.public?`${b("Public")}: anyone with the link can open it ${s}`:`Private: only people in ${o.workspace.name} can open it ${g(s)}`)}var He=e=>/\.csv$/i.test(e),Ge="Sheets don't keep History entries yet; `dockito changes <doc> --since <version>` lists what changed.";async function Te(e,t,n){t||p(`Name a sheet: \`dockito ${n}\``);let o=await e.doc(M(t));return o.doc.kind!=="sheet"&&p(`${o.doc.title} is a ${o.doc.kind==="md"?"Markdown":"HTML"} doc, not a sheet: \`dockito get\` reads it.`),o}function ee(e,t){t?h(JSON.stringify(e,null,2)):h(e.text.trimEnd()),e.ok||(process.exitCode=1)}var Z=(e,t,n)=>{if(e===void 0)return;let o=Number(e);return(!/^\d+$/.test(e.trim())||o<n)&&p(`--${t} takes a whole number, ${n} or more.`),o},Ue=e=>e===void 0?void 0:e.split(",").map(t=>t.trim()).filter(Boolean);async function Nn(e,t,n){let o=T(),s=await Te(o,e,"read <doc> [range]"),i=Z(n.chart,"chart",1);if(i!==void 0){let c=Object.entries(n).filter(([d,l])=>d!=="chart"&&d!=="json"&&l!==void 0);return(t||c.length)&&p(`--chart goes alone: it gives the chart's points as drawn, not ${[...t?["a range"]:[],...c.map(([d])=>`--${d}`)].join(", ")}.`),ee(await o.readSheet(s.doc.id,{read:{chart:i}}),n.json)}let a=await o.readSheet(s.doc.id,{read:{range:t,sheet:n.sheet,where:n.where,select:Ue(n.select),orderBy:n["order-by"],group:Ue(n.group),totals:Ue(n.totals),from:Z(n.from,"from",1),limit:Z(n.limit,"limit",1),formulas:n.formulas,styles:n.styles,format:n.csv?"csv":void 0}});ee(a,n.json)}async function Pn(e,t,n){let o=T(),s=await Te(o,e,"apply <doc> <changes.json>"),i=Z(n.undo,"undo",1),a;if(i!==void 0)(t||n.base!==void 0)&&p("--undo goes alone: no changes file and no --base with it."),a={undo:i,note:n.message,dryRun:n["dry-run"]||void 0};else{!t&&process.stdin.isTTY&&p("Give the changes: `dockito apply <doc> changes.json`, or pipe them in with `-`. `dockito apply --help` shows their form.");let d=!t||t==="-"?xe(0,"utf8"):Tt(t)?xe(t,"utf8"):p(`${t} isn't a file.`),l;try{l=JSON.parse(d)}catch{p(`${t&&t!=="-"?t:"The input"} isn't JSON: a list of changes, or {"base": N, "ops": [...]}.`)}let m=Array.isArray(l)?{ops:l}:l;(typeof m!="object"||m===null)&&p('The changes are a list, or {"base": N, "ops": [...]}.');let f=Z(n.base,"base",0)??m.base;f===void 0&&p("--base is the version you read: v N in `dockito get` or `dockito read`."),a={...m,base:f,note:n.message??m.note,dryRun:n["dry-run"]||m.dryRun||void 0}}let c=n.key??Cn(a);ee(await o.changeSheet(s.doc.id,{key:c,...a}),n.json)}var Cn=e=>At(new TextEncoder().encode(JSON.stringify(e))).slice(0,32);async function En(e,t){let n=T(),o=await Te(n,e,"check <doc>");ee(await n.readSheet(o.doc.id,{check:t.sheet?{sheet:t.sheet}:{}}),t.json)}async function In(e,t){let n=T(),o=await Te(n,e,"changes <doc> --since <version>");t.since===void 0&&p('--since is a version (412) or a time in UTC (2026-10-13, "2026-10-13 09:00").');let s=/^\s*\d+\s*$/.test(t.since)?Number(t.since):t.since;ee(await n.readSheet(o.doc.id,{changes:{since:s,max:Z(t.max,"max",1)}}),t.json)}var We=`Dockito CLI ${qe}
280
+ --no-browser prints the link instead.`},logout:{use:"logout",about:"Sign out this machine",page:"Signs this machine out and removes its token."},whoami:{use:"whoami",about:"Who you are and what this login can reach",page:"Your name, the login's expiry, and its access in each workspace."},agent:{use:"agent",about:"What an AI agent needs to know",page:"Prints what Dockito is and how an AI agent should use this CLI."}};var Ge="0.0.1-beta.15",P=dt(),he=ut(P),ze=`dockito-cli/${Ge} (${process.platform})`;function x(){let e=be(he);return e||h("Not logged in. Run `dockito login`."),ye({url:P,token:e,source:"cli",userAgent:ze})}function At(e){let[t,o]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["cmd",["/c","start","",e]]:["xdg-open",[e]];try{let n=ao(t,o,{stdio:"ignore",detached:!0});return n.on("error",()=>{}),n.unref(),!0}catch{return!1}}var fo=()=>!process.env.SSH_CONNECTION&&!process.env.CI&&(process.platform!=="linux"||!!process.env.DISPLAY||!!process.env.WAYLAND_DISPLAY),mo=e=>new Promise(t=>setTimeout(t,e)),wo=()=>uo().replace(/\.local$/,"").slice(0,60)||"Unknown device";function M(e){return _e(e)??h(`"${e}" isn't a doc link or code. \`dockito ls\` lists docs with their codes.`)}function vt(e){let t=po(Ot(e)),o=bt("git",["rev-parse","HEAD"],{cwd:t,encoding:"utf8"});if(o.error||o.status!==0)return null;let n=bt("git",["status","--porcelain","--",Je(e)],{cwd:t,encoding:"utf8"});return n.status!==0||n.stdout.trim()?null:o.stdout.trim()}var St=e=>co("sha256").update(e).digest("hex");async function yo(e){let t=be(he);e.update&&!t&&h("Not logged in. Run `dockito login` first."),process.env.DOCKITO_TOKEN&&$(E("DOCKITO_TOKEN is set, so commands will keep using it instead of this login."));let o=ye({url:P,token:e.update?t:null,userAgent:ze}),n=await o.startLogin("Dockito CLI",wo()),s=!e["no-browser"]&&fo()&&At(n.verification_uri_complete);u(s?`Opening ${n.verification_uri_complete}`:`Open ${A(n.verification_uri)}`),u(`Code: ${A(n.user_code)}`),$(g("Waiting for approval\u2026"));let i=n.interval*1e3,r=Date.now()+n.expires_in*1e3;for(;Date.now()<r;){await mo(i);try{let c=await o.pollLogin(n.device_code);if(!c)continue;let d=ht(he,c.token);u(`${k("\u2713")} Logged in as ${c.user.name}${c.user.username?g(` (${c.user.username})`):""}`);for(let l of c.workspaces)u(` ${l.name} ${g(`${l.slug} \xB7 ${V[l.level]}`)}`);d==="file"&&$(E("No keychain found, so the token is in a file only you can read, in ~/.config/dockito."));return}catch(c){if(c instanceof O&&c.code==="access_denied"&&h("Denied in the browser."),c instanceof O&&c.code==="expired_token"&&h("The code expired. Run `dockito login` again."),c instanceof O&&c.code==="offline"){i=Math.min(i*2,3e4);continue}throw c}}h("The code expired. Run `dockito login` again.")}async function ko(){let e=be(he);if(!e)return u("Not logged in.");process.env.DOCKITO_TOKEN&&h("DOCKITO_TOKEN is set; unset it or revoke that token in Account \u2192 Tokens.");try{await ye({url:P,token:e,userAgent:ze}).logout()}catch(t){if(!(t instanceof O)||t.code!=="invalid_token"&&t.code!=="not_found")throw t}gt(he),u(`${k("\u2713")} Logged out`)}async function bo(e){let t=await x().whoami();if(e.json)return u(JSON.stringify(t,null,2));u(`${t.user.name} ${g(`${t.user.username} \xB7 ${t.user.email}`)}`),t.token&&u(g(`${t.token.name} \xB7 ${t.token.source==="cli"?"ends after 90 days without use":`expires ${new Date(t.token.expiresAt).toLocaleDateString()}`}`)),u(),X(t.workspaces.map(o=>[o.slug,o.name,g(o.level?V[o.level]:o.role)])),P!=="https://dockito-app.dockito.workers.dev"&&$(g(`
281
+ ${P}`))}async function $o(e,t){e.length||h("Name a file: `dockito publish notes.md --to <project/folder>`"),t.doc&&e.length>1&&h("--doc takes one file."),t.title!==void 0&&e.length>1&&h("--title takes one file."),t.title!==void 0&&!t.title.trim()&&h("--title needs a title."),t.public&&t.doc&&h("--public is only for a new doc; an existing one can be made public from Share in the app.");for(let l of e)(!Tt(l)||!lo(l).isFile())&&h(`${l} isn't a file${ho(l)?"":` (looked for ${Ot(l)})`}.`);e.filter(xe).length&&t.doc&&h("A .csv makes a new sheet; it can't replace a doc. Leave out --doc, or change a sheet with `dockito apply`.");let n=x(),s=[];if(t.doc){let l=await n.doc(M(t.doc)),f=new Uint8Array(Oe(e[0])),m=await n.update(l.doc.id,Je(e[0]),f,{note:t.message,commit:vt(e[0])});return t.json?u(JSON.stringify([{file:e[0],...m,link:n.linkOf(m.slug)}],null,2)):u(m.unchanged?`${g("=")} ${e[0]} ${g("unchanged")} ${n.linkOf(m.slug)}`:`${k("\u2713")} ${e[0]} \u2192 ${n.linkOf(m.slug)}`)}let i=await n.tree(),r=W(i,t.workspace);R(r)||h(`This login has View access in ${r.slug}. Run \`dockito login --update\`.`);let c=H(r,t.to),d=await n.files(r.id,c.project);for(let l of e){let f=new Uint8Array(Oe(l)),m=Je(l),w=t.new?[]:ie(d,c.folder,m),v=w.filter(D=>D.kind==="sheet");v.length&&h(`${m} in ${c.label} is ${v.length>1?`${v.length} live sheets`:"a live sheet"} (${v.map(D=>n.linkOf(D.slug)).join(", ")}): change ${v.length>1?"one":"it"} with \`dockito apply\`. --new makes a new sheet from the file instead.`),w.length>1&&h(`${w.length} docs in ${c.label} are named ${m}: ${w.map(D=>n.linkOf(D.slug)).join(", ")}. Pick one with --doc, or publish a new one with --new.`);let b=w[0];if(b&&t.public&&!(await n.doc(b.slug)).doc.public&&h(`${m} is already a private doc in ${c.label} (${n.linkOf(b.slug)}). Only Share in the app can make it public; \`--new --public\` publishes a new public doc.`),b&&b.sha256===St(f)&&!t.message){s.push({file:l,id:b.id,slug:b.slug,version:b.version,unchanged:!0,link:n.linkOf(b.slug)}),t.json||u(`${g("=")} ${l} ${g("unchanged")} ${n.linkOf(b.slug)}`);continue}let j=b?await n.update(b.id,m,f,{note:t.message,commit:vt(l)}):await n.create(r.id,m,f,{project:c.project,folder:c.folder,public:t.public,title:t.title});!b&&t.message&&await n.save(j.id,xe(l)?1:j.version,t.message);let B="public"in j&&j.public===!0;s.push({file:l,...j,...xe(l)?{type:"sheet"}:{},link:n.linkOf(j.slug),new:!b}),t.json||u(`${k("\u2713")} ${l} \u2192 ${n.linkOf(j.slug)} ${g(b?j.unchanged?"unchanged":"updated":`new ${xe(l)?"sheet ":""}in ${c.label}`)}${B?` ${k("\xB7 public")}`:""}${t.message?g(` \xB7 saved: ${t.message}`):""}`)}t.json?u(JSON.stringify(s,null,2)):!t.to&&s.some(l=>l.new)&&$(g("In the workspace's Docs. To put docs in a project: --to <project/folder>; `dockito tree` lists them."))}var Ae=e=>e==="edit"?"":g(e==="comment"?"comment only":"view only");async function vo(e){let t=await x().tree();if(e.json)return u(JSON.stringify(t,null,2));let o=n=>g(`${n} doc${n===1?"":"s"}`);for(let n of t.workspaces){u(`${A(n.slug)} ${n.name} ${g(`\xB7 ${n.level?V[n.level]:n.role}`)}`);let s=[[g("Docs"),"",o(n.docs)]];for(let i of n.projects){s.push([i.slug,i.name,o(i.docs),Ae(i.role)]);let r=(c,d)=>{for(let l of i.folders.filter(f=>f.parentId===c))s.push([`${d}/${l.name}`,"",o(l.docs),Ae(l.role)]),r(l.id,`${d}/${l.name}`)};r(null,i.slug)}X(s.map(i=>[" ",...i])),u()}$(g("Publish into a place: dockito publish <file> -w <workspace> --to <project/folder>"))}async function jo(e,t){e&&t.to&&e!==t.to&&h(`Two places: ${e} and --to ${t.to}. Give one.`);let o=e??t.to,n=x(),s=await n.tree(),i=W(s,t.workspace),r=H(i,o),c=await n.docs(i.id,{project:r.project,folder:r.folder}),d=r.project?i.projects.find(w=>w.id===r.project):null,l=o?[]:i.projects,f=d?d.folders.filter(w=>w.parentId===r.folder):[];if(t.json)return u(JSON.stringify({workspace:i.slug,place:o??null,projects:l,folders:f,docs:c},null,2));let m=[];for(let w of l)m.push([A(`${w.slug}/`),w.name,g(`${w.docs} doc${w.docs===1?"":"s"}`),Ae(w.role)]);for(let w of f)m.push([A(`${w.name}/`),"",g(`${w.docs} doc${w.docs===1?"":"s"}`),Ae(w.role)]);for(let w of c)m.push([w.slug,w.title+g(w.kind==="html"?" .html":w.kind==="sheet"?" sheet":" .md"),g(ae(w.updatedAt)),g(w.updatedByName??"")]);if(!m.length)return $(g(`Nothing in ${r.label} yet.`));X(m)}async function xo(e,t){let[o,...n]=(e??"").split("/").filter(Boolean);o||h("`dockito mkdir <project>[/folder[/subfolder]]`"),n.length>2&&h("Folders go at most 2 deep inside a project.");let s=x(),i=W(await s.tree(),t.workspace),r=U(i,o),c=r?.id??(await s.createProject(i.id,o)).id,d=!r;r||u(`${k("\u2713")} Project ${o}`);let l=G(r),f=null;for(let m=0;m<n.length;m++){let w=l.idOf(n.slice(0,m+1));if(w){f=w;continue}f=(await s.createFolder(c,n[m],f)).id,l.add(n.slice(0,m+1),f),d=!0,u(`${k("\u2713")} Folder ${n.slice(0,m+1).join("/")}`)}d||$(g("Already there."))}async function To(e,t,o){e||h("`dockito move <doc> [project/folder]` or `dockito move <project/folder> <project[/folder]>`"),t&&o.to&&t!==o.to&&h(`Two places: ${t} and --to ${o.to}. Give one.`);let n=t??o.to,s=x(),i=_e(e),r,c,d;if(i){let l=await s.doc(i),f=H(W(await s.tree(),l.workspace.slug),n);d=await s.moveDoc(l.doc.id,{projectId:f.project,folderId:f.folder}),r=l.doc.title,c=f.label}else{let l=W(await s.tree(),o.workspace),[f,...m]=e.split("/").filter(Boolean),w=H(l,e);(!m.length||!w.folder)&&h(`"${e}" isn't a doc or a folder. A folder is written project/folder; \`dockito ls\` lists docs with their codes.`),n||h(`Where to? \`dockito move ${e} ${f}\` puts it at the top of ${f}.`);let v=H(l,n);v.project!==w.project&&h(`A folder moves within its project: to ${f} or ${f}/<folder>.`),d=await s.moveFolder(w.folder,v.folder),r=w.label,c=v.label}if(o.json)return u(JSON.stringify({moved:r,to:c,lostAccess:d.loses},null,2));u(`${k("\u2713")} ${r} ${g(`\u2192 ${c}`)}`),d.loses.length&&$(g(`No longer open to ${d.loses.join(", ")} (or less than before).`))}async function Oo(e,t){e?.includes("/")||h("`dockito rmdir <project/folder>`: an empty folder. A project is removed in the app.");let o=x(),n=H(W(await o.tree(),t.workspace),e);await o.deleteFolder(n.folder),u(`${k("\u2713")} Removed ${n.label}`)}async function Ao(e){e||h("Name a doc: `dockito open <link or code>`");let t=`${P}/${M(e)}`;At(t)||u(t)}async function So(e,t){e||h("Name a doc: `dockito get <link or code> [-o file]`"),t.outline&&(t.lines!==void 0||t.text)&&h("--outline is the headings only; leave out --lines and --text.");let o=x(),n=await o.doc(M(e));if(n.doc.kind==="sheet"){let d=["entry","outline","lines","text"].filter(f=>t[f]!==void 0);d.length&&h(`A sheet has no file to read by ${d.map(f=>`--${f}`).join(" or ")}: \`dockito read\` gives its rows, a range or totals.`);let l=await o.readSheet(n.doc.id,{outline:!0});return t.output?($t(t.output,`${l.text.trimEnd()}
282
+ `),$(`${k("\u2713")} ${t.output} ${g(`\u2190 ${o.linkOf(n.doc.slug)} outline`)}`)):ne(l)}let s=t.entry?Number(t.entry):null;t.entry&&(!Number.isInteger(s)||s<1)&&h("--entry takes an entry number from `dockito history`.");let i=await o.content(n.doc.id,s,{outline:t.outline,lines:t.lines,text:t.text}),r=i.bytes.length&&i.lines!==null&&!t.output&&i.bytes[i.bytes.length-1]!==10?new Uint8Array([...i.bytes,10]):i.bytes;if(!t.output)return void process.stdout.write(r);$t(t.output,r);let c=t.outline?" outline":i.shown?` lines ${i.shown} of ${i.lines}`:t.text?" as text":"";$(`${k("\u2713")} ${t.output} ${g(`\u2190 ${o.linkOf(n.doc.slug)}${s?` entry ${s}`:""}${c}`)}`)}async function Do(e,t){e||h("Name a doc: `dockito history <link or code>`");let o=await x().doc(M(e));if(t.json)return u(JSON.stringify(o.versions,null,2));if(o.doc.kind==="sheet")return u(`${A(o.doc.title)} ${g(`${P}/${o.doc.slug}`)}`),X(o.versions.map(n=>[String(n.number),g(ae(n.createdAt)),g(`v ${n.roomSeq??0}`),(n.people??[]).map(s=>s.name).join(", ")||(n.byName??""),n.savedAt?`${E("saved")}${n.note?` ${n.note}`:""}`:""]));u(`${A(o.doc.title)} ${g(`${P}/${o.doc.slug}`)}`),X(o.versions.map((n,s)=>[String(n.number),s===0?k("current"):g(ae(n.createdAt)),s===0?g(ae(n.createdAt)):"",n.byName??"",g(n.source),n.savedAt?`${E("saved")}${n.note?` ${n.note}`:""}`:"",n.gitCommit?g(n.gitCommit.slice(0,7)):""]))}async function No(e,t){let o=Number(t);(!e||!Number.isInteger(o)||o<1)&&h("`dockito restore <doc> <entry>`; entries are in `dockito history`.");let n=x(),s=await n.doc(M(e));if(s.doc.kind==="sheet"){let r=await n.restoreSheet(s.doc.id,o);return r.ok||h(r.text),u(`${k("\u2713")} Restored entry ${o} as v ${r.seq} ${g(n.linkOf(s.doc.slug))}`)}let i=await n.restore(s.doc.id,o,s.versions[0]?.number??null);u(`${k("\u2713")} Restored entry ${o} as entry ${i.version} ${g(n.linkOf(i.slug))}`)}async function Po(e,t,o){e||h('`dockito save <doc> [entry] -m "note"`');let n=x(),s=await n.doc(M(e));if(s.doc.kind==="sheet"&&!t){let r=await n.saveSheetNow(s.doc.id,o.message??null);return u(`${k("\u2713")} Saved the sheet as it is now as entry ${r.number} ${g(`v ${r.version}`)}${o.message?g(` \xB7 ${o.message}`):""}`)}let i=t?Number(t):s.versions[0]?.number;(!Number.isInteger(i)||i<1)&&h("Entries are in `dockito history`."),await n.save(s.doc.id,i,o.message??null),u(`${k("\u2713")} Saved entry ${i}${o.message?g(` \xB7 ${o.message}`):""}`)}async function Co(e,t){e||h("`dockito share <link or code> [--private]`"),t.public&&h("An existing doc can be made public only from Share in the app. `dockito publish <file> --public` makes a new doc public.");let o=x(),n=await o.doc(M(e)),s=o.linkOf(n.doc.slug);if(t.private)return n.doc.public&&await o.makePrivate(n.doc.id),t.json?u(JSON.stringify({public:!1,link:s},null,2)):u(`${k("\u2713")} Private: only people in ${n.workspace.name} can open it ${g(s)}`);if(t.json)return u(JSON.stringify({public:n.doc.public,link:s},null,2));u(n.doc.public?`${k("Public")}: anyone with the link can open it ${s}`:`Private: only people in ${n.workspace.name} can open it ${g(s)}`)}var xe=e=>/\.csv$/i.test(e);async function Se(e,t,o){t||h(`Name a sheet: \`dockito ${o}\``);let n=await e.doc(M(t));return n.doc.kind!=="sheet"&&h(`${n.doc.title} is a ${n.doc.kind==="md"?"Markdown":"HTML"} doc, not a sheet: \`dockito get\` reads it.`),n}function ne(e,t){t?u(JSON.stringify(e,null,2)):u(e.text.trimEnd()),e.ok||(process.exitCode=1)}var oe=(e,t,o)=>{if(e===void 0)return;let n=Number(e);return(!/^\d+$/.test(e.trim())||n<o)&&h(`--${t} takes a whole number, ${o} or more.`),n},Be=e=>e===void 0?void 0:e.split(",").map(t=>t.trim()).filter(Boolean);async function Eo(e,t,o){let n=x(),s=await Se(n,e,"read <doc> [range]"),i=oe(o.chart,"chart",1);if(i!==void 0){let c=Object.entries(o).filter(([d,l])=>d!=="chart"&&d!=="json"&&l!==void 0);return(t||c.length)&&h(`--chart goes alone: it gives the chart's points as drawn, not ${[...t?["a range"]:[],...c.map(([d])=>`--${d}`)].join(", ")}.`),ne(await n.readSheet(s.doc.id,{read:{chart:i}}),o.json)}let r=await n.readSheet(s.doc.id,{read:{range:t,sheet:o.sheet,where:o.where,select:Be(o.select),orderBy:o["order-by"],group:Be(o.group),totals:Be(o.totals),from:oe(o.from,"from",1),limit:oe(o.limit,"limit",1),formulas:o.formulas,styles:o.styles,format:o.csv?"csv":void 0}});ne(r,o.json)}async function Io(e,t,o){let n=x(),s=await Se(n,e,"apply <doc> <changes.json>"),i=oe(o.undo,"undo",1),r;if(i!==void 0)(t||o.base!==void 0)&&h("--undo goes alone: no changes file and no --base with it."),r={undo:i,note:o.message,dryRun:o["dry-run"]||void 0};else{!t&&process.stdin.isTTY&&h("Give the changes: `dockito apply <doc> changes.json`, or pipe them in with `-`. `dockito apply --help` shows their form.");let d=!t||t==="-"?Oe(0,"utf8"):Tt(t)?Oe(t,"utf8"):h(`${t} isn't a file.`),l;try{l=JSON.parse(d)}catch{h(`${t&&t!=="-"?t:"The input"} isn't JSON: a list of changes, or {"base": N, "ops": [...]}.`)}let f=Array.isArray(l)?{ops:l}:l;(typeof f!="object"||f===null)&&h('The changes are a list, or {"base": N, "ops": [...]}.');let m=oe(o.base,"base",0)??f.base;m===void 0&&h("--base is the version you read: v N in `dockito get` or `dockito read`."),r={...f,base:m,note:o.message??f.note,dryRun:o["dry-run"]||f.dryRun||void 0}}let c=o.key??Ro(r);ne(await n.changeSheet(s.doc.id,{key:c,...r}),o.json)}var Ro=e=>St(new TextEncoder().encode(JSON.stringify(e))).slice(0,32);async function _o(e,t){let o=x(),n=await Se(o,e,"check <doc>"),s=await o.readSheet(n.doc.id,{check:t.sheet?{sheet:t.sheet}:{}});ne(s,t.json),s.ok&&!s.text.includes("no problems found")&&(process.exitCode=1)}async function Lo(e,t){let o=x(),n=await Se(o,e,"changes <doc> --since <version>");t.since===void 0&&h('--since is a version (412) or a time in UTC (2026-10-13, "2026-10-13 09:00").');let s=/^\s*\d+\s*$/.test(t.since)?Number(t.since):t.since;ne(await o.readSheet(n.doc.id,{changes:{since:s,max:oe(t.max,"max",1)}}),t.json)}var qe=`Dockito CLI ${Ge}
256
283
 
257
- ${Ke}
284
+ ${He}
258
285
 
259
286
  Usage: dockito <command>
260
287
 
261
- ${Object.values(ve).map(e=>` ${e.use.padEnd(30)}${e.about}`).join(`
288
+ ${Object.values(je).map(e=>` ${e.use.padEnd(30)}${e.about}`).join(`
262
289
  `)}
263
290
 
264
291
  <doc> is a link or a code. \`dockito <command> --help\` for more.
265
292
  -w, --workspace <slug> Another workspace (default: your personal one)
266
293
  --json Machine-readable output
267
294
 
268
- Environment: DOCKITO_TOKEN (use this token), DOCKITO_URL (another Dockito)`,jt=e=>{let t=ve[e];return t?`dockito ${t.use}
295
+ Environment: DOCKITO_TOKEN (use this token), DOCKITO_URL (another Dockito)`,jt=e=>{let t=je[e];return t?`dockito ${t.use}
269
296
 
270
- ${t.page}`:null},_n={login:{flags:["update","no-browser"],args:0},logout:{flags:[],args:0},whoami:{flags:["json"],args:0},sync:{flags:["to","workspace","json","yes","dry-run","create","fold"],args:1},publish:{flags:["message","to","doc","new","title","workspace","json","public"],args:1/0},tree:{flags:["json"],args:0},ls:{flags:["to","workspace","json"],args:1},mkdir:{flags:["workspace"],args:1},share:{flags:["public","private","json"],args:1},open:{flags:[],args:1},get:{flags:["output","entry","outline","lines","text"],args:1},history:{flags:["json"],args:1},restore:{flags:[],args:2},save:{flags:["message"],args:2},agent:{flags:[],args:0},read:{flags:["sheet","where","select","order-by","group","totals","from","limit","formulas","styles","csv","chart","json"],args:2},apply:{flags:["base","message","dry-run","undo","key","json"],args:2},check:{flags:["sheet","json"],args:1},changes:{flags:["since","max","json"],args:1}},xt={message:"-m",workspace:"-w",output:"-o",yes:"-y"};function Rn(e,t,n){let o=_n[e];if(!o)return;let s=`\`dockito ${ve[e]?.use??e}\`; \`dockito ${e} --help\` for more.`,i=t.filter(a=>!o.flags.includes(a));if(i.length){let a=i.map(d=>`--${d}`).join(", "),c=o.flags.length?` It takes ${o.flags.map(d=>xt[d]?`${xt[d]}/--${d}`:`--${d}`).join(", ")}.`:"";p(`${e} doesn't take ${a}.${c} Use: ${s}`)}n.length>o.args&&p(`${e} takes ${o.args===0?"no arguments":`at most ${o.args} argument${o.args===1?"":"s"}`}; got ${n.map(a=>`"${a}"`).join(" ")}. Use: ${s}`)}async function Ln(){let{values:e,positionals:t}=hn({allowPositionals:!0,options:{message:{type:"string",short:"m"},to:{type:"string"},doc:{type:"string"},new:{type:"boolean"},title:{type:"string"},workspace:{type:"string",short:"w"},output:{type:"string",short:"o"},entry:{type:"string"},outline:{type:"boolean"},lines:{type:"string"},text:{type:"boolean"},json:{type:"boolean"},update:{type:"boolean"},yes:{type:"boolean",short:"y"},"dry-run":{type:"boolean"},create:{type:"boolean"},fold:{type:"boolean"},"no-browser":{type:"boolean"},public:{type:"boolean"},private:{type:"boolean"},sheet:{type:"string"},where:{type:"string"},select:{type:"string"},"order-by":{type:"string"},group:{type:"string"},totals:{type:"string"},from:{type:"string"},limit:{type:"string"},formulas:{type:"boolean"},styles:{type:"boolean"},csv:{type:"boolean"},chart:{type:"string"},base:{type:"string"},undo:{type:"string"},key:{type:"string"},since:{type:"string"},max:{type:"string"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}}),[n,...o]=t;if(e.version)return h(qe);if(n==="help"&&o[0])return h(jt(o[0])??We);if(e.help&&n)return h(jt(n)??We);if(e.help||!n||n==="help")return h(We);switch(Rn(n,Object.keys(e).filter(s=>e[s]!==void 0),o),n){case"login":return wn(e);case"logout":return yn();case"whoami":return kn(e);case"sync":return kt(T(),o[0],e);case"publish":return bn(o,e);case"tree":return $n(e);case"ls":return vn(o[0],e);case"mkdir":return jn(o[0],e);case"share":return Dn(o[0],e);case"open":return xn(o[0]);case"get":return Tn(o[0],e);case"history":return On(o[0],e);case"restore":return Sn(o[0],o[1]);case"save":return An(o[0],o[1],e);case"agent":return h(Ke);case"read":return Nn(o[0],o[1],e);case"apply":return Pn(o[0],o[1],e);case"check":return En(o[0],e);case"changes":return In(o[0],e);default:p(`Unknown command "${n}". Run \`dockito --help\`.`)}}function je(e){let t=2,n=()=>--t===0&&process.exit(e);process.stdout.write("",n),process.stderr.write("",n)}Ln().then(()=>je(Number(process.exitCode??0)),e=>{let t=process.argv.includes("--json");if(e instanceof N)return t?h(JSON.stringify({error:e.code,message:e.message.replace(/\x1b\[[0-9;]*m/g,""),...e.data},null,2)):$(e.message),je(2);if(e instanceof ie)return je(130);t&&e instanceof x?h(JSON.stringify({error:e.code,message:re(e,C),...e.detail},null,2)):t&&e instanceof J?h(JSON.stringify({error:"failed",message:e.message},null,2)):e instanceof J?$(_(e.message)):e instanceof x?$(_(re(e,C))):e instanceof TypeError&&/parseArgs|option|positional/i.test(String(e.message))?$(_(`${e.message}. Run \`dockito --help\`.`)):$(_(String(e?.stack??e))),je(1)});
297
+ ${t.page}`:null},Fo={login:{flags:["update","no-browser"],args:0},logout:{flags:[],args:0},whoami:{flags:["json"],args:0},sync:{flags:["to","workspace","json","yes","dry-run","create","fold"],args:1},publish:{flags:["message","to","doc","new","title","workspace","json","public"],args:1/0},tree:{flags:["json"],args:0},ls:{flags:["to","workspace","json"],args:1},mkdir:{flags:["workspace"],args:1},move:{flags:["to","workspace","json"],args:2},rmdir:{flags:["workspace"],args:1},share:{flags:["public","private","json"],args:1},open:{flags:[],args:1},get:{flags:["output","entry","outline","lines","text"],args:1},history:{flags:["json"],args:1},restore:{flags:[],args:2},save:{flags:["message"],args:2},agent:{flags:[],args:0},read:{flags:["sheet","where","select","order-by","group","totals","from","limit","formulas","styles","csv","chart","json"],args:2},apply:{flags:["base","message","dry-run","undo","key","json"],args:2},check:{flags:["sheet","json"],args:1},changes:{flags:["since","max","json"],args:1}},xt={message:"-m",workspace:"-w",output:"-o",yes:"-y"};function Mo(e,t,o){let n=Fo[e];if(!n)return;let s=`\`dockito ${je[e]?.use??e}\`; \`dockito ${e} --help\` for more.`,i=t.filter(r=>!n.flags.includes(r));if(i.length){let r=i.map(d=>`--${d}`).join(", "),c=n.flags.length?` It takes ${n.flags.map(d=>xt[d]?`${xt[d]}/--${d}`:`--${d}`).join(", ")}.`:"";h(`${e} doesn't take ${r}.${c} Use: ${s}`)}o.length>n.args&&h(`${e} takes ${n.args===0?"no arguments":`at most ${n.args} argument${n.args===1?"":"s"}`}; got ${o.map(r=>`"${r}"`).join(" ")}. Use: ${s}`)}async function Ko(){let{values:e,positionals:t}=go({allowPositionals:!0,options:{message:{type:"string",short:"m"},to:{type:"string"},doc:{type:"string"},new:{type:"boolean"},title:{type:"string"},workspace:{type:"string",short:"w"},output:{type:"string",short:"o"},entry:{type:"string"},outline:{type:"boolean"},lines:{type:"string"},text:{type:"boolean"},json:{type:"boolean"},update:{type:"boolean"},yes:{type:"boolean",short:"y"},"dry-run":{type:"boolean"},create:{type:"boolean"},fold:{type:"boolean"},"no-browser":{type:"boolean"},public:{type:"boolean"},private:{type:"boolean"},sheet:{type:"string"},where:{type:"string"},select:{type:"string"},"order-by":{type:"string"},group:{type:"string"},totals:{type:"string"},from:{type:"string"},limit:{type:"string"},formulas:{type:"boolean"},styles:{type:"boolean"},csv:{type:"boolean"},chart:{type:"string"},base:{type:"string"},undo:{type:"string"},key:{type:"string"},since:{type:"string"},max:{type:"string"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}}),[o,...n]=t;if(e.version)return u(Ge);if(o==="help"&&n[0])return u(jt(n[0])??qe);if(e.help&&o)return u(jt(o)??qe);if(e.help||!o||o==="help")return u(qe);switch(Mo(o,Object.keys(e).filter(s=>e[s]!==void 0),n),o){case"login":return yo(e);case"logout":return ko();case"whoami":return bo(e);case"sync":return kt(x(),n[0],e);case"publish":return $o(n,e);case"tree":return vo(e);case"ls":return jo(n[0],e);case"mkdir":return xo(n[0],e);case"move":return To(n[0],n[1],e);case"rmdir":return Oo(n[0],e);case"share":return Co(n[0],e);case"open":return Ao(n[0]);case"get":return So(n[0],e);case"history":return Do(n[0],e);case"restore":return No(n[0],n[1]);case"save":return Po(n[0],n[1],e);case"agent":return u(He);case"read":return Eo(n[0],n[1],e);case"apply":return Io(n[0],n[1],e);case"check":return _o(n[0],e);case"changes":return Lo(n[0],e);default:h(`Unknown command "${o}". Run \`dockito --help\`.`)}}function Te(e){let t=2,o=()=>--t===0&&process.exit(e);process.stdout.write("",o),process.stderr.write("",o)}Ko().then(()=>Te(Number(process.exitCode??0)),e=>{let t=process.argv.includes("--json");if(e instanceof N)return t?u(JSON.stringify({error:e.code,message:e.message.replace(/\x1b\[[0-9;]*m/g,""),...e.data},null,2)):$(e.message),Te(2);if(e instanceof le)return Te(130);t&&e instanceof O?u(JSON.stringify({error:e.code,message:ce(e,P),...e.detail},null,2)):t&&e instanceof z?u(JSON.stringify({error:"failed",message:e.message},null,2)):e instanceof z?$(_(e.message)):e instanceof O?$(_(ce(e,P))):e instanceof TypeError&&/parseArgs|option|positional/i.test(String(e.message))?$(_(`${e.message}. Run \`dockito --help\`.`)):$(_(String(e?.stack??e))),Te(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dockito",
3
- "version": "0.0.1-beta.13",
3
+ "version": "0.0.1-beta.15",
4
4
  "description": "Dockito CLI: publish the docs your AI writes as links you can share.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://dockito-site.dockito.workers.dev",