tiendu 0.6.0 → 0.7.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/lib/dev.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  import { watch } from "node:fs";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import * as p from "@clack/prompts";
5
- import { loadConfigOrFail, isBuiltTheme, getDistDir } from "./config.mjs";
4
+ import { getDistDir, loadConfigOrFail } from "./config.mjs";
6
5
  import {
7
6
  fetchPreviewDetails,
8
7
  resolvePreviewKeyInteractively,
@@ -16,6 +15,7 @@ import { isDotfile } from "./fs-utils.mjs";
16
15
  import { startLocalPreviewServer } from "./local-preview.mjs";
17
16
  import { pushPreparedDirectoryToPreview } from "./push.mjs";
18
17
  import { retryAsync } from "./retry.mjs";
18
+ import * as ui from "./ui.mjs";
19
19
 
20
20
  const RETRY_ATTEMPTS = 3;
21
21
  const MAX_SYNC_FILE_SIZE_BYTES = 20 * 1024 * 1024;
@@ -59,7 +59,7 @@ const runCleanupStep = async (label, cleanupFn) => {
59
59
  }),
60
60
  ]);
61
61
  } catch (error) {
62
- p.log.warn(error.message);
62
+ ui.log.warn(error.message);
63
63
  } finally {
64
64
  if (timeoutId) clearTimeout(timeoutId);
65
65
  }
