tiendu 0.2.3 → 0.3.1

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/README.md CHANGED
@@ -24,27 +24,33 @@ npm install -g tiendu
24
24
 
25
25
  ## Quick start
26
26
 
27
+ ### Buildless theme (simple)
28
+
27
29
  ```bash
28
- # Create a working directory and enter it
29
30
  mkdir my-theme && cd my-theme
30
-
31
- # Connect to your store
32
31
  tiendu init
33
-
34
- # Download the current live theme
35
32
  tiendu pull
33
+ tiendu dev
34
+ ```
35
+
36
+ ### Built theme (TypeScript, npm packages, bundling)
37
+
38
+ Clone the default theme template, connect to your store, and start developing:
36
39
 
37
- # Start developing with live preview
40
+ ```bash
41
+ git clone <default-theme-repo> my-theme && cd my-theme
42
+ npm install
43
+ tiendu init
38
44
  tiendu dev
39
45
  ```
40
46
 
41
- `tiendu dev` creates a remote preview of your theme, uploads your local files, watches for changes and syncs them automatically, and prints a shareable URL like:
47
+ `tiendu dev` creates a remote preview, builds your source files, uploads the output, and watches for changes. It prints a shareable URL like:
42
48
 
43
49
  ```
44
50
  http://preview-xxxxxxxxxxxx.tiendu.uy/
45
51
  ```
46
52
 
47
- The preview renders with the real Tiendu engine — same output as production. Share the URL with your client or team before publishing.
53
+ The preview renders with the real Tiendu engine — same output as production.
48
54
 
49
55
  ---
50
56
 
@@ -64,7 +70,10 @@ tiendu init
64
70
 
65
71
  ### `tiendu pull`
66
72
 
67
- Downloads the current live theme from your store as local files. Run this once to get started, or to reset your local files to the published version.
73
+ Downloads the current live theme from your store as local files.
74
+
75
+ - **Buildless themes:** extracts to the current directory.
76
+ - **Built themes** (with `tiendu.config.json`): extracts to `dist/`.
68
77
 
69
78
  ```bash
70
79
  tiendu pull
@@ -72,9 +81,34 @@ tiendu pull
72
81
 
73
82
  ---
74
83
 
84
+ ### `tiendu build`
85
+
86
+ Builds a theme into its deployable output directory (`dist/`). Only available for built themes (requires `tiendu.config.json`).
87
+
88
+ ```bash
89
+ tiendu build
90
+ ```
91
+
92
+ The build:
93
+
94
+ 1. Copies theme files (`layout/`, `templates/`, `snippets/`, `assets/`) to `dist/`
95
+ 2. Discovers entry points in `src/layout/` and `src/templates/`
96
+ 3. Bundles JS/TS and CSS via esbuild into `dist/assets/`
97
+
98
+ Entry naming convention:
99
+
100
+ - `src/layout/theme.ts` → `dist/assets/layout-theme.bundle.js`
101
+ - `src/templates/product.ts` → `dist/assets/template-product.bundle.js`
102
+ - `src/layout/theme.css` → `dist/assets/layout-theme.bundle.css`
103
+
104
+ ---
105
+
75
106
  ### `tiendu dev`
76
107
 
77
- The main development command. On first run it creates a remote preview, uploads your local files, and starts watching for changes. Every file you save is automatically synced to the preview.
108
+ The main development command.
109
+
110
+ - **Buildless themes:** watches the current directory and syncs file changes to the preview.
111
+ - **Built themes:** runs `tiendu build` in watch mode first, then watches `dist/` and syncs changes to the preview.
78
112
 
79
113
  ```bash
80
114
  tiendu dev
@@ -89,7 +123,10 @@ tiendu dev
89
123
 
90
124
  ### `tiendu push`
91
125
 
92
- Zips all local files (excluding dotfiles) and uploads them to the active preview, replacing its content entirely. Use this instead of `tiendu dev` if you prefer manual syncs.
126
+ Zips and uploads files to the active preview, replacing its content entirely.
127
+
128
+ - **Buildless themes:** uploads from the current directory.
129
+ - **Built themes:** uploads from `dist/`.
93
130
 
