tiendu 0.8.1 → 0.8.2

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
@@ -69,6 +69,8 @@ The preview renders with the real Tiendu engine — same output as production.
69
69
  When `tiendu dev` starts, it always re-syncs your current local files to the active preview before watching for changes.
70
70
  It also starts a local live-preview URL that proxies the preview and auto-reloads after successful syncs.
71
71
 
72
+ By default, the CLI preserves editor-managed theme state so local development does not overwrite changes made in the theme editor. State files are `templates/*.json`, section group files like `sections/header-group.json`, and `config/settings_data.json`. Use `--override-state` when your local state JSON files should override the editor state.
73
+
72
74
  ---
73
75
 
74
76
  ## Commands
@@ -138,10 +140,13 @@ Builds or stages the current theme into its deployable output directory (`dist/`
138
140
 
139
141
  ```bash
140
142
  tiendu build
141
- tiendu build --skip-instances
143
+ tiendu build --override-state
142
144
  ```
143
145
 
144
- - Use `--skip-instances` to omit template JSON, section group JSON, and `config/settings_data.json` from `dist/`. This is useful when you want to preserve the existing page/section instances on the preview.
146
+ - By default, `build` omits editor-managed state files from `dist/`.
147
+ - Use `--override-state` to include template JSON, section group JSON, and `config/settings_data.json` in `dist/`.
148
+ - `--include-instances` is still accepted as a deprecated alias for `--override-state`.
149
+ - `--skip-instances` is still accepted as a deprecated alias for the default preserve behavior.
145
150
 
146
151
  The build:
147
152
 
@@ -170,10 +175,11 @@ The main development command.
170
175
 
171
176
  ```bash
172
177
  tiendu dev
173
- tiendu dev --skip-instances
178
+ tiendu dev --override-state
174
179
  ```
175
180
 
176
- - Use `--skip-instances` to sync everything except template JSON, section group JSON, and `config/settings_data.json`. Existing instances on the preview are preserved.
181
+ - By default, `dev` preserves template JSON, section group JSON, and `config/settings_data.json` on the preview so theme editor changes are not overwritten.
182
+ - Use `--override-state` to sync those state files from your local project too.
177
183
  - Prints the preview URL on start
178
184
  - Re-syncs the full local theme to the preview on startup
179
185
  - Syncs file creates, edits and deletes
@@ -195,10 +201,11 @@ Zips and uploads `dist/` to the active preview, replacing its content entirely.
195
201
  tiendu push
196
202
  tiendu push --skip-build
197
203
  tiendu push --skip-build --non-interactive
198
- tiendu push --skip-instances
204
+ tiendu push --override-state
199
205
  ```
200
206
 
201
- - Use `--skip-instances` to upload everything except template JSON, section group JSON, and `config/settings_data.json`. Existing instances on the preview are preserved.
207
+ - By default, `push` uploads code/assets while preserving editor-managed state on the preview.
208
+ - Use `--override-state` to upload local template JSON, section group JSON, and `config/settings_data.json`.
202
209
 
203
210
  ---
204
211
 
@@ -213,13 +220,28 @@ Publishes the active preview to the live storefront. Visitors will see the new t
213
220
  tiendu publish
214
221
  tiendu publish --skip-build
215
222
  tiendu publish --skip-build --non-interactive
216
- tiendu publish --skip-instances
223
+ tiendu publish --override-state
217
224
  ```
218
225
 
219
- - Use `--skip-instances` to publish everything except template JSON, section group JSON, and `config/settings_data.json`. Existing instances on the preview are preserved.
226
+ - By default, `publish` syncs code/assets before publishing while preserving editor-managed state.
227
+ - Use `--override-state` to publish local template JSON, section group JSON, and `config/settings_data.json`.
220
228
 
221
229
  In non-interactive mode, the publish confirmation is skipped.
222
230
 
231
+ ### State sync defaults
232
+
233
+ You can set the default for a project in `tiendu.config.json`:
234
+
235
+ ```json
236
+ {
237
+ "sync": {
238
+ "state": false
239
+ }
240
+ }
241
+ ```
242
+
243
+ Use `true` when local state JSON files should override editor state by default.
244
+
223
245
  ---
224
246
 
225
247
  ### `tiendu check-updates`
package/bin/tiendu.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  checkForUpdatesNow,
22
22
  getCurrentVersion,
23
23
  } from "../lib/update-check.mjs";
24
+ import { resolveIncludeInstances } from "../lib/config.mjs";
24
25
  import { configureUi } from "../lib/ui.mjs";
25
26
 
