pesanle-theme 0.0.0-stage → 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -2
  3. package/dist/cli.js +415 -0
  4. package/package.json +47 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pesanle
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 CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # pesanle-theme
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Compile and push a storefront theme to a [Pesanle](https://pesanle.id) shop:
4
+ sign in, scaffold a project, push it as a draft, preview it, publish it live.
5
+
6
+ ```bash
7
+ npx pesanle-theme login # once per machine
8
+ npx pesanle-theme init ./my-theme --shop my-shop
9
+ cd my-theme
10
+ npx pesanle-theme push # compile → the shop's draft
11
+ npx pesanle-theme preview # a signed link that draws the draft
12
+ npx pesanle-theme publish # the draft → live
13
+ ```
14
+
15
+ The CLI talks to **Pesanle** and nothing else — no endpoint, project, database
16
+ or API key. `login` signs you in with your Pesanle email and password; the
17
+ dashboard mints a token for the machine and does all the compiling, storing
18
+ and publishing server-side. Themes are authored in an HTML-first,
19
+ Liquid-shaped language and **compiled** to a JavaScript render function:
20
+ there is no interpreter at request time, and an unknown filter, a typo'd drop
21
+ property or a missing snippet fails the push, not a shopper's page.
22
+
23
+ - CLI reference: <https://github.com/ridhotamma/pesanle/blob/main/docs/THEME-CLI.md>
24
+ - Language reference: <https://github.com/ridhotamma/pesanle/blob/main/docs/TEMPLATE-LANGUAGE.md>
25
+ - How the engine works: <https://github.com/ridhotamma/pesanle/blob/main/docs/TEMPLATE_ENGINE.md>
26
+
27
+ ## Commands
28
+
29
+ | Command | What it does |
30
+ | --------------------------------- | ---------------------------------------------------------------------------------- |
31
+ | `login` | Sign in to Pesanle and store a token for this machine. |
32
+ | `logout` | Revoke the token and forget it locally. |
33
+ | `whoami` | Show the signed-in account. |
34
+ | `init [dir]` | Scaffold a working theme project and write `.pesanle.json` with `--shop`. |
35
+ | `push [dir]` | Send the folder to Pesanle, which compiles it and writes the shop's **draft**. |
36
+ | `publish [dir]` | Copy the draft live, with one version snapshot per publish. `--push` pushes first. |
37
+ | `preview [dir]` | Print a signed link that draws the **draft** for an hour. |
38
+ | `pull [dir]` | Write the shop's published theme back into the folder (`--draft` for the draft). |
39
+ | `status [dir]` | The shop, its engine, and the draft and published versions. |
40
+ | `engine <template\|page-builder>` | Flip which engine draws the shop. |
41
+
42
+ Flags: `--shop`, `--name`, `--api`, `--email`, `--force`, `--draft`, `--push`,
43
+ `--no-engine`.
44
+
45
+ ## Configuration
46
+
47
+ - **`.pesanle.json`** — written by `init --shop`, safe to commit:
48
+ `{ "shop": "my-shop", "name": "My theme" }`.
49
+ - **`~/.pesanle/credentials.json`** — the token `login` minted (mode `0600`);
50
+ `PESANLE_HOME` moves it. Never the password.
51
+ - **`PESANLE_EMAIL` / `PESANLE_PASSWORD`** — for a non-interactive login (CI).
52
+ - **`PESANLE_API_URL`** or **`--api`** — a self-hosted Pesanle dashboard.
53
+
54
+ ## What a theme looks like
55
+
56
+ ```
57
+ my-theme/
58
+ layout/theme.tpl the shell around every page of the shop
59
+ templates/index.tpl home
60
+ templates/product.tpl a product page
61
+ templates/catalog.tpl the catalogue
62
+ snippets/product-card.tpl a partial
63
+ assets/theme.css scoped under .theme
64
+ ```
65
+
66
+ A route the theme does not define is drawn by the platform's default theme,
67
+ so a project can start with one page and grow. Interactive pieces — the buy
68
+ box, reviews, the order-tracking form — arrive as host slots
69
+ (`{% slot 'productPurchase' %}`); a theme never ships its own JavaScript.
70
+
71
+ ## Requirements
72
+
73
+ Node 20.19 or newer. The CLI sends your theme files to your Pesanle dashboard
74
+ and nothing else.
75
+
76
+ ## License
77
+
78
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+ import{existsSync as F0,mkdirSync as j,writeFileSync as T0}from"fs";import{basename as w,join as l,resolve as d}from"path";class G extends Error{status;diagnostics;name="CliApiError";constructor($,K,N=[]){super($);this.status=K;this.diagnostics=N}}class H{options;constructor($){this.options=$}async request($,K={}){let N={"content-type":"application/json"};if(this.options.token)N.authorization=`Bearer ${this.options.token}`;let W;try{W=await fetch(`${this.options.api.replace(/\/+$/,"")}${$}`,{method:K.method??"GET",headers:N,body:K.body===void 0?void 0:JSON.stringify(K.body)})}catch(Q){throw new G(`Could not reach ${this.options.api} — ${Q.message}`,0)}let b=await W.json().catch(()=>({}));if(!W.ok||b.ok!==!0)throw new G(b.error??`The API answered ${W.status}.`,W.status,b.diagnostics??[]);return b.data}async login($,K,N){return this.request("/api/cli/login",{method:"POST",body:{email:$,password:K,name:N}})}async logout(){await this.request("/api/cli/logout",{method:"POST"})}async me(){return this.request("/api/cli/me")}async theme($){return this.request(`/api/cli/shops/${encodeURIComponent($)}/theme`)}async push($,K,N){return this.request(`/api/cli/shops/${encodeURIComponent($)}/theme/draft`,{method:"PUT",body:{files:K,name:N}})}async publish($){return this.request(`/api/cli/shops/${encodeURIComponent($)}/theme/publish`,{method:"POST"})}async files($,K){return this.request(`/api/cli/shops/${encodeURIComponent($)}/theme/files${K?"?draft=1":""}`)}async preview($){return this.request(`/api/cli/shops/${encodeURIComponent($)}/theme/preview`)}async engine($,K){return this.request(`/api/cli/shops/${encodeURIComponent($)}/engine`,{method:"POST",body:{engine:K}})}}import{chmodSync as n,existsSync as S,mkdirSync as t,readFileSync as E,rmSync as s,writeFileSync as y}from"node:fs";import{homedir as r}from"node:os";import{dirname as a,join as q}from"node:path";var x=".pesanle.json",I="https://dashboard.pesanle.com";function D($){let K=q($,x);if(!S(K))return null;try{let N=JSON.parse(E(K,"utf8"));if(typeof N.shop!=="string"||!N.shop)return null;return{shop:N.shop,name:typeof N.name==="string"?N.name:void 0}}catch{return null}}function L($,K){y(q($,x),`${JSON.stringify({shop:K.shop,...K.name?{name:K.name}:{}},null,2)}
4
+ `)}function _(){let $=process.env.PESANLE_HOME?.trim()||q(r(),".pesanle");return q($,"credentials.json")}function M(){let $=_();if(!S($))return null;try{let K=JSON.parse(E($,"utf8"));if(typeof K.token!=="string"||!K.token)return null;return{api:typeof K.api==="string"&&K.api?K.api:I,token:K.token,email:typeof K.email==="string"?K.email:void 0,name:typeof K.name==="string"?K.name:void 0}}catch{return null}}function v($){let K=_();t(a(K),{recursive:!0,mode:448}),y(K,`${JSON.stringify($,null,2)}
5
+ `,{mode:384}),n(K,384)}function k(){s(_(),{force:!0})}function O($){return $?.trim()||process.env.PESANLE_API_URL?.trim()||M()?.api||I}import{readdirSync as e,readFileSync as $0}from"node:fs";import{join as K0,relative as N0,sep as W0}from"node:path";var b0=[".tpl",".css"];function m($){let K={},N=(W)=>{for(let b of e(W,{withFileTypes:!0})){if(b.name.startsWith("."))continue;let Q=K0(W,b.name);if(b.isDirectory())N(Q);else if(b0.some((B)=>b.name.endsWith(B)))K[N0($,Q).split(W0).join("/")]=$0(Q,"utf8")}};return N($),K}var T=($)=>`\x1B[1m${$}\x1B[0m`,u=($)=>`\x1B[2m${$}\x1B[0m`,z0=($)=>`\x1B[32m${$}\x1B[0m`,Q0=($)=>`\x1B[33m${$}\x1B[0m`,V0=($)=>`\x1B[31m${$}\x1B[0m`,F=0;function X($){F+=1,console.log(`
6
+ ${u(`[${F}]`)} ${T($)}`)}function V($){console.log(` ${z0("OK")} ${$}`)}function z($){console.log(` ${u($)}`)}function R($){console.log(` ${Q0("WARN")} ${$}`)}function U($){console.error(` ${V0("FAIL")} ${$}`)}function f(){F=0}import{createInterface as Z0}from"node:readline";async function h($,K=""){if(!process.stdin.isTTY)return K;let N=Z0({input:process.stdin,output:process.stdout});try{return(await new Promise((b)=>N.question(`${$}: `,b))).trim()||K}finally{N.close()}}async function p($,K){let N=process.env[K]?.trim();if(N)return N;if(!process.stdin.isTTY)throw Error(`No terminal to prompt on: set ${K}.`);let W=process.stdin,b=W.isRaw;return process.stdout.write(`${$}: `),new Promise((Q,B)=>{let Z="",A=(i)=>{for(let Y of i.toString("utf8")){if(Y===`
7
+ `||Y==="\r"){if(C(),process.stdout.write(`
8
+ `),!Z)B(Error("Nothing was entered."));else Q(Z);return}if(Y==="\x03"){C(),process.stdout.write(`
9
+ `),B(Error("Cancelled."));return}if(Y===""||Y==="\b"){Z=Z.slice(0,-1);continue}Z+=Y}},C=()=>{if(W.removeListener("data",A),W.setRawMode)W.setRawMode(b??!1);W.pause()};if(W.resume(),W.setRawMode)W.setRawMode(!0);W.on("data",A)})}import{existsSync as B0,mkdirSync as X0,writeFileSync as Y0}from"node:fs";import{dirname as H0,join as J0}from"node:path";var G0=`<div class="theme">
10
+ <header class="theme-header">
11
+ <a class="theme-brand" href="{{ routes.home }}">{{ shop.name }}</a>
12
+ <nav class="theme-nav">
13
+ <a href="{{ routes.products }}">Shop</a>
14
+ {% for category in categories limit: 4 %}
15
+ <a href="{{ category.url }}">{{ category.name }}</a>
16
+ {% endfor %}
17
+ </nav>
18
+ <div class="theme-actions">
19
+ <a href="{{ routes.search }}">Search</a>
20
+ {% slot 'cartTrigger' %}
21
+ </div>
22
+ </header>
23
+
24
+ <main class="theme-main">{{ content_for_layout }}</main>
25
+
26
+ <footer class="theme-footer">
27
+ <p>{{ shop.name }}</p>
28
+ <p>{{ shop.address | default: shop.email }}</p>
29
+ </footer>
30
+
31
+ {% slot 'cartDrawer' %}
32
+ </div>
33
+ `,M0=`<section class="hero">
34
+ <div>
35
+ <h1>{{ shop.name }}</h1>
36
+ <p>{{ shop.description | default: 'Everything we make, in one place.' }}</p>
37
+ <a class="button" href="{{ routes.products }}">Shop all</a>
38
+ </div>
39
+ {% if shop.banner_url %}
40
+ <img class="hero-image" src="{{ shop.banner_url }}" alt="{{ shop.name }}" />
41
+ {% endif %}
42
+ </section>
43
+
44
+ <section class="band">
45
+ <h2>New arrivals</h2>
46
+ <div class="grid">
47
+ {% for product in latest limit: 8 %}
48
+ {% render 'product-card', product: product %}
49
+ {% endfor %}
50
+ </div>
51
+ </section>
52
+ `,q0=`<article class="product">
53
+ <div class="gallery">
54
+ {% for image in product.images limit: 4 %}
55
+ {% if image.url %}<img src="{{ image.url }}" alt="{{ product.title }}" />{% endif %}
56
+ {% endfor %}
57
+ </div>
58
+
59
+ <div class="product-info">
60
+ <h1>{{ product.title }}</h1>
61
+ <p class="price">
62
+ {{ product.price | money }}
63
+ {% if product.on_sale %}<s>{{ product.compare_at_price | money }}</s>{% endif %}
64
+ </p>
65
+ {% if product.available != blank and product.available <= 0 %}
66
+ <p class="sold-out">Sold out</p>
67
+ {% endif %}
68
+
69
+ <div class="buy">{% slot 'productPurchase' %}</div>
70
+
71
+ {% if product.description %}<p class="description">{{ product.description }}</p>{% endif %}
72
+ </div>
73
+ </article>
74
+
75
+ {% if product.related.size > 0 %}
76
+ <section class="band">
77
+ <h2>You may also like</h2>
78
+ <div class="grid">
79
+ {% for related in product.related limit: 4 %}
80
+ {% render 'product-card', product: related %}
81
+ {% endfor %}
82
+ </div>
83
+ </section>
84
+ {% endif %}
85
+ `,I0=`<section class="listing-head">
86
+ <h1>{{ listing.title }}</h1>
87
+ {% if listing.description %}<p>{{ listing.description }}</p>{% endif %}
88
+ </section>
89
+
90
+ <p class="count">{{ listing.total }} products</p>
91
+
92
+ <div class="grid">
93
+ {% for product in listing.products limit: 12 %}
94
+ {% render 'product-card', product: product %}
95
+ {% endfor %}
96
+ </div>
97
+
98
+ {% if listing.pages > 1 %}
99
+ <nav class="pager">
100
+ {% for pageNumber in (1..listing.pages) %}
101
+ {% if pageNumber == listing.page %}
102
+ <span class="current">{{ pageNumber }}</span>
103
+ {% else %}
104
+ <a href="{{ pageNumber | page_url }}">{{ pageNumber }}</a>
105
+ {% endif %}
106
+ {% endfor %}
107
+ </nav>
108
+ {% endif %}
109
+ `,O0=`<article class="page">
110
+ <p class="eyebrow">{{ shop.name }}</p>
111
+ <h1>{{ page.title }}</h1>
112
+ <p>
113
+ This page is the theme's own: <code>templates/pages/about.tpl</code> answers
114
+ <code>/about</code>. Rename the file to change the address, or delete it and
115
+ build the page in the dashboard instead.
116
+ </p>
117
+ </article>
118
+ `,R0=`<a class="card" href="{{ product.url }}">
119
+ <span class="card-media">
120
+ {% if product.image_url %}
121
+ <img src="{{ product.image_url }}" alt="{{ product.title }}" loading="lazy" />
122
+ {% endif %}
123
+ {% if product.on_sale %}<span class="badge">Sale</span>{% endif %}
124
+ </span>
125
+ <span class="card-name">{{ product.title }}</span>
126
+ <span class="card-price">
127
+ {{ product.price | money }}
128
+ {% if product.on_sale %}<s>{{ product.compare_at_price | money }}</s>{% endif %}
129
+ </span>
130
+ </a>
131
+ `,D0=`/* Scoped under .theme — the wrapper the storefront puts around the theme. */
132
+ .theme {
133
+ background: #faf9f6;
134
+ color: #171512;
135
+ font-family: var(--chakra-fonts-body);
136
+ min-height: 100dvh;
137
+ }
138
+
139
+ .theme a {
140
+ color: inherit;
141
+ text-decoration: none;
142
+ }
143
+
144
+ .theme img {
145
+ display: block;
146
+ max-width: 100%;
147
+ }
148
+
149
+ .theme-header {
150
+ align-items: center;
151
+ border-bottom: 1px solid #e6e2da;
152
+ display: flex;
153
+ gap: 2rem;
154
+ justify-content: space-between;
155
+ padding: 1rem clamp(1rem, 4vw, 2.5rem);
156
+ }
157
+
158
+ .theme-brand {
159
+ font-family: var(--chakra-fonts-heading);
160
+ font-size: 1.3rem;
161
+ font-weight: 700;
162
+ }
163
+
164
+ .theme-nav,
165
+ .theme-actions {
166
+ display: flex;
167
+ gap: 1.2rem;
168
+ font-size: 0.95rem;
169
+ }
170
+
171
+ .theme-main {
172
+ margin: 0 auto;
173
+ max-width: 1200px;
174
+ padding: 0 clamp(1rem, 4vw, 2.5rem);
175
+ }
176
+
177
+ .theme-footer {
178
+ border-top: 1px solid #e6e2da;
179
+ color: #6f6a60;
180
+ font-size: 0.9rem;
181
+ margin-top: 4rem;
182
+ padding: 2rem clamp(1rem, 4vw, 2.5rem);
183
+ }
184
+
185
+ .hero {
186
+ align-items: center;
187
+ display: grid;
188
+ gap: 2.5rem;
189
+ padding: clamp(2rem, 5vw, 4rem) 0;
190
+ }
191
+
192
+ @media (min-width: 900px) {
193
+ .hero {
194
+ grid-template-columns: 1fr 1fr;
195
+ }
196
+ }
197
+
198
+ .hero h1 {
199
+ font-family: var(--chakra-fonts-heading);
200
+ font-size: clamp(2.2rem, 5vw, 3.8rem);
201
+ letter-spacing: -0.02em;
202
+ margin: 0;
203
+ }
204
+
205
+ .hero p {
206
+ color: #6f6a60;
207
+ line-height: 1.7;
208
+ margin: 1rem 0 1.5rem;
209
+ max-width: 32rem;
210
+ }
211
+
212
+ .hero-image {
213
+ aspect-ratio: 4 / 3;
214
+ border-radius: 1rem;
215
+ object-fit: cover;
216
+ width: 100%;
217
+ }
218
+
219
+ .button {
220
+ background: #171512;
221
+ border-radius: 999px;
222
+ color: #faf9f6;
223
+ display: inline-block;
224
+ font-weight: 600;
225
+ padding: 0.75rem 1.5rem;
226
+ }
227
+
228
+ .band {
229
+ padding-top: clamp(2rem, 5vw, 3.5rem);
230
+ }
231
+
232
+ .band h2 {
233
+ font-family: var(--chakra-fonts-heading);
234
+ font-size: clamp(1.4rem, 3vw, 2rem);
235
+ margin: 0 0 1.25rem;
236
+ }
237
+
238
+ .grid {
239
+ display: grid;
240
+ gap: 1rem;
241
+ grid-template-columns: repeat(2, 1fr);
242
+ }
243
+
244
+ @media (min-width: 800px) {
245
+ .grid {
246
+ grid-template-columns: repeat(4, 1fr);
247
+ }
248
+ }
249
+
250
+ .card {
251
+ display: flex;
252
+ flex-direction: column;
253
+ gap: 0.5rem;
254
+ }
255
+
256
+ .card-media {
257
+ aspect-ratio: 1;
258
+ background: #fff;
259
+ border: 1px solid #e6e2da;
260
+ border-radius: 0.9rem;
261
+ overflow: hidden;
262
+ position: relative;
263
+ }
264
+
265
+ .card-media img {
266
+ height: 100%;
267
+ object-fit: cover;
268
+ width: 100%;
269
+ }
270
+
271
+ .badge {
272
+ background: #d9f24e;
273
+ border-radius: 999px;
274
+ font-size: 0.7rem;
275
+ font-weight: 700;
276
+ left: 0.6rem;
277
+ padding: 0.2rem 0.55rem;
278
+ position: absolute;
279
+ text-transform: uppercase;
280
+ top: 0.6rem;
281
+ }
282
+
283
+ .card-name {
284
+ font-weight: 600;
285
+ }
286
+
287
+ .card-price,
288
+ .price {
289
+ align-items: baseline;
290
+ display: flex;
291
+ gap: 0.5rem;
292
+ }
293
+
294
+ .card-price s,
295
+ .price s {
296
+ color: #6f6a60;
297
+ }
298
+
299
+ .product {
300
+ display: grid;
301
+ gap: 2.5rem;
302
+ padding-top: 2rem;
303
+ }
304
+
305
+ @media (min-width: 900px) {
306
+ .product {
307
+ grid-template-columns: 1.1fr 1fr;
308
+ }
309
+ }
310
+
311
+ .gallery {
312
+ display: grid;
313
+ gap: 0.75rem;
314
+ grid-template-columns: 1fr 1fr;
315
+ }
316
+
317
+ .gallery img {
318
+ aspect-ratio: 1;
319
+ border-radius: 0.9rem;
320
+ object-fit: cover;
321
+ width: 100%;
322
+ }
323
+
324
+ .product-info {
325
+ align-self: start;
326
+ display: flex;
327
+ flex-direction: column;
328
+ gap: 0.9rem;
329
+ }
330
+
331
+ .product-info h1 {
332
+ font-family: var(--chakra-fonts-heading);
333
+ font-size: clamp(1.8rem, 4vw, 2.6rem);
334
+ margin: 0;
335
+ }
336
+
337
+ .description {
338
+ color: #6f6a60;
339
+ line-height: 1.75;
340
+ white-space: pre-line;
341
+ }
342
+
343
+ .sold-out {
344
+ color: #b42318;
345
+ font-weight: 600;
346
+ }
347
+
348
+ .listing-head {
349
+ padding-top: 2.5rem;
350
+ }
351
+
352
+ .listing-head h1 {
353
+ font-family: var(--chakra-fonts-heading);
354
+ font-size: clamp(1.9rem, 4vw, 3rem);
355
+ margin: 0;
356
+ }
357
+
358
+ .count {
359
+ color: #6f6a60;
360
+ font-size: 0.9rem;
361
+ margin: 1.5rem 0 1rem;
362
+ }
363
+
364
+ .pager {
365
+ display: flex;
366
+ gap: 0.5rem;
367
+ justify-content: center;
368
+ padding-top: 2rem;
369
+ }
370
+
371
+ .pager a,
372
+ .pager span {
373
+ border: 1px solid #e6e2da;
374
+ border-radius: 0.5rem;
375
+ padding: 0.4rem 0.75rem;
376
+ }
377
+
378
+ .pager .current {
379
+ background: #171512;
380
+ color: #faf9f6;
381
+ }
382
+ `,L0=`# Theme project
383
+
384
+ A storefront theme for Pesanle, authored in the compiled template language.
385
+ The reference is \`docs/TEMPLATE-LANGUAGE.md\` in the Pesanle repository.
386
+
387
+ \`\`\`bash
388
+ pesanle-theme push # compile and write the shop's draft
389
+ pesanle-theme publish # make the draft live
390
+ pesanle-theme pull # write the live theme back into this folder
391
+ pesanle-theme status # the shop, its engine, and both versions
392
+ \`\`\`
393
+
394
+ The API key comes from \`PESANLE_API_KEY\`; the shop and project live in
395
+ \`.pesanle.json\`. The key is never written to the file.
396
+ `;function _0(){return{"layout/theme.tpl":G0,"templates/index.tpl":M0,"templates/product.tpl":q0,"templates/catalog.tpl":I0,"snippets/product-card.tpl":R0,"templates/pages/about.tpl":O0,"assets/theme.css":D0,"README.md":L0}}function c($){let K=[],N=[];for(let[W,b]of Object.entries(_0())){let Q=J0($.dir,W);if(B0(Q)&&!$.force){N.push(W);continue}X0(H0(Q),{recursive:!0}),Y0(Q,b),K.push(W)}return{written:K,skipped:N}}function U0($){let K={command:$[0]??"",dir:"",shop:"",name:"",api:"",email:"",engine:"",force:!1,draft:!1,push:!1,noEngine:!1},N=$.slice(1);for(let W=0;W<N.length;W+=1){let b=N[W],[Q,B]=b.startsWith("--")?b.split("=",2):["",""],Z=()=>B??N[++W]??"";switch(Q){case"--dir":K.dir=Z();break;case"--shop":K.shop=Z();break;case"--name":K.name=Z();break;case"--api":K.api=Z();break;case"--email":K.email=Z();break;case"--force":K.force=!0;break;case"--draft":K.draft=!0;break;case"--push":K.push=!0;break;case"--no-engine":K.noEngine=!0;break;case"":if(!K.dir)K.dir=b;else if(!K.engine&&P(b))K.engine=b;break;default:throw Error(`Unknown flag "${b}".`)}}if(K.command==="engine"&&!K.engine&&P(K.dir))K.engine=K.dir,K.dir="";return K}function P($){return $==="template"||$==="page-builder"}function j0($){let K=M();if(!K)throw Error('Not signed in. Run "pesanle-theme login" first.');return new H({api:O($)||K.api,token:K.token})}function J($){let K=d($.dir||"."),N=D(K),W=$.shop||N?.shop||"";if(!W)throw Error('No shop given: pass --shop <slug>, or run "pesanle-theme init --shop <slug>" to write .pesanle.json.');return{dir:K,shop:W,name:$.name||N?.name||w(K),api:j0($.api)}}function P0($){for(let K of $.diagnostics){let N=K.file?`${K.file}${K.loc?.start?.line?`:${K.loc.start.line}:${K.loc.start.column??1}`:""} `:"";console.error(` ${N}${K.code?`${K.code}: `:""}${K.message}`)}}async function w0($){let K=O($.api),N=$.email||process.env.PESANLE_EMAIL||await h("Email");if(!N)throw Error("An email is required.");let W=await p("Password (hidden)","PESANLE_PASSWORD"),b=$.name||`pesanle-theme on ${w(process.env.HOME??"this machine")}`;X("Sign in");let B=await new H({api:K}).login(N,W,b);v({api:K,token:B.token,email:B.user.email,name:b}),V(`Signed in as ${B.user.name} <${B.user.email}>`),z(`API: ${K}`),z("Next: pesanle-theme init ./my-theme --shop <shop>")}async function A0(){let $=M();if(X("Sign out"),!$){z("Not signed in.");return}try{await new H({api:$.api,token:$.token}).logout(),V("Token revoked")}catch(K){R(K.message)}k(),V("Signed out on this machine")}async function C0($){let K=M();if(!K)throw Error('Not signed in. Run "pesanle-theme login" first.');let W=await new H({api:O($.api)||K.api,token:K.token}).me();V(`${W.name} <${W.email}>`),z(`API: ${K.api}`)}async function S0($){let K=d($.dir||".");j(K,{recursive:!0});let N=$.name||w(K);X("Scaffold");let W=c({dir:K,name:N,force:$.force});V(`${W.written.length} files written${W.skipped.length?`, ${W.skipped.length} kept`:""}`);for(let b of W.skipped)z(`kept ${b}`);if($.shop)L(K,{shop:$.shop,name:N}),V(`.pesanle.json points at "${$.shop}"`);else R("No --shop given: add .pesanle.json (or pass --shop) before pushing.");console.log(`
397
+ ${N} is ready.`),z(`cd ${$.dir||"."} && pesanle-theme push`)}async function o($){let K=J($);X("Compile & push");let N=m(K.dir);if(Object.keys(N).length===0)throw Error(`No theme files under ${K.dir}. A theme needs at least templates/index.tpl \u2014 run "pesanle-theme init" first.`);let W=await K.api.push(K.shop,N,K.name);if(V(`${Object.keys(N).length} files \u2192 version ${W.version}`),W.warnings.length>0){R(`${W.warnings.length} warning(s)`);for(let b of W.warnings)z(`${b.file??""}${b.file?": ":""}${b.message}`)}if(!$.noEngine)V("Shop is on the template engine");z("Make it live with: pesanle-theme publish")}async function E0($){if($.push)await o($);let K=J($);X("Publish");let N=await K.api.publish(K.shop);V(`Live \u2014 version ${N.version}`),z(N.url)}async function y0($){let K=J($);X("Preview");let N=await K.api.preview(K.shop);V(`Draft version ${N.version}`),console.log(`
398
+ ${T("Open:")} ${N.url}
399
+ `),z(`The link (and its cookie) lasts ${Math.round(N.expiresInSeconds/60)} minutes.`)}async function x0($){let K=J($);X("Pull");let N=await K.api.files(K.shop,$.draft);j(K.dir,{recursive:!0});let W=0,b=0;for(let[Q,B]of Object.entries(N.files)){let Z=l(K.dir,Q);if(F0(Z)&&!$.force){b+=1;continue}j(l(Z,".."),{recursive:!0}),T0(Z,B),W+=1}if(V(`${W} files written, ${b} kept (--force overwrites)`),!D(K.dir))L(K.dir,{shop:K.shop,name:K.name}),V(".pesanle.json written");z(`Pulled version ${N.version}`)}async function v0($){let K=J($);X("Shop");let N=await K.api.theme(K.shop);if(V(`${N.shop.name} (${N.shop.slug})`),z(`Engine: ${N.engine}`),X("Theme"),!N.draft&&!N.published){z("No template theme has been pushed to this shop.");return}if(N.draft)V(`Draft: ${N.draft.version} \xB7 compiled ${N.draft.compiledAt} \xB7 ${N.draft.files} files`),z(`Routes: ${N.draft.routes.join(", ")}`);else z("Draft: none");z(N.published?`Published: ${N.published.version} \xB7 ${N.published.publishedAt??"unknown date"}`:"Published: none")}async function k0($){if(!P($.engine))throw Error("Usage: pesanle-theme engine <template|page-builder>");let K=J($),N=await K.api.engine(K.shop,$.engine);V(`${K.shop} now uses the ${N.engine} engine`)}var g=`pesanle-theme \u2014 compile and push a storefront theme to Pesanle
400
+
401
+ login [--api url] [--email you@example.com]
402
+ logout
403
+ whoami
404
+ init [dir] --shop <slug> [--name "My theme"] [--force]
405
+ push [dir] [--shop <slug>] [--name "My theme"] [--no-engine]
406
+ publish [dir] [--shop <slug>] [--push]
407
+ preview [dir] [--shop <slug>]
408
+ pull [dir] [--shop <slug>] [--draft] [--force]
409
+ status [dir] [--shop <slug>]
410
+ engine <template|page-builder> [--shop <slug>]
411
+
412
+ Sign in once per machine; the shop lives in the project's .pesanle.json.
413
+ Self-hosted Pesanle: pass --api (default ${I}) or set PESANLE_API_URL.`;async function m0(){let $=U0(process.argv.slice(2));switch(f(),$.command){case"login":return w0($);case"logout":return A0();case"whoami":return C0($);case"init":return S0($);case"push":return o($);case"publish":return E0($);case"preview":return y0($);case"pull":return x0($);case"status":return v0($);case"engine":return k0($);case"help":case"--help":case"-h":case"":console.log(g);return;default:throw Error(`Unknown command "${$.command}".
414
+
415
+ ${g}`)}}m0().catch(($)=>{if($ instanceof G){if(U($.message),P0($),$.status===401)z('Run "pesanle-theme login" to sign in again.');if($.diagnostics.length>0)z("Nothing was pushed; the shop keeps serving its current theme.")}else U($.message);process.exitCode=1});
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "pesanle-theme",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Compile and push a storefront theme to a Pesanle shop: scaffold, draft, publish, preview.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "pesanle-theme": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20.19.0"
17
+ },
18
+ "keywords": [
19
+ "pesanle",
20
+ "theme",
21
+ "storefront",
22
+ "ecommerce",
23
+ "cli",
24
+ "template"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/ridhotamma/pesanle.git",
29
+ "directory": "packages/theme-cli"
30
+ },
31
+ "homepage": "https://github.com/ridhotamma/pesanle/blob/main/docs/THEME-CLI.md",
32
+ "bugs": {
33
+ "url": "https://github.com/ridhotamma/pesanle/issues"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "bun run scripts/build.ts",
40
+ "prepublishOnly": "bun run build",
41
+ "typecheck": "tsc --noEmit",
42
+ "test": "bun test"
43
+ },
44
+ "devDependencies": {
45
+ "@repo/typescript-config": "workspace:*",
46
+ "@types/bun": "latest",
47
+ "typescript": "^5.9.3"
48
+ }
49
+ }