94
131
  ```bash
95
132
  tiendu push
@@ -107,9 +144,30 @@ tiendu publish
107
144
 
108
145
  ---
109
146
 
147
+ ### `tiendu check-updates`
148
+
149
+ Checks npm for a newer `tiendu` version on demand.
150
+
151
+ ```bash
152
+ tiendu check-updates
153
+ ```
154
+
155
+ ---
156
+
157
+ ### `tiendu --version` / `tiendu -v`
158
+
159
+ Prints the current CLI version.
160
+
161
+ ```bash
162
+ tiendu --version
163
+ tiendu -v
164
+ ```
165
+
166
+ ---
167
+
110
168
  ### `tiendu preview create [name]`
111
169
 
112
- Creates a new remote preview. Fails with a conflict error if one already exists for your account on this store — delete it first with `tiendu preview delete`.
170
+ Creates a new remote preview.
113
171
 
114
172
  ```bash
115
173
  tiendu preview create
@@ -150,6 +208,8 @@ tiendu preview open
150
208
 
151
209
  ## Typical workflow
152
210
 
211
+ ### Buildless
212
+
153
213
  ```
154
214
  tiendu init # one time: connect to your store
155
215
  tiendu pull # one time: download the live theme
@@ -159,6 +219,18 @@ tiendu dev # develop: edit locally, see changes live at the preview URL
159
219
  tiendu publish # when ready: push to the live storefront
160
220
  ```
161
221
 
222
+ ### Built theme
223
+
224
+ ```
225
+ git clone <template-repo> my-theme
226
+ cd my-theme && npm install
227
+ tiendu init # one time: connect to your store
228
+
229
+ tiendu dev # develop: builds src/, watches dist/, syncs to preview
230
+
231
+ tiendu publish # when ready: push to the live storefront
232
+ ```
233
+
162
234
  ---
163
235
 
164
236
  ## How previews work
@@ -173,18 +245,54 @@ A **theme preview** is a remote copy of your theme hosted by Tiendu. It renders
173
245
 
174
246
  ---
175
247
 
176
- ## Local project structure
248
+ ## Built themes
249
+
250
+ A **built theme** is a theme that uses `tiendu.config.json` to enable the build pipeline. It allows:
251
+
252
+ - npm packages via a local `package.json`
253
+ - TypeScript (`.ts`) for browser code
254
+ - JS bundling (multiple modules → single versioned bundle)
255
+ - CSS bundling (`@import` support)
177
256
 
178
- After `tiendu pull` your directory will look like your store's theme. The `.cli/` folder holds local CLI configuration and is never uploaded to Tiendu.
257
+ ### Project structure
179
258
 
180
259
  ```
181
260
  my-theme/
182
- ├── .cli/ # local config (API key, store ID, active preview key)
183
- ├── layout/
184
- ├── templates/
185
- ├── snippets/
186
- ├── assets/
187
- └── ...
261
+ ├── tiendu.config.json # marks this as a built theme
262
+ ├── package.json # npm dependencies
263
+ ├── .gitignore
264
+ ├── src/
265
+ ├── layout/
266
+ │ │ ├── theme.ts # layout TS entry → layout-theme.bundle.js
267
+ │ │ └── theme.css # layout CSS entry → layout-theme.bundle.css
268
+ │ ├── templates/
269
+ │ │ ├── product.ts # template TS entry → template-product.bundle.js
270
+ │ │ └── product.css # template CSS entry → template-product.bundle.css
271
+ │ ├── lib/ # shared modules (bundled into entries, not served)
272
+ │ └── css/ # shared CSS (imported by entry CSS)
273
+ ├── layout/ # Liquid layouts
274
+ ├── templates/ # Liquid templates
275
+ ├── snippets/ # Liquid snippets
276
+ ├── assets/ # static assets (SVGs, images, fonts)
277
+ └── dist/ # build output (gitignored, uploaded to Tiendu)
278
+ ```
279
+
280
+ ### How it works
281
+
282
+ 1. Source JS/TS/CSS in `src/` is bundled by esbuild into `dist/assets/`
283
+ 2. Liquid files and static assets are copied from root to `dist/`
284
+ 3. `dist/` is what gets uploaded — it looks like a normal Tiendu theme
285
+ 4. Liquid templates reference bundles via `asset_url` (e.g. `{{ 'layout-theme.bundle.js' | asset_url | script_tag }}`)
286
+
287
+ ### tiendu.config.json
288
+
289
+ Minimal config — the build conventions are hardcoded:
290
+
291
+ ```json
292
+ {
293
+ "name": "my-theme",
294
+ "version": "1.0.0"
295
+ }
188
296
  ```
