tiendu 0.1.2 → 0.1.3

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 (3) hide show
  1. package/lib/api.mjs +10 -11
  2. package/lib/init.mjs +116 -77
  3. package/package.json +2 -1
package/lib/api.mjs CHANGED
@@ -43,21 +43,16 @@ const checkAuthErrors = (response) => {
43
43
  };
44
44
 
45
45
  /**
46
- * Validate API key and store access with a HEAD request to the download endpoint.
46
+ * Fetch all stores accessible by the current API key.
47
+ * Also serves as API key validation — 401/403 means invalid key.
47
48
  *
48
49
  * @param {string} apiBaseUrl
49
50
  * @param {string} apiKey
50
- * @param {number} storeId
51
- * @returns {Promise<{ ok: true, data: { name: string } } | { ok: false, error: string }>}
51
+ * @returns {Promise<{ ok: true, data: Array<{ id: number, name: string, hostname: string }> } | { ok: false, error: string }>}
52
52
  */
53
- export const fetchStoreInfo = async (apiBaseUrl, apiKey, storeId) => {
53
+ export const fetchUserStores = async (apiBaseUrl, apiKey) => {
54
54
  try {
55
- const response = await apiFetch(
56
- apiBaseUrl,
57
- apiKey,
58
- `/api/admin/stores/${storeId}/code/download`,
59
- { method: "HEAD" },
60
- );
55
+ const response = await apiFetch(apiBaseUrl, apiKey, `/api/v2/stores`);
61
56
 
62
57
  const authError = checkAuthErrors(response);
63
58
  if (authError) return authError;
@@ -69,7 +64,11 @@ export const fetchStoreInfo = async (apiBaseUrl, apiKey, storeId) => {
69
64
  };
70
65
  }
71
66
 
72
- return { ok: true, data: { name: `Tienda #${storeId}` } };
67
+ const stores = await response.json();
68
+ return {
69
+ ok: true,
70
+ data: Array.isArray(stores) ? stores : [],
71
+ };
73
72
  } catch (error) {
74
73
  return {
75
74
  ok: false,
package/lib/init.mjs CHANGED
@@ -1,97 +1,136 @@
1
- import * as readline from "node:readline/promises";
2
- import { stdin, stdout } from "node:process";
1
+ import * as p from "@clack/prompts";
3
2
  import {
4
3
  readConfig,
5
4
  readCredentials,
6
5
  writeConfig,
7
6
  writeCredentials,
8
7
  } from "./config.mjs";
9
- import { fetchStoreInfo } from "./api.mjs";
8
+ import { fetchUserStores } from "./api.mjs";
9
+
10
+ /** @param {string} key */
11
+ const maskApiKey = (key) => {
12
+ if (key.length <= 8) return "****";
13
+ return key.slice(0, 4) + "..." + key.slice(-4);
14
+ };
15
+
16
+ /** @param {string} url */
17
+ const normalizeBaseUrl = (url) => {
18
+ return url.endsWith("/") ? url.slice(0, -1) : url;
19
+ };
10
20
 
11
21
  export const init = async () => {
12
22
  const existingConfig = await readConfig();
13
23
  const existingCredentials = await readCredentials();
14
24
 
15
- const rl = readline.createInterface({ input: stdin, output: stdout });
16
-
17
- try {
18
- console.log("");
19
- console.log("Tiendu CLI — Inicialización");
20
- console.log("===========================");
21
- console.log("");
22
-
23
- // API key
24
- const defaultApiKey = existingCredentials?.apiKey ?? "";
25
- const apiKeyPrompt = defaultApiKey
26
- ? `API Key [${maskApiKey(defaultApiKey)}]: `
27
- : "API Key: ";
28
- const apiKeyInput = (await rl.question(apiKeyPrompt)).trim();
29
- const apiKey = apiKeyInput || defaultApiKey;
30
-
31
- if (!apiKey) {
32
- console.error("La API Key es requerida.");
33
- process.exit(1);
34
- }
25
+ p.intro("Tiendu CLI Inicialización");
35
26
 
36
- // API base URL
37
- const defaultBaseUrl = existingConfig?.apiBaseUrl ?? "https://tiendu.uy";
38
- const baseUrlInput = (
39
- await rl.question(`URL base de la API [${defaultBaseUrl}]: `)
40
- ).trim();
41
- const apiBaseUrl = normalizeBaseUrl(baseUrlInput || defaultBaseUrl);
42
-
43
- // Store ID
44
- const defaultStoreId = existingConfig?.storeId ?? "";
45
- const storeIdPrompt = defaultStoreId
46
- ? `Store ID [${defaultStoreId}]: `
47
- : "Store ID: ";
48
- const storeIdInput = (await rl.question(storeIdPrompt)).trim();
49
- const storeIdRaw = storeIdInput || String(defaultStoreId);
50
- const storeId = Number(storeIdRaw);
51
-
52
- if (!Number.isInteger(storeId) || storeId <= 0) {
53
- console.error("El Store ID debe ser un número entero positivo.");
54
- process.exit(1);
55
- }
27
+ // ─── API Key ──────────────────────────────────────────────────────────────
28
+ const apiKeyDefault = existingCredentials?.apiKey ?? "";
29
+
30
+ const apiKeyInput = await p.password({
31
+ message: "API Key",
32
+ mask: "*",
33
+ validate: (value) => {
34
+ const resolved = value.trim() || apiKeyDefault;
35
+ if (!resolved) return "La API Key es requerida.";
36
+ },
37
+ });
38
+
39
+ if (p.isCancel(apiKeyInput)) {
40
+ p.cancel("Inicialización cancelada.");
41
+ process.exit(0);
42
+ }
43
+
44
+ const apiKey = apiKeyInput.trim() || apiKeyDefault;
45
+
46
+ // ─── API Base URL ─────────────────────────────────────────────────────────
47
+ const baseUrlDefault = existingConfig?.apiBaseUrl ?? "https://tiendu.uy";
48
+
49
+ const baseUrlInput = await p.text({
50
+ message: "URL base de la API",
51
+ placeholder: baseUrlDefault,
52
+ defaultValue: baseUrlDefault,
53
+ validate: (value) => {
54
+ const resolved = value.trim() || baseUrlDefault;
55
+ try {
56
+ new URL(resolved);
57
+ } catch {
58
+ return "URL inválida.";
59
+ }
60
+ },
61
+ });
62
+
63
+ if (p.isCancel(baseUrlInput)) {
64
+ p.cancel("Inicialización cancelada.");
65
+ process.exit(0);
66
+ }
56
67
 
57
- // Validate credentials against the server
58
- console.log("");
59
- console.log("Verificando credenciales...");
68
+ const apiBaseUrl = normalizeBaseUrl(baseUrlInput.trim() || baseUrlDefault);
60
69
 
61
- const storeInfo = await fetchStoreInfo(apiBaseUrl, apiKey, storeId);
62
- if (!storeInfo.ok) {
63
- console.error(`Error: ${storeInfo.error}`);
64
- process.exit(1);
70
+ // ─── Fetch stores (validates API key implicitly) ───────────────────────────
71
+ const spinner = p.spinner();
72
+ spinner.start("Verificando credenciales y obteniendo tiendas...");
73
+
74
+ const storesResult = await fetchUserStores(apiBaseUrl, apiKey);
75
+
76
+ if (!storesResult.ok) {
77
+ spinner.stop("Error al verificar credenciales.", 1);
78
+ p.cancel(storesResult.error);
79
+ process.exit(1);
80
+ }
81
+
82
+ const stores = storesResult.data;
83
+
84
+ if (stores.length === 0) {
85
+ spinner.stop("No se encontraron tiendas.", 1);
86
+ p.cancel("Tu API Key no tiene acceso a ninguna tienda.");
87
+ process.exit(1);
88
+ }
89
+
90
+ spinner.stop(
91
+ `${stores.length} tienda${stores.length === 1 ? "" : "s"} encontrada${stores.length === 1 ? "" : "s"}.`,
92
+ );
93
+
94
+ // ─── Select store ─────────────────────────────────────────────────────────
95
+ let storeId;
96
+
97
+ if (stores.length === 1) {
98
+ // Auto-select if only one store
99
+ storeId = stores[0].id;
100
+ p.note(`${stores[0].name} (ID: ${storeId})`, "Tienda seleccionada");
101
+ } else {
102
+ const selectedId = await p.select({
103
+ message: "Seleccioná una tienda",
104
+ options: stores.map((store) => ({
105
+ value: store.id,
106
+ label: store.name,
107
+ hint: `ID: ${store.id}`,
108
+ })),
109
+ initialValue: existingConfig?.storeId ?? stores[0].id,
110
+ });
111
+
112
+ if (p.isCancel(selectedId)) {
113
+ p.cancel("Inicialización cancelada.");
114
+ process.exit(0);
65
115
  }
66
116
 
67
- console.log(`Tienda: ${storeInfo.data.name} (ID: ${storeId})`);
68
- console.log("");
69
-
70
- // Save
71
- await writeConfig({ storeId, apiBaseUrl });
72
- await writeCredentials({ apiKey });
73
-
74
- console.log("Configuración guardada en .cli/");
75
- console.log("");
76
- console.log('Próximo paso: ejecutá "tiendu pull" para descargar el tema.');
77
- console.log("");
78
- console.log("Nota: habilitá el modo dev en la plataforma Tiendu");
79
- console.log(
80
- "(Ajustes > General) para que los datos del preview se muestren correctamente.",
81
- );
82
- console.log("");
83
- } finally {
84
- rl.close();
117
+ storeId = selectedId;
85
118
  }
86
- };
87
119
 
88
- /** @param {string} key */
89
- const maskApiKey = (key) => {
90
- if (key.length <= 8) return "****";
91
- return key.slice(0, 4) + "..." + key.slice(-4);
92
- };
120
+ // ─── Save ─────────────────────────────────────────────────────────────────
121
+ await writeConfig({ storeId, apiBaseUrl });
122
+ await writeCredentials({ apiKey });
93
123
 
94
- /** @param {string} url */
95
- const normalizeBaseUrl = (url) => {
96
- return url.endsWith("/") ? url.slice(0, -1) : url;
124
+ p.note(
125
+ [
126
+ 'Ejecutá "tiendu pull" para descargar el tema.',
127
+ "",
128
+ "Nota: habilitá el modo dev en la plataforma Tiendu",
129
+ "(Ajustes > General) para que los datos del preview",
130
+ "se muestren correctamente.",
131
+ ].join("\n"),
132
+ "Próximos pasos",
133
+ );
134
+
135
+ p.outro("Configuración guardada en .cli/");
97
136
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiendu",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI para desarrollar y publicar temas en Tiendu",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
+ "@clack/prompts": "^1.1.0",
39
40
  "fflate": "^0.8.2"
40
41
  }
41
42
  }