create-ecomiq 0.1.1 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ecomiq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,48 +1,17 @@
1
1
  # create-ecomiq
2
2
 
3
- Scaffold interactivo de storefronts Ecomiq (estilo `create-vite` / `create-next-app`).
4
-
5
- Usa siempre el template **`scaffold`** (home, catálogo, PDP, carrito, login y registro).
6
-
7
- ```bash
8
- npm create ecomiq@latest
9
- # o
10
- npx create-ecomiq@latest my-store
11
- ```
12
-
13
- ## Flujo
14
-
15
- 1. Nombre del proyecto
16
- 2. **Domain** — se valida con `GET /v1/settings` + `x-Store-Domain`
17
- 3. Package manager + install
18
-
19
- ## Flags
3
+ Crea el proyecto de tu tienda online de Ecomiq en un solo paso.
20
4
 
21
5
  ```bash
22
- create-ecomiq my-store \
23
- --domain marketplace \
24
- --pm pnpm \
25
- --yes
6
+ npm create ecomiq@latest mi-tienda
7
+ cd mi-tienda && npm install
8
+ npx ecomiq dev
26
9
  ```
27
10
 
28
- | Flag | Descripción |
29
- | --- | --- |
30
- | `--domain` | Domain de la tienda (obligatorio con `--yes`) |
31
- | `--pm` | `npm` \| `pnpm` \| `yarn` \| `bun` |
32
- | `--yes` / `-y` | Sin prompts |
33
- | `--skip-install` | Solo archivos |
34
-
35
- ## Template
36
-
37
- El zip se descarga desde R2:
11
+ Inicia sesión con el CLI [`ecomiq`](https://www.npmjs.com/package/ecomiq), eliges
12
+ la tienda en el navegador y baja su código a la carpeta. Sin carpeta, usa el
13
+ slug de la tienda. La carpeta debe estar vacía o no existir.
38
14
 
39
- `https://pub-2b5396b516d940dbb1088d750701749c.r2.dev/template-scaffold.zip`
15
+ Necesita Node.js 22.22 o superior.
40
16
 
41
- Tras el scaffold:
42
-
43
- ```bash
44
- cd my-store
45
- npm run dev # ecomiq dev
46
- npm run build
47
- npm run deploy
48
- ```
17
+ Guía completa: https://docs.ecomiq.pe/es/docs/guides/storefront/themes
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { spawnSync } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+ // src/create.ts
6
+ const HELP = `Uso: npm create ecomiq@latest [carpeta]
7
+
8
+ Inicia sesión, eliges tu tienda en el navegador y baja su código a una
9
+ carpeta nueva. Sin carpeta, usa el slug de la tienda.
10
+
11
+ --local Usa el backend local de desarrollo
12
+ `;
13
+ function cliCommands(argv) {
14
+ const flags = argv.filter((arg) => arg === "--local");
15
+ const [folder, ...extra] = argv.filter((arg) => arg !== "--local");
16
+ if (folder?.startsWith("-") || extra.length > 0) throw new Error(`Argumentos no válidos: ${argv.join(" ")}`);
17
+ const init = ["init"];
18
+ if (folder !== void 0) init.push(folder);
19
+ return [["login", ...flags], [...init, ...flags]];
20
+ }
21
+ function create(argv, run) {
22
+ for (const args of cliCommands(argv)) {
23
+ const status = run(args);
24
+ if (status !== 0) return status;
25
+ }
26
+ return 0;
27
+ }
28
+ // src/index.ts
29
+ const require = createRequire(import.meta.url);
30
+ const manifest = require.resolve("ecomiq/package.json");
31
+ const { bin } = require(manifest);
32
+ const cli = join(dirname(manifest), bin.ecomiq);
33
+ const argv = process.argv.slice(2);
34
+ if (argv.includes("--help") || argv.includes("-h")) console.log(HELP);
35
+ else try {
36
+ process.exitCode = create(argv, (args) => spawnSync(process.execPath, [cli, ...args], { stdio: "inherit" }).status ?? 1);
37
+ } catch (error) {
38
+ console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
39
+ console.log(HELP);
40
+ process.exitCode = 1;
41
+ }
42
+ export {};
package/package.json CHANGED
@@ -1,47 +1,39 @@
1
1
  {
2
2
  "name": "create-ecomiq",
3
- "version": "0.1.1",
4
- "description": "Scaffold interactivo de storefronts Ecomiq (estilo create-vite)",
3
+ "version": "1.0.0",
4
+ "description": "Crea el proyecto de tu tienda online de Ecomiq con npm create ecomiq",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Ecomiq",
8
+ "homepage": "https://docs.ecomiq.pe/es/docs/guides/storefront/themes",
8
9
  "repository": {
9
10
  "type": "git",
10
11
  "url": "git+https://github.com/ecomiq/ecomiq-storefront.git",
11
12
  "directory": "packages/create-ecomiq"
12
13
  },
13
- "homepage": "https://github.com/ecomiq/ecomiq-storefront/tree/main/packages/create-ecomiq",
14
- "bugs": {
15
- "url": "https://github.com/ecomiq/ecomiq-storefront/issues"
16
- },
17
- "keywords": [
18
- "ecomiq",
19
- "create-ecomiq",
20
- "scaffold",
21
- "storefront",
22
- "ecommerce"
23
- ],
24
14
  "bin": {
25
- "create-ecomiq": "./bin/create-ecomiq.mjs"
15
+ "create-ecomiq": "./dist/index.js"
26
16
  },
27
17
  "files": [
28
- "bin",
29
- "src",
30
- "README.md"
18
+ "dist"
31
19
  ],
32
20
  "engines": {
33
- "node": ">=20"
34
- },
35
- "publishConfig": {
36
- "access": "public",
37
- "registry": "https://registry.npmjs.org/"
21
+ "node": ">=22.22.0"
38
22
  },
39
23
  "dependencies": {
40
- "@clack/prompts": "^0.11.0",
41
- "adm-zip": "^0.6.0"
24
+ "ecomiq": "^0.1.0"
42
25
  },
43
26
  "devDependencies": {
44
- "@types/adm-zip": "^0.5.8"
27
+ "@types/node": "^26.1.1",
28
+ "tsdown": "^0.23.0",
29
+ "typescript": "^7.0.2"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org"
45
34
  },
46
- "scripts": {}
35
+ "scripts": {
36
+ "build": "tsdown",
37
+ "typecheck": "tsc --noEmit -p tsconfig.json"
38
+ }
47
39
  }
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runCreate } from '../src/index.mjs'
3
-
4
- try {
5
- await runCreate(process.argv.slice(2))
6
- } catch (error) {
7
- const message = error instanceof Error ? error.message : 'Error desconocido'
8
- console.error(`\n ✖ ${message}\n`)
9
- process.exitCode = 1
10
- }
package/src/index.mjs DELETED
@@ -1,243 +0,0 @@
1
- /**
2
- * CLI create-ecomiq — scaffold interactivo de storefronts.
3
- *
4
- * npx create-ecomiq@latest [dir]
5
- * npm create ecomiq@latest
6
- */
7
- import * as p from '@clack/prompts'
8
- import { basename, resolve } from 'node:path'
9
- import { verifyDomain } from './verify-domain.mjs'
10
- import { downloadTemplateZip } from './templates.mjs'
11
- import {
12
- detectPackageManager,
13
- extractTemplateZip,
14
- installDependencies,
15
- resolveTargetDirectory,
16
- slugifyDeployName,
17
- writeProjectConfig,
18
- } from './scaffold.mjs'
19
-
20
- /** Único template que descarga create-ecomiq (CI/CD puede publicar varios). */
21
- export const CREATE_TEMPLATE_ID = 'scaffold'
22
-
23
- /**
24
- * @param {string[]} argv
25
- */
26
- export function parseArgs(argv) {
27
- /** @type {{
28
- * dir?: string
29
- * domain?: string
30
- * pm?: 'npm' | 'pnpm' | 'yarn' | 'bun'
31
- * yes: boolean
32
- * skipInstall: boolean
33
- * help: boolean
34
- * }} */
35
- const out = {
36
- yes: false,
37
- skipInstall: false,
38
- help: false,
39
- }
40
-
41
- for (let i = 0; i < argv.length; i += 1) {
42
- const arg = argv[i]
43
- if (arg === '--help' || arg === '-h') {
44
- out.help = true
45
- continue
46
- }
47
- if (arg === '--yes' || arg === '-y') {
48
- out.yes = true
49
- continue
50
- }
51
- if (arg === '--skip-install') {
52
- out.skipInstall = true
53
- continue
54
- }
55
- if (arg === '--domain') {
56
- out.domain = argv[++i]
57
- continue
58
- }
59
- if (arg === '--pm') {
60
- const value = argv[++i]
61
- if (value === 'npm' || value === 'pnpm' || value === 'yarn' || value === 'bun') {
62
- out.pm = value
63
- } else {
64
- throw new Error(`Package manager no soportado: ${value}`)
65
- }
66
- continue
67
- }
68
- if (arg.startsWith('-')) {
69
- throw new Error(`Flag desconocida: ${arg}`)
70
- }
71
- if (!out.dir) out.dir = arg
72
- }
73
-
74
- return out
75
- }
76
-
77
- function printHelp() {
78
- console.log(`
79
- Uso
80
- create-ecomiq [dir] [opciones]
81
-
82
- Opciones
83
- --domain <slug> Domain de la tienda (x-Store-Domain)
84
- --pm <npm|pnpm|yarn|bun>
85
- --yes, -y Sin prompts (requiere --domain)
86
- --skip-install No ejecutar install
87
- --help, -h
88
-
89
- Ejemplos
90
- npx create-ecomiq@latest my-store
91
- npm create ecomiq@latest
92
- create-ecomiq . --domain marketplace --yes
93
- `)
94
- }
95
-
96
- /**
97
- * @param {string[]} argv
98
- */
99
- export async function runCreate(argv) {
100
- const args = parseArgs(argv)
101
- if (args.help) {
102
- printHelp()
103
- return
104
- }
105
-
106
- p.intro('◈ create-ecomiq')
107
-
108
- if (args.yes && !args.domain) {
109
- throw new Error('Con --yes debes pasar --domain')
110
- }
111
-
112
- let projectName = args.dir
113
- if (!projectName && !args.yes) {
114
- const answer = await p.text({
115
- message: '¿Nombre del proyecto?',
116
- placeholder: 'my-store',
117
- defaultValue: 'my-store',
118
- validate: (value) => {
119
- if (!value.trim()) return 'El nombre es obligatorio'
120
- },
121
- })
122
- if (p.isCancel(answer)) {
123
- p.cancel('Cancelado')
124
- process.exitCode = 1
125
- return
126
- }
127
- projectName = answer.trim()
128
- }
129
- projectName = (projectName ?? 'my-store').trim() || 'my-store'
130
-
131
- const targetDir = resolveTargetDirectory(
132
- projectName === '.' ? process.cwd() : resolve(process.cwd(), projectName),
133
- )
134
- const folderName = basename(targetDir)
135
-
136
- let domain = args.domain?.trim()
137
- let storeName = domain ?? ''
138
-
139
- if (!args.yes) {
140
- while (true) {
141
- const answer = await p.text({
142
- message: 'Domain de la tienda (x-Store-Domain)',
143
- placeholder: 'marketplace',
144
- initialValue: domain ?? '',
145
- validate: (value) => {
146
- if (!value.trim()) return 'El domain es obligatorio'
147
- },
148
- })
149
- if (p.isCancel(answer)) {
150
- p.cancel('Cancelado')
151
- process.exitCode = 1
152
- return
153
- }
154
-
155
- const spinner = p.spinner()
156
- spinner.start('Verificando domain en la API…')
157
- const result = await verifyDomain(answer.trim())
158
- if (!result.ok) {
159
- spinner.stop(result.message)
160
- p.log.error(result.message)
161
- domain = answer.trim()
162
- continue
163
- }
164
- spinner.stop(`Tienda: ${result.storeName}`)
165
- domain = result.domain
166
- storeName = result.storeName
167
- break
168
- }
169
- } else {
170
- const spinner = p.spinner()
171
- spinner.start('Verificando domain en la API…')
172
- const result = await verifyDomain(domain)
173
- if (!result.ok) {
174
- spinner.stop(result.message)
175
- throw new Error(result.message)
176
- }
177
- spinner.stop(`Tienda: ${result.storeName}`)
178
- domain = result.domain
179
- storeName = result.storeName
180
- }
181
-
182
- const templateId = CREATE_TEMPLATE_ID
183
-
184
- let pm = args.pm ?? detectPackageManager()
185
- if (!args.yes && !args.pm) {
186
- const selected = await p.select({
187
- message: 'Package manager',
188
- options: [
189
- { value: 'npm', label: 'npm' },
190
- { value: 'pnpm', label: 'pnpm' },
191
- { value: 'yarn', label: 'yarn' },
192
- { value: 'bun', label: 'bun' },
193
- ],
194
- initialValue: pm,
195
- })
196
- if (p.isCancel(selected)) {
197
- p.cancel('Cancelado')
198
- process.exitCode = 1
199
- return
200
- }
201
- pm = selected
202
- }
203
-
204
- const deployName = slugifyDeployName(folderName)
205
- const downloadSpinner = p.spinner()
206
- downloadSpinner.start('Descargando template…')
207
- const zipBytes = await downloadTemplateZip(templateId)
208
- downloadSpinner.stop('Template descargado')
209
-
210
- const extractSpinner = p.spinner()
211
- extractSpinner.start('Extrayendo archivos…')
212
- extractTemplateZip(zipBytes, targetDir, templateId)
213
- writeProjectConfig(targetDir, { domain, deployName })
214
- extractSpinner.stop('Proyecto listo')
215
-
216
- if (!args.skipInstall) {
217
- const installSpinner = p.spinner()
218
- installSpinner.start(`Instalando con ${pm}…`)
219
- try {
220
- await installDependencies(targetDir, pm)
221
- installSpinner.stop('Dependencias instaladas')
222
- } catch (error) {
223
- installSpinner.stop('Install falló')
224
- throw error
225
- }
226
- }
227
-
228
- const rel =
229
- targetDir === process.cwd() ? '.' : projectName === '.' ? '.' : projectName
230
-
231
- p.outro(
232
- [
233
- `Storefront para ${storeName} (${domain})`,
234
- '',
235
- ` cd ${rel}`,
236
- args.skipInstall ? ` ${pm} install` : null,
237
- ` ${pm === 'npm' ? 'npm run' : pm} dev`,
238
- '',
239
- ]
240
- .filter(Boolean)
241
- .join('\n'),
242
- )
243
- }
package/src/scaffold.mjs DELETED
@@ -1,251 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- mkdtempSync,
6
- readFileSync,
7
- readdirSync,
8
- rmSync,
9
- writeFileSync,
10
- } from 'node:fs'
11
- import { tmpdir } from 'node:os'
12
- import { dirname, join, resolve } from 'node:path'
13
- import AdmZip from 'adm-zip'
14
-
15
- /**
16
- * @param {string} name
17
- */
18
- export function slugifyDeployName(name) {
19
- const slug = name
20
- .trim()
21
- .toLowerCase()
22
- .replace(/[^a-z0-9]+/g, '-')
23
- .replace(/^-+|-+$/g, '')
24
- return slug || 'my-storefront'
25
- }
26
-
27
- /**
28
- * @param {{ domain: string, deployName: string }} options
29
- */
30
- export function buildEcomiqConfig({ domain, deployName }) {
31
- return `import { defineConfig } from '@ecomiq/storefront/config'
32
-
33
- export default defineConfig({
34
- // Identidad para x-Store-Domain: slug corto o dominio completo
35
- // (marketplace | marketplace.ecomiq.app | marketplace.pe)
36
- domain: ${JSON.stringify(domain)},
37
-
38
- theme: {
39
- primary: 'oklch(0.45 0.15 165)',
40
- radius: '0.75rem',
41
- },
42
-
43
- // Portable: Node por defecto. Opt-in a workerd con runtime: 'workerd'.
44
- dev: { port: 3000 },
45
-
46
- deploy: {
47
- target: 'cloudflare',
48
- name: ${JSON.stringify(deployName)},
49
- // routes: [{ pattern: 'tienda.example.com', custom_domain: true }],
50
- },
51
- })
52
- `
53
- }
54
-
55
- /**
56
- * @param {string} configSource
57
- * @param {{ domain: string, deployName: string }} patch
58
- */
59
- export function patchEcomiqConfigSource(configSource, patch) {
60
- let next = configSource
61
- if (/domain:\s*['"][^'"]*['"]/.test(next)) {
62
- next = next.replace(/domain:\s*['"][^'"]*['"]/, `domain: ${JSON.stringify(patch.domain)}`)
63
- }
64
- if (/name:\s*['"][^'"]*['"]/.test(next)) {
65
- // Solo el name dentro de deploy (primera aparición tras deploy suele bastar;
66
- // fallback: reescribir archivo completo si el patrón es ambiguo).
67
- next = next.replace(
68
- /(deploy:\s*\{[\s\S]*?name:\s*)['"][^'"]*['"]/,
69
- `$1${JSON.stringify(patch.deployName)}`,
70
- )
71
- }
72
- return next
73
- }
74
-
75
- /**
76
- * Extrae un zip a destino (JS puro vía adm-zip; Windows/macOS/Linux sin CLI).
77
- * Espera raíz `template-{id}/` o archivos sueltos.
78
- * @param {Uint8Array | Buffer} zipBytes
79
- * @param {string} destDir
80
- * @param {string} templateId
81
- */
82
- export function extractTemplateZip(zipBytes, destDir, templateId) {
83
- mkdirSync(destDir, { recursive: true })
84
- const staging = mkdtempSync(join(tmpdir(), 'create-ecomiq-'))
85
-
86
- try {
87
- const zip = new AdmZip(Buffer.from(zipBytes))
88
- zip.extractAllTo(staging, /* overwrite */ true)
89
-
90
- const nested = join(staging, `template-${templateId}`)
91
- const source = existsSync(nested) ? nested : findExtractedRoot(staging)
92
- if (!source) {
93
- throw new Error(`No se encontró el contenido del template "${templateId}" en el zip`)
94
- }
95
-
96
- copyTree(source, destDir)
97
- } finally {
98
- rmSync(staging, { recursive: true, force: true })
99
- }
100
- }
101
-
102
- function findExtractedRoot(staging) {
103
- const entries = readdirSync(staging, { withFileTypes: true })
104
- const dirs = entries.filter((entry) => entry.isDirectory())
105
- if (dirs.length === 1) return join(staging, dirs[0].name)
106
- if (entries.some((entry) => entry.name === 'ecomiq.config.js' || entry.name === 'package.json')) {
107
- return staging
108
- }
109
- return null
110
- }
111
-
112
- function copyTree(from, to) {
113
- mkdirSync(to, { recursive: true })
114
- for (const entry of readdirSync(from, { withFileTypes: true })) {
115
- const src = join(from, entry.name)
116
- const dest = join(to, entry.name)
117
- if (entry.isDirectory()) {
118
- copyTree(src, dest)
119
- } else {
120
- mkdirSync(dirname(dest), { recursive: true })
121
- // rename across devices can fail; read/write is safer for tmp → project
122
- writeFileSync(dest, readFileSync(src))
123
- }
124
- }
125
- }
126
-
127
- /**
128
- * @param {string} destDir
129
- * @param {{ domain: string, deployName: string }} options
130
- */
131
- export function writeProjectConfig(destDir, { domain, deployName }) {
132
- const configPath = join(destDir, 'ecomiq.config.js')
133
- if (existsSync(configPath)) {
134
- const current = readFileSync(configPath, 'utf8')
135
- writeFileSync(configPath, patchEcomiqConfigSource(current, { domain, deployName }))
136
- return
137
- }
138
- writeFileSync(configPath, buildEcomiqConfig({ domain, deployName }))
139
- }
140
-
141
- /**
142
- * Dependencias nativas / postinstall del stack @ecomiq/storefront (Vite + workerd).
143
- * pnpm 10+ bloquea sus scripts hasta aprobarlos (`ERR_PNPM_IGNORED_BUILDS`).
144
- */
145
- export const PNPM_ALLOW_BUILDS = Object.freeze(['esbuild', 'workerd'])
146
-
147
- /**
148
- * Escribe/actualiza `pnpm-workspace.yaml` para permitir builds de deps críticas.
149
- * Compatible con pnpm 10 (`onlyBuiltDependencies`) y 11 (`allowBuilds`).
150
- * @param {string} destDir
151
- * @param {readonly string[]} [packages]
152
- */
153
- export function ensurePnpmAllowBuilds(destDir, packages = PNPM_ALLOW_BUILDS) {
154
- const path = join(destDir, 'pnpm-workspace.yaml')
155
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : ''
156
- const lines = []
157
-
158
- if (existing.trim()) {
159
- // Conserva contenido previo (packages:, etc.) y reescribe bloques de builds.
160
- const withoutBuildBlocks = existing
161
- .replace(/\n?allowBuilds:\n(?:[ \t]+.+\n)*/g, '\n')
162
- .replace(/\n?onlyBuiltDependencies:\n(?:[ \t]+-.+\n)*/g, '\n')
163
- .trimEnd()
164
- if (withoutBuildBlocks.trim()) lines.push(withoutBuildBlocks, '')
165
- }
166
-
167
- lines.push('allowBuilds:')
168
- for (const name of packages) {
169
- lines.push(` ${name}: true`)
170
- }
171
- lines.push('')
172
- lines.push('onlyBuiltDependencies:')
173
- for (const name of packages) {
174
- lines.push(` - ${name}`)
175
- }
176
- lines.push('')
177
-
178
- writeFileSync(path, `${lines.join('\n')}\n`)
179
- return path
180
- }
181
-
182
- /**
183
- * @param {string} command
184
- * @param {string[]} args
185
- * @param {string} cwd
186
- * @param {NodeJS.ProcessEnv} [env]
187
- */
188
- function runCommand(command, args, cwd, env = process.env) {
189
- return new Promise((resolvePromise, reject) => {
190
- const child = spawn(command, args, {
191
- cwd,
192
- stdio: 'inherit',
193
- shell: process.platform === 'win32',
194
- env,
195
- })
196
- child.on('exit', (code) => {
197
- if (code === 0) resolvePromise()
198
- else reject(new Error(`${command} ${args.join(' ')} salió con código ${code}`))
199
- })
200
- child.on('error', reject)
201
- })
202
- }
203
-
204
- /**
205
- * @param {string} destDir
206
- * @param {'npm' | 'pnpm' | 'yarn' | 'bun'} pm
207
- */
208
- export async function installDependencies(destDir, pm) {
209
- // No forzar CI=true: en pnpm activa frozen-lockfile / strict builds y rompe scaffolds locales.
210
- const env = { ...process.env }
211
-
212
- if (pm === 'pnpm') {
213
- ensurePnpmAllowBuilds(destDir)
214
- await runCommand('pnpm', ['install'], destDir, env)
215
- return
216
- }
217
-
218
- await runCommand(pm, ['install'], destDir, env)
219
- }
220
-
221
- /**
222
- * Detecta package manager desde user-agent de npm_config o locks.
223
- * @param {string} [cwd]
224
- * @returns {'npm' | 'pnpm' | 'yarn' | 'bun'}
225
- */
226
- export function detectPackageManager(cwd = process.cwd()) {
227
- const ua = process.env.npm_config_user_agent ?? ''
228
- if (ua.includes('pnpm')) return 'pnpm'
229
- if (ua.includes('yarn')) return 'yarn'
230
- if (ua.includes('bun')) return 'bun'
231
- if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
232
- if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'
233
- if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
234
- return 'npm'
235
- }
236
-
237
- /**
238
- * @param {string} targetPath
239
- */
240
- export function resolveTargetDirectory(targetPath) {
241
- const resolved = resolve(targetPath)
242
- if (existsSync(resolved)) {
243
- const entries = readdirSync(resolved)
244
- if (entries.length > 0) {
245
- throw new Error(`El directorio ya existe y no está vacío: ${resolved}`)
246
- }
247
- } else {
248
- mkdirSync(resolved, { recursive: true })
249
- }
250
- return resolved
251
- }
package/src/templates.mjs DELETED
@@ -1,84 +0,0 @@
1
- /**
2
- * Catálogo y descarga de templates desde R2 público.
3
- */
4
- export const DEFAULT_TEMPLATES_BASE_URL =
5
- 'https://pub-2b5396b516d940dbb1088d750701749c.r2.dev'
6
-
7
- export const FALLBACK_TEMPLATES = [
8
- {
9
- id: 'scaffold',
10
- name: 'Scaffold',
11
- description: 'Mínimo: home, catálogo, PDP, carrito, login y registro',
12
- },
13
- ]
14
-
15
- /**
16
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }} [options]
17
- * @returns {Promise<Array<{ id: string, name: string, description?: string }>>}
18
- */
19
- export async function listTemplates(options = {}) {
20
- const baseUrl = (options.baseUrl ?? DEFAULT_TEMPLATES_BASE_URL).replace(/\/$/, '')
21
- const fetchImpl = options.fetchImpl ?? globalThis.fetch
22
-
23
- if (typeof fetchImpl !== 'function') return [...FALLBACK_TEMPLATES]
24
-
25
- try {
26
- const response = await fetchImpl(`${baseUrl}/templates.json`, {
27
- headers: { Accept: 'application/json' },
28
- })
29
- if (!response.ok) return [...FALLBACK_TEMPLATES]
30
- const json = await response.json()
31
- const items = Array.isArray(json)
32
- ? json
33
- : Array.isArray(json?.templates)
34
- ? json.templates
35
- : null
36
- if (!items?.length) return [...FALLBACK_TEMPLATES]
37
-
38
- return items
39
- .filter((item) => item && typeof item.id === 'string' && item.id.trim())
40
- .map((item) => ({
41
- id: item.id.trim(),
42
- name: typeof item.name === 'string' && item.name.trim() ? item.name.trim() : item.id,
43
- description:
44
- typeof item.description === 'string' ? item.description : undefined,
45
- }))
46
- } catch {
47
- return [...FALLBACK_TEMPLATES]
48
- }
49
- }
50
-
51
- /**
52
- * @param {string} templateId
53
- * @param {{ baseUrl?: string }} [options]
54
- */
55
- export function templateZipUrl(templateId, options = {}) {
56
- const baseUrl = (options.baseUrl ?? DEFAULT_TEMPLATES_BASE_URL).replace(/\/$/, '')
57
- return `${baseUrl}/template-${templateId}.zip`
58
- }
59
-
60
- /**
61
- * @param {string} templateId
62
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }} [options]
63
- * @returns {Promise<Uint8Array>}
64
- */
65
- export async function downloadTemplateZip(templateId, options = {}) {
66
- const fetchImpl = options.fetchImpl ?? globalThis.fetch
67
- if (typeof fetchImpl !== 'function') {
68
- throw new Error('fetch no está disponible en este runtime')
69
- }
70
-
71
- const url = templateZipUrl(templateId, options)
72
- const response = await fetchImpl(url)
73
- if (!response.ok) {
74
- throw new Error(
75
- `No se pudo descargar el template "${templateId}" (${response.status}): ${url}`,
76
- )
77
- }
78
-
79
- const buffer = new Uint8Array(await response.arrayBuffer())
80
- if (buffer.byteLength === 0) {
81
- throw new Error(`El zip del template "${templateId}" está vacío`)
82
- }
83
- return buffer
84
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Verifica que un domain exista en la API storefront (GET /v1/settings).
3
- */
4
- export const DEFAULT_SETTINGS_URL = 'https://storefront.ecomiq.pe/v1/settings'
5
-
6
- /**
7
- * @param {string} domain
8
- * @param {{ settingsUrl?: string, fetchImpl?: typeof fetch }} [options]
9
- * @returns {Promise<{ ok: true, storeName: string, domain: string, settings: unknown } | { ok: false, status: number, message: string }>}
10
- */
11
- export async function verifyDomain(domain, options = {}) {
12
- const trimmed = domain.trim()
13
- if (!trimmed) {
14
- return { ok: false, status: 0, message: 'El domain es obligatorio' }
15
- }
16
-
17
- const settingsUrl = options.settingsUrl ?? DEFAULT_SETTINGS_URL
18
- const fetchImpl = options.fetchImpl ?? globalThis.fetch
19
- if (typeof fetchImpl !== 'function') {
20
- return { ok: false, status: 0, message: 'fetch no está disponible en este runtime' }
21
- }
22
-
23
- let response
24
- try {
25
- response = await fetchImpl(settingsUrl, {
26
- method: 'GET',
27
- headers: {
28
- Accept: 'application/json',
29
- 'x-Store-Domain': trimmed,
30
- },
31
- })
32
- } catch (error) {
33
- const detail = error instanceof Error ? error.message : 'Error de red'
34
- return { ok: false, status: 0, message: `No se pudo contactar la API: ${detail}` }
35
- }
36
-
37
- if (!response.ok) {
38
- return {
39
- ok: false,
40
- status: response.status,
41
- message:
42
- response.status === 404
43
- ? `No existe una tienda con domain "${trimmed}"`
44
- : `La API respondió ${response.status} para domain "${trimmed}"`,
45
- }
46
- }
47
-
48
- let settings
49
- try {
50
- settings = await response.json()
51
- } catch {
52
- return { ok: false, status: response.status, message: 'La API no devolvió JSON válido' }
53
- }
54
-
55
- const storeName =
56
- settings &&
57
- typeof settings === 'object' &&
58
- settings.store &&
59
- typeof settings.store === 'object' &&
60
- typeof settings.store.name === 'string'
61
- ? settings.store.name
62
- : trimmed
63
-
64
- return { ok: true, storeName, domain: trimmed, settings }
65
- }