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/README.md +106 -42
- package/bin/tiendu.js +130 -55
- package/lib/build.mjs +240 -131
- package/lib/config.mjs +68 -12
- package/lib/dev.mjs +26 -30
- package/lib/init.mjs +116 -106
- package/lib/preview.mjs +63 -42
- package/lib/publish.mjs +14 -16
- package/lib/pull.mjs +9 -6
- package/lib/push.mjs +8 -10
- package/lib/stores.mjs +91 -0
- package/lib/ui.mjs +138 -0
- package/lib/update-check.mjs +8 -4
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
123
|
-
const rootDir = builtTheme ? getDistDir() : process.cwd();
|
|
122
|
+
const rootDir = getDistDir();
|
|
124
123
|
let buildCleanup = null;
|
|
125
124
|
let localPreviewServer = null;
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
ui.log.message(`Local live preview: ${localPreviewServer.url}`);
|
|
188
184
|
}
|
|
189
|
-
|
|
185
|
+
ui.log.message(`Sharable preview: ${previewUrl}`);
|
|
190
186
|
|
|
191
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
246
|
+
console.log(`UPLOAD ${relativePath}`);
|
|
251
247
|
if (fileStat.size > MAX_SYNC_FILE_SIZE_BYTES) {
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
const existingCredentials = await readCredentials();
|
|
24
|
+
const prepareWorkDir = async (dirArg) => {
|
|
25
|
+
if (!dirArg) return;
|
|
46
26
|
|
|
47
|
-
|
|
27
|
+
const targetDir = path.resolve(process.cwd(), dirArg);
|
|
48
28
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 (
|
|
62
|
-
|
|
48
|
+
if (ui.isCancel(confirmed) || !confirmed) {
|
|
49
|
+
ui.cancel("Setup cancelled.");
|
|
63
50
|
process.exit(0);
|
|
64
51
|
}
|
|
52
|
+
};
|
|
65
53
|
|
|
66
|
-
|
|
54
|
+
const collectInteractiveInputs = async () => {
|
|
55
|
+
ui.intro("Tiendu CLI — Setup");
|
|
67
56
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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 (
|
|
86
|
-
|
|
84
|
+
if (ui.isCancel(baseUrlInput)) {
|
|
85
|
+
ui.cancel("Setup cancelled.");
|
|
87
86
|
process.exit(0);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
const apiBaseUrl = normalizeBaseUrl(
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
ui.log.error("Your API key does not have access to any stores.");
|
|
111
146
|
process.exit(1);
|
|
112
147
|
}
|
|
113
148
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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 (
|
|
122
|
-
|
|
123
|
-
p.log.info(`Store: ${stores[0].name} (ID: ${storeId})`);
|
|
165
|
+
if (ui.isInteractive()) {
|
|
166
|
+
ui.note(summary, "Setup complete");
|
|
124
167
|
} else {
|
|
125
|
-
|
|
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
|
-
|
|
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
|
};
|