dockito 0.0.1-beta.10 → 0.0.1-beta.12

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 +70 -23
  2. package/package.json +1 -1
package/dockito.js CHANGED
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as rn,spawnSync as kt}from"node:child_process";import{createHash as an}from"node:crypto";import{existsSync as xt,readFileSync as xe,statSync as cn,writeFileSync as bt}from"node:fs";import{hostname as ln}from"node:os";import{basename as Be,dirname as dn,isAbsolute as un,resolve as Tt}from"node:path";import{parseArgs as pn}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}`,te=(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 fe(e){let t=e.url.replace(/\/+$/,"");async function n(a,c,u={}){let l=new URL(`${t}/api${c}`);for(let[k,v]of Object.entries(u.query??{}))v!=null&&v!==""&&l.searchParams.set(k,String(v));let m={},f=u.token===void 0?e.token:u.token;f&&(m.authorization=`Bearer ${f}`),e.userAgent&&(m["user-agent"]=e.userAgent);let w;u.json!==void 0?(m["content-type"]="application/json",w=JSON.stringify(u.json)):u.body&&(m["content-type"]="application/octet-stream",w=u.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:u,...l}=c;throw new x(typeof u=="string"?u:`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:u,...l}=c;throw new x(typeof u=="string"?u:`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}),u=await c.json().catch(()=>({}));if(c.ok)return u;if(u.error==="authorization_pending"||u.error==="slow_down")return null;throw new x(u.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,u)=>o(await n("POST",`/projects/${i(a)}/folders`,{json:{name:c,parentId:u}})),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,u={})=>{let l=await n("GET",`/docs/${i(a)}/content`,{query:{v:c,outline:u.outline?1:null,lines:u.lines,text:u.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,u,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:u})),update:async(a,c,u,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:u})),restore:async(a,c,u)=>o(await n("POST",`/docs/${i(a)}/history/${c}/restore`,{query:{base:u}})),save:async(a,c,u)=>o(await n("PUT",`/docs/${i(a)}/history/${c}/saved`,{json:{note:u}})),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 It}from"node:readline";import{createInterface as _t}from"node:readline/promises";var Ct=!process.env.NO_COLOR&&process.stdout.isTTY,z=e=>t=>Ct?`\x1B[${e}m${t}\x1B[0m`:t,g=z("2"),S=z("1"),b=z("32"),_=z("31"),R=z("33"),Ce=z("36"),h=(e="")=>process.stdout.write(e+`
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+`
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 ne(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 oe(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"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,V=e=>process.stdout.write(e);function at(e,t,n=0){return new Promise((o,s)=>{let i=n,a=l=>{l||V(`\x1B[${t.length}A`);for(let[m,f]of t.entries())V(`\x1B[2K${m===i?Ce("\u276F "+f.label):" "+f.label}${f.hint?g(" "+f.hint):""}
5
- `)};V(`${b("?")} ${S(e)} ${g("(\u2191\u2193, Enter)")}
6
- \x1B[?25l`),a(!0),It(process.stdin),process.stdin.setRawMode(!0),process.stdin.resume();let c=()=>{process.stdin.off("keypress",u),process.stdin.setRawMode(!1),process.stdin.pause(),V("\x1B[?25h")},u=(l,m)=>{if(m.ctrl&&m.name==="c")return c(),V(`
7
- `),s(new se);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(),V(`\x1B[${t.length+1}A\x1B[0J${b("?")} ${S(e)} ${Ce(t[i].label)}
8
- `),o(i)):void 0;a(!1)};process.stdin.on("keypress",u)})}async function we(e,t=""){let n=_t({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 se=class extends Error{};import{homedir as Rt}from"node:os";import{join as Re}from"node:path";var Lt="https://dockito-app.dockito.workers.dev";function ct(){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(Rt(),".config"),"dockito")}var lt=()=>(process.env.DOCKITO_URL||Lt).replace(/\/+$/,""),dt=e=>new URL(e).host;import{spawnSync as Ft}from"node:child_process";import{chmodSync as Mt,existsSync as Le,mkdirSync as Kt,readFileSync as Ht,rmSync as ut,writeFileSync as Ut}from"node:fs";import{dirname as Wt,join as Bt}from"node:path";var Y="dockito";function X(e,t,n){try{let o=Ft(e,t,{input:n,encoding:"utf8",stdio:["pipe","pipe","pipe"]});return o.error?null:o}catch{return null}}var re=e=>Bt(ct(),"tokens",`${e.replace(/[^a-z0-9.-]/gi,"_")}`);function qt(e,t){if(process.platform==="darwin"){let n=X("security",["-i"],`add-generic-password -U -s ${Y} -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",Y,"account",e],t);return!!n&&n.status===0}return!1}function Jt(e){let t=process.platform==="darwin"?X("security",["find-generic-password","-s",Y,"-a",e,"-w"]):process.platform==="linux"?X("secret-tool",["lookup","service",Y,"account",e]):null;return(t?.status===0?t.stdout.trim():"")||null}function Gt(e){process.platform==="darwin"?X("security",["delete-generic-password","-s",Y,"-a",e]):process.platform==="linux"&&X("secret-tool",["clear","service",Y,"account",e])}function pt(e,t){if(qt(e,t))return Le(re(e))&&ut(re(e)),"keychain";let n=re(e);return Kt(Wt(n),{recursive:!0,mode:448}),Ut(n,t,{mode:384}),Mt(n,384),"file"}function ye(e){if(process.env.DOCKITO_TOKEN)return process.env.DOCKITO_TOKEN.trim();let t=Jt(e);if(t)return t;let n=re(e);return Le(n)&&Ht(n,"utf8").trim()||null}function ht(e){Gt(e);let t=re(e);Le(t)&&ut(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 zt}from"node:crypto";import{spawnSync as Vt}from"node:child_process";import{existsSync as ae,readFileSync as Yt,readdirSync as Xt,statSync as mt}from"node:fs";import{basename as gt,join as L,relative as Qt,resolve as Me,sep as Zt}from"node:path";var en=4,ie=e=>e<1024*1024?`${e?Math.max(1,Math.round(e/1024)):0} KB`:`${(e/1048576).toFixed(1)} MB`,tn=e=>zt("sha256").update(e).digest("hex"),wt=e=>e.split(Zt).join("/"),Q=(e,t)=>`${e} ${t}${e===1?"":"s"}`,nn=(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 on(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 Xt(L(e,i),{withFileTypes:!0}).flatMap(a=>a.name.startsWith(".")||a.name==="node_modules"?[]:a.isDirectory()?s(L(i,a.name)):[wt(L(i,a.name))])})("")).filter(s=>t(s)&&ae(L(e,s))&&mt(L(e,s)).isFile()).sort()}function sn(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)&&ae(L(t,s))))]}function ft(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 yt(e,t,n){let o=Ie()&&!n.json,s=await e.tree(),i=r=>!!nn(s.kinds,r);s.workspaces.some(I)||p("This login can't publish anywhere. Run `dockito login --update`.");let a=t;a||(o||ft(s,ae("docs")?"docs":"."),a=await we("Folder to sync",ae("docs")?"docs":"."));let c=Me(a);(!ae(c)||!mt(c).isDirectory())&&p(`${a} isn't a folder.`);let u=wt(Qt(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||ft(s,u);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}]),d=r[await at("Project",r)];l=d.w,m=d.p??await we("Project name",gt(c===process.cwd()?c:Me(c,".."))),n.create||=!d.p}if(!l){let r=s.workspaces.filter(y=>I(y)&&H(y,m.split("/")[0])),d=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&&d?d: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=on(c,i);v.length||p(`No ${s.kinds.flatMap(r=>r.extensions).join(", ")} files in ${u}.`);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",d=`${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",`${d}
12
- Ask the user, then run again with --fold.`,{files:D});if($(d),!await _e("Fold them like that?"))return $(g("Nothing changed."));n.fold=!0}let ze=r=>{let d=r.split("/");return{folder:[...O,...d.slice(0,-1).slice(0,U)],dropped:d.slice(0,-1).slice(U),file:d[d.length-1]}},Oe=new Map;for(let r of v){let d=ze(r),y=[...d.folder,d.file].join("/").toLowerCase();Oe.set(y,(Oe.get(y)??0)+1)}let Ve=k?await e.files(f.id,k.id):[],Z=q(k),K=v.map(r=>{let{folder:d,dropped:y,file:j}=ze(r),F=y.length&&Oe.get([...d,j].join("/").toLowerCase())>1?[...y,j].join("-"):j,P=new Uint8Array(Yt(L(c,r))),B={rel:r,name:F,folder:d,bytes:P,hash:tn(P),action:"new",doc:null,others:[],assets:[]};/\.html?$/i.test(r)&&(B.assets=sn(new TextDecoder().decode(P),L(c,r,".."),i));let rt=Z.idOf(d);if(rt===void 0)return B;let ee=te(Ve,rt,F);return ee.length>1?{...B,action:"ambiguous",others:ee}:ee.length?{...B,doc:ee[0],action:ee[0].sha256===B.hash?"same":"update"}:B}),Ye=K.filter(r=>r.bytes.length>f.maxFile);Ye.length&&p(`Over the ${ie(f.maxFile)} limit per file in ${f.slug}:
13
- ${Ye.map(r=>` ${r.rel} ${ie(r.bytes.length)}`).join(`
14
- `)}`);let Se=new Map;for(let r of K){let d=[...r.folder,r.name].join("/").toLowerCase();Se.set(d,[...Se.get(d)??[],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:
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
15
  ${Xe.map(r=>" "+r.join(" and ")).join(`
16
- `)}`);let C=[],Qe=new Set;for(let r of K)for(let d=1;d<=r.folder.length;d++){let y=r.folder.slice(0,d),j=y.join("/").toLowerCase();Z.idOf(y)===void 0&&!Qe.has(j)&&(Qe.add(j),C.push(y))}C.sort((r,d)=>r.length-d.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,At=new Set(K.flatMap(r=>r.doc?[r.doc.id]:r.others.map(d=>d.id))),De=Ve.filter(r=>!At.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:C.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(d=>e.linkOf(d.slug))})),notHere:De};if(!n.json){h(`${S(u==="."?"./":u+"/")} \u2192 ${S(et)}`),k||h(` ${b("+ project")} ${w}`);for(let d of C)h(` ${b("+ folder ")} ${d.join("/")}`);if(A.length>15)for(let[d,y]of[["new",b("+ new ")],["update",R("~ update ")]]){let j=new Map;for(let F of A.filter(P=>P.action===d))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 d of A){let y=d.name!==gt(d.rel)?g(` \u2192 ${[...d.folder,d.name].join("/")}`):"";h(` ${d.action==="new"?b("+ new "):R("~ update ")} ${d.rel}${y}`)}Ae&&h(g(` ${Ae} unchanged`));for(let d of Ze)h(` ${_("? skip ")} ${d.rel} ${g(`${d.others.length} docs there have this name: ${d.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, ${ie(A.reduce((d,y)=>d+y.bytes.length,0))}`));let r=A.filter(d=>d.assets.length);r.length&&h(R(` ! ${r.length===1?r[0].rel:`${r.length} HTML files`} load ${[...new Set(r.flatMap(d=>d.assets))].join(", ")} from next to ${r.length===1?"it":"them"}; only HTML files are published`))}if(!A.length&&k&&!C.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||C.length)&&!n.yes){let r=["dockito sync",u,`-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 C){let d=await e.createFolder(Ne.id,r[r.length-1],r.length>1?Z.idOf(r.slice(0,-1)):null);Z.add(r,d.id)}C.length&&!n.json&&h(`${b("\u2713")} ${Q(C.length,"folder")}`);let pe=$e(["rev-parse","--show-toplevel"],c)?.trim()??null,tt=pe?$e(["rev-parse","HEAD"],pe)?.trim()??null:null,Dt=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(),Nt=A.reduce((r,d)=>r+d.bytes.length,0),nt=0,ot=()=>ge&&process.stdout.write(`\r\x1B[2K${g(`Sending ${W.length} of ${A.length} \xB7 ${ie(nt)} of ${ie(Nt)}`)}`);async function Pt(r){let d=tt&&!Dt.has(L(c,r.rel))?tt:null;for(;;)try{let y=r.doc?await e.update(r.doc.id,r.name,r.bytes,{commit:d}):await e.create(f.id,r.name,r.bytes,{project:Ne.id,folder:Z.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?oe(y,e.url):String(y)}}}ot(),await Promise.all(Array.from({length:Math.min(en,Pe.length)},async()=>{for(let r=Pe.shift();r;r=Pe.shift()){let d=await Pt(r);W.push(d),nt+=r.bytes.length,ge&&process.stdout.write("\r\x1B[2K"),d.error&&!n.json?h(`${_("\u2717")} ${d.file} ${_(d.error)}`):!n.json&&!ge&&h(`${b("\u2713")} ${d.file} ${g(d.link)}`),ot()}})),ge&&process.stdout.write("\r\x1B[2K");let Ee=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(`${Ee.length?R(`${W.length-Ee.length} of ${W.length} sent`):b("\u2713 Synced")} ${g(st)}`),Ee.length&&(process.exitCode=1)}var Ke=`Dockito hosts Markdown and HTML docs, and live sheets, at a short link,
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,
18
18
  with History.
19
19
 
20
20
  workspace a person's or a team's space; its slug is in every link
@@ -120,7 +120,8 @@ version to apply changes at; \`dockito read\` gives its rows.
120
120
  dockito get <doc> --lines 120-260 --text`},read:{use:"read <doc> [range]",about:"A sheet's rows, a range or totals",page:`Prints rows of a sheet as a text table: row numbers, values as shown, and
121
121
  the version (v N) to apply changes at. A range, or a sheet's rows under its
122
122
  header row, filtered and sorted; with --group or --totals, totals instead.
123
- 200 rows unless --limit says, at most 1,000; page on with --from.
123
+ 200 rows unless --limit says, at most 1,000; page on with --from. --chart
124
+ gives a chart's points instead.
124
125
 
125
126
  [range] Tasks!A1:G40, Tasks!B:B, Tasks!3:5, a defined name,
126
127
  or E17 when there's one sheet
@@ -134,12 +135,15 @@ header row, filtered and sorted; with --group or --totals, totals instead.
134
135
  --totals <list> count, sum Hours, avg Hours, min Due, max Due
135
136
  --from <n>, --limit <n> Which rows: from 1; 200 by default, 1,000 at most
136
137
  --formulas Formulas with their results: =F2*50 \u2192 50
137
- --styles Formatting in words, merges, sizes, frozen panes
138
+ --styles Formatting in words, merges, sizes, frozen panes;
139
+ cells a conditional format colours say so
138
140
  --csv As CSV instead of a table
141
+ --chart <n> Chart #n's points as a table, as drawn (alone)
139
142
 
140
143
  dockito read <doc> --where "Status = Blocked" --select ID,Owner,Due
141
144
  dockito read <doc> --group Owner --totals "count,sum Hours"
142
- dockito read <doc> "Tasks!A1:G20" --formulas`},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
145
+ dockito read <doc> "Tasks!A1:G20" --formulas
146
+ 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
143
147
  changes showed. Addresses, rows and headers mean what they meant then,
144
148
  wherever they moved since. Every change applies, or none: a cell someone
145
149
  changed since is refused, naming who (exit 1). Read again and apply on the
@@ -178,6 +182,49 @@ The file (or - for stdin) is a list of changes, or {"base": N, "ops": [...],
178
182
  frozenCols, hidden, tabColor), setName, deleteName, setTable, deleteTable
179
183
  {"addTable": {"range": "Tasks!A1:D40", "name": "Tasks"}}: a header row
180
184
  and banded rows in one change; a cell's own fill shows over them
185
+ {"validate": {"range": "Tasks!D2:D", "list": ["Open", "Done"], "reject": true}}:
186
+ a rule from row 2 down (rows added later too); reject refuses other
187
+ values, else they're taken and marked. One kind each: list,
188
+ from ("Lists!A2:A20"), checkbox (true), number ({"between": [0, 100]},
189
+ "at most", "at least", \u2026), date ({"after": "2026-01-01"}), text
190
+ ({"contains": "x"}), email, url, length ({"at most": 50}), formula
191
+ ("=C2<D2"); "help" is shown with a refusal; "remove": true takes it off
192
+ {"conditional": {"range": "Tasks!D2:D", "text is": "Blocked",
193
+ "format": {"fill": "#f6d3d0", "bold": true}}}: the outline lists rules
194
+ in order (format #N); the first that matches a cell wins; new ones go
195
+ last. One condition each: empty, "not empty", "text contains", "text not
196
+ contains", "text starts with", "text ends with", "text is", "date is",
197
+ "date before", "date after" ("today", "past week", "2026-01-01", \u2026),
198
+ number ({"greater than": 8}), formula ("=$F2<TODAY()"), scale ({"min":
199
+ "#ffffff", "max": "#57bb8a"}, no format), duplicate, unique, top (10 or
200
+ "10%"), bottom, "above average", "below average". Format: bold, italic,
201
+ underline, strike, color, fill. "before": 2 or "first": true places it;
202
+ {"rule": 3, ...} changes or moves rule 3; "remove": true with rule or
203
+ range takes rules off
204
+ {"replace": {"find": "Lee", "with": "Priya", "in": "Owner"}}: find and
205
+ replace as one change; "in" is a column by header or letter (its rows
206
+ under the header) or a range ("Tasks!C2:C"); left out, the whole sheet
207
+ ("sheet" names it). "with" may be "". "case": true, "whole": true (the
208
+ whole cell), "regex": true (RE2; $1 in "with"), "formulas": true
209
+ (change formulas; else one matches by its result and is left). Says
210
+ what was left and why; over 100,000 cells changes nothing
211
+ {"chart": {"range": "Tasks!A1:C40", "type": "column", "title": "Costs"}}:
212
+ the outline lists charts as chart #N. "header" (first row names the
213
+ series) and "labels" (first column labels the points) are guessed when
214
+ left out. Type: column, bar, line, area, pie, donut, scatter (first
215
+ column: x), or "stacked column", "stacked bar", "stacked area",
216
+ "doughnut", "horizontal bar"; "stacked": true for column, bar, area.
217
+ "at": "E2", "below", "beside", "below chart #2" or "beside chart #2";
218
+ left out, beside the range on its first row, past whatever fills those
219
+ rows (under a wide range when that's clear), never over filled cells
220
+ or another chart (the sheet grows to fit). "width",
221
+ "height" in px, 40 to 4,000 (left out: fitted, 480 \xD7 300, wider past
222
+ 24 points; a pie 420 \xD7 300). "title" (left out: the series by the
223
+ label column, "Kitchen and Bath by Month"), "x title", "y title";
224
+ "legend": right, bottom, top, none; "colors": ["#4285f4", null] per
225
+ series. The answer says what it draws and warns about a total row or
226
+ column, or a column with no numbers. {"chart": 2, ...} changes chart 2;
227
+ {"chart": 2, "remove": true} removes it
181
228
  Values: a number, true, false, null (empty), a string ("=\u2026" is a formula),
182
229
  {"text": "=as typed"} or {"date": "2026-10-19"}.
183
230
 
@@ -200,11 +247,11 @@ Ends at the current version, to apply changes at.
200
247
  --since <v|time> 412, 2026-10-13 or "2026-10-13 09:00"
201
248
  --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,
202
249
  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
203
- never drops it, with an optional note.`},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
250
+ 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
204
251
  in each workspace: View, Publish or Everything. --update changes that access.
205
- --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.10",E=lt(),ce=dt(E),Je=`dockito-cli/${qe} (${process.platform})`;function T(){let e=ye(ce);return e||p("Not logged in. Run `dockito login`."),fe({url:E,token:e,source:"cli",userAgent:Je})}function Ot(e){let[t,n]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["cmd",["/c","start","",e]]:["xdg-open",[e]];try{let o=rn(t,n,{stdio:"ignore",detached:!0});return o.on("error",()=>{}),o.unref(),!0}catch{return!1}}var hn=()=>!process.env.SSH_CONNECTION&&!process.env.CI&&(process.platform!=="linux"||!!process.env.DISPLAY||!!process.env.WAYLAND_DISPLAY),gn=e=>new Promise(t=>setTimeout(t,e)),fn=()=>ln().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 $t(e){let t=dn(Tt(e)),n=kt("git",["rev-parse","HEAD"],{cwd:t,encoding:"utf8"});if(n.error||n.status!==0)return null;let o=kt("git",["status","--porcelain","--",Be(e)],{cwd:t,encoding:"utf8"});return o.status!==0||o.stdout.trim()?null:n.stdout.trim()}var St=e=>an("sha256").update(e).digest("hex");async function mn(e){let t=ye(ce);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:E,token:e.update?t:null,userAgent:Je}),o=await n.startLogin("Dockito CLI",fn()),s=!e["no-browser"]&&hn()&&Ot(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 gn(i);try{let c=await n.pollLogin(o.device_code);if(!c)continue;let u=pt(ce,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]}`)}`);u==="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 wn(){let e=ye(ce);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:E,token:e,userAgent:Je}).logout()}catch(t){if(!(t instanceof x)||t.code!=="invalid_token"&&t.code!=="not_found")throw t}ht(ce),h(`${b("\u2713")} Logged out`)}async function yn(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(),ne(t.workspaces.map(n=>[n.slug,n.name,g(n.level?G[n.level]:n.role)])),E!=="https://dockito-app.dockito.workers.dev"&&$(g(`
206
- ${E}`))}async function kn(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)(!xt(l)||!cn(l).isFile())&&p(`${l} isn't a file${un(l)?"":` (looked for ${Tt(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:$t(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),u=await o.files(a.id,c.project);for(let l of e){let m=new Uint8Array(xe(l)),f=Be(l),w=t.new?[]:te(u,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===St(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:$t(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 bn(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,u)=>{for(let l of i.folders.filter(m=>m.parentId===c))s.push([`${u}/${l.name}`,"",n(l.docs)]),a(l.id,`${u}/${l.name}`)};a(null,i.slug)}ne(s.map(i=>[" ",...i])),h()}$(g("Publish into a place: dockito publish <file> -w <workspace> --to <project/folder>"))}async function $n(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}),u=a.project?i.projects.find(w=>w.id===a.project):null,l=n?[]:i.projects,m=u?u.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.`));ne(f)}async function vn(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,u=!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),u=!0,h(`${b("\u2713")} Folder ${o.slice(0,f+1).join("/")}`)}u||$(g("Already there."))}async function jn(e){e||p("Name a doc: `dockito open <link or code>`");let t=`${E}/${M(e)}`;Ot(t)||h(t)}async function xn(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 u=["entry","outline","lines","text"].filter(m=>t[m]!==void 0);u.length&&p(`A sheet has no file to read by ${u.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?(bt(t.output,`${l.text.trimEnd()}
207
- `),$(`${b("\u2713")} ${t.output} ${g(`\u2190 ${n.linkOf(o.doc.slug)} outline`)}`)):de(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);bt(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 Tn(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(`${E}/${n.doc.slug}`)}`),ne(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 On(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 Sn(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 An(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 de(e,t){t?h(JSON.stringify(e,null,2)):h(e.text.trimEnd()),e.ok||(process.exitCode=1)}var le=(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 Dn(e,t,n){let o=T(),s=await Te(o,e,"read <doc> [range]"),i=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:le(n.from,"from",1),limit:le(n.limit,"limit",1),formulas:n.formulas,styles:n.styles,format:n.csv?"csv":void 0}});de(i,n.json)}async function Nn(e,t,n){let o=T(),s=await Te(o,e,"apply <doc> <changes.json>"),i=le(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 u=!t||t==="-"?xe(0,"utf8"):xt(t)?xe(t,"utf8"):p(`${t} isn't a file.`),l;try{l=JSON.parse(u)}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=le(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??Pn(a);de(await o.changeSheet(s.doc.id,{key:c,...a}),n.json)}var Pn=e=>St(new TextEncoder().encode(JSON.stringify(e))).slice(0,32);async function En(e,t){let n=T(),o=await Te(n,e,"check <doc>");de(await n.readSheet(o.doc.id,{check:t.sheet?{sheet:t.sheet}:{}}),t.json)}async function Cn(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;de(await n.readSheet(o.doc.id,{changes:{since:s,max:le(t.max,"max",1)}}),t.json)}var We=`Dockito CLI ${qe}
252
+ --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.12",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(`
253
+ ${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()}
254
+ `),$(`${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}
208
255
 
209
256
  ${Ke}
210
257
 
@@ -217,6 +264,6 @@ ${Object.values(ve).map(e=>` ${e.use.padEnd(30)}${e.about}`).join(`
217
264
  -w, --workspace <slug> Another workspace (default: your personal one)
218
265
  --json Machine-readable output
219
266
 
220
- Environment: DOCKITO_TOKEN (use this token), DOCKITO_URL (another Dockito)`,vt=e=>{let t=ve[e];return t?`dockito ${t.use}
267
+ Environment: DOCKITO_TOKEN (use this token), DOCKITO_URL (another Dockito)`,jt=e=>{let t=ve[e];return t?`dockito ${t.use}
221
268
 
222
- ${t.page}`:null},In={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","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}},jt={message:"-m",workspace:"-w",output:"-o",yes:"-y"};function _n(e,t,n){let o=In[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(u=>`--${u}`).join(", "),c=o.flags.length?` It takes ${o.flags.map(u=>jt[u]?`${jt[u]}/--${u}`:`--${u}`).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 Rn(){let{values:e,positionals:t}=pn({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"},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(vt(o[0])??We);if(e.help&&n)return h(vt(n)??We);if(e.help||!n||n==="help")return h(We);switch(_n(n,Object.keys(e).filter(s=>e[s]!==void 0),o),n){case"login":return mn(e);case"logout":return wn();case"whoami":return yn(e);case"sync":return yt(T(),o[0],e);case"publish":return kn(o,e);case"tree":return bn(e);case"ls":return $n(o[0],e);case"mkdir":return vn(o[0],e);case"share":return An(o[0],e);case"open":return jn(o[0]);case"get":return xn(o[0],e);case"history":return Tn(o[0],e);case"restore":return On(o[0],o[1]);case"save":return Sn(o[0],o[1],e);case"agent":return h(Ke);case"read":return Dn(o[0],o[1],e);case"apply":return Nn(o[0],o[1],e);case"check":return En(o[0],e);case"changes":return Cn(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)}Rn().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 se)return je(130);t&&e instanceof x?h(JSON.stringify({error:e.code,message:oe(e,E),...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?$(_(oe(e,E))):e instanceof TypeError&&/parseArgs|option|positional/i.test(String(e.message))?$(_(`${e.message}. Run \`dockito --help\`.`)):$(_(String(e?.stack??e))),je(1)});
269
+ ${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)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dockito",
3
- "version": "0.0.1-beta.10",
3
+ "version": "0.0.1-beta.12",
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",