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.
- package/dockito.js +64 -37
- package/package.json +1 -1
package/dockito.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as
|
|
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
|
-
`),
|
|
5
|
-
`)};
|
|
6
|
-
\x1B[?25l`),
|
|
7
|
-
`),s(new
|
|
8
|
-
`),
|
|
9
|
-
`);return!!
|
|
10
|
-
`),{choices:
|
|
11
|
-
To make it, run again with --create.`,{workspace:
|
|
12
|
-
Ask the user, then run again with --fold.`,{files:D});if($(
|
|
13
|
-
${
|
|
14
|
-
`)}`);let
|
|
15
|
-
${
|
|
16
|
-
`)}`);let
|
|
17
|
-
Nothing changed yet. Show the user this plan, then run: ${
|
|
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.`,
|
|
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
|
|
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`},
|
|
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
|
|
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": {...}}
|
|
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
|
|
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
|
|
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, ...}
|
|
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
|
|
242
|
-
dates typed as text, totals that stop before
|
|
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
|
|
251
|
-
|
|
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
|
|
254
|
-
${
|
|
255
|
-
`),$(`${
|
|
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
|
-
${
|
|
284
|
+
${He}
|
|
258
285
|
|
|
259
286
|
Usage: dockito <command>
|
|
260
287
|
|
|
261
|
-
${Object.values(
|
|
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=
|
|
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},
|
|
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