189
297
 
190
298
  ---
package/bin/tiendu.js CHANGED
@@ -5,6 +5,7 @@ import { pull } from "../lib/pull.mjs";
5
5
  import { push } from "../lib/push.mjs";
6
6
  import { dev } from "../lib/dev.mjs";
7
7
  import { publish } from "../lib/publish.mjs";
8
+ import { build } from "../lib/build.mjs";
8
9
  import {
9
10
  previewCreate,
10
11
  previewShow,
@@ -12,7 +13,11 @@ import {
12
13
  previewDelete,
13
14
  previewOpen,
14
15
  } from "../lib/preview.mjs";
15
- import { checkForUpdates } from "../lib/update-check.mjs";
16
+ import {
17
+ checkForUpdates,
18
+ checkForUpdatesNow,
19
+ getCurrentVersion,
20
+ } from "../lib/update-check.mjs";
16
21
 
17
22
  const HELP = `
18
23
  tiendu — Tiendu theme development CLI
@@ -20,6 +25,7 @@ tiendu — Tiendu theme development CLI
20
25
  Usage:
21
26
  tiendu init [dir] Set up a theme project (optionally in a new directory)
22
27
  tiendu pull Download the live theme from your store
28
+ tiendu build Build a theme (requires tiendu.config.json)
23
29
  tiendu push Upload local files to the active preview (full replace)
24
30
  tiendu dev Start dev mode: auto-sync changes to a live preview URL
25
31
  tiendu publish Publish the active preview to the live storefront
@@ -30,12 +36,17 @@ Usage:
30
36
  tiendu preview delete Delete the active preview
31
37
  tiendu preview open Open the active preview URL in your browser
32
38
 
39
+ tiendu check-updates Check npm for a newer CLI version
40
+ tiendu version Show the current CLI version
41
+
33
42
  tiendu help Show this help message
43
+ tiendu --version, -v Show the current CLI version
34
44
 
35
45
  Typical workflow:
36
46
  tiendu init my-store Set up a new project in ./my-store
37
47
  cd my-store
38
48
  tiendu pull Download the current live theme
49
+ tiendu build Build the theme (for themes with tiendu.config.json)
39
50
  tiendu dev Edit locally — preview updates in real time
40
51
  tiendu publish Ship to the live storefront when ready
41
52
  `;
@@ -45,8 +56,14 @@ const main = async () => {
45
56
  const command = args[0];
46
57
  const subcommand = args[1];
47
58
 
48
- // Check for updates at most once per day (non-blocking)
49
- await checkForUpdates();
59
+ if (
60
+ command === "version" ||
61
+ command === "--version" ||
62
+ command === "-v"
63
+ ) {
64
+ console.log(getCurrentVersion());
65
+ process.exit(0);
66
+ }
50
67
 
51
68
  if (
52
69
  !command ||
@@ -58,6 +75,14 @@ const main = async () => {
58
75
  process.exit(0);
59
76
  }
60
77
 
78
+ if (command === "check-updates") {
79
+ await checkForUpdatesNow();
80
+ return;
81
+ }
82
+
83
+ // Check for updates at most once per day (non-blocking)
84
+ await checkForUpdates();
85
+
61
86
  if (command === "init") {
62
87
  await init(args[1]); // optional directory name
63
88
  return;
@@ -68,6 +93,12 @@ const main = async () => {
68
93
  return;
69
94
  }
70
95
 
96
+ if (command === "build") {
97
+ const result = await build();
98
+ if (!result.ok) process.exit(1);
99
+ return;
100
+ }
101
+
71
102
  if (command === "push") {
72
103
  await push();
73
104
  return;
package/bin/tiendu.mjs CHANGED
@@ -11,6 +11,11 @@ import {
11
11
  previewDelete,
12
12
  previewOpen,
13
13
  } from "../lib/preview.mjs";
14
+ import {
15
+ checkForUpdates,
16
+ checkForUpdatesNow,
17
+ getCurrentVersion,
18
+ } from "../lib/update-check.mjs";
14
19
 
15
20
  const HELP = `
16
21
  tiendu — CLI para desarrollar temas de Tiendu
@@ -27,10 +32,14 @@ Uso:
27
32
  tiendu preview delete Eliminar el preview activo
28
33
  tiendu preview open Abrir la URL del preview en el navegador
29
34
 
35
+ tiendu check-updates Buscar una nueva version del CLI
36
+ tiendu version Mostrar la version actual del CLI
37
+
30
38
  tiendu help Mostrar esta ayuda
31
39
 
32
- Opciones:
40
+ Opciones:
33
41
  --help, -h Mostrar esta ayuda
42
+ --version, -v Mostrar la version actual del CLI
34
43
  `;
35
44
 
36
45
  const main = async () => {
@@ -38,6 +47,15 @@ const main = async () => {
38
47
  const command = args[0];
39
48
  const subcommand = args[1];
40
49
 
50
+ if (
51
+ command === "version" ||
52
+ command === "--version" ||
53
+ command === "-v"
54
+ ) {
55
+ console.log(getCurrentVersion());
56
+ process.exit(0);
57
+ }
58
+
41
59
  if (
42
60
  !command ||
43
61
  command === "help" ||
@@ -48,6 +66,13 @@ const main = async () => {
48
66
  process.exit(0);
49
67
  }
50
68
 
69
+ if (command === "check-updates") {
70
+ await checkForUpdatesNow();
71
+ return;
72
+ }
73
+
74
+ await checkForUpdates();
75
+
51
76
  if (command === "init") {
52
77
  await init();
53
78
  return;
package/lib/build.mjs ADDED
@@ -0,0 +1,214 @@
1
+ import { watch } from "node:fs";
2
+ import {
3
+ cp,
4
+ mkdir,
5
+ readdir,
6
+ rm,
7
+ stat,
8
+ copyFile,
9
+ } from "node:fs/promises";
10
+ import path from "node:path";
11
+ import * as esbuild from "esbuild";
12
+ import * as p from "@clack/prompts";
13
+ import { readThemeConfig } from "./config.mjs";
14
+
15
+ const THEME_DIRS = ["layout", "templates", "snippets", "assets"];
16
+
17
+ /**
18
+ * Discover JS/TS and CSS entry points from src/layout and src/templates.
19
+ * Returns separate maps for JS and CSS to avoid key collisions.
20
+ */
21
+ const discoverEntryPoints = async (srcDir) => {
22
+ const jsEntries = {};
23
+ const cssEntries = {};
24
+
25
+ for (const [dir, prefix] of [
26
+ ["layout", "layout"],
27
+ ["templates", "template"],
28
+ ]) {
29
+ const dirPath = path.join(srcDir, dir);
30
+ let files;
31
+ try {
32
+ files = await readdir(dirPath);
33
+ } catch {
34
+ continue;
35
+ }
36
+ for (const file of files) {
37
+ const ext = path.extname(file);
38
+ if (![".js", ".ts", ".css"].includes(ext)) continue;
39
+ const name = path.basename(file, ext);
40
+ const key = `${prefix}-${name}.bundle`;
41
+ const fullPath = path.join(dirPath, file);
42
+ if (ext === ".css") {
43
+ cssEntries[key] = fullPath;
44
+ } else {
45
+ jsEntries[key] = fullPath;
46
+ }
47
+ }
48
+ }
49
+
50
+ return { jsEntries, cssEntries };
51
+ };
52
+
53
+ /**
54
+ * Copy theme directories (layout/, templates/, snippets/, assets/) to dist/.
55
+ */
56
+ const copyThemeFiles = async (rootDir, distDir) => {
57
+ for (const dir of THEME_DIRS) {
58
+ const src = path.join(rootDir, dir);
59
+ const dest = path.join(distDir, dir);
60
+ try {
61
+ await stat(src);
62
+ } catch {
63
+ continue;
64
+ }
65
+ await cp(src, dest, { recursive: true });
66
+ }
67
+ };
68
+
69
+ /**
70
+ * Copy a single file from root to dist, preserving relative path.
71
+ */
72
+ const copySingleFile = async (rootDir, distDir, relativePath) => {
73
+ const src = path.join(rootDir, relativePath);
74
+ const dest = path.join(distDir, relativePath);
75
+ await mkdir(path.dirname(dest), { recursive: true });
76
+ await copyFile(src, dest);
77
+ };
78
+
79
+ /**
80
+ * Run a one-shot build or start watch mode.
81
+ * @param {{ watch?: boolean }} options
82
+ * @returns {Promise<{ ok: boolean, cleanup?: () => Promise<void> }>}
83
+ */
84
+ export const build = async ({ watch: watchMode = false } = {}) => {
85
+ const rootDir = process.cwd();
86
+ const srcDir = path.join(rootDir, "src");
87
+ const distDir = path.join(rootDir, "dist");
88
+
89
+ const themeConfig = await readThemeConfig();
90
+ if (!themeConfig) {
91
+ p.log.error("No tiendu.config.json found. This is not a built theme.");
92
+ return { ok: false };
93
+ }
94
+
95
+ // Clean and recreate dist
96
+ await rm(distDir, { recursive: true, force: true });
97
+ await mkdir(distDir, { recursive: true });
98
+
99
+ // Copy theme files first
100
+ await copyThemeFiles(rootDir, distDir);
101
+
102
+ // Discover entry points (JS and CSS separately to avoid key collisions)
103
+ const { jsEntries, cssEntries } = await discoverEntryPoints(srcDir);
104
+ const jsCount = Object.keys(jsEntries).length;
105
+ const cssCount = Object.keys(cssEntries).length;
106
+ const entryCount = jsCount + cssCount;
107
+
108
+ if (entryCount === 0) {
109
+ p.log.warn("No entry points found in src/layout or src/templates.");
110
+ return { ok: true };
111
+ }
112
+
113
+ const outdir = path.join(distDir, "assets");
114
+ const jsBuildOptions = jsCount > 0
115
+ ? { entryPoints: jsEntries, bundle: true, format: "esm", target: "es2020", outdir, logLevel: "warning", write: true }
116
+ : null;
117
+ const cssBuildOptions = cssCount > 0
118
+ ? { entryPoints: cssEntries, bundle: true, outdir, logLevel: "warning", write: true }
119
+ : null;
120
+
121
+ if (!watchMode) {
122
+ // One-shot build
123
+ try {
124
+ const builds = [];
125
+ if (jsBuildOptions) builds.push(esbuild.build(jsBuildOptions));
126
+ if (cssBuildOptions) builds.push(esbuild.build(cssBuildOptions));
127
+ await Promise.all(builds);
128
+ p.log.success(
129
+ `Built ${entryCount} entry point${entryCount === 1 ? "" : "s"} to dist/`,
130
+ );
131
+ return { ok: true };
132
+ } catch (error) {
133
+ p.log.error(`Build failed: ${error.message}`);
134
+ return { ok: false };
135
+ }
136
+ }
137
+
138
+ // Watch mode — create contexts for both JS and CSS
139
+ const contexts = [];
140
+ try {
141
+ if (jsBuildOptions) {
142
+ const jsCtx = await esbuild.context(jsBuildOptions);
143
+ await jsCtx.watch();
144
+ contexts.push(jsCtx);
145
+ }
146
+ if (cssBuildOptions) {
147
+ const cssCtx = await esbuild.context(cssBuildOptions);
148
+ await cssCtx.watch();
149
+ contexts.push(cssCtx);
150
+ }
151
+ } catch (error) {
152
+ p.log.error(`Build failed: ${error.message}`);
153
+ for (const ctx of contexts) await ctx.dispose();
154
+ return { ok: false };
155
+ }
156
+
157
+ p.log.success(
158
+ `Built ${entryCount} entry point${entryCount === 1 ? "" : "s"}. Watching for changes...`,
159
+ );
160
+
161
+ // Watch theme directories for Liquid/static asset changes and copy to dist
162
+ const themeWatchers = [];
163
+ const debounceMap = new Map();
164
+ const DEBOUNCE_MS = 200;
165
+
166
+ for (const dir of THEME_DIRS) {
167
+ const dirPath = path.join(rootDir, dir);
168
+ try {
169
+ await stat(dirPath);
170
+ } catch {
171
+ continue;
172
+ }
173
+
174
+ const watcher = watch(dirPath, { recursive: true }, (eventType, filename) => {
175
+ if (!filename) return;
176
+ const relativePath = path.join(dir, filename);
177
+
178
+ const existing = debounceMap.get(relativePath);
179
+ if (existing) clearTimeout(existing);
180
+
181
+ const timer = setTimeout(async () => {
182
+ debounceMap.delete(relativePath);
183
+ try {
184
+ const fileStat = await stat(path.join(rootDir, relativePath)).catch(
185
+ () => null,
186
+ );
187
+ if (fileStat && fileStat.isFile()) {
188
+ await copySingleFile(rootDir, distDir, relativePath);
189
+ console.log(`⟳ ${relativePath}`);
190
+ } else if (!fileStat) {
191
+ // File deleted — remove from dist
192
+ const dest = path.join(distDir, relativePath);
193
+ await rm(dest, { force: true });
194
+ console.log(`✕ ${relativePath}`);
195
+ }
196
+ } catch (error) {
197
+ p.log.warn(`Error copying ${relativePath}: ${error.message}`);
198
+ }
199
+ }, DEBOUNCE_MS);
200
+
201
+ debounceMap.set(relativePath, timer);
202
+ });
203
+
204
+ themeWatchers.push(watcher);
205
+ }
206
+
207
+ const cleanup = async () => {
208
+ for (const w of themeWatchers) w.close();
209
+ for (const timer of debounceMap.values()) clearTimeout(timer);
210
+ for (const ctx of contexts) await ctx.dispose();
211
+ };
212
+
213
+ return { ok: true, cleanup };
214
+ };
package/lib/config.mjs CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  const CONFIG_DIR = ".cli";
5
5
  const CONFIG_FILE = "config.json";
6
6
  const CREDENTIALS_FILE = "credentials.json";
7
+ const THEME_CONFIG_FILE = "tiendu.config.json";
7
8
 
8
9
  /**
9
10
  * @typedef {{ storeId: number, apiBaseUrl: string, previewKey?: string }} TienduConfig
@@ -54,6 +55,25 @@ export const writeCredentials = async (credentials) => {
54
55
  );
55
56
  };
56
57
 
58
+ /** @returns {Promise<object | null>} */
59
+ export const readThemeConfig = async () => {
60
+ try {
61
+ const raw = await readFile(
62
+ path.resolve(process.cwd(), THEME_CONFIG_FILE),
63
+ "utf-8",
64
+ );
65
+ return JSON.parse(raw);
66
+ } catch {
67
+ return null;
68
+ }
69
+ };
70
+
71
+ /** @returns {Promise<boolean>} */
72
+ export const isBuiltTheme = async () => (await readThemeConfig()) !== null;
73
+
74
+ /** @returns {string} */
75
+ export const getDistDir = () => path.resolve(process.cwd(), "dist");
76
+
57
77
  /**
58
78
  * @returns {Promise<{ config: TienduConfig, credentials: TienduCredentials }>}
59
79
  */
package/lib/dev.mjs CHANGED
@@ -3,7 +3,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import * as p from "@clack/prompts";
5
5
  import { zipSync } from "fflate";
6
- import { loadConfigOrFail, writeConfig } from "./config.mjs";
6
+ import { loadConfigOrFail, writeConfig, isBuiltTheme, getDistDir } from "./config.mjs";
7
7
  import {
8
8
  createPreview,
9
9
  listPreviews,
@@ -14,6 +14,7 @@ import {
14
14
  uploadPreviewFileMultipart,
15
15
  uploadPreviewZip,
16
16
  } from "./api.mjs";
17
+ import { build } from "./build.mjs";
17
18
 
18
19
  const isDotfile = (name) => name.startsWith(".");
19
20
 
@@ -56,7 +57,19 @@ export const dev = async () => {
56
57
  const { config, credentials } = await loadConfigOrFail();
57
58
  const { apiBaseUrl, storeId } = config;
58
59
  const { apiKey } = credentials;
59
- const rootDir = process.cwd();
60
+ const builtTheme = await isBuiltTheme();
61
+ const rootDir = builtTheme ? getDistDir() : process.cwd();
62
+ let buildCleanup = null;
63
+
64
+ // For built themes, run the build first (with watch mode)
65
+ if (builtTheme) {
66
+ const buildResult = await build({ watch: true });
67
+ if (!buildResult.ok) {
68
+ p.log.error("Initial build failed. Fix errors and try again.");
69
+ process.exit(1);
70
+ }
71
+ buildCleanup = buildResult.cleanup;
72
+ }
60
73
 
61
74
  const existingPreviewsResult = await listPreviews(
62
75
  apiBaseUrl,
@@ -199,9 +212,10 @@ export const dev = async () => {
199
212
  debounceMap.set(relativePath, timer);
200
213
  });
201
214
 
202
- const cleanup = () => {
215
+ const cleanup = async () => {
203
216
  watcher.close();
204
217
  for (const timer of debounceMap.values()) clearTimeout(timer);
218
+ if (buildCleanup) await buildCleanup();
205
219
  p.outro("Dev mode stopped.");
206
220
  process.exit(0);
207
221
  };
package/lib/pull.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as p from "@clack/prompts";
2
- import { loadConfigOrFail } from "./config.mjs";
2
+ import { loadConfigOrFail, isBuiltTheme, getDistDir } from "./config.mjs";
3
3
  import { downloadStorefrontArchive } from "./api.mjs";
4
4
  import { extractZip } from "./zip.mjs";
5
5
 
@@ -32,7 +32,7 @@ export const pull = async () => {
32
32
  `Archive received (${formatBytes(result.data.length)}). Extracting...`,
33
33
  );
34
34
 
35
- const outputDir = process.cwd();
35
+ const outputDir = (await isBuiltTheme()) ? getDistDir() : process.cwd();
36
36
  const extractedFiles = await extractZip(result.data, outputDir);
37
37
 
38
38
  p.log.success(
package/lib/push.mjs CHANGED
@@ -2,7 +2,7 @@ import { readdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import * as p from "@clack/prompts";
4
4
  import { zipSync } from "fflate";
5
- import { loadConfigOrFail } from "./config.mjs";
5
+ import { loadConfigOrFail, isBuiltTheme, getDistDir } from "./config.mjs";
6
6
  import { uploadPreviewZip } from "./api.mjs";
7
7
 
8
8
  /** @param {number} bytes */
@@ -59,7 +59,7 @@ export const push = async () => {
59
59
  process.exit(1);
60
60
  }
61
61
 
62
- const rootDir = process.cwd();
62
+ const rootDir = (await isBuiltTheme()) ? getDistDir() : process.cwd();
63
63
  const spinner = p.spinner();
64
64
  spinner.start("Packing files...");
65
65
 
@@ -72,10 +72,7 @@ const isOlderVersion = (a, b) => {
72
72
  * Reads local package.json version.
73
73
  * @returns {string}
74
74
  */
75
- const getCurrentVersion = () => {
76
- // Resolved at import time via static path relative to this file
77
- return TIENDU_CLI_VERSION;
78
- };
75
+ export const getCurrentVersion = () => TIENDU_CLI_VERSION;
79
76
 
80
77
  // This constant is replaced at build time via package.json version
81
78
  // We read it dynamically to avoid hardcoding.
@@ -99,7 +96,7 @@ export const checkForUpdates = async () => {
99
96
 
100
97
  // If checked within the last 24h, use cached result
101
98
  if (state && now - state.lastChecked < ONE_DAY_MS) {
102
- const currentVersion = TIENDU_CLI_VERSION;
99
+ const currentVersion = getCurrentVersion();
103
100
  if (
104
101
  state.latestVersion &&
105
102
  isOlderVersion(currentVersion, state.latestVersion)
@@ -119,12 +116,40 @@ export const checkForUpdates = async () => {
119
116
  return;
120
117
  }
121
118
 
122
- const currentVersion = TIENDU_CLI_VERSION;
119
+ const currentVersion = getCurrentVersion();
123
120
  if (isOlderVersion(currentVersion, latestVersion)) {
124
121
  showUpdateNote(currentVersion, latestVersion);
125
122
  }
126
123
  };
127
124
 
125
+ export const checkForUpdatesNow = async () => {
126
+ const currentVersion = getCurrentVersion();
127
+ const latestVersion = await fetchLatestVersion();
128
+
129
+ await writeUpdateCheckState({
130
+ lastChecked: Date.now(),
131
+ latestVersion,
132
+ });
133
+
134
+ if (!latestVersion) {
135
+ p.log.error("Could not check for updates right now.");
136
+ return { ok: false };
137
+ }
138
+
139
+ if (isOlderVersion(currentVersion, latestVersion)) {
140
+ showUpdateNote(currentVersion, latestVersion);
141
+ } else {
142
+ p.log.success(`You're on the latest version (${currentVersion}).`);
143
+ }
144
+
145
+ return {
146
+ ok: true,
147
+ currentVersion,
148
+ latestVersion,
149
+ updateAvailable: isOlderVersion(currentVersion, latestVersion),
150
+ };
151
+ };
152
+
128
153
  /**
129
154
  * @param {string} current
130
155
  * @param {string} latest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiendu",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "CLI para desarrollar y publicar temas en Tiendu",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@clack/prompts": "^1.1.0",
40
+ "esbuild": "^0.24.0",
40
41
  "fflate": "^0.8.2"
41
42
  }
42
43
  }