dockito 0.0.1-beta.4 → 0.0.1-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dockito.js +31 -24
  2. package/package.json +1 -1
package/dockito.js CHANGED
@@ -1,21 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as qt,spawnSync as lt}from"node:child_process";import{createHash as zt}from"node:crypto";import{existsSync as Vt,readFileSync as dt,statSync as Yt,writeFileSync as Xt}from"node:fs";import{hostname as Qt}from"node:os";import{basename as Le,dirname as Zt,resolve as en}from"node:path";import{parseArgs as tn}from"node:util";var x=class extends Error{constructor(t,o,s={}){super(t);this.code=t;this.status=o;this.detail=s}code;status;detail};function pe(e){let n=e.url.replace(/\/+$/,"");async function t(r,a,c={}){let f=new URL(`${n}/api${a}`);for(let[j,b]of Object.entries(c.query??{}))b!=null&&b!==""&&f.searchParams.set(j,String(b));let w={},g=c.token===void 0?e.token:c.token;g&&(w.authorization=`Bearer ${g}`),e.userAgent&&(w["user-agent"]=e.userAgent);let u;c.json!==void 0?(w["content-type"]="application/json",u=JSON.stringify(c.json)):c.body&&(w["content-type"]="application/octet-stream",u=c.body);let k;try{k=await fetch(f,{method:r,headers:w,body:u})}catch{throw new x("offline",0)}return k}async function o(r){let a=await r.json().catch(()=>({}));if(!r.ok){let{error:c,...f}=a;throw new x(typeof c=="string"?c:`http_${r.status}`,r.status,f)}return a}let s=encodeURIComponent;return{url:n,linkOf:r=>`${n}/${r}`,startLogin:async(r,a)=>o(await t("POST","/device/code",{json:{client:r,device:a}})),pollLogin:async r=>{let a=await t("POST","/device/token",{json:{device_code:r},token:null}),c=await a.json().catch(()=>({}));if(a.ok)return c;if(c.error==="authorization_pending"||c.error==="slow_down")return null;throw new x(c.error??`http_${a.status}`,a.status)},logout:async()=>o(await t("DELETE","/tokens/current")),whoami:async()=>o(await t("GET","/whoami")),tree:async()=>o(await t("GET","/tree")),projects:async r=>(await o(await t("GET",`/workspaces/${s(r)}/projects`))).projects,docs:async(r,a={})=>(await o(await t("GET",`/workspaces/${s(r)}/docs`,{query:a}))).docs,files:async(r,a)=>(await o(await t("GET",`/workspaces/${s(r)}/files`,{query:{project:a}}))).docs,createProject:async(r,a)=>o(await t("POST",`/workspaces/${s(r)}/projects`,{json:{name:a}})),createFolder:async(r,a,c)=>o(await t("POST",`/projects/${s(r)}/folders`,{json:{name:a,parentId:c}})),recent:async r=>(await o(await t("GET",`/workspaces/${s(r)}/docs/recent`))).docs,doc:async r=>o(await t("GET",`/docs/by-code/${s(r)}`)),content:async(r,a)=>{let c=await t("GET",`/docs/${s(r)}/content`,{query:{v:a}});return c.ok||await o(c),{bytes:new Uint8Array(await c.arrayBuffer()),entry:Number(c.headers.get("x-doc-version"))}},create:async(r,a,c,f={})=>o(await t("POST",`/workspaces/${s(r)}/docs`,{query:{name:a,project:f.project,folder:f.folder,public:f.public?1:null,source:e.source},body:c})),update:async(r,a,c,f={})=>o(await t("POST",`/docs/${s(r)}/versions`,{query:{name:a,note:f.note,commit:f.commit,base:f.base,source:e.source},body:c})),restore:async(r,a,c)=>o(await t("POST",`/docs/${s(r)}/history/${a}/restore`,{query:{base:c}})),save:async(r,a,c)=>o(await t("PUT",`/docs/${s(r)}/history/${a}/saved`,{json:{note:c}})),makePrivate:async r=>o(await t("PATCH",`/docs/${s(r)}`,{json:{public:!1}})),rename:async(r,a)=>o(await t("PATCH",`/docs/${s(r)}`,{json:{title:a}}))}}import{emitKeypressEvents as $t}from"node:readline";import{createInterface as jt}from"node:readline/promises";var bt=!process.env.NO_COLOR&&process.stdout.isTTY,B=e=>n=>bt?`\x1B[${e}m${n}\x1B[0m`:n,p=B("2"),T=B("1"),y=B("32"),_=B("31"),I=B("33"),Se=B("36"),d=(e="")=>process.stdout.write(e+`
2
+ import{spawn as Vt,spawnSync as pt}from"node:child_process";import{createHash as Xt}from"node:crypto";import{existsSync as Qt,readFileSync as gt,statSync as Zt,writeFileSync as en}from"node:fs";import{hostname as tn}from"node:os";import{basename as Me,dirname as nn,isAbsolute as on,resolve as wt}from"node:path";import{parseArgs as sn}from"node:util";var _=e=>e.level===null||e.level==="publish"||e.level==="everything",M=(e,n)=>e.projects.find(t=>t.slug===n)??e.projects.find(t=>t.name.toLowerCase()===n.toLowerCase())??null;function H(e){let n=new Map((e?.folders??[]).map(s=>[s.id,s])),t=s=>{let r=n.get(s);return[...r.parentId?t(r.parentId):[],r.name]},o=new Map((e?.folders??[]).map(s=>[t(s.id).join("/").toLowerCase(),s.id]));return{idOf:s=>s.length?o.get(s.join("/").toLowerCase()):null,pathOf:s=>s&&n.has(s)?t(s):[],add:(s,r)=>{o.set(s.join("/").toLowerCase(),r)}}}var vt=e=>e.fileName??`${e.title}.${e.kind}`,ee=(e,n,t)=>e.filter(o=>(o.folderId??null)===n&&vt(o).toLowerCase()===t.toLowerCase());function Ze(e){let n=e.trim();if(/^https?:\/\//.test(n))try{n=new URL(n).pathname}catch{return null}let t=(n.split("/").filter(Boolean).pop()??"").replace(/\.[a-z]+$/,"");return/^[a-z0-9]{12}$/.test(t)&&/\d/.test(t)?t:null}var x=class extends Error{constructor(t,o,s={}){super(t);this.code=t;this.status=o;this.detail=s}code;status;detail};function ge(e){let n=e.url.replace(/\/+$/,"");async function t(r,a,c={}){let u=new URL(`${n}/api${a}`);for(let[j,b]of Object.entries(c.query??{}))b!=null&&b!==""&&u.searchParams.set(j,String(b));let k={},w=c.token===void 0?e.token:c.token;w&&(k.authorization=`Bearer ${w}`),e.userAgent&&(k["user-agent"]=e.userAgent);let h;c.json!==void 0?(k["content-type"]="application/json",h=JSON.stringify(c.json)):c.body&&(k["content-type"]="application/octet-stream",h=c.body);let g;try{g=e.fetch?await e.fetch(new Request(u,{method:r,headers:k,body:h})):await fetch(u,{method:r,headers:k,body:h})}catch{throw new x("offline",0)}return g}async function o(r){let a=await r.json().catch(()=>({}));if(!r.ok){let{error:c,...u}=a;throw new x(typeof c=="string"?c:`http_${r.status}`,r.status,u)}return a}let s=encodeURIComponent;return{url:n,linkOf:r=>`${n}/${r}`,startLogin:async(r,a)=>o(await t("POST","/device/code",{json:{client:r,device:a}})),pollLogin:async r=>{let a=await t("POST","/device/token",{json:{device_code:r},token:null}),c=await a.json().catch(()=>({}));if(a.ok)return c;if(c.error==="authorization_pending"||c.error==="slow_down")return null;throw new x(c.error??`http_${a.status}`,a.status)},logout:async()=>o(await t("DELETE","/tokens/current")),whoami:async()=>o(await t("GET","/whoami")),tree:async()=>o(await t("GET","/tree")),projects:async r=>(await o(await t("GET",`/workspaces/${s(r)}/projects`))).projects,docs:async(r,a={})=>(await o(await t("GET",`/workspaces/${s(r)}/docs`,{query:a}))).docs,files:async(r,a)=>(await o(await t("GET",`/workspaces/${s(r)}/files`,{query:{project:a}}))).docs,createProject:async(r,a)=>o(await t("POST",`/workspaces/${s(r)}/projects`,{json:{name:a}})),createFolder:async(r,a,c)=>o(await t("POST",`/projects/${s(r)}/folders`,{json:{name:a,parentId:c}})),recent:async r=>(await o(await t("GET",`/workspaces/${s(r)}/docs/recent`))).docs,doc:async r=>o(await t("GET",`/docs/by-code/${s(r)}`)),content:async(r,a)=>{let c=await t("GET",`/docs/${s(r)}/content`,{query:{v:a}});return c.ok||await o(c),{bytes:new Uint8Array(await c.arrayBuffer()),entry:Number(c.headers.get("x-doc-version"))}},create:async(r,a,c,u={})=>o(await t("POST",`/workspaces/${s(r)}/docs`,{query:{name:a,title:u.title,project:u.project,folder:u.folder,public:u.public?1:null,source:e.source},body:c})),update:async(r,a,c,u={})=>o(await t("POST",`/docs/${s(r)}/versions`,{query:{name:a,note:u.note,commit:u.commit,base:u.base,source:e.source},body:c})),restore:async(r,a,c)=>o(await t("POST",`/docs/${s(r)}/history/${a}/restore`,{query:{base:c}})),save:async(r,a,c)=>o(await t("PUT",`/docs/${s(r)}/history/${a}/saved`,{json:{note:c}})),makePrivate:async r=>o(await t("PATCH",`/docs/${s(r)}`,{json:{public:!1}})),rename:async(r,a)=>o(await t("PATCH",`/docs/${s(r)}`,{json:{title:a}}))}}import{emitKeypressEvents as Tt}from"node:readline";import{createInterface as Ot}from"node:readline/promises";var xt=!process.env.NO_COLOR&&process.stdout.isTTY,G=e=>n=>xt?`\x1B[${e}m${n}\x1B[0m`:n,p=G("2"),T=G("1"),y=G("32"),I=G("31"),E=G("33"),Se=G("36"),d=(e="")=>process.stdout.write(e+`
3
3
  `),$=(e="")=>process.stderr.write(e+`
4
- `),W=class extends Error{},m=e=>{throw new W(e)},H={view:"View",publish:"Publish",everything:"Everything"};function fe(e){let n=Math.round((Date.now()-new Date(e).getTime())/1e3);if(n<60)return"just now";let t=Math.round(n/60);if(t<60)return`${t} min ago`;let o=Math.round(t/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 Z(e){let n=o=>o.replace(/\x1b\[[0-9;]*m/g,""),t=e[0]?.map((o,s)=>Math.max(...e.map(r=>n(r[s]??"").length)))??[];for(let o of e)d(o.map((s,r)=>r===o.length-1?s:s+" ".repeat(t[r]-n(s).length)).join(" ").trimEnd())}function ee(e,n){switch(e.code){case"offline":return`Couldn't reach Dockito at ${n}.`;case"invalid_token":return"Your login has ended or was signed out. Run `dockito login`.";case"token_level":return`This needs ${H[e.detail.needs]??e.detail.needs} access; this login has ${H[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"deleted":return"That doc was deleted.";case"conflict":return"Someone published a newer version since yours. Get it with `dockito get`, or publish anyway with --force.";case"rate_limited":return"Too many requests. Try again in a minute.";case"too_large":return`The file is too large (limit ${Math.round(Number(e.detail.limit??0)/1048576)} MB).`;case"out_of_space":return"The workspace is out of storage.";case"unsupported":return"Dockito takes .md and .html files.";case"other_kind":return"That doc is a different kind of file (Markdown and HTML can't replace each other).";case"empty":return"The file is empty.";case"not_text":return"The file isn't text.";case"too_deep":return"Folders go at most 2 deep inside a project.";case"invalid_name":return"Names need 1 to 80 characters.";case"folder_gone":return"That project or folder no longer exists.";default:return`Dockito said: ${e.code}`}}var Pe=()=>!!process.stdin.isTTY&&!!process.stdout.isTTY&&!process.env.CI,J=e=>process.stdout.write(e);function Xe(e,n,t=0){return new Promise((o,s)=>{let r=t,a=w=>{w||J(`\x1B[${n.length}A`);for(let[g,u]of n.entries())J(`\x1B[2K${g===r?Se("\u276F "+u.label):" "+u.label}${u.hint?p(" "+u.hint):""}
5
- `)};J(`${y("?")} ${T(e)} ${p("(\u2191\u2193, Enter)")}
6
- \x1B[?25l`),a(!0),$t(process.stdin),process.stdin.setRawMode(!0),process.stdin.resume();let c=()=>{process.stdin.off("keypress",f),process.stdin.setRawMode(!1),process.stdin.pause(),J("\x1B[?25h")},f=(w,g)=>{if(g.ctrl&&g.name==="c")return c(),J(`
7
- `),s(new te);if(g.name==="up"||g.name==="k")r=(r-1+n.length)%n.length;else if(g.name==="down"||g.name==="j")r=(r+1)%n.length;else return g.name==="return"?(c(),J(`\x1B[${n.length+1}A\x1B[0J${y("?")} ${T(e)} ${Se(n[r].label)}
8
- `),o(r)):void 0;a(!1)};process.stdin.on("keypress",f)})}async function ge(e,n=""){let t=jt({input:process.stdin,output:process.stdout});try{return(await t.question(`${y("?")} ${T(e)}${n?p(` (${n})`):""} `)).trim()||n}finally{t.close()}}async function Ae(e,n=!0){let t=(await ge(`${e} ${p(n?"[Y/n]":"[y/N]")}`)).toLowerCase();return t?t.startsWith("y"):n}var te=class extends Error{};import{homedir as vt}from"node:os";import{join as Ne}from"node:path";var xt="https://dockito-app.dockito.workers.dev";function Qe(){return process.env.DOCKITO_CONFIG_DIR?process.env.DOCKITO_CONFIG_DIR:process.platform==="win32"&&process.env.APPDATA?Ne(process.env.APPDATA,"dockito"):Ne(process.env.XDG_CONFIG_HOME||Ne(vt(),".config"),"dockito")}var Ze=()=>(process.env.DOCKITO_URL||xt).replace(/\/+$/,""),et=e=>new URL(e).host;import{spawnSync as Tt}from"node:child_process";import{chmodSync as Ot,existsSync as _e,mkdirSync as Dt,readFileSync as St,rmSync as tt,writeFileSync as Pt}from"node:fs";import{dirname as At,join as Nt}from"node:path";var G="dockito";function q(e,n,t){try{let o=Tt(e,n,{input:t,encoding:"utf8",stdio:["pipe","pipe","pipe"]});return o.error?null:o}catch{return null}}var ne=e=>Nt(Qe(),"tokens",`${e.replace(/[^a-z0-9.-]/gi,"_")}`);function _t(e,n){if(process.platform==="darwin"){let t=q("security",["-i"],`add-generic-password -U -s ${G} -a ${e} -l Dockito -w ${n}
9
- `);return!!t&&t.status===0&&!/error/i.test(t.stderr??"")}if(process.platform==="linux"){let t=q("secret-tool",["store","--label=Dockito","service",G,"account",e],n);return!!t&&t.status===0}return!1}function It(e){let n=process.platform==="darwin"?q("security",["find-generic-password","-s",G,"-a",e,"-w"]):process.platform==="linux"?q("secret-tool",["lookup","service",G,"account",e]):null;return(n?.status===0?n.stdout.trim():"")||null}function Et(e){process.platform==="darwin"?q("security",["delete-generic-password","-s",G,"-a",e]):process.platform==="linux"&&q("secret-tool",["clear","service",G,"account",e])}function nt(e,n){if(_t(e,n))return _e(ne(e))&&tt(ne(e)),"keychain";let t=ne(e);return Dt(At(t),{recursive:!0,mode:448}),Pt(t,n,{mode:384}),Ot(t,384),"file"}function he(e){if(process.env.DOCKITO_TOKEN)return process.env.DOCKITO_TOKEN.trim();let n=It(e);if(n)return n;let t=ne(e);return _e(t)&&St(t,"utf8").trim()||null}function ot(e){Et(e);let n=ne(e);_e(n)&&tt(n)}var D=class extends Error{constructor(t,o,s={}){super(o);this.code=t;this.data=s}code;data},L=e=>e.level===null||e.level==="publish"||e.level==="everything";function me(e,n){let t=e.workspaces;if(n)return t.find(s=>s.slug===n)??m(`This login can't reach workspace "${n}". \`dockito tree\` lists the ones it can.`);let o=t.length===1?t[0]:t.find(s=>s.kind==="personal"&&s.role==="owner");if(o)return o;throw new D("needs_workspace",`Pick a workspace with -w: ${t.map(s=>s.slug).join(", ")}`,{workspaces:t.map(s=>s.slug)})}var z=(e,n)=>e.projects.find(t=>t.slug===n)??e.projects.find(t=>t.name.toLowerCase()===n.toLowerCase())??null;function oe(e){let n=new Map((e?.folders??[]).map(s=>[s.id,s])),t=s=>{let r=n.get(s);return[...r.parentId?t(r.parentId):[],r.name]},o=new Map((e?.folders??[]).map(s=>[t(s.id).join("/").toLowerCase(),s.id]));return{idOf:s=>s.length?o.get(s.join("/").toLowerCase()):null,pathOf:s=>s&&n.has(s)?t(s):[],add:(s,r)=>{o.set(s.join("/").toLowerCase(),r)}}}function Ie(e,n){if(!n)return{project:null,folder:null,label:`${e.slug} Docs`};let[t,...o]=n.split("/").filter(Boolean),s=z(e,t);if(!s)return m(`No project "${t}" in ${e.slug}. \`dockito tree\` lists them; \`dockito mkdir ${n}\` makes it.`);let r=oe(s).idOf(o);return r===void 0?m(`No folder "${o.join("/")}" in ${s.slug}. \`dockito mkdir ${s.slug}/${o.join("/")}\` makes it.`):{project:s.id,folder:r,label:[e.slug,s.slug,...o].join("/")}}var Ct=e=>e.fileName??`${e.title}.${e.kind}`,we=(e,n,t)=>e.filter(o=>(o.folderId??null)===n&&Ct(o).toLowerCase()===t.toLowerCase());import{createHash as Lt}from"node:crypto";import{spawnSync as Mt}from"node:child_process";import{existsSync as re,readFileSync as Ft,readdirSync as Rt,statSync as it}from"node:fs";import{basename as st,join as E,relative as Kt,resolve as Ee,sep as Ut}from"node:path";var ke=2,Wt=4,se=e=>e<1024*1024?`${e?Math.max(1,Math.round(e/1024)):0} KB`:`${(e/1048576).toFixed(1)} MB`,Ht=e=>Lt("sha256").update(e).digest("hex"),at=e=>e.split(Ut).join("/"),V=(e,n)=>`${e} ${n}${e===1?"":"s"}`,Bt=(e,n)=>{let t=/\.[^./]+$/.exec(n.toLowerCase())?.[0]??"";return e.find(o=>o.extensions.includes(t))?.id??null};function ye(e,n){let t=Mt("git",e,{cwd:n,encoding:"utf8",maxBuffer:67108864});return t.error||t.status!==0?null:t.stdout}function Jt(e,n){let t=ye(["ls-files","-z","--cached","--others","--exclude-standard","--","."],e);return(t!==null?t.split("\0").filter(Boolean):(function s(r){return Rt(E(e,r),{withFileTypes:!0}).flatMap(a=>a.name.startsWith(".")||a.name==="node_modules"?[]:a.isDirectory()?s(E(r,a.name)):[at(E(r,a.name))])})("")).filter(s=>n(s)&&re(E(e,s))&&it(E(e,s)).isFile()).sort()}function Gt(e,n,t){let o=[...e.matchAll(/\b(?:src|href)=["']([^"'#?]+)/gi)].map(s=>s[1]);return[...new Set(o.filter(s=>!/^([a-z]+:|\/\/|\/)/i.test(s)&&!t(s)&&re(E(n,s))))]}function rt(e,n){let t=(r,a,c="")=>`dockito sync ${n} -w ${r} --to ${a}${c}`,o=e.workspaces.filter(L).flatMap(r=>[...r.projects.map(a=>({workspace:r.slug,project:a.slug,name:a.name,docs:a.docs,command:t(r.slug,a.slug)})),{workspace:r.slug,project:null,name:"a new project",docs:0,command:t(r.slug,"<name>"," --create")}]),s=Math.max(...o.map(r=>r.command.length));throw new D("needs_place",[`Which project should ${n==="."?"this folder":n+"/"} go to? Ask the user, then run one of:`,...o.map(r=>` ${r.command.padEnd(s)} ${p(r.project?`${r.name} \xB7 ${V(r.docs,"doc")}`:r.name)}`),p("Subfolders become folders. `dockito tree` shows every place.")].join(`
10
- `),{choices:o})}async function ct(e,n,t){let o=Pe()&&!t.json,s=await e.tree(),r=i=>!!Bt(s.kinds,i);s.workspaces.some(L)||m("This login can't publish anywhere. Run `dockito login --update`.");let a=n;a||(o||rt(s,re("docs")?"docs":"."),a=await ge("Folder to sync",re("docs")?"docs":"."));let c=Ee(a);(!re(c)||!it(c).isDirectory())&&m(`${a} isn't a folder.`);let f=at(Kt(process.cwd(),c))||".",w;t.workspace&&(w=s.workspaces.find(i=>i.slug===t.workspace)??m(`This login can't reach workspace "${t.workspace}".`));let g=t.to;if(!g){o||rt(s,f);let i=(w?[w]:s.workspaces.filter(L)).flatMap(h=>[...h.projects.map(v=>({w:h,p:v.slug,label:v.name,hint:`${h.slug} \xB7 ${V(v.docs,"doc")}`})),{w:h,p:null,label:"New project",hint:h.slug}]),l=i[await Xe("Project",i)];w=l.w,g=l.p??await ge("Project name",st(c===process.cwd()?c:Ee(c,".."))),t.create||=!l.p}if(!w){let i=s.workspaces.filter(h=>L(h)&&z(h,g.split("/")[0])),l=s.workspaces.find(h=>h.kind==="personal"&&h.role==="owner");if(w=i.length===1?i[0]:s.workspaces.length===1?s.workspaces[0]:!i.length&&l?l:void 0,!w)throw new D("needs_workspace",`Which workspace? Run again with -w: ${(i.length?i:s.workspaces).map(h=>h.slug).join(", ")}`)}let u=w;L(u)||m(`This login has View access in ${u.slug}; syncing needs Publish. Run \`dockito login --update\`.`);let[k,...j]=g.split("/").filter(Boolean);k||m("--to takes a project, or project/folder."),j.length>ke&&m(`Folders go at most ${ke} deep inside a project.`);let b=z(u,k);if(!b&&!t.create)throw new D("no_project",`No project "${k}" in ${u.slug}. Projects: ${u.projects.map(i=>i.slug).join(", ")||"none"}.
11
- To make it, run again with --create.`,{workspace:u.slug,projects:u.projects.map(i=>i.slug)});let R=Jt(c,r);R.length||m(`No ${s.kinds.flatMap(i=>i.extensions).join(", ")} files in ${f}.`);let ae=ke-j.length,Y=R.filter(i=>i.split("/").length-1>ae);if(Y.length&&!t.fold){let i=h=>h.split("/").slice(0,ae).join("/")||"the project's top",l=`${V(Y.length,"file")} ${Y.length===1?"is":"are"} more than ${ke} folders deep inside the project. With --fold they go in ${[...new Set(Y.map(i))].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 D("too_deep",`${l}
12
- Ask the user, then run again with --fold.`,{files:Y});if($(l),!await Ae("Fold them like that?"))return $(p("Nothing changed."));t.fold=!0}let Re=i=>{let l=i.split("/");return{folder:[...j,...l.slice(0,-1).slice(0,ae)],dropped:l.slice(0,-1).slice(ae),file:l[l.length-1]}},$e=new Map;for(let i of R){let l=Re(i),h=[...l.folder,l.file].join("/").toLowerCase();$e.set(h,($e.get(h)??0)+1)}let Ke=b?await e.files(u.id,b.id):[],X=oe(b),M=R.map(i=>{let{folder:l,dropped:h,file:v}=Re(i),C=h.length&&$e.get([...l,v].join("/").toLowerCase())>1?[...h,v].join("-"):v,P=new Uint8Array(Ft(E(c,i))),U={rel:i,name:C,folder:l,bytes:P,hash:Ht(P),action:"new",doc:null,others:[],assets:[]};/\.html?$/i.test(i)&&(U.assets=Gt(new TextDecoder().decode(P),E(c,i,".."),r));let Ye=X.idOf(l);if(Ye===void 0)return U;let Q=we(Ke,Ye,C);return Q.length>1?{...U,action:"ambiguous",others:Q}:Q.length?{...U,doc:Q[0],action:Q[0].sha256===U.hash?"same":"update"}:U}),Ue=M.filter(i=>i.bytes.length>u.maxFile);Ue.length&&m(`Over the ${se(u.maxFile)} limit per file in ${u.slug}:
13
- ${Ue.map(i=>` ${i.rel} ${se(i.bytes.length)}`).join(`
14
- `)}`);let je=new Map;for(let i of M){let l=[...i.folder,i.name].join("/").toLowerCase();je.set(l,[...je.get(l)??[],i.rel])}let We=[...je.values()].filter(i=>i.length>1);We.length&&m(`These files would be the same doc; rename one of each:
15
- ${We.map(i=>" "+i.join(" and ")).join(`
16
- `)}`);let N=[],He=new Set;for(let i of M)for(let l=1;l<=i.folder.length;l++){let h=i.folder.slice(0,l),v=h.join("/").toLowerCase();X.idOf(h)===void 0&&!He.has(v)&&(He.add(v),N.push(h))}N.sort((i,l)=>i.length-l.length);let O=M.filter(i=>i.action==="new"||i.action==="update"),Be=M.filter(i=>i.action==="ambiguous"),ve=M.filter(i=>i.action==="same").length,mt=new Set(M.flatMap(i=>i.doc?[i.doc.id]:i.others.map(l=>l.id))),xe=Ke.filter(i=>!mt.has(i.id)).length,Je=`${u.slug}/${b?.slug??k}${j.length?"/"+j.join("/"):""}`,ce={workspace:u.slug,to:Je,create:{project:b?null:k,folders:N.map(i=>i.join("/"))},docs:O.map(i=>({file:i.rel,action:i.action,folder:i.folder.join("/")||null,name:i.name,link:i.doc?e.linkOf(i.doc.slug):null})),unchanged:ve,ambiguous:Be.map(i=>({file:i.rel,docs:i.others.map(l=>e.linkOf(l.slug))})),notHere:xe};if(!t.json){d(`${T(f==="."?"./":f+"/")} \u2192 ${T(Je)}`),b||d(` ${y("+ project")} ${k}`);for(let l of N)d(` ${y("+ folder ")} ${l.join("/")}`);if(O.length>15)for(let[l,h]of[["new",y("+ new ")],["update",I("~ update ")]]){let v=new Map;for(let C of O.filter(P=>P.action===l))v.set(C.folder.join("/"),[...v.get(C.folder.join("/"))??[],C.name]);for(let[C,P]of v)d(` ${h} ${P.length} in ${C||"the project's top"}: ${p(P.slice(0,4).join(", ")+(P.length>4?`, ${P.length-4} more`:""))}`)}else for(let l of O){let h=l.name!==st(l.rel)?p(` \u2192 ${[...l.folder,l.name].join("/")}`):"";d(` ${l.action==="new"?y("+ new "):I("~ update ")} ${l.rel}${h}`)}ve&&d(p(` ${ve} unchanged`));for(let l of Be)d(` ${_("? skip ")} ${l.rel} ${p(`${l.others.length} docs there have this name: ${l.others.map(h=>e.linkOf(h.slug)).join(", ")}`)}`);xe&&d(p(` ${V(xe,"doc")} in the project with no file here, left as they are`)),O.length&&d(p(` ${V(O.length,"file")} to send, ${se(O.reduce((l,h)=>l+h.bytes.length,0))}`));let i=O.filter(l=>l.assets.length);i.length&&d(I(` ! ${i.length===1?i[0].rel:`${i.length} HTML files`} load ${[...new Set(i.flatMap(l=>l.assets))].join(", ")} from next to ${i.length===1?"it":"them"}; only HTML files are published`))}if(!O.length&&b&&!N.length){t.json?d(JSON.stringify({...ce,applied:!0},null,2)):d(`${y("\u2713")} Up to date`);return}if(t["dry-run"]){t.json&&d(JSON.stringify({...ce,applied:!1},null,2));return}if((!b||N.length)&&!t.yes){let i=["dockito sync",f,`-w ${u.slug}`,`--to ${g}`,t.create&&!b?"--create":"",t.fold?"--fold":"","--yes"].filter(Boolean).join(" ");if(!o){if(t.json)return d(JSON.stringify({...ce,applied:!1,next:i},null,2));throw new D("confirm",`
17
- Nothing changed yet. Show the user this plan, then run: ${i}`,{next:i})}if(!await Ae("Apply?"))return $(p("Nothing changed."))}if(!b){let i=await e.createProject(u.id,k);b={id:i.id,slug:i.slug,name:k,docs:0,folders:[]},t.json||d(`${y("\u2713")} Project ${k}`)}let Te=b;for(let i of N){let l=await e.createFolder(Te.id,i[i.length-1],i.length>1?X.idOf(i.slice(0,-1)):null);X.add(i,l.id)}N.length&&!t.json&&d(`${y("\u2713")} ${V(N.length,"folder")}`);let le=ye(["rev-parse","--show-toplevel"],c)?.trim()??null,Ge=le?ye(["rev-parse","HEAD"],le)?.trim()??null:null,wt=new Set(le?(ye(["status","--porcelain","-z","--","."],c)??"").split("\0").filter(Boolean).map(i=>Ee(le,i.slice(3))):[]),K=[],de=null,Oe=[...O],ue=!t.json&&Pe(),kt=O.reduce((i,l)=>i+l.bytes.length,0),qe=0,ze=()=>ue&&process.stdout.write(`\r\x1B[2K${p(`Sending ${K.length} of ${O.length} \xB7 ${se(qe)} of ${se(kt)}`)}`);async function yt(i){let l=Ge&&!wt.has(E(c,i.rel))?Ge:null;for(;;)try{let h=i.doc?await e.update(i.doc.id,i.name,i.bytes,{commit:l}):await e.create(u.id,i.name,i.bytes,{project:Te.id,folder:X.idOf(i.folder)??null});return{file:i.rel,action:i.action,link:e.linkOf(h.slug)}}catch(h){if(h instanceof x&&h.code==="rate_limited"){de||(t.json||$(p("\rUpload limit reached; carrying on in a minute\u2026")),de=new Promise(v=>setTimeout(v,3e4)).then(()=>{de=null})),await de;continue}return{file:i.rel,action:i.action,error:h instanceof x?ee(h,e.url):String(h)}}}ze(),await Promise.all(Array.from({length:Math.min(Wt,Oe.length)},async()=>{for(let i=Oe.shift();i;i=Oe.shift()){let l=await yt(i);K.push(l),qe+=i.bytes.length,ue&&process.stdout.write("\r\x1B[2K"),l.error&&!t.json?d(`${_("\u2717")} ${l.file} ${_(l.error)}`):!t.json&&!ue&&d(`${y("\u2713")} ${l.file} ${p(l.link)}`),ze()}})),ue&&process.stdout.write("\r\x1B[2K");let De=K.filter(i=>i.error),Ve=`${e.url}/${u.slug}/project/${Te.slug}`;t.json?d(JSON.stringify({...ce,applied:!0,link:Ve,results:K},null,2)):d(`${De.length?I(`${K.length-De.length} of ${K.length} sent`):y("\u2713 Synced")} ${p(Ve)}`),De.length&&(process.exitCode=1)}var Me="0.0.1-beta.4",A=Ze(),ie=et(A),Fe=`dockito-cli/${Me} (${process.platform})`;function S(){let e=he(ie);return e||m("Not logged in. Run `dockito login`."),pe({url:A,token:e,source:"cli",userAgent:Fe})}function ft(e){let[n,t]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["cmd",["/c","start","",e]]:["xdg-open",[e]];try{let o=qt(n,t,{stdio:"ignore",detached:!0});return o.on("error",()=>{}),o.unref(),!0}catch{return!1}}var nn=()=>!process.env.SSH_CONNECTION&&!process.env.CI&&(process.platform!=="linux"||!!process.env.DISPLAY||!!process.env.WAYLAND_DISPLAY),on=e=>new Promise(n=>setTimeout(n,e)),sn=()=>Qt().replace(/\.local$/,"").slice(0,60)||"Unknown device",rn=/^[a-z0-9]{12}$/;function F(e){let t=(/^https?:\/\//.test(e)?new URL(e).pathname.split("/").filter(Boolean).pop()??"":e).replace(/\.[a-z]+$/,"");return rn.test(t)&&/\d/.test(t)?t:m(`"${e}" isn't a doc link or code. \`dockito ls\` lists docs with their codes.`)}function ut(e){let n=Zt(en(e)),t=lt("git",["rev-parse","HEAD"],{cwd:n,encoding:"utf8"});if(t.error||t.status!==0)return null;let o=lt("git",["status","--porcelain","--",Le(e)],{cwd:n,encoding:"utf8"});return o.status!==0||o.stdout.trim()?null:t.stdout.trim()}var an=e=>zt("sha256").update(e).digest("hex");async function cn(e){let n=he(ie);e.update&&!n&&m("Not logged in. Run `dockito login` first."),process.env.DOCKITO_TOKEN&&$(I("DOCKITO_TOKEN is set, so commands will keep using it instead of this login."));let t=pe({url:A,token:e.update?n:null,userAgent:Fe}),o=await t.startLogin("Dockito CLI",sn()),s=!e["no-browser"]&&nn()&&ft(o.verification_uri_complete);d(s?`Opening ${o.verification_uri_complete}`:`Open ${T(o.verification_uri)}`),d(`Code: ${T(o.user_code)}`),$(p("Waiting for approval\u2026"));let r=o.interval*1e3,a=Date.now()+o.expires_in*1e3;for(;Date.now()<a;){await on(r);try{let c=await t.pollLogin(o.device_code);if(!c)continue;let f=nt(ie,c.token);d(`${y("\u2713")} Logged in as ${c.user.name}${c.user.username?p(` (${c.user.username})`):""}`);for(let w of c.workspaces)d(` ${w.name} ${p(`${w.slug} \xB7 ${H[w.level]}`)}`);f==="file"&&$(I("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"&&m("Denied in the browser."),c instanceof x&&c.code==="expired_token"&&m("The code expired. Run `dockito login` again."),c instanceof x&&c.code==="offline"){r=Math.min(r*2,3e4);continue}throw c}}m("The code expired. Run `dockito login` again.")}async function ln(){let e=he(ie);if(!e)return d("Not logged in.");process.env.DOCKITO_TOKEN&&m("DOCKITO_TOKEN is set; unset it or revoke that token in Account \u2192 Tokens.");try{await pe({url:A,token:e,userAgent:Fe}).logout()}catch(n){if(!(n instanceof x)||n.code!=="invalid_token"&&n.code!=="not_found")throw n}ot(ie),d(`${y("\u2713")} Logged out`)}async function dn(e){let n=await S().whoami();if(e.json)return d(JSON.stringify(n,null,2));d(`${n.user.name} ${p(`${n.user.username} \xB7 ${n.user.email}`)}`),n.token&&d(p(`${n.token.name} \xB7 ${n.token.source==="cli"?"ends after 90 days without use":`expires ${new Date(n.token.expiresAt).toLocaleDateString()}`}`)),d(),Z(n.workspaces.map(t=>[t.slug,t.name,p(t.level?H[t.level]:t.role)])),A!=="https://dockito-app.dockito.workers.dev"&&$(p(`
18
- ${A}`))}async function un(e,n){e.length||m("Name a file: `dockito publish notes.md --to <project/folder>`"),n.doc&&e.length>1&&m("--doc takes one file."),n.public&&n.doc&&m("--public is only for a new doc; an existing one can be made public from Share in the app.");for(let f of e)(!Vt(f)||!Yt(f).isFile())&&m(`${f} isn't a file.`);let t=S(),o=[];if(n.doc){let f=await t.doc(F(n.doc)),w=new Uint8Array(dt(e[0])),g=await t.update(f.doc.id,Le(e[0]),w,{note:n.message,commit:ut(e[0])});return n.json?d(JSON.stringify([{file:e[0],...g,link:t.linkOf(g.slug)}],null,2)):d(g.unchanged?`${p("=")} ${e[0]} ${p("unchanged")} ${t.linkOf(g.slug)}`:`${y("\u2713")} ${e[0]} \u2192 ${t.linkOf(g.slug)}`)}let s=await t.tree(),r=me(s,n.workspace);L(r)||m(`This login has View access in ${r.slug}. Run \`dockito login --update\`.`);let a=Ie(r,n.to),c=await t.files(r.id,a.project);for(let f of e){let w=new Uint8Array(dt(f)),g=Le(f),u=n.new?[]:we(c,a.folder,g);u.length>1&&m(`${u.length} docs in ${a.label} are named ${g}: ${u.map(R=>t.linkOf(R.slug)).join(", ")}. Pick one with --doc, or publish a new one with --new.`);let k=u[0];if(k&&n.public&&!(await t.doc(k.slug)).doc.public&&m(`${g} is already a private doc in ${a.label} (${t.linkOf(k.slug)}). Only Share in the app can make it public; \`--new --public\` publishes a new public doc.`),k&&k.sha256===an(w)&&!n.message){o.push({file:f,id:k.id,slug:k.slug,version:k.version,unchanged:!0,link:t.linkOf(k.slug)}),n.json||d(`${p("=")} ${f} ${p("unchanged")} ${t.linkOf(k.slug)}`);continue}let j=k?await t.update(k.id,g,w,{note:n.message,commit:ut(f)}):await t.create(r.id,g,w,{project:a.project,folder:a.folder,public:n.public});!k&&n.message&&await t.save(j.id,j.version,n.message);let b="public"in j&&j.public===!0;o.push({file:f,...j,link:t.linkOf(j.slug),new:!k}),n.json||d(`${y("\u2713")} ${f} \u2192 ${t.linkOf(j.slug)} ${p(k?j.unchanged?"unchanged":"updated":`new in ${a.label}`)}${b?` ${y("\xB7 public")}`:""}${n.message?p(` \xB7 saved: ${n.message}`):""}`)}n.json?d(JSON.stringify(o,null,2)):!n.to&&o.some(f=>f.new)&&$(p("In the workspace's Docs. To put docs in a project: --to <project/folder>; `dockito tree` lists them."))}async function pn(e){let n=await S().tree();if(e.json)return d(JSON.stringify(n,null,2));let t=o=>p(`${o} doc${o===1?"":"s"}`);for(let o of n.workspaces){d(`${T(o.slug)} ${o.name} ${p(`\xB7 ${o.level?H[o.level]:o.role}`)}`);let s=[[p("Docs"),"",t(o.docs)]];for(let r of o.projects){s.push([r.slug,r.name,t(r.docs)]);let a=(c,f)=>{for(let w of r.folders.filter(g=>g.parentId===c))s.push([`${f}/${w.name}`,"",t(w.docs)]),a(w.id,`${f}/${w.name}`)};a(null,r.slug)}Z(s.map(r=>[" ",...r])),d()}$(p("Publish into a place: dockito publish <file> -w <workspace> --to <project/folder>"))}async function fn(e,n){let t=S(),o=await t.tree(),s=me(o,n.workspace),r=Ie(s,e),a=await t.docs(s.id,{project:r.project,folder:r.folder}),c=r.project?s.projects.find(u=>u.id===r.project):null,f=e?[]:s.projects,w=c?c.folders.filter(u=>u.parentId===r.folder):[];if(n.json)return d(JSON.stringify({workspace:s.slug,place:e??null,projects:f,folders:w,docs:a},null,2));let g=[];for(let u of f)g.push([T(`${u.slug}/`),u.name,p(`${u.docs} doc${u.docs===1?"":"s"}`),""]);for(let u of w)g.push([T(`${u.name}/`),"",p(`${u.docs} doc${u.docs===1?"":"s"}`),""]);for(let u of a)g.push([u.slug,u.title+p(u.kind==="html"?" .html":" .md"),p(fe(u.updatedAt)),p(u.updatedByName??"")]);if(!g.length)return $(p(`Nothing in ${r.label} yet.`));Z(g)}async function gn(e,n){let[t,...o]=(e??"").split("/").filter(Boolean);t||m("`dockito mkdir <project>[/folder[/subfolder]]`"),o.length>2&&m("Folders go at most 2 deep inside a project.");let s=S(),r=me(await s.tree(),n.workspace),a=z(r,t),c=a?.id??(await s.createProject(r.id,t)).id,f=!a;a||d(`${y("\u2713")} Project ${t}`);let w=oe(a),g=null;for(let u=0;u<o.length;u++){let k=w.idOf(o.slice(0,u+1));if(k){g=k;continue}g=(await s.createFolder(c,o[u],g)).id,w.add(o.slice(0,u+1),g),f=!0,d(`${y("\u2713")} Folder ${o.slice(0,u+1).join("/")}`)}f||$(p("Already there."))}async function hn(e){e||m("Name a doc: `dockito open <link or code>`");let n=`${A}/${F(e)}`;ft(n)||d(n)}async function mn(e,n){e||m("Name a doc: `dockito get <link or code> [-o file]`");let t=S(),o=await t.doc(F(e)),s=n.entry?Number(n.entry):null;n.entry&&(!Number.isInteger(s)||s<1)&&m("--entry takes an entry number from `dockito history`.");let{bytes:r}=await t.content(o.doc.id,s);if(!n.output)return void process.stdout.write(r);Xt(n.output,r),$(`${y("\u2713")} ${n.output} ${p(`\u2190 ${t.linkOf(o.doc.slug)}${s?` entry ${s}`:""}`)}`)}async function wn(e,n){e||m("Name a doc: `dockito history <link or code>`");let t=await S().doc(F(e));if(n.json)return d(JSON.stringify(t.versions,null,2));d(`${T(t.doc.title)} ${p(`${A}/${t.doc.slug}`)}`),Z(t.versions.map((o,s)=>[String(o.number),s===0?y("current"):p(fe(o.createdAt)),s===0?p(fe(o.createdAt)):"",o.byName??"",p(o.source),o.savedAt?`${I("saved")}${o.note?` ${o.note}`:""}`:"",o.gitCommit?p(o.gitCommit.slice(0,7)):""]))}async function kn(e,n){let t=Number(n);(!e||!Number.isInteger(t)||t<1)&&m("`dockito restore <doc> <entry>`; entries are in `dockito history`.");let o=S(),s=await o.doc(F(e)),r=await o.restore(s.doc.id,t,s.versions[0]?.number??null);d(`${y("\u2713")} Restored entry ${t} as entry ${r.version} ${p(o.linkOf(r.slug))}`)}async function yn(e,n,t){e||m('`dockito save <doc> [entry] -m "note"`');let o=S(),s=await o.doc(F(e)),r=n?Number(n):s.versions[0]?.number;(!Number.isInteger(r)||r<1)&&m("Entries are in `dockito history`."),await o.save(s.doc.id,r,t.message??null),d(`${y("\u2713")} Saved entry ${r}${t.message?p(` \xB7 ${t.message}`):""}`)}async function bn(e,n){e||m("`dockito share <link or code> [--private]`"),n.public&&m("An existing doc can be made public only from Share in the app. `dockito publish <file> --public` makes a new doc public.");let t=S(),o=await t.doc(F(e)),s=t.linkOf(o.doc.slug);if(n.private)return o.doc.public&&await t.makePrivate(o.doc.id),n.json?d(JSON.stringify({public:!1,link:s},null,2)):d(`${y("\u2713")} Private: only people in ${o.workspace.name} can open it ${p(s)}`);if(n.json)return d(JSON.stringify({public:o.doc.public,link:s},null,2));d(o.doc.public?`${y("Public")}: anyone with the link can open it ${s}`:`Private: only people in ${o.workspace.name} can open it ${p(s)}`)}var gt=`Dockito hosts Markdown and HTML docs at a short link, with History.
4
+ `),B=class extends Error{},f=e=>{throw new B(e)},J={view:"View",publish:"Publish",everything:"Everything"};function fe(e){let n=Math.round((Date.now()-new Date(e).getTime())/1e3);if(n<60)return"just now";let t=Math.round(n/60);if(t<60)return`${t} min ago`;let o=Math.round(t/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 te(e){let n=o=>o.replace(/\x1b\[[0-9;]*m/g,""),t=e[0]?.map((o,s)=>Math.max(...e.map(r=>n(r[s]??"").length)))??[];for(let o of e)d(o.map((s,r)=>r===o.length-1?s:s+" ".repeat(t[r]-n(s).length)).join(" ").trimEnd())}function ne(e,n){switch(e.code){case"offline":return`Couldn't reach Dockito at ${n}.`;case"invalid_token":return"Your login has ended or was signed out. Run `dockito login`.";case"token_level":return`This needs ${J[e.detail.needs]??e.detail.needs} access; this login has ${J[e.detail.has]??e.detail.has}. Run \`dockito login --update\`.`;case"not_for_tokens":return"That can only be done in the browser.";case"not_found":return"Not found, or this login has no access to it. `dockito whoami` lists what it can reach.";case"forbidden":return"You can open this but not change it. Ask its owner or a workspace admin for Edit.";case"public_off":return"This workspace doesn't allow public links. Its owner can turn them on in Workspace settings.";case"deleted":return"That doc was deleted.";case"conflict":return"Someone published a newer version since yours. Get it with `dockito get`, or publish anyway with --force.";case"rate_limited":return"Too many requests. Try again in a minute.";case"too_large":return`The file is too large (limit ${Math.round(Number(e.detail.limit??0)/1048576)} MB).`;case"out_of_space":return"The workspace is out of storage.";case"unsupported":return"Dockito takes .md and .html files.";case"other_kind":return"That doc is a different kind of file (Markdown and HTML can't replace each other).";case"empty":return"The file is empty.";case"not_text":return"The file isn't text.";case"too_deep":return"Folders go at most 2 deep inside a project.";case"invalid_name":return"Names need 1 to 80 characters.";case"folder_gone":return"That project or folder no longer exists.";default:return`Dockito said: ${e.code}`}}var Ae=()=>!!process.stdin.isTTY&&!!process.stdout.isTTY&&!process.env.CI,q=e=>process.stdout.write(e);function et(e,n,t=0){return new Promise((o,s)=>{let r=t,a=k=>{k||q(`\x1B[${n.length}A`);for(let[w,h]of n.entries())q(`\x1B[2K${w===r?Se("\u276F "+h.label):" "+h.label}${h.hint?p(" "+h.hint):""}
5
+ `)};q(`${y("?")} ${T(e)} ${p("(\u2191\u2193, Enter)")}
6
+ \x1B[?25l`),a(!0),Tt(process.stdin),process.stdin.setRawMode(!0),process.stdin.resume();let c=()=>{process.stdin.off("keypress",u),process.stdin.setRawMode(!1),process.stdin.pause(),q("\x1B[?25h")},u=(k,w)=>{if(w.ctrl&&w.name==="c")return c(),q(`
7
+ `),s(new oe);if(w.name==="up"||w.name==="k")r=(r-1+n.length)%n.length;else if(w.name==="down"||w.name==="j")r=(r+1)%n.length;else return w.name==="return"?(c(),q(`\x1B[${n.length+1}A\x1B[0J${y("?")} ${T(e)} ${Se(n[r].label)}
8
+ `),o(r)):void 0;a(!1)};process.stdin.on("keypress",u)})}async function he(e,n=""){let t=Ot({input:process.stdin,output:process.stdout});try{return(await t.question(`${y("?")} ${T(e)}${n?p(` (${n})`):""} `)).trim()||n}finally{t.close()}}async function Ne(e,n=!0){let t=(await he(`${e} ${p(n?"[Y/n]":"[y/N]")}`)).toLowerCase();return t?t.startsWith("y"):n}var oe=class extends Error{};import{homedir as Dt}from"node:os";import{join as _e}from"node:path";var Pt="https://dockito-app.dockito.workers.dev";function tt(){return process.env.DOCKITO_CONFIG_DIR?process.env.DOCKITO_CONFIG_DIR:process.platform==="win32"&&process.env.APPDATA?_e(process.env.APPDATA,"dockito"):_e(process.env.XDG_CONFIG_HOME||_e(Dt(),".config"),"dockito")}var nt=()=>(process.env.DOCKITO_URL||Pt).replace(/\/+$/,""),ot=e=>new URL(e).host;import{spawnSync as St}from"node:child_process";import{chmodSync as At,existsSync as Ie,mkdirSync as Nt,readFileSync as _t,rmSync as st,writeFileSync as It}from"node:fs";import{dirname as Et,join as Ct}from"node:path";var z="dockito";function Y(e,n,t){try{let o=St(e,n,{input:t,encoding:"utf8",stdio:["pipe","pipe","pipe"]});return o.error?null:o}catch{return null}}var se=e=>Ct(tt(),"tokens",`${e.replace(/[^a-z0-9.-]/gi,"_")}`);function Lt(e,n){if(process.platform==="darwin"){let t=Y("security",["-i"],`add-generic-password -U -s ${z} -a ${e} -l Dockito -w ${n}
9
+ `);return!!t&&t.status===0&&!/error/i.test(t.stderr??"")}if(process.platform==="linux"){let t=Y("secret-tool",["store","--label=Dockito","service",z,"account",e],n);return!!t&&t.status===0}return!1}function Rt(e){let n=process.platform==="darwin"?Y("security",["find-generic-password","-s",z,"-a",e,"-w"]):process.platform==="linux"?Y("secret-tool",["lookup","service",z,"account",e]):null;return(n?.status===0?n.stdout.trim():"")||null}function Mt(e){process.platform==="darwin"?Y("security",["delete-generic-password","-s",z,"-a",e]):process.platform==="linux"&&Y("secret-tool",["clear","service",z,"account",e])}function rt(e,n){if(Lt(e,n))return Ie(se(e))&&st(se(e)),"keychain";let t=se(e);return Nt(Et(t),{recursive:!0,mode:448}),It(t,n,{mode:384}),At(t,384),"file"}function me(e){if(process.env.DOCKITO_TOKEN)return process.env.DOCKITO_TOKEN.trim();let n=Rt(e);if(n)return n;let t=se(e);return Ie(t)&&_t(t,"utf8").trim()||null}function it(e){Mt(e);let n=se(e);Ie(n)&&st(n)}var D=class extends Error{constructor(t,o,s={}){super(o);this.code=t;this.data=s}code;data};function we(e,n){let t=e.workspaces;if(n)return t.find(s=>s.slug===n)??f(`This login can't reach workspace "${n}". \`dockito tree\` lists the ones it can.`);let o=t.length===1?t[0]:t.find(s=>s.kind==="personal"&&s.role==="owner");if(o)return o;throw new D("needs_workspace",`Pick a workspace with -w: ${t.map(s=>s.slug).join(", ")}`,{workspaces:t.map(s=>s.slug)})}function Ee(e,n){if(!n)return{project:null,folder:null,label:`${e.slug} Docs`};let[t,...o]=n.split("/").filter(Boolean),s=M(e,t);if(!s)return f(`No project "${t}" in ${e.slug}. \`dockito tree\` lists them; \`dockito mkdir ${n}\` makes it.`);let r=H(s).idOf(o);return r===void 0?f(`No folder "${o.join("/")}" in ${s.slug}. \`dockito mkdir ${s.slug}/${o.join("/")}\` makes it.`):{project:s.id,folder:r,label:[e.slug,s.slug,...o].join("/")}}import{createHash as Ft}from"node:crypto";import{spawnSync as Kt}from"node:child_process";import{existsSync as ie,readFileSync as Ut,readdirSync as Wt,statSync as lt}from"node:fs";import{basename as at,join as C,relative as Ht,resolve as Ce,sep as Bt}from"node:path";var Jt=4,re=e=>e<1024*1024?`${e?Math.max(1,Math.round(e/1024)):0} KB`:`${(e/1048576).toFixed(1)} MB`,Gt=e=>Ft("sha256").update(e).digest("hex"),dt=e=>e.split(Bt).join("/"),V=(e,n)=>`${e} ${n}${e===1?"":"s"}`,qt=(e,n)=>{let t=/\.[^./]+$/.exec(n.toLowerCase())?.[0]??"";return e.find(o=>o.extensions.includes(t))?.id??null};function ye(e,n){let t=Kt("git",e,{cwd:n,encoding:"utf8",maxBuffer:67108864});return t.error||t.status!==0?null:t.stdout}function zt(e,n){let t=ye(["ls-files","-z","--cached","--others","--exclude-standard","--","."],e);return(t!==null?t.split("\0").filter(Boolean):(function s(r){return Wt(C(e,r),{withFileTypes:!0}).flatMap(a=>a.name.startsWith(".")||a.name==="node_modules"?[]:a.isDirectory()?s(C(r,a.name)):[dt(C(r,a.name))])})("")).filter(s=>n(s)&&ie(C(e,s))&&lt(C(e,s)).isFile()).sort()}function Yt(e,n,t){let o=[...e.matchAll(/\b(?:src|href)=["']([^"'#?]+)/gi)].map(s=>s[1]);return[...new Set(o.filter(s=>!/^([a-z]+:|\/\/|\/)/i.test(s)&&!t(s)&&ie(C(n,s))))]}function ct(e,n){let t=(r,a,c="")=>`dockito sync ${n} -w ${r} --to ${a}${c}`,o=e.workspaces.filter(_).flatMap(r=>[...r.projects.map(a=>({workspace:r.slug,project:a.slug,name:a.name,docs:a.docs,command:t(r.slug,a.slug)})),{workspace:r.slug,project:null,name:"a new project",docs:0,command:t(r.slug,"<name>"," --create")}]),s=Math.max(...o.map(r=>r.command.length));throw new D("needs_place",[`Which project should ${n==="."?"this folder":n+"/"} go to? Ask the user, then run one of:`,...o.map(r=>` ${r.command.padEnd(s)} ${p(r.project?`${r.name} \xB7 ${V(r.docs,"doc")}`:r.name)}`),p("Subfolders become folders. `dockito tree` shows every place.")].join(`
10
+ `),{choices:o})}async function ut(e,n,t){let o=Ae()&&!t.json,s=await e.tree(),r=i=>!!qt(s.kinds,i);s.workspaces.some(_)||f("This login can't publish anywhere. Run `dockito login --update`.");let a=n;a||(o||ct(s,ie("docs")?"docs":"."),a=await he("Folder to sync",ie("docs")?"docs":"."));let c=Ce(a);(!ie(c)||!lt(c).isDirectory())&&f(`${a} isn't a folder.`);let u=dt(Ht(process.cwd(),c))||".",k;t.workspace&&(k=s.workspaces.find(i=>i.slug===t.workspace)??f(`This login can't reach workspace "${t.workspace}".`));let w=t.to;if(!w){o||ct(s,u);let i=(k?[k]:s.workspaces.filter(_)).flatMap(m=>[...m.projects.map(v=>({w:m,p:v.slug,label:v.name,hint:`${m.slug} \xB7 ${V(v.docs,"doc")}`})),{w:m,p:null,label:"New project",hint:m.slug}]),l=i[await et("Project",i)];k=l.w,w=l.p??await he("Project name",at(c===process.cwd()?c:Ce(c,".."))),t.create||=!l.p}if(!k){let i=s.workspaces.filter(m=>_(m)&&M(m,w.split("/")[0])),l=s.workspaces.find(m=>m.kind==="personal"&&m.role==="owner");if(k=i.length===1?i[0]:s.workspaces.length===1?s.workspaces[0]:!i.length&&l?l:void 0,!k)throw new D("needs_workspace",`Which workspace? Run again with -w: ${(i.length?i:s.workspaces).map(m=>m.slug).join(", ")}`)}let h=k;_(h)||f(`This login has View access in ${h.slug}; syncing needs Publish. Run \`dockito login --update\`.`);let[g,...j]=w.split("/").filter(Boolean);g||f("--to takes a project, or project/folder."),j.length>2&&f(`Folders go at most ${2} deep inside a project.`);let b=M(h,g);if(!b&&!t.create)throw new D("no_project",`No project "${g}" in ${h.slug}. Projects: ${h.projects.map(i=>i.slug).join(", ")||"none"}.
11
+ To make it, run again with --create.`,{workspace:h.slug,projects:h.projects.map(i=>i.slug)});let K=zt(c,r);K.length||f(`No ${s.kinds.flatMap(i=>i.extensions).join(", ")} files in ${u}.`);let ce=2-j.length,X=K.filter(i=>i.split("/").length-1>ce);if(X.length&&!t.fold){let i=m=>m.split("/").slice(0,ce).join("/")||"the project's top",l=`${V(X.length,"file")} ${X.length===1?"is":"are"} more than ${2} folders deep inside the project. With --fold they go in ${[...new Set(X.map(i))].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 D("too_deep",`${l}
12
+ Ask the user, then run again with --fold.`,{files:X});if($(l),!await Ne("Fold them like that?"))return $(p("Nothing changed."));t.fold=!0}let Ue=i=>{let l=i.split("/");return{folder:[...j,...l.slice(0,-1).slice(0,ce)],dropped:l.slice(0,-1).slice(ce),file:l[l.length-1]}},je=new Map;for(let i of K){let l=Ue(i),m=[...l.folder,l.file].join("/").toLowerCase();je.set(m,(je.get(m)??0)+1)}let We=b?await e.files(h.id,b.id):[],Q=H(b),R=K.map(i=>{let{folder:l,dropped:m,file:v}=Ue(i),L=m.length&&je.get([...l,v].join("/").toLowerCase())>1?[...m,v].join("-"):v,S=new Uint8Array(Ut(C(c,i))),W={rel:i,name:L,folder:l,bytes:S,hash:Gt(S),action:"new",doc:null,others:[],assets:[]};/\.html?$/i.test(i)&&(W.assets=Yt(new TextDecoder().decode(S),C(c,i,".."),r));let Qe=Q.idOf(l);if(Qe===void 0)return W;let Z=ee(We,Qe,L);return Z.length>1?{...W,action:"ambiguous",others:Z}:Z.length?{...W,doc:Z[0],action:Z[0].sha256===W.hash?"same":"update"}:W}),He=R.filter(i=>i.bytes.length>h.maxFile);He.length&&f(`Over the ${re(h.maxFile)} limit per file in ${h.slug}:
13
+ ${He.map(i=>` ${i.rel} ${re(i.bytes.length)}`).join(`
14
+ `)}`);let ve=new Map;for(let i of R){let l=[...i.folder,i.name].join("/").toLowerCase();ve.set(l,[...ve.get(l)??[],i.rel])}let Be=[...ve.values()].filter(i=>i.length>1);Be.length&&f(`These files would be the same doc; rename one of each:
15
+ ${Be.map(i=>" "+i.join(" and ")).join(`
16
+ `)}`);let N=[],Je=new Set;for(let i of R)for(let l=1;l<=i.folder.length;l++){let m=i.folder.slice(0,l),v=m.join("/").toLowerCase();Q.idOf(m)===void 0&&!Je.has(v)&&(Je.add(v),N.push(m))}N.sort((i,l)=>i.length-l.length);let O=R.filter(i=>i.action==="new"||i.action==="update"),Ge=R.filter(i=>i.action==="ambiguous"),xe=R.filter(i=>i.action==="same").length,yt=new Set(R.flatMap(i=>i.doc?[i.doc.id]:i.others.map(l=>l.id))),Te=We.filter(i=>!yt.has(i.id)).length,qe=`${h.slug}/${b?.slug??g}${j.length?"/"+j.join("/"):""}`,le={workspace:h.slug,to:qe,create:{project:b?null:g,folders:N.map(i=>i.join("/"))},docs:O.map(i=>({file:i.rel,action:i.action,folder:i.folder.join("/")||null,name:i.name,link:i.doc?e.linkOf(i.doc.slug):null})),unchanged:xe,ambiguous:Ge.map(i=>({file:i.rel,docs:i.others.map(l=>e.linkOf(l.slug))})),notHere:Te};if(!t.json){d(`${T(u==="."?"./":u+"/")} \u2192 ${T(qe)}`),b||d(` ${y("+ project")} ${g}`);for(let l of N)d(` ${y("+ folder ")} ${l.join("/")}`);if(O.length>15)for(let[l,m]of[["new",y("+ new ")],["update",E("~ update ")]]){let v=new Map;for(let L of O.filter(S=>S.action===l))v.set(L.folder.join("/"),[...v.get(L.folder.join("/"))??[],L.name]);for(let[L,S]of v)d(` ${m} ${S.length} in ${L||"the project's top"}: ${p(S.slice(0,4).join(", ")+(S.length>4?`, ${S.length-4} more`:""))}`)}else for(let l of O){let m=l.name!==at(l.rel)?p(` \u2192 ${[...l.folder,l.name].join("/")}`):"";d(` ${l.action==="new"?y("+ new "):E("~ update ")} ${l.rel}${m}`)}xe&&d(p(` ${xe} unchanged`));for(let l of Ge)d(` ${I("? skip ")} ${l.rel} ${p(`${l.others.length} docs there have this name: ${l.others.map(m=>e.linkOf(m.slug)).join(", ")}`)}`);Te&&d(p(` ${V(Te,"doc")} in the project with no file here, left as they are`)),O.length&&d(p(` ${V(O.length,"file")} to send, ${re(O.reduce((l,m)=>l+m.bytes.length,0))}`));let i=O.filter(l=>l.assets.length);i.length&&d(E(` ! ${i.length===1?i[0].rel:`${i.length} HTML files`} load ${[...new Set(i.flatMap(l=>l.assets))].join(", ")} from next to ${i.length===1?"it":"them"}; only HTML files are published`))}if(!O.length&&b&&!N.length){t.json?d(JSON.stringify({...le,applied:!0},null,2)):d(`${y("\u2713")} Up to date`);return}if(t["dry-run"]){t.json&&d(JSON.stringify({...le,applied:!1},null,2));return}if((!b||N.length)&&!t.yes){let i=["dockito sync",u,`-w ${h.slug}`,`--to ${w}`,t.create&&!b?"--create":"",t.fold?"--fold":"","--yes"].filter(Boolean).join(" ");if(!o){if(t.json)return d(JSON.stringify({...le,applied:!1,next:i},null,2));throw new D("confirm",`
17
+ Nothing changed yet. Show the user this plan, then run: ${i}`,{next:i})}if(!await Ne("Apply?"))return $(p("Nothing changed."))}if(!b){let i=await e.createProject(h.id,g);b={id:i.id,slug:i.slug,name:g,docs:0,folders:[]},t.json||d(`${y("\u2713")} Project ${g}`)}let Oe=b;for(let i of N){let l=await e.createFolder(Oe.id,i[i.length-1],i.length>1?Q.idOf(i.slice(0,-1)):null);Q.add(i,l.id)}N.length&&!t.json&&d(`${y("\u2713")} ${V(N.length,"folder")}`);let de=ye(["rev-parse","--show-toplevel"],c)?.trim()??null,ze=de?ye(["rev-parse","HEAD"],de)?.trim()??null:null,bt=new Set(de?(ye(["status","--porcelain","-z","--","."],c)??"").split("\0").filter(Boolean).map(i=>Ce(de,i.slice(3))):[]),U=[],ue=null,De=[...O],pe=!t.json&&Ae(),$t=O.reduce((i,l)=>i+l.bytes.length,0),Ye=0,Ve=()=>pe&&process.stdout.write(`\r\x1B[2K${p(`Sending ${U.length} of ${O.length} \xB7 ${re(Ye)} of ${re($t)}`)}`);async function jt(i){let l=ze&&!bt.has(C(c,i.rel))?ze:null;for(;;)try{let m=i.doc?await e.update(i.doc.id,i.name,i.bytes,{commit:l}):await e.create(h.id,i.name,i.bytes,{project:Oe.id,folder:Q.idOf(i.folder)??null});return{file:i.rel,action:i.action,link:e.linkOf(m.slug)}}catch(m){if(m instanceof x&&m.code==="rate_limited"){ue||(t.json||$(p("\rUpload limit reached; carrying on in a minute\u2026")),ue=new Promise(v=>setTimeout(v,3e4)).then(()=>{ue=null})),await ue;continue}return{file:i.rel,action:i.action,error:m instanceof x?ne(m,e.url):String(m)}}}Ve(),await Promise.all(Array.from({length:Math.min(Jt,De.length)},async()=>{for(let i=De.shift();i;i=De.shift()){let l=await jt(i);U.push(l),Ye+=i.bytes.length,pe&&process.stdout.write("\r\x1B[2K"),l.error&&!t.json?d(`${I("\u2717")} ${l.file} ${I(l.error)}`):!t.json&&!pe&&d(`${y("\u2713")} ${l.file} ${p(l.link)}`),Ve()}})),pe&&process.stdout.write("\r\x1B[2K");let Pe=U.filter(i=>i.error),Xe=`${e.url}/${h.slug}/project/${Oe.slug}`;t.json?d(JSON.stringify({...le,applied:!0,link:Xe,results:U},null,2)):d(`${Pe.length?E(`${U.length-Pe.length} of ${U.length} sent`):y("\u2713 Synced")} ${p(Xe)}`),Pe.length&&(process.exitCode=1)}var Le=`Dockito hosts Markdown and HTML docs at a short link, with History.
19
18
 
20
19
  workspace a person's or a team's space; its slug is in every link
21
20
  project docs grouped in a workspace, with folders up to 2 deep
@@ -37,7 +36,7 @@ For AI agents:
37
36
  - A folder of docs: \`dockito sync <folder> --to <project>\`. It shows a
38
37
  plan; show it to the user before running with --yes.
39
38
  - \`dockito <command> --help\` explains each command. Use --json; on
40
- failure, read "error" and the commands it suggests.`,ht={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
39
+ failure, read "error" and the commands it suggests.`,be={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
41
40
  file updates the doc with the same file name in the same folder, or becomes
42
41
  a new doc there. Only files whose content changed are sent. Nothing is moved
43
42
  or deleted in Dockito. The CLI remembers nothing: pass --to every time.
@@ -70,12 +69,15 @@ from Share in the app.
70
69
  -w, --workspace <slug> Another workspace (default: your personal one)
71
70
  -m, --message <note> Keep this entry as a saved point, with a note
72
71
  --public Make the new doc public (only when the user asked)
72
+ --title <title> A new doc's title (default: the file name); an
73
+ existing doc keeps its own
73
74
  --doc <doc> Update this doc (link or code) instead
74
75
  --new A new doc even if the name is already there
75
76
 
76
77
  dockito publish plan.md private, in your Docs
77
78
  dockito publish plan.md --to handbook/specs private, in a folder
78
- dockito publish plan.md --public the user asked for a public link`},share:{use:"share <doc>",about:"Who can open a doc; --private takes a public link back",page:`Says whether a doc is private (only people in its workspace) or public
79
+ dockito publish plan.md --public the user asked for a public link
80
+ dockito publish q3.html --title "Q3 plan" named Q3 plan, not q3`},share:{use:"share <doc>",about:"Who can open a doc; --private takes a public link back",page:`Says whether a doc is private (only people in its workspace) or public
79
81
  (anyone with the link). --private makes it private again at once.
80
82
 
81
83
  An existing doc is made public only from Share in the app; a new doc can be
@@ -83,25 +85,30 @@ published public with \`dockito publish <file> --public\` when the user asks.
83
85
 
84
86
  dockito share <link or code>
85
87
  dockito share <link or code> --private`},tree:{use:"tree",about:"Every workspace, project and folder you can use",page:"Lists every workspace this login can reach, its projects and their folders,\nwith doc counts, each written as the --to it takes. Docs themselves: `dockito ls`."},ls:{use:"ls [project/folder]",about:"What's in a place",page:`Lists the docs (with their codes), projects and folders in a place: your
86
- workspace's top level, a project, or a folder. -w for another workspace.`},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.
88
+ workspace's top level, a project, or a folder. The place can also be given
89
+ as --to, like publish. -w for another workspace.
90
+
91
+ dockito ls handbook/specs
92
+ 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.
87
93
 
88
94
  dockito mkdir handbook/specs/drafts`},open:{use:"open <doc>",about:"Open in the browser",page:"Opens a doc (link or code) in the browser."},get:{use:"get <doc> [-o file]",about:"Download a doc",page:"Prints a doc's file, or writes it with -o. --entry <n> gets a History entry\n(numbers from `dockito history`)."},history:{use:"history <doc>",about:"History entries",page:`Lists a doc's History: entry, when, who, from where (upload, edit, cli,
89
95
  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
90
96
  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
91
97
  in each workspace: View, Publish or Everything. --update changes that access.
92
- --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."}},Ce=`Dockito CLI ${Me}
98
+ --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 Fe="0.0.1-beta.6",A=nt(),ae=ot(A),Ke=`dockito-cli/${Fe} (${process.platform})`;function P(){let e=me(ae);return e||f("Not logged in. Run `dockito login`."),ge({url:A,token:e,source:"cli",userAgent:Ke})}function kt(e){let[n,t]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["cmd",["/c","start","",e]]:["xdg-open",[e]];try{let o=Vt(n,t,{stdio:"ignore",detached:!0});return o.on("error",()=>{}),o.unref(),!0}catch{return!1}}var rn=()=>!process.env.SSH_CONNECTION&&!process.env.CI&&(process.platform!=="linux"||!!process.env.DISPLAY||!!process.env.WAYLAND_DISPLAY),an=e=>new Promise(n=>setTimeout(n,e)),cn=()=>tn().replace(/\.local$/,"").slice(0,60)||"Unknown device";function F(e){return Ze(e)??f(`"${e}" isn't a doc link or code. \`dockito ls\` lists docs with their codes.`)}function ft(e){let n=nn(wt(e)),t=pt("git",["rev-parse","HEAD"],{cwd:n,encoding:"utf8"});if(t.error||t.status!==0)return null;let o=pt("git",["status","--porcelain","--",Me(e)],{cwd:n,encoding:"utf8"});return o.status!==0||o.stdout.trim()?null:t.stdout.trim()}var ln=e=>Xt("sha256").update(e).digest("hex");async function dn(e){let n=me(ae);e.update&&!n&&f("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 t=ge({url:A,token:e.update?n:null,userAgent:Ke}),o=await t.startLogin("Dockito CLI",cn()),s=!e["no-browser"]&&rn()&&kt(o.verification_uri_complete);d(s?`Opening ${o.verification_uri_complete}`:`Open ${T(o.verification_uri)}`),d(`Code: ${T(o.user_code)}`),$(p("Waiting for approval\u2026"));let r=o.interval*1e3,a=Date.now()+o.expires_in*1e3;for(;Date.now()<a;){await an(r);try{let c=await t.pollLogin(o.device_code);if(!c)continue;let u=rt(ae,c.token);d(`${y("\u2713")} Logged in as ${c.user.name}${c.user.username?p(` (${c.user.username})`):""}`);for(let k of c.workspaces)d(` ${k.name} ${p(`${k.slug} \xB7 ${J[k.level]}`)}`);u==="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 x&&c.code==="access_denied"&&f("Denied in the browser."),c instanceof x&&c.code==="expired_token"&&f("The code expired. Run `dockito login` again."),c instanceof x&&c.code==="offline"){r=Math.min(r*2,3e4);continue}throw c}}f("The code expired. Run `dockito login` again.")}async function un(){let e=me(ae);if(!e)return d("Not logged in.");process.env.DOCKITO_TOKEN&&f("DOCKITO_TOKEN is set; unset it or revoke that token in Account \u2192 Tokens.");try{await ge({url:A,token:e,userAgent:Ke}).logout()}catch(n){if(!(n instanceof x)||n.code!=="invalid_token"&&n.code!=="not_found")throw n}it(ae),d(`${y("\u2713")} Logged out`)}async function pn(e){let n=await P().whoami();if(e.json)return d(JSON.stringify(n,null,2));d(`${n.user.name} ${p(`${n.user.username} \xB7 ${n.user.email}`)}`),n.token&&d(p(`${n.token.name} \xB7 ${n.token.source==="cli"?"ends after 90 days without use":`expires ${new Date(n.token.expiresAt).toLocaleDateString()}`}`)),d(),te(n.workspaces.map(t=>[t.slug,t.name,p(t.level?J[t.level]:t.role)])),A!=="https://dockito-app.dockito.workers.dev"&&$(p(`
99
+ ${A}`))}async function gn(e,n){e.length||f("Name a file: `dockito publish notes.md --to <project/folder>`"),n.doc&&e.length>1&&f("--doc takes one file."),n.title!==void 0&&e.length>1&&f("--title takes one file."),n.title!==void 0&&!n.title.trim()&&f("--title needs a title."),n.public&&n.doc&&f("--public is only for a new doc; an existing one can be made public from Share in the app.");for(let u of e)(!Qt(u)||!Zt(u).isFile())&&f(`${u} isn't a file${on(u)?"":` (looked for ${wt(u)})`}.`);let t=P(),o=[];if(n.doc){let u=await t.doc(F(n.doc)),k=new Uint8Array(gt(e[0])),w=await t.update(u.doc.id,Me(e[0]),k,{note:n.message,commit:ft(e[0])});return n.json?d(JSON.stringify([{file:e[0],...w,link:t.linkOf(w.slug)}],null,2)):d(w.unchanged?`${p("=")} ${e[0]} ${p("unchanged")} ${t.linkOf(w.slug)}`:`${y("\u2713")} ${e[0]} \u2192 ${t.linkOf(w.slug)}`)}let s=await t.tree(),r=we(s,n.workspace);_(r)||f(`This login has View access in ${r.slug}. Run \`dockito login --update\`.`);let a=Ee(r,n.to),c=await t.files(r.id,a.project);for(let u of e){let k=new Uint8Array(gt(u)),w=Me(u),h=n.new?[]:ee(c,a.folder,w);h.length>1&&f(`${h.length} docs in ${a.label} are named ${w}: ${h.map(K=>t.linkOf(K.slug)).join(", ")}. Pick one with --doc, or publish a new one with --new.`);let g=h[0];if(g&&n.public&&!(await t.doc(g.slug)).doc.public&&f(`${w} is already a private doc in ${a.label} (${t.linkOf(g.slug)}). Only Share in the app can make it public; \`--new --public\` publishes a new public doc.`),g&&g.sha256===ln(k)&&!n.message){o.push({file:u,id:g.id,slug:g.slug,version:g.version,unchanged:!0,link:t.linkOf(g.slug)}),n.json||d(`${p("=")} ${u} ${p("unchanged")} ${t.linkOf(g.slug)}`);continue}let j=g?await t.update(g.id,w,k,{note:n.message,commit:ft(u)}):await t.create(r.id,w,k,{project:a.project,folder:a.folder,public:n.public,title:n.title});!g&&n.message&&await t.save(j.id,j.version,n.message);let b="public"in j&&j.public===!0;o.push({file:u,...j,link:t.linkOf(j.slug),new:!g}),n.json||d(`${y("\u2713")} ${u} \u2192 ${t.linkOf(j.slug)} ${p(g?j.unchanged?"unchanged":"updated":`new in ${a.label}`)}${b?` ${y("\xB7 public")}`:""}${n.message?p(` \xB7 saved: ${n.message}`):""}`)}n.json?d(JSON.stringify(o,null,2)):!n.to&&o.some(u=>u.new)&&$(p("In the workspace's Docs. To put docs in a project: --to <project/folder>; `dockito tree` lists them."))}async function fn(e){let n=await P().tree();if(e.json)return d(JSON.stringify(n,null,2));let t=o=>p(`${o} doc${o===1?"":"s"}`);for(let o of n.workspaces){d(`${T(o.slug)} ${o.name} ${p(`\xB7 ${o.level?J[o.level]:o.role}`)}`);let s=[[p("Docs"),"",t(o.docs)]];for(let r of o.projects){s.push([r.slug,r.name,t(r.docs)]);let a=(c,u)=>{for(let k of r.folders.filter(w=>w.parentId===c))s.push([`${u}/${k.name}`,"",t(k.docs)]),a(k.id,`${u}/${k.name}`)};a(null,r.slug)}te(s.map(r=>[" ",...r])),d()}$(p("Publish into a place: dockito publish <file> -w <workspace> --to <project/folder>"))}async function hn(e,n){e&&n.to&&e!==n.to&&f(`Two places: ${e} and --to ${n.to}. Give one.`);let t=e??n.to,o=P(),s=await o.tree(),r=we(s,n.workspace),a=Ee(r,t),c=await o.docs(r.id,{project:a.project,folder:a.folder}),u=a.project?r.projects.find(g=>g.id===a.project):null,k=t?[]:r.projects,w=u?u.folders.filter(g=>g.parentId===a.folder):[];if(n.json)return d(JSON.stringify({workspace:r.slug,place:t??null,projects:k,folders:w,docs:c},null,2));let h=[];for(let g of k)h.push([T(`${g.slug}/`),g.name,p(`${g.docs} doc${g.docs===1?"":"s"}`),""]);for(let g of w)h.push([T(`${g.name}/`),"",p(`${g.docs} doc${g.docs===1?"":"s"}`),""]);for(let g of c)h.push([g.slug,g.title+p(g.kind==="html"?" .html":" .md"),p(fe(g.updatedAt)),p(g.updatedByName??"")]);if(!h.length)return $(p(`Nothing in ${a.label} yet.`));te(h)}async function mn(e,n){let[t,...o]=(e??"").split("/").filter(Boolean);t||f("`dockito mkdir <project>[/folder[/subfolder]]`"),o.length>2&&f("Folders go at most 2 deep inside a project.");let s=P(),r=we(await s.tree(),n.workspace),a=M(r,t),c=a?.id??(await s.createProject(r.id,t)).id,u=!a;a||d(`${y("\u2713")} Project ${t}`);let k=H(a),w=null;for(let h=0;h<o.length;h++){let g=k.idOf(o.slice(0,h+1));if(g){w=g;continue}w=(await s.createFolder(c,o[h],w)).id,k.add(o.slice(0,h+1),w),u=!0,d(`${y("\u2713")} Folder ${o.slice(0,h+1).join("/")}`)}u||$(p("Already there."))}async function wn(e){e||f("Name a doc: `dockito open <link or code>`");let n=`${A}/${F(e)}`;kt(n)||d(n)}async function kn(e,n){e||f("Name a doc: `dockito get <link or code> [-o file]`");let t=P(),o=await t.doc(F(e)),s=n.entry?Number(n.entry):null;n.entry&&(!Number.isInteger(s)||s<1)&&f("--entry takes an entry number from `dockito history`.");let{bytes:r}=await t.content(o.doc.id,s);if(!n.output)return void process.stdout.write(r);en(n.output,r),$(`${y("\u2713")} ${n.output} ${p(`\u2190 ${t.linkOf(o.doc.slug)}${s?` entry ${s}`:""}`)}`)}async function yn(e,n){e||f("Name a doc: `dockito history <link or code>`");let t=await P().doc(F(e));if(n.json)return d(JSON.stringify(t.versions,null,2));d(`${T(t.doc.title)} ${p(`${A}/${t.doc.slug}`)}`),te(t.versions.map((o,s)=>[String(o.number),s===0?y("current"):p(fe(o.createdAt)),s===0?p(fe(o.createdAt)):"",o.byName??"",p(o.source),o.savedAt?`${E("saved")}${o.note?` ${o.note}`:""}`:"",o.gitCommit?p(o.gitCommit.slice(0,7)):""]))}async function bn(e,n){let t=Number(n);(!e||!Number.isInteger(t)||t<1)&&f("`dockito restore <doc> <entry>`; entries are in `dockito history`.");let o=P(),s=await o.doc(F(e)),r=await o.restore(s.doc.id,t,s.versions[0]?.number??null);d(`${y("\u2713")} Restored entry ${t} as entry ${r.version} ${p(o.linkOf(r.slug))}`)}async function $n(e,n,t){e||f('`dockito save <doc> [entry] -m "note"`');let o=P(),s=await o.doc(F(e)),r=n?Number(n):s.versions[0]?.number;(!Number.isInteger(r)||r<1)&&f("Entries are in `dockito history`."),await o.save(s.doc.id,r,t.message??null),d(`${y("\u2713")} Saved entry ${r}${t.message?p(` \xB7 ${t.message}`):""}`)}async function jn(e,n){e||f("`dockito share <link or code> [--private]`"),n.public&&f("An existing doc can be made public only from Share in the app. `dockito publish <file> --public` makes a new doc public.");let t=P(),o=await t.doc(F(e)),s=t.linkOf(o.doc.slug);if(n.private)return o.doc.public&&await t.makePrivate(o.doc.id),n.json?d(JSON.stringify({public:!1,link:s},null,2)):d(`${y("\u2713")} Private: only people in ${o.workspace.name} can open it ${p(s)}`);if(n.json)return d(JSON.stringify({public:o.doc.public,link:s},null,2));d(o.doc.public?`${y("Public")}: anyone with the link can open it ${s}`:`Private: only people in ${o.workspace.name} can open it ${p(s)}`)}var Re=`Dockito CLI ${Fe}
93
100
 
94
- ${gt}
101
+ ${Le}
95
102
 
96
103
  Usage: dockito <command>
97
104
 
98
- ${Object.values(ht).map(e=>` ${e.use.padEnd(30)}${e.about}`).join(`
105
+ ${Object.values(be).map(e=>` ${e.use.padEnd(30)}${e.about}`).join(`
99
106
  `)}
100
107
 
101
108
  <doc> is a link or a code. \`dockito <command> --help\` for more.
102
109
  -w, --workspace <slug> Another workspace (default: your personal one)
103
110
  --json Machine-readable output
104
111
 
105
- Environment: DOCKITO_TOKEN (use this token), DOCKITO_URL (another Dockito)`,pt=e=>{let n=ht[e];return n?`dockito ${n.use}
112
+ Environment: DOCKITO_TOKEN (use this token), DOCKITO_URL (another Dockito)`,ht=e=>{let n=be[e];return n?`dockito ${n.use}
106
113
 
107
- ${n.page}`:null};async function $n(){let{values:e,positionals:n}=tn({allowPositionals:!0,options:{message:{type:"string",short:"m"},to:{type:"string"},doc:{type:"string"},new:{type:"boolean"},workspace:{type:"string",short:"w"},output:{type:"string",short:"o"},entry:{type:"string"},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"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}}),[t,...o]=n;if(e.version)return d(Me);if(t==="help"&&o[0])return d(pt(o[0])??Ce);if(e.help&&t)return d(pt(t)??Ce);if(e.help||!t||t==="help")return d(Ce);switch(t){case"login":return cn(e);case"logout":return ln();case"whoami":return dn(e);case"sync":return ct(S(),o[0],e);case"publish":return un(o,e);case"tree":return pn(e);case"ls":return fn(o[0],e);case"mkdir":return gn(o[0],e);case"share":return bn(o[0],e);case"open":return hn(o[0]);case"get":return mn(o[0],e);case"history":return wn(o[0],e);case"restore":return kn(o[0],o[1]);case"save":return yn(o[0],o[1],e);case"agent":return d(gt);default:m(`Unknown command "${t}". Run \`dockito --help\`.`)}}function be(e){let n=2,t=()=>--n===0&&process.exit(e);process.stdout.write("",t),process.stderr.write("",t)}$n().then(()=>be(Number(process.exitCode??0)),e=>{let n=process.argv.includes("--json");if(e instanceof D)return n?d(JSON.stringify({error:e.code,message:e.message.replace(/\x1b\[[0-9;]*m/g,""),...e.data},null,2)):$(e.message),be(2);if(e instanceof te)return be(130);n&&e instanceof x?d(JSON.stringify({error:e.code,message:ee(e,A),...e.detail},null,2)):n&&e instanceof W?d(JSON.stringify({error:"failed",message:e.message},null,2)):e instanceof W?$(_(e.message)):e instanceof x?$(_(ee(e,A))):e instanceof TypeError&&/parseArgs|option|positional/i.test(String(e.message))?$(_(`${e.message}. Run \`dockito --help\`.`)):$(_(String(e?.stack??e))),be(1)});
114
+ ${n.page}`:null},vn={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"],args:1},history:{flags:["json"],args:1},restore:{flags:[],args:2},save:{flags:["message"],args:2},agent:{flags:[],args:0}},mt={message:"-m",workspace:"-w",output:"-o",yes:"-y"};function xn(e,n,t){let o=vn[e];if(!o)return;let s=`\`dockito ${be[e]?.use??e}\`; \`dockito ${e} --help\` for more.`,r=n.filter(a=>!o.flags.includes(a));if(r.length){let a=r.map(u=>`--${u}`).join(", "),c=o.flags.length?` It takes ${o.flags.map(u=>mt[u]?`${mt[u]}/--${u}`:`--${u}`).join(", ")}.`:"";f(`${e} doesn't take ${a}.${c} Use: ${s}`)}t.length>o.args&&f(`${e} takes ${o.args===0?"no arguments":`at most ${o.args} argument${o.args===1?"":"s"}`}; got ${t.map(a=>`"${a}"`).join(" ")}. Use: ${s}`)}async function Tn(){let{values:e,positionals:n}=sn({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"},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"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}}),[t,...o]=n;if(e.version)return d(Fe);if(t==="help"&&o[0])return d(ht(o[0])??Re);if(e.help&&t)return d(ht(t)??Re);if(e.help||!t||t==="help")return d(Re);switch(xn(t,Object.keys(e).filter(s=>e[s]!==void 0),o),t){case"login":return dn(e);case"logout":return un();case"whoami":return pn(e);case"sync":return ut(P(),o[0],e);case"publish":return gn(o,e);case"tree":return fn(e);case"ls":return hn(o[0],e);case"mkdir":return mn(o[0],e);case"share":return jn(o[0],e);case"open":return wn(o[0]);case"get":return kn(o[0],e);case"history":return yn(o[0],e);case"restore":return bn(o[0],o[1]);case"save":return $n(o[0],o[1],e);case"agent":return d(Le);default:f(`Unknown command "${t}". Run \`dockito --help\`.`)}}function $e(e){let n=2,t=()=>--n===0&&process.exit(e);process.stdout.write("",t),process.stderr.write("",t)}Tn().then(()=>$e(Number(process.exitCode??0)),e=>{let n=process.argv.includes("--json");if(e instanceof D)return n?d(JSON.stringify({error:e.code,message:e.message.replace(/\x1b\[[0-9;]*m/g,""),...e.data},null,2)):$(e.message),$e(2);if(e instanceof oe)return $e(130);n&&e instanceof x?d(JSON.stringify({error:e.code,message:ne(e,A),...e.detail},null,2)):n&&e instanceof B?d(JSON.stringify({error:"failed",message:e.message},null,2)):e instanceof B?$(I(e.message)):e instanceof x?$(I(ne(e,A))):e instanceof TypeError&&/parseArgs|option|positional/i.test(String(e.message))?$(I(`${e.message}. Run \`dockito --help\`.`)):$(I(String(e?.stack??e))),$e(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dockito",
3
- "version": "0.0.1-beta.4",
3
+ "version": "0.0.1-beta.6",
4
4
  "description": "Dockito CLI: publish the docs your AI writes as links you can share.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://dockito-site.dockito.workers.dev",