@pepitahq/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/index.js +16 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Barely Notable OÜ (https://barelynotable.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
> **Snapshot.** Factored out of the private pepita monorepo, built and released from there,
|
|
2
|
+
> and **not standalone-buildable**. PRs are applied in the monorepo. https://pepita.dev
|
|
3
|
+
|
|
4
|
+
# @pepitahq/cli
|
|
5
|
+
|
|
6
|
+
Command-line access to your [pepita](https://pepita.dev) sites. Talks to
|
|
7
|
+
`app.pepita.dev` over HTTPS; you sign in once via a browser-based device
|
|
8
|
+
authorization (OAuth-style).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -g @pepitahq/cli # then: pepita <command>
|
|
14
|
+
# or one-off:
|
|
15
|
+
npx @pepitahq/cli <command>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Use
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pepita login # opens the browser to authorize this device
|
|
22
|
+
pepita list
|
|
23
|
+
pepita pull my-site # the live site (default)
|
|
24
|
+
pepita pull my-site --state unsaved # your current working copy
|
|
25
|
+
# …edit files locally…
|
|
26
|
+
pepita apply my-site # upload as unsaved changes
|
|
27
|
+
pepita save my-site # save unsaved → draft
|
|
28
|
+
pepita publish my-site # publish draft → live
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### The three states (`pull --state …`)
|
|
32
|
+
|
|
33
|
+
| `--state` | what you get | URL |
|
|
34
|
+
|-----------|--------------|-----|
|
|
35
|
+
| `live` (default) | the published site | `my-site.pepita.dev` |
|
|
36
|
+
| `draft` | the saved staging site (excludes unsaved edits) | `my-site--draft.pepita.dev` |
|
|
37
|
+
| `unsaved` | your current working copy, including un-saved edits | — |
|
|
38
|
+
|
|
39
|
+
`live`/`draft` are complete checkouts (same as the editor's "Download .zip");
|
|
40
|
+
`unsaved` is the editable working set. `apply` always uploads into the
|
|
41
|
+
`unsaved` state — then `save` promotes it to `draft`, and `publish` to `live`.
|
|
42
|
+
|
|
43
|
+
- The token is stored in `~/.pepita/config.json` (mode 600). Revoke any device
|
|
44
|
+
in **Connected devices** in the editor. `PEPITA_API_BASE` overrides the host.
|
|
45
|
+
|
|
46
|
+
## Notes
|
|
47
|
+
|
|
48
|
+
- `pull` writes/overwrites files locally but does NOT delete local files that
|
|
49
|
+
are absent from the fetched state.
|
|
50
|
+
- `apply` will DELETE files from the unsaved working copy that exist remotely
|
|
51
|
+
but not in your local directory — it shows a plan and asks for confirmation
|
|
52
|
+
unless `--yes` is passed. Run `apply` from a complete copy of the site
|
|
53
|
+
(ideally a fresh `pull --state unsaved`) to avoid surprise deletions.
|
|
54
|
+
|
|
55
|
+
## Security
|
|
56
|
+
|
|
57
|
+
- The server stores only `sha256(token)`; the raw token lives only on your machine.
|
|
58
|
+
- One-time PKCE code, 120 s TTL, loopback-only redirect.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {hostname,userInfo,homedir}from'os';import {join,dirname,relative,sep}from'path';import {mkdirSync,writeFileSync,readFileSync,readdirSync,statSync}from'fs';import {createServer}from'http';import {spawn}from'child_process';import {unzipSync}from'fflate';import {createInterface}from'readline/promises';var ve=Object.defineProperty;var l=(e,t)=>()=>(e&&(t=e(e=0)),t);var g=(e,t)=>{for(var n in t)ve(e,n,{get:t[n],enumerable:true});};function L(){return process.env.PEPITA_CONFIG_DIR??join(homedir(),".pepita")}function _(){return join(L(),"config.json")}function x(){return process.env.PEPITA_API_BASE??m().apiBase??S}function m(){try{let e=readFileSync(_(),"utf-8"),t=JSON.parse(e);return {apiBase:S,...t}}catch{return {apiBase:S}}}function T(e){mkdirSync(L(),{recursive:true}),writeFileSync(_(),JSON.stringify(e,null,2),{mode:384});}function F(){let e=m();T({apiBase:e.apiBase});}var S,A=l(()=>{S="https://app.pepita.dev";});var N=l(()=>{});var B=l(()=>{});var H=l(()=>{});async function Te(e){let t=new TextEncoder().encode(e),n=await crypto.subtle.digest("SHA-256",t);return new Uint8Array(n)}function Ae(e){let t="";for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async function I(e){return Ae(await Te(e))}var M=l(()=>{});function z(e){let t=e.fetchImpl??fetch,n=e.apiBase.replace(/\/+$/,""),s=i=>encodeURIComponent(i);async function o(i,c={}){let a=new Headers(c.headers);return e.token&&a.set("authorization",`Bearer ${e.token}`),t(`${n}${i}`,{...c,headers:a})}async function r(i,c){let a=await o(i,c);if(!a.ok)throw new d(a.status,`${c?.method??"GET"} ${i} \u2192 ${a.status} ${await a.text()}`);return await a.json()}return {raw:o,json:r,async listSites(){return (await r("/api/sites")).sites},getTree(i,c="develop"){return r(`/api/sites/${s(i)}/tree?branch=${c}`)},async writeFile(i,c,a,y){let h=await o(`/api/sites/${s(i)}/draft/write`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({path:c,content:a,encoding:y})});if(!h.ok)throw new d(h.status,`write ${c} \u2192 ${h.status} ${await h.text()}`)},async deleteFile(i,c){let a=await o(`/api/sites/${s(i)}/draft/delete`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({path:c})});if(!a.ok)throw new d(a.status,`delete ${c} \u2192 ${a.status} ${await a.text()}`)},async flush(i,c){let a=await o(`/api/sites/${s(i)}/flush`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({branch:"develop",...c})});if(a.status===409)throw new d(409,"save conflict \u2014 pull latest and retry");if(!a.ok)throw new d(a.status,`save \u2192 ${a.status} ${await a.text()}`)},publish(i){return r(`/api/sites/${s(i)}/publish`,{method:"POST"})}}}var d,D=l(()=>{d=class extends Error{status;constructor(t,n){super(n),this.name="PepitaHttpError",this.status=t;}};});var k=l(()=>{N();B();H();M();D();});function u(){return z({apiBase:x(),token:m().token??""})}async function J(e,t={}){let n=m(),s=new Headers(t.headers);n.token&&s.set("authorization",`Bearer ${n.token}`);let o=await fetch(`${x()}${e}`,{...t,headers:s});if(o.status===401)throw new P("Not logged in \u2014 run `pepita login`.");return o}var P,p,f=l(()=>{A();k();P=class extends Error{},p=class extends Error{};});function W(e=32){let t=new Uint8Array(e);return globalThis.crypto.getRandomValues(t),[...t].map(n=>n.toString(16).padStart(2,"0")).join("")}function Oe(e){let t=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",n=process.platform==="win32"?["/c","start",'""',e]:[e];spawn(t,n,{stdio:"ignore",detached:true}).unref();}async function q(){let e=W(32),t=await I(e),n=W(16),s=`${hostname()} (${userInfo().username})`,{code:o}=await new Promise((y,h)=>{let U=0,$=createServer((w,v)=>{let E=new URL(w.url??"","http://127.0.0.1");if(E.pathname!=="/callback"){v.writeHead(404).end();return}let j=E.searchParams.get("code"),$e=E.searchParams.get("state");v.writeHead(200,{"content-type":"text/html"}).end('<html><body style="font-family:sans-serif;text-align:center;padding:3rem"><h2>pepita CLI</h2><p>You can close this tab and return to the terminal.</p></body></html>'),$.close(),clearTimeout(C),!j||$e!==n?h(new Error("Authorization failed (state mismatch).")):y({code:j});}),C=setTimeout(()=>{$.close(),h(new Error("Login timed out (no response). If you clicked Cancel, run `pepita login` again."));},je);$.on("error",w=>{clearTimeout(C),h(w);}),$.listen(0,"127.0.0.1",()=>{let w=$.address();U=typeof w=="object"&&w?w.port:0;let v=`${x()}/auth/cli/authorize?port=${U}&state=${n}&code_challenge=${t}&label=${encodeURIComponent(s)}`;console.log("Opening your browser to authorize\u2026"),console.log(v),Oe(v);});}),r=await fetch(`${x()}/auth/cli/token`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({code:o,code_verifier:e})});if(!r.ok)throw new Error(`Token exchange failed: ${r.status} ${await r.text()}`);let{token:i,user:c}=await r.json(),a=m();T({...a,token:i,email:c.email}),console.log(`Logged in as ${c.email}.`);}async function X(){if(m().token)try{await J("/api/cli-tokens/current",{method:"DELETE"});}catch{}F(),console.log("Logged out.");}function G(){let e=m();console.log(e.token?e.email??"logged in":"Not logged in \u2014 run `pepita login`.");}var je,b=l(()=>{k();A();f();je=18e4;});var Y={};g(Y,{run:()=>Le});var Le,V=l(()=>{b();Le=q;});var Z={};g(Z,{run:()=>_e});var _e,K=l(()=>{b();_e=X;});var Q={};g(Q,{run:()=>Fe});async function Fe(){G();}var ee=l(()=>{b();});var te={};g(te,{run:()=>Ne});async function Ne(){let e=await u().listSites();if(e.length===0)return console.log("No sites yet.");for(let t of e)console.log(t.slug);}var ne=l(()=>{f();});function Ge(e){return e.split("/").pop()??e}function Ye(e){let t=Ge(e);return Xe.has(t)||qe.has(t.split(".").pop()?.toLowerCase()??"")?"utf-8":"base64"}function se(e){if(!e||e.startsWith("/")||e.startsWith("\\"))return false;let t=e.split(/[/\\]/);for(let n of t)if(n==="..")return false;return true}function Ve(e,t){let n=[],s=[];for(let[o,r]of e){let i=t.get(o);(!i||i.content!==r.content||i.encoding!==r.encoding)&&n.push(o);}for(let o of t.keys())e.has(o)||s.push(o);return {writes:n,deletes:s}}async function re(e,t){let n=await u().getTree(e,t);return new Map(n.files.map(s=>[s.path,{content:s.content,encoding:s.encoding}]))}async function Ze(e,t){let n=await u().raw(`/api/sites/${encodeURIComponent(e)}/download?branch=${t}`);if(!n.ok)throw new Error(`GET /download?branch=${t} \u2192 ${n.status} ${await n.text()}`);let s=new Uint8Array(await n.arrayBuffer());return new Map(Object.entries(unzipSync(s)))}async function ae(e,t,n){let s;if(t==="unsaved"){s=new Map;for(let[r,i]of await re(e,"develop"))s.set(r,Buffer.from(i.content,i.encoding==="base64"?"base64":"utf-8"));}else s=await Ze(e,t==="live"?"main":"develop");let o=0;for(let[r,i]of s){if(!se(r)){console.warn(`skipping unsafe remote path: ${r}`);continue}let c=join(n,...r.split("/"));mkdirSync(dirname(c),{recursive:true}),writeFileSync(c,i),o++;}return o}function Ke(e){let t=new Map,n=s=>{for(let o of readdirSync(s)){if(o===".git"||o==="node_modules")continue;let r=join(s,o);if(statSync(r).isDirectory())n(r);else {let i=relative(e,r).split(sep).join("/");if(!se(i)){console.warn(`skipping unsafe local path: ${i}`);continue}let c=Ye(i),a=c==="base64"?readFileSync(r).toString("base64"):readFileSync(r,"utf-8");t.set(i,{content:a,encoding:c});}}};return n(e),t}async function ce(e,t,n,s){let o=Ke(t),r=await re(e,"develop"),i=Ve(o,r);if(i.writes.length===0&&i.deletes.length===0)return {written:0,deleted:0};if(!n&&!await s(i))return {written:0,deleted:0};let c=u();for(let a of i.writes){let y=o.get(a);await c.writeFile(e,a,y.content,y.encoding);}for(let a of i.deletes)await c.deleteFile(e,a);return {written:i.writes.length,deleted:i.deletes.length}}var qe,Xe,R=l(()=>{f();qe=new Set(["txt","xml","html","htm","js","css","webmanifest","svg"]),Xe=new Set(["_headers"]);});var le={};g(le,{run:()=>et});async function et(e){let t=e[0];if(!t)throw new p("usage: pepita pull <slug> [--state live|draft|unsaved] [--dir <path>]");let n=e.includes("--state")?e[e.indexOf("--state")+1]:"live";if(!Qe.includes(n))throw new p(`unknown --state '${n}' (expected: live | draft | unsaved)`);let s=n,o=e.includes("--dir")?e[e.indexOf("--dir")+1]:`./${t}`,r=await ae(t,s,o);console.log(`Pulled ${r} file(s) from ${t} (${s}) into ${o}`);}var Qe,pe=l(()=>{R();f();Qe=["live","draft","unsaved"];});var ue={};g(ue,{run:()=>nt});async function nt(e){let t=e[0];if(!t)throw new p("usage: pepita apply <slug> [--dir <path>] [--yes]");let n=e.includes("--dir")?e[e.indexOf("--dir")+1]:`./${t}`,s=e.includes("--yes"),r=await ce(t,n,s,async i=>{console.log(`Plan for ${t}: +${i.writes.length} write(s), -${i.deletes.length} delete(s)`);let c=createInterface({input:process.stdin,output:process.stdout}),a=(await c.question("Apply as unsaved changes? [y/N] ")).trim().toLowerCase();return c.close(),a==="y"||a==="yes"});console.log(`Applied to ${t}: ${r.written} written, ${r.deleted} deleted (unsaved). Run \`pepita save ${t}\` to save.`);}var de=l(()=>{R();f();});var fe={};g(fe,{run:()=>ot});async function ot(e){let t=e[0];if(!t)throw new p("usage: pepita save <slug>");let n=u(),s=await n.getTree(t,"develop"),o=s.files.filter(r=>r.dirty);await n.flush(t,{expectedHeadSha:s.headSha,files:o.map(r=>({path:r.path,content:r.content,encoding:r.encoding})),deletions:s.deletions}),console.log(`Saved ${t} to draft. Run \`pepita publish ${t}\` to go live.`);}var ge=l(()=>{f();});var me={};g(me,{run:()=>it});async function it(e){let t=e[0];if(!t)throw new p("usage: pepita publish <slug>");let n=await u().publish(t);console.log(`Published ${t} to live \u2192 ${n.productionUrl}`);}var he=l(()=>{f();});var we={};g(we,{run:()=>st});async function st(e){let t=e[0];if(!t){let o=await u().listSites();if(o.length===0){console.log("No sites yet.");return}console.log(`${o.length} site${o.length===1?"":"s"}:`);for(let r of o)console.log(` ${r.slug}`),console.log(` draft: https://${r.slug}--draft.pepita.dev live: https://${r.slug}.pepita.dev`);console.log("\nRun `pepita status <slug>` to see unsaved changes for one site.");return}let n=await u().getTree(t,"develop"),s=n.files.filter(o=>o.dirty).map(o=>o.path);console.log(`Site: ${t}`),console.log(`Draft: https://${t}--draft.pepita.dev`),console.log(`Live: https://${t}.pepita.dev`),console.log(`Unsaved: ${s.length} changed, ${n.deletions.length} deleted`);for(let o of s)console.log(` ~ ${o}`);for(let o of n.deletions)console.log(` - ${o}`);}var ye=l(()=>{f();});f();var xe=`pepita \u2014 command line for pepita sites
|
|
3
|
+
|
|
4
|
+
Usage: pepita <command> [args]
|
|
5
|
+
|
|
6
|
+
login Authorize this device in the browser
|
|
7
|
+
logout Remove the local token
|
|
8
|
+
whoami Show the logged-in account
|
|
9
|
+
list List your sites
|
|
10
|
+
pull <slug> [--state live|draft|unsaved] [--dir d] Download a site's files (default: live)
|
|
11
|
+
apply <slug> [--dir d] [--yes] Upload local files as unsaved changes
|
|
12
|
+
save <slug> Save unsaved changes to the draft
|
|
13
|
+
publish <slug> Publish the draft to live
|
|
14
|
+
status <slug> Show unsaved changes + URLs
|
|
15
|
+
`,rt={login:()=>Promise.resolve().then(()=>(V(),Y)),logout:()=>Promise.resolve().then(()=>(K(),Z)),whoami:()=>Promise.resolve().then(()=>(ee(),Q)),list:()=>Promise.resolve().then(()=>(ne(),te)),pull:()=>Promise.resolve().then(()=>(pe(),le)),apply:()=>Promise.resolve().then(()=>(de(),ue)),save:()=>Promise.resolve().then(()=>(ge(),fe)),publish:()=>Promise.resolve().then(()=>(he(),me)),status:()=>Promise.resolve().then(()=>(ye(),we))};async function at(){let[,,e,...t]=process.argv;if(!e||e==="--help"||e==="-h"||e==="help"){console.log(xe);return}let n=rt[e];if(!n){console.error(`Unknown command: ${e}
|
|
16
|
+
`),console.log(xe),process.exitCode=1;return}await(await n()).run(t);}at().catch(e=>{e instanceof p?(console.error(e.message),process.exitCode=1):e instanceof P||e instanceof d&&e.status===401?(console.error("Not logged in \u2014 run `pepita login`."),process.exitCode=2):(console.error(`Error: ${e?.message??e}`),process.exitCode=1);});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pepitahq/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line for pepita sites — pull, apply, save, publish.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Zoltan Gobolos <zgobolos@barelynotable.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pepita": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/pepitahq/pepita-cli.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/pepitahq/pepita-cli#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/pepitahq/pepita-cli/issues"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"fflate": "^0.8.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"tsx": "^4.19.0",
|
|
36
|
+
"typescript": "^5.7.0",
|
|
37
|
+
"vitest": "^3.0.0",
|
|
38
|
+
"@pepitahq/shared": "0.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"dev": "tsx src/index.ts",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
+
"test": "vitest run"
|
|
45
|
+
}
|
|
46
|
+
}
|