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.
- package/dockito.js +70 -23
- 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 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
|
|
5
|
-
`)};
|
|
6
|
-
\x1B[?25l`),a(!0),
|
|
7
|
-
`),s(new
|
|
8
|
-
`),o(i)):void 0;a(!1)};process.stdin.on("keypress",
|
|
9
|
-
`);return!!n&&n.status===0&&!/error/i.test(n.stderr??"")}if(process.platform==="linux"){let n=X("secret-tool",["store","--label=Dockito","service",
|
|
10
|
-
`),{choices:o})}async function
|
|
11
|
-
To make it, run again with --create.`,{workspace:f.slug,projects:f.projects.map(r=>r.slug)});let v=
|
|
12
|
-
Ask the user, then run again with --fold.`,{files:D});if($(
|
|
13
|
-
${
|
|
14
|
-
`)}`);let Se=new Map;for(let r of K){let
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
206
|
-
${
|
|
207
|
-
`),$(`${b("\u2713")} ${t.output} ${g(`\u2190 ${n.linkOf(o.doc.slug)} outline`)}`)):
|
|
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)`,
|
|
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},
|
|
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