@@ -119,20 +119,16 @@ export const dev = async () => {
119
119
  const { config, credentials } = await loadConfigOrFail();
120
120
  const { apiBaseUrl, storeId } = config;
121
121
  const { apiKey } = credentials;
122
- const builtTheme = await isBuiltTheme();
123
- const rootDir = builtTheme ? getDistDir() : process.cwd();
122
+ const rootDir = getDistDir();
124
123
  let buildCleanup = null;
125
124
  let localPreviewServer = null;
126
125
 
127
- // For built themes, run the build first (with watch mode)
128
- if (builtTheme) {
129
- const buildResult = await build({ watch: true });
130
- if (!buildResult.ok) {
131
- p.log.error("Initial build failed. Fix errors and try again.");
132
- process.exit(1);
133
- }
134
- buildCleanup = buildResult.cleanup;
126
+ const buildResult = await build({ watch: true });
127
+ if (!buildResult.ok) {
128
+ ui.log.error("Initial build failed. Fix errors and try again.");
129
+ process.exit(1);
135
130
  }
131
+ buildCleanup = buildResult.cleanup;
136
132
 
137
133
  // Resolve preview via shared interactive picker
138
134
  const previewKey = await resolvePreviewKeyInteractively({ config, credentials });
@@ -145,14 +141,14 @@ export const dev = async () => {
145
141
  previewKey,
146
142
  );
147
143
  if (!previewResult.ok) {
148
- p.log.error(`Preview ${previewKey} not found.`);
144
+ ui.log.error(`Preview ${previewKey} not found.`);
149
145
  process.exit(1);
150
146
  }
151
147
 
152
148
  const previewHostname = previewResult.data.preview.previewHostname;
153
149
  const previewUrl = previewResult.data.url;
154
150
 
155
- const spinner = p.spinner();
151
+ const spinner = ui.spinner();
156
152
  spinner.start("Compressing files...");
157
153
 
158
154
  const uploadResult = await pushPreparedDirectoryToPreview({
@@ -169,7 +165,7 @@ export const dev = async () => {
169
165
 
170
166
  if (!uploadResult.ok) {
171
167
  spinner.stop("Initial push failed.", 1);
172
- p.log.error(uploadResult.error);
168
+ ui.log.error(uploadResult.error);
173
169
  process.exit(1);
174
170
  }
175
171
 
@@ -179,16 +175,16 @@ export const dev = async () => {
179
175
  previewHostname,
180
176
  });
181
177
  } catch (error) {
182
- p.log.warn(`Could not start local live preview: ${error.message}`);
178
+ ui.log.warn(`Could not start local live preview: ${error.message}`);
183
179
  }
184
180
 
185
181
  spinner.stop(`Preview ready (${previewKey}).`);
186
182
  if (localPreviewServer) {
187
- p.log.message(`Local live preview: ${localPreviewServer.url}`);
183
+ ui.log.message(`Local live preview: ${localPreviewServer.url}`);
188
184
  }
189
- p.log.message(`Sharable preview: ${previewUrl}`);
185
+ ui.log.message(`Sharable preview: ${previewUrl}`);
190
186
 
191
- p.log.message("Watching for changes \u2014 press Ctrl+C to stop.");
187
+ ui.log.message("Watching for changes - press Ctrl+C to stop.");
192
188
 
193
189
  // ── File watcher ──────────────────────────────────────────────────────────
194
190
  /** @type {Map<string, NodeJS.Timeout>} */
@@ -223,7 +219,7 @@ export const dev = async () => {
223
219
 
224
220
  if (!fileStat || !fileStat.isFile()) {
225
221
  if (!fileStat) {
226
- console.log(`\u2715 ${relativePath}`);
222
+ console.log(`DELETE ${relativePath}`);
227
223
  const result = await deleteFileWithRetries(
228
224
  apiBaseUrl,
229
225
  apiKey,
@@ -231,14 +227,14 @@ export const dev = async () => {
231
227
  previewKey,
232
228
  relativePath,
233
229
  async (_, nextAttempt) => {
234
- p.log.warn(
230
+ ui.log.warn(
235
231
  ` Retry delete ${relativePath} (${nextAttempt}/${RETRY_ATTEMPTS})`,
236
232
  );
237
233
  },
238
234
  );
239
235
 
240
236
  if (!result.ok) {
241
- p.log.warn(` Failed to delete after ${RETRY_ATTEMPTS} attempts: ${result.error}`);
237
+ ui.log.warn(` Failed to delete after ${RETRY_ATTEMPTS} attempts: ${result.error}`);
242
238
  } else {
243
239
  localPreviewServer?.notifyReload();
244
240
  }
@@ -247,9 +243,9 @@ export const dev = async () => {
247
243
  return;
248
244
  }
249
245
 
250
- console.log(`\u2191 ${relativePath}`);
246
+ console.log(`UPLOAD ${relativePath}`);
251
247
  if (fileStat.size > MAX_SYNC_FILE_SIZE_BYTES) {
252
- p.log.warn(
248
+ ui.log.warn(
253
249
  ` Skipping ${relativePath}: file is ${(fileStat.size / (1024 * 1024)).toFixed(1)} MB (limit ${(MAX_SYNC_FILE_SIZE_BYTES / (1024 * 1024)).toFixed(0)} MB).`,
254
250
  );
255
251
  return;
@@ -264,19 +260,19 @@ export const dev = async () => {
264
260
  relativePath,
265
261
  content,
266
262
  async (_, nextAttempt) => {
267
- p.log.warn(
263
+ ui.log.warn(
268
264
  ` Retry upload ${relativePath} (${nextAttempt}/${RETRY_ATTEMPTS})`,
269
265
  );
270
266
  },
271
267
  );
272
268
 
273
269
  if (!result.ok) {
274
- p.log.warn(` Failed to upload after ${RETRY_ATTEMPTS} attempts: ${result.error}`);
270
+ ui.log.warn(` Failed to upload after ${RETRY_ATTEMPTS} attempts: ${result.error}`);
275
271
  } else {
276
272
  localPreviewServer?.notifyReload();
277
273
  }
278
274
  } catch (error) {
279
- p.log.warn(` Error processing ${relativePath}: ${error.message}`);
275
+ ui.log.warn(` Error processing ${relativePath}: ${error.message}`);
280
276
  } finally {
281
277
  inFlightPaths.delete(relativePath);
282
278
 
@@ -289,7 +285,7 @@ export const dev = async () => {
289
285
  const watcher = watch(rootDir, { recursive: true }, (eventType, filename) => {
290
286
  if (!filename) return;
291
287
  if (hasDotfileSegment(filename)) return;
292
- if (shouldIgnoreWatchedPath(filename, builtTheme)) return;
288
+ if (shouldIgnoreWatchedPath(filename, true)) return;
293
289
 
294
290
  const relativePath = filename.split(path.sep).join("/");
295
291
  queueSync(relativePath);
@@ -306,7 +302,7 @@ export const dev = async () => {
306
302
  await runCleanupStep("Local preview shutdown", () => localPreviewServer?.close());
307
303
  await runCleanupStep("Build watcher shutdown", buildCleanup);
308
304
 
309
- p.outro("Dev mode stopped.");
305
+ ui.outro("Dev mode stopped.");
310
306
  process.exit(0);
311
307
  };
312
308
 
package/lib/init.mjs CHANGED
@@ -1,74 +1,73 @@
1
1
  import { mkdir, access } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import * as p from "@clack/prompts";
4
- import {
5
- readConfig,
6
- readCredentials,
7
- writeConfig,
8
- writeCredentials,
9
- } from "./config.mjs";
3
+ import { readConfig, readCredentials, writeConfig, writeCredentials } from "./config.mjs";
10
4
  import { fetchUserStores } from "./api.mjs";
5
+ import { formatInitSummary } from "./stores.mjs";
6
+ import * as ui from "./ui.mjs";
7
+
8
+ const DEFAULT_API_BASE_URL = "https://tiendu.uy";
11
9
 
12
10
  /** @param {string} url */
13
11
  const normalizeBaseUrl = (url) => (url.endsWith("/") ? url.slice(0, -1) : url);
14
12
 
15
- /**
16
- * @param {string | undefined} dirArg optional directory name passed as CLI arg
17
- */
18
- export const init = async (dirArg) => {
19
- // ─── Resolve working directory ────────────────────────────────────────────
20
- let workDir = process.cwd();
21
-
22
- if (dirArg) {
23
- const targetDir = path.resolve(process.cwd(), dirArg);
24
-
25
- // Fail clearly if the directory already exists
26
- try {
27
- await access(targetDir);
28
- // access succeeded → it exists
29
- p.intro("Tiendu CLI — Setup");
30
- p.cancel(`Directory "${dirArg}" already exists.`);
31
- process.exit(1);
32
- } catch {
33
- // access failed → doesn't exist, safe to create
34
- }
35
-
36
- await mkdir(targetDir, { recursive: true });
37
- workDir = targetDir;
38
-
39
- // Change cwd so config is written inside the new directory
40
- process.chdir(workDir);
13
+ const resolveBaseUrlOrFail = (baseUrlArg) => {
14
+ const candidate = normalizeBaseUrl((baseUrlArg ?? DEFAULT_API_BASE_URL).trim());
15
+ try {
16
+ new URL(candidate);
17
+ } catch {
18
+ ui.log.error("Invalid base URL.");
19
+ process.exit(1);
41
20
  }
21
+ return candidate;
22
+ };
42
23
 
43
- // Re-read config after potential chdir
44
- const existingConfig = await readConfig();
45
- const existingCredentials = await readCredentials();
24
+ const prepareWorkDir = async (dirArg) => {
25
+ if (!dirArg) return;
46
26
 
47
- p.intro("Tiendu CLI Setup");
27
+ const targetDir = path.resolve(process.cwd(), dirArg);
48
28
 
49
- // ─── API Key ──────────────────────────────────────────────────────────────
50
- const apiKeyDefault = existingCredentials?.apiKey ?? "";
29
+ try {
30
+ await access(targetDir);
31
+ ui.cancel(`Directory "${dirArg}" already exists.`);
32
+ process.exit(1);
33
+ } catch {
34
+ // Safe to create the target directory.
35
+ }
51
36
 
52
- const apiKeyInput = await p.password({
53
- message: "API Key",
54
- mask: "*",
55
- validate: (value) => {
56
- const resolved = (value ?? "").trim() || apiKeyDefault;
57
- if (!resolved) return "API Key is required.";
58
- },
37
+ await mkdir(targetDir, { recursive: true });
38
+ process.chdir(targetDir);
39
+ };
40
+
41
+ const ensureResetAllowed = async (hasExistingSetup) => {
42
+ if (!hasExistingSetup) return;
43
+
44
+ const confirmed = await ui.confirm({
45
+ message: "Existing Tiendu configuration found. Reset it and continue?",
59
46
  });
60
47
 
61
- if (p.isCancel(apiKeyInput)) {
62
- p.cancel("Setup cancelled.");
48
+ if (ui.isCancel(confirmed) || !confirmed) {
49
+ ui.cancel("Setup cancelled.");
63
50
  process.exit(0);
64
51
  }
52
+ };
65
53
 
66
- const apiKey = (apiKeyInput ?? "").trim() || apiKeyDefault;
54
+ const collectInteractiveInputs = async () => {
55
+ ui.intro("Tiendu CLI — Setup");
67
56
 
68
- // ─── API Base URL ─────────────────────────────────────────────────────────
69
- const baseUrlDefault = existingConfig?.apiBaseUrl ?? "https://tiendu.uy";
57
+ const apiKeyInput = await ui.password({
58
+ message: "API Key",
59
+ mask: "*",
60
+ validate: (value) => (!(value ?? "").trim() ? "API Key is required." : undefined),
61
+ });
70
62
 
71
- const baseUrlInput = await p.text({
63
+ if (ui.isCancel(apiKeyInput)) {
64
+ ui.cancel("Setup cancelled.");
65
+ process.exit(0);
66
+ }
67
+
68
+ const apiKey = (apiKeyInput ?? "").trim();
69
+ const baseUrlDefault = DEFAULT_API_BASE_URL;
70
+ const baseUrlInput = await ui.text({
72
71
  message: "API base URL",
73
72
  placeholder: baseUrlDefault,
74
73
  defaultValue: baseUrlDefault,
@@ -82,81 +81,92 @@ export const init = async (dirArg) => {
82
81
  },
83
82
  });
84
83
 
85
- if (p.isCancel(baseUrlInput)) {
86
- p.cancel("Setup cancelled.");
84
+ if (ui.isCancel(baseUrlInput)) {
85
+ ui.cancel("Setup cancelled.");
87
86
  process.exit(0);
88
87
  }
89
88
 
90
- const apiBaseUrl = normalizeBaseUrl(
91
- (baseUrlInput ?? "").trim() || baseUrlDefault,
92
- );
89
+ const apiBaseUrl = normalizeBaseUrl((baseUrlInput ?? "").trim() || baseUrlDefault);
90
+ return {
91
+ apiKey,
92
+ apiBaseUrl,
93
+ usedDefaultBaseUrl: apiBaseUrl === DEFAULT_API_BASE_URL,
94
+ };
95
+ };
93
96
 
94
- // ─── Fetch stores (validates API key implicitly) ───────────────────────────
95
- const spinner = p.spinner();
97
+ const collectDirectInputs = (apiKeyArg, baseUrlArg) => {
98
+ const apiKey = (apiKeyArg ?? "").trim();
99
+ if (!apiKey) {
100
+ ui.log.error("API Key is required. Use: tiendu init <api-key> [base-url]");
101
+ process.exit(1);
102
+ }
103
+
104
+ return {
105
+ apiKey,
106
+ apiBaseUrl: resolveBaseUrlOrFail(baseUrlArg),
107
+ usedDefaultBaseUrl: !baseUrlArg,
108
+ };
109
+ };
110
+
111
+ export const init = async ({ dirArg, apiKeyArg, baseUrlArg } = {}) => {
112
+ await prepareWorkDir(dirArg);
113
+
114
+ const existingConfig = await readConfig();
115
+ const existingCredentials = await readCredentials();
116
+ const hasExistingSetup = Boolean(existingConfig || existingCredentials);
117
+ const directMode = Boolean(apiKeyArg);
118
+
119
+ if (!directMode && !ui.isInteractive()) {
120
+ ui.log.error("Non-interactive init requires an API key. Use: tiendu init <api-key> [base-url] --non-interactive");
121
+ process.exit(1);
122
+ }
123
+
124
+ if (!directMode) {
125
+ await ensureResetAllowed(hasExistingSetup);
126
+ }
127
+
128
+ const { apiKey, apiBaseUrl, usedDefaultBaseUrl } = directMode
129
+ ? collectDirectInputs(apiKeyArg, baseUrlArg)
130
+ : await collectInteractiveInputs();
131
+
132
+ const spinner = ui.spinner();
96
133
  spinner.start("Verifying credentials...");
97
134
 
98
135
  const storesResult = await fetchUserStores(apiBaseUrl, apiKey);
99
-
100
136
  if (!storesResult.ok) {
101
137
  spinner.stop("Failed to verify credentials.", 1);
102
- p.cancel(storesResult.error);
138
+ ui.log.error(storesResult.error);
103
139
  process.exit(1);
104
140
  }
105
141
 
106
142
  const stores = storesResult.data;
107
-
108
143
  if (stores.length === 0) {
109
144
  spinner.stop("No stores found.", 1);
110
- p.cancel("Your API Key does not have access to any store.");
145
+ ui.log.error("Your API key does not have access to any stores.");
111
146
  process.exit(1);
112
147
  }
113
148
 
114
- spinner.stop(
115
- `${stores.length} store${stores.length === 1 ? "" : "s"} found.`,
116
- );
149
+ const selectedStore = stores.length === 1 ? stores[0] : null;
150
+ spinner.stop(`Connected to Tiendu. ${stores.length} store${stores.length === 1 ? "" : "s"} available.`);
117
151
 
118
- // ─── Select store ─────────────────────────────────────────────────────────
119
- let storeId;
152
+ await writeCredentials({ apiKey });
153
+ await writeConfig({
154
+ apiBaseUrl,
155
+ ...(selectedStore ? { storeId: selectedStore.id } : {}),
156
+ });
157
+
158
+ const summary = formatInitSummary({
159
+ apiBaseUrl,
160
+ usedDefaultBaseUrl,
161
+ stores,
162
+ selectedStore,
163
+ });
120
164
 
121
- if (stores.length === 1) {
122
- storeId = stores[0].id;
123
- p.log.info(`Store: ${stores[0].name} (ID: ${storeId})`);
165
+ if (ui.isInteractive()) {
166
+ ui.note(summary, "Setup complete");
124
167
  } else {
125
- const selectedId = await p.select({
126
- message: "Select a store",
127
- options: stores.map((store) => ({
128
- value: store.id,
129
- label: store.name,
130
- hint: `ID: ${store.id}`,
131
- })),
132
- initialValue: existingConfig?.storeId ?? stores[0].id,
133
- });
134
-
135
- if (p.isCancel(selectedId)) {
136
- p.cancel("Setup cancelled.");
137
- process.exit(0);
138
- }
139
-
140
- storeId = selectedId;
168
+ ui.log.message(summary);
141
169
  }
142
170
 
143
- // ─── Save ─────────────────────────────────────────────────────────────────
144
- await writeConfig({ storeId, apiBaseUrl });
145
- await writeCredentials({ apiKey });
146
-
147
- const nextSteps = dirArg
148
- ? [`cd ${dirArg}`, `tiendu pull # download the current live theme`]
149
- : [`tiendu pull # download the current live theme`];
150
-
151
- p.note(
152
- [
153
- ...nextSteps,
154
- "",
155
- "Tip: enable Dev Mode in the Tiendu platform",
156
- "(Settings → General) for preview data to load correctly.",
157
- ].join("\n"),
158
- "Next steps",
159
- );
160
-
161
- p.outro("Configuration saved to .cli/");
171
+ ui.outro("Configuration saved to .cli/");
162
172
  };