26
27
  const HELP = `
@@ -32,11 +33,13 @@ Usage:
32
33
  tiendu stores list List stores available for the configured API key
33
34
  tiendu stores set <storeId> Select the active store
34
35
  tiendu pull [previewKey] Download the live theme or a preview into dist/
35
- tiendu build Build or stage the current theme into dist/
36
- tiendu push [previewKey] [--skip-build]
36
+ tiendu build [--override-state]
37
+ Build or stage the current theme into dist/
38
+ tiendu push [previewKey] [--skip-build] [--override-state]
37
39
  Upload dist/ to the attached or specified preview
38
- tiendu dev Start dev mode: auto-sync changes to a live preview URL
39
- tiendu publish [previewKey] [--skip-build]
40
+ tiendu dev [--override-state]
41
+ Start dev mode: auto-sync changes to a live preview URL
42
+ tiendu publish [previewKey] [--skip-build] [--override-state]
40
43
  Build/sync dist/ and publish the preview live
41
44
 
42
45
  tiendu preview Show the attached preview details
@@ -55,7 +58,10 @@ Global options:
55
58
  --non-interactive Disable prompts, print plain text output, and skip confirmations
56
59
  --dir <path> Create the project inside a new directory during init
57
60
  --skip-build Reuse the existing dist/ output for push or publish
58
- --skip-instances Skip template/section group JSON and settings_data.json (preserves existing instances on the preview)
61
+ --override-state Sync local theme state JSON and override editor state
62
+ --preserve-state Preserve editor-managed state JSON (default)
63
+ --include-instances Deprecated alias for --override-state
64
+ --skip-instances Deprecated alias for --preserve-state
59
65
  --help, -h Show this help message
60
66
  --version, -v Show the current CLI version
61
67
 
@@ -88,6 +94,13 @@ Pipeline behavior:
88
94
  pipeline.postcss enables PostCSS for compiled style entries.
89
95
  With no config file, or with no enabled pipeline steps, build just stages theme files into dist/.
90
96
 
97
+ Theme state behavior:
98
+ By default, the CLI preserves editor-managed state files: templates/*.json,
99
+ sections/*.json, and config/settings_data.json.
100
+ Use --override-state when your local state JSON should overwrite preview/editor state.
101
+ In tiendu.config.json, set { "sync": { "state": true } } to make local
102
+ state JSON the project default, or false to keep the safe default explicit.
103
+
91
104
  Typical workflow:
92
105
  tiendu init Connect to Tiendu and save your credentials
93
106
  tiendu stores list See available stores
@@ -133,7 +146,12 @@ const main = async () => {
133
146
  const command = positionals[0];
134
147
  const subcommand = positionals[1];
135
148
  const skipBuild = flags.has("--skip-build");
136
- const skipInstances = flags.has("--skip-instances");
149
+ const overrideStateFlag =
150
+ flags.has("--override-state") || flags.has("--include-instances");
151
+ const preserveStateFlag =
152
+ flags.has("--preserve-state") ||
153
+ flags.has("--preserve-instances") ||
154
+ flags.has("--skip-instances");
137
155
  const nonInteractive =
138
156
  flags.has("--non-interactive") || !process.stdin.isTTY || !process.stdout.isTTY;
139
157
 
@@ -192,23 +210,39 @@ const main = async () => {
192
210
  }
193
211
 
194
212
  if (command === "build") {
195
- const result = await build({ skipInstances });
213
+ const includeInstances = await resolveIncludeInstances({
214
+ overrideStateFlag,
215
+ preserveStateFlag,
216
+ });
217
+ const result = await build({ includeInstances });
196
218
  if (!result.ok) process.exit(1);
197
219
  return;
198
220
  }
199
221
 
200
222
  if (command === "push") {
201
- await push({ skipBuild, previewKey: positionals[1], skipInstances });
223
+ const includeInstances = await resolveIncludeInstances({
224
+ overrideStateFlag,
225
+ preserveStateFlag,
226
+ });
227
+ await push({ skipBuild, previewKey: positionals[1], includeInstances });
202
228
  return;
203
229
  }
204
230
 
205
231
  if (command === "dev") {
206
- await dev({ skipInstances });
232
+ const includeInstances = await resolveIncludeInstances({
233
+ overrideStateFlag,
234
+ preserveStateFlag,
235
+ });
236
+ await dev({ includeInstances });
207
237
  return;
208
238
  }
209
239
 
210
240
  if (command === "publish") {
211
- await publish({ skipBuild, previewKey: positionals[1], skipInstances });
241
+ const includeInstances = await resolveIncludeInstances({
242
+ overrideStateFlag,
243
+ preserveStateFlag,
244
+ });
245
+ await publish({ skipBuild, previewKey: positionals[1], includeInstances });
212
246
  return;
213
247
  }
214
248
 
package/lib/build.mjs CHANGED
@@ -129,10 +129,10 @@ const rewriteDirectAssetPaths = (source, knownAssetLogicalPaths) =>
129
129
  return flattened ? `/assets/${flattened}${suffix}` : match;
130
130
  });
131
131
 
132
- const shouldCopyThemeSourceFile = (sourceRelativePath, outputRelativePath, skipInstances = false) => {
132
+ const shouldCopyThemeSourceFile = (sourceRelativePath, outputRelativePath, includeInstances = false) => {
133
133
  const extension = path.extname(sourceRelativePath).toLowerCase();
134
134
  if (ENTRY_SOURCE_EXTENSIONS.has(extension)) return false;
135
- if (skipInstances && isInstanceFile(outputRelativePath)) return false;
135
+ if (!includeInstances && isInstanceFile(outputRelativePath)) return false;
136
136
  return true;
137
137
  };
138
138
 
@@ -165,7 +165,7 @@ const copyThemeFiles = async (
165
165
  distDir,
166
166
  themeSourceDirs,
167
167
  knownAssetLogicalPaths,
168
- skipInstances = false,
168
+ includeInstances = false,
169
169
  ) => {
170
170
  for (const sourceDir of themeSourceDirs) {
171
171
  const absoluteSourceDir = path.join(rootDir, sourceDir.sourceRelativeDir);
@@ -181,7 +181,7 @@ const copyThemeFiles = async (
181
181
  sourceDir.outputRelativeDir,
182
182
  nestedRelativePath,
183
183
  );
184
- if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, skipInstances)) continue;
184
+ if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, includeInstances)) continue;
185
185
  await copyThemeSourceFile(
186
186
  rootDir,
187
187
  distDir,
@@ -261,7 +261,7 @@ const runEntryBuilds = async (jsBuildOptions, cssBuildOptions) => {
261
261
  * @param {{ watch?: boolean }} options
262
262
  * @returns {Promise<{ ok: boolean, cleanup?: () => Promise<void> }>}
263
263
  */
264
- export const build = async ({ watch: watchMode = false, skipInstances = false } = {}) => {
264
+ export const build = async ({ watch: watchMode = false, includeInstances = false } = {}) => {
265
265
  const rootDir = process.cwd();
266
266
  const distDir = path.join(rootDir, "dist");
267
267
 
@@ -324,7 +324,7 @@ export const build = async ({ watch: watchMode = false, skipInstances = false }
324
324
  distDir,
325
325
  themeSourceDirs,
326
326
  knownAssetLogicalPaths,
327
- skipInstances,
327
+ includeInstances,
328
328
  );
329
329
 
330
330
  if (cssCount > 0 && pipeline.postcss) {
@@ -512,7 +512,7 @@ export const build = async ({ watch: watchMode = false, skipInstances = false }
512
512
  const timer = setTimeout(async () => {
513
513
  debounceMap.delete(sourceRelativePath);
514
514
  try {
515
- if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, skipInstances)) {
515
+ if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, includeInstances)) {
516
516
  return;
517
517
  }
518
518
 
package/lib/config.mjs CHANGED
@@ -10,7 +10,8 @@ const THEME_CONFIG_FILE = "tiendu.config.json";
10
10
  * @typedef {{ storeId?: number, apiBaseUrl: string, previewKey?: string }} TienduConfig
11
11
  * @typedef {{ apiKey: string }} TienduCredentials
12
12
  * @typedef {{ compileScripts: boolean, compileStyles: boolean, postcss: boolean }} TienduPipelineConfig
13
- * @typedef {{ pipeline?: Partial<TienduPipelineConfig> }} TienduThemeConfig
13
+ * @typedef {{ state?: boolean, instances?: "preserve" | "include" }} TienduSyncConfig
14
+ * @typedef {{ pipeline?: Partial<TienduPipelineConfig>, sync?: TienduSyncConfig, preserveInstances?: boolean }} TienduThemeConfig
14
15
  */
15
16
 
16
17
  const getConfigDir = () => path.resolve(process.cwd(), CONFIG_DIR);
@@ -41,6 +42,35 @@ const validateThemeConfig = (themeConfig) => {
41
42
  }
42
43
  }
43
44
 
45
+ if (themeConfig.sync !== undefined && !isPlainObject(themeConfig.sync)) {
46
+ throw new Error('tiendu.config.json: "sync" must be an object.');
47
+ }
48
+
49
+ if (
50
+ themeConfig.sync?.state !== undefined &&
51
+ typeof themeConfig.sync.state !== "boolean"
52
+ ) {
53
+ throw new Error('tiendu.config.json: "sync.state" must be true or false.');
54
+ }
55
+
56
+ if (
57
+ themeConfig.sync?.instances !== undefined &&
58
+ !["preserve", "include"].includes(themeConfig.sync.instances)
59
+ ) {
60
+ throw new Error(
61
+ 'tiendu.config.json: "sync.instances" must be "preserve" or "include".',
62
+ );
63
+ }
64
+
65
+ if (
66
+ themeConfig.preserveInstances !== undefined &&
67
+ typeof themeConfig.preserveInstances !== "boolean"
68
+ ) {
69
+ throw new Error(
70
+ 'tiendu.config.json: "preserveInstances" must be true or false.',
71
+ );
72
+ }
73
+
44
74
  return themeConfig;
45
75
  };
46
76
 
@@ -119,6 +149,38 @@ export const getThemePipelineConfig = (themeConfig) => ({
119
149
  export const readThemePipelineConfig = async () =>
120
150
  getThemePipelineConfig(await readThemeConfig());
121
151
 
152
+ /**
153
+ * @param {TienduThemeConfig | null} themeConfig
154
+ * @returns {boolean}
155
+ */
156
+ export const getThemeIncludeInstancesConfig = (themeConfig) => {
157
+ if (themeConfig?.sync?.state !== undefined) return themeConfig.sync.state;
158
+ if (themeConfig?.sync?.instances === "include") return true;
159
+ if (themeConfig?.sync?.instances === "preserve") return false;
160
+ if (themeConfig?.preserveInstances === false) return true;
161
+ return false;
162
+ };
163
+
164
+ /**
165
+ * @param {{ overrideStateFlag?: boolean, preserveStateFlag?: boolean }} [options]
166
+ * @returns {Promise<boolean>}
167
+ */
168
+ export const resolveIncludeInstances = async ({
169
+ overrideStateFlag = false,
170
+ preserveStateFlag = false,
171
+ } = {}) => {
172
+ if (overrideStateFlag && preserveStateFlag) {
173
+ throw new Error(
174
+ "Use either --override-state or --preserve-state, not both.",
175
+ );
176
+ }
177
+
178
+ if (overrideStateFlag) return true;
179
+ if (preserveStateFlag) return false;
180
+
181
+ return getThemeIncludeInstancesConfig(await readThemeConfig());
182
+ };
183
+
122
184
  /** @returns {string} */
123
185
  export const getDistDir = () => path.resolve(process.cwd(), "dist");
124
186
 
package/lib/dev.mjs CHANGED
@@ -115,7 +115,7 @@ const deleteFileWithRetries = (
115
115
  },
116
116
  );
117
117
 
118
- export const dev = async ({ skipInstances = false } = {}) => {
118
+ export const dev = async ({ includeInstances = false } = {}) => {
119
119
  const { config, credentials } = await loadConfigOrFail();
120
120
  const { apiBaseUrl, storeId } = config;
121
121
  const { apiKey } = credentials;
@@ -123,7 +123,7 @@ export const dev = async ({ skipInstances = false } = {}) => {
123
123
  let buildCleanup = null;
124
124
  let localPreviewServer = null;
125
125
 
126
- const buildResult = await build({ watch: true, skipInstances });
126
+ const buildResult = await build({ watch: true, includeInstances });
127
127
  if (!buildResult.ok) {
128
128
  ui.log.error("Initial build failed. Fix errors and try again.");
129
129
  process.exit(1);
@@ -161,7 +161,7 @@ export const dev = async ({ skipInstances = false } = {}) => {
161
161
  compressMessage: "Compressing files...",
162
162
  retryMessage: (result, nextAttempt) =>
163
163
  `Initial push failed. Retrying ${nextAttempt}/${RETRY_ATTEMPTS}... ${result.error}`,
164
- skipInstances,
164
+ includeInstances,
165
165
  });
166
166
 
167
167
  if (!uploadResult.ok) {
@@ -184,6 +184,9 @@ export const dev = async ({ skipInstances = false } = {}) => {
184
184
  ui.log.message(`Local live preview: ${localPreviewServer.url}`);
185
185
  }
186
186
  ui.log.message(`Sharable preview: ${previewUrl}`);
187
+ ui.log.message(
188
+ `Theme state: ${includeInstances ? "overridden from local files" : "preserved from the theme editor"}`,
189
+ );
187
190
 
188
191
  ui.log.message("Watching for changes - press Ctrl+C to stop.");
189
192
 
@@ -289,7 +292,7 @@ export const dev = async ({ skipInstances = false } = {}) => {
289
292
  if (shouldIgnoreWatchedPath(filename, true)) return;
290
293
 
291
294
  const relativePath = filename.split(path.sep).join("/");
292
- if (skipInstances && isInstanceFile(relativePath)) return;
295
+ if (!includeInstances && isInstanceFile(relativePath)) return;
293
296
  queueSync(relativePath);
294
297
  });
295
298
 
package/lib/publish.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  import { push } from "./push.mjs";
8
8
  import * as ui from "./ui.mjs";
9
9
 
10
- export const publish = async ({ skipBuild = false, previewKey: previewKeyArg, skipInstances = false } = {}) => {
10
+ export const publish = async ({ skipBuild = false, previewKey: previewKeyArg, includeInstances = false } = {}) => {
11
11
  const { config, credentials } = await loadConfigOrFail();
12
12
 
13
13
  // Resolve preview key: explicit arg > interactive picker
@@ -42,7 +42,7 @@ export const publish = async ({ skipBuild = false, previewKey: previewKeyArg, sk
42
42
  ? "Syncing existing dist/ output to the preview before publishing..."
43
43
  : "Building and syncing the latest dist/ output before publishing...",
44
44
  );
45
- await push({ skipBuild, previewKey, skipInstances });
45
+ await push({ skipBuild, previewKey, includeInstances });
46
46
 
47
47
  const spinner = ui.spinner();
48
48
  spinner.start("Publishing preview to live storefront...");
package/lib/push.mjs CHANGED
@@ -26,11 +26,11 @@ export const pushPreparedDirectoryToPreview = async ({
26
26
  compressMessage = "Compressing files...",
27
27
  uploadMessage,
28
28
  retryMessage,
29
- skipInstances = false,
29
+ includeInstances = false,
30
30
  }) => {
31
31
  spinner.message(compressMessage);
32
32
 
33
- const shouldInclude = skipInstances
33
+ const shouldInclude = !includeInstances
34
34
  ? (relativePath) => !isInstanceFile(relativePath)
35
35
  : undefined;
36
36
 
@@ -40,7 +40,7 @@ export const pushPreparedDirectoryToPreview = async ({
40
40
  );
41
41
 
42
42
  return retryAsync(
43
- () => uploadPreviewZip(apiBaseUrl, apiKey, storeId, previewKey, zipBuffer, skipInstances),
43
+ () => uploadPreviewZip(apiBaseUrl, apiKey, storeId, previewKey, zipBuffer, !includeInstances),
44
44
  {
45
45
  attempts: 3,
46
46
  shouldRetry: (uploadResult) => !uploadResult.ok && Boolean(uploadResult.retriable),
@@ -54,11 +54,11 @@ export const pushPreparedDirectoryToPreview = async ({
54
54
  );
55
55
  };
56
56
 
57
- export const push = async ({ skipBuild = false, previewKey: previewKeyArg, skipInstances = false } = {}) => {
57
+ export const push = async ({ skipBuild = false, previewKey: previewKeyArg, includeInstances = false } = {}) => {
58
58
  const { config, credentials } = await loadConfigOrFail();
59
59
 
60
60
  if (!skipBuild) {
61
- const result = await build({ skipInstances });
61
+ const result = await build({ includeInstances });
62
62
  if (!result.ok) {
63
63
  process.exit(1);
64
64
  }
@@ -89,7 +89,7 @@ export const push = async ({ skipBuild = false, previewKey: previewKeyArg, skipI
89
89
  previewKey,
90
90
  rootDir,
91
91
  spinner,
92
- skipInstances,
92
+ includeInstances,
93
93
  });
94
94
 
95
95
  if (!result.ok) {
@@ -99,5 +99,8 @@ export const push = async ({ skipBuild = false, previewKey: previewKeyArg, skipI
99
99
  }
100
100
 
101
101
  spinner.stop(`Files uploaded to preview ${previewKey}.`);
102
+ ui.log.message(
103
+ `Theme state: ${includeInstances ? "overridden from local files" : "preserved from the theme editor"}`,
104
+ );
102
105
  ui.log.message(` ${previewDetails.data.url}`);
103
106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiendu",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "CLI para desarrollar y publicar temas en Tiendu",
5
5
  "type": "module",
6
6
  "bin": {