create-shopify-firebase-app 1.3.0 → 2.0.1

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 (44) hide show
  1. package/lib/index.js +704 -456
  2. package/package.json +2 -2
  3. package/templates/js/functions/package.json +24 -0
  4. package/templates/js/functions/src/admin-api.js +292 -0
  5. package/templates/js/functions/src/auth.js +147 -0
  6. package/templates/js/functions/src/config.js +14 -0
  7. package/templates/js/functions/src/firebase.js +12 -0
  8. package/templates/js/functions/src/index.js +93 -0
  9. package/templates/js/functions/src/proxy.js +60 -0
  10. package/templates/js/functions/src/verify-token.js +39 -0
  11. package/templates/js/functions/src/webhooks.js +111 -0
  12. package/templates/{firebase.json → shared/firebase.json} +1 -0
  13. package/templates/shopify.app.toml +1 -1
  14. package/templates/ts/functions/src/admin-api.ts +290 -0
  15. package/templates/web/css/app.css +1287 -47
  16. package/templates/web/index.html +84 -49
  17. package/templates/web/js/app.js +177 -0
  18. package/templates/web/js/pages/home.js +90 -0
  19. package/templates/web/js/pages/polaris-demo.js +190 -0
  20. package/templates/web/js/pages/products.js +319 -0
  21. package/templates/web/js/pages/settings.js +241 -0
  22. package/templates/web/polaris.html +1149 -0
  23. package/templates/web/products.html +86 -0
  24. package/templates/web/settings.html +40 -0
  25. package/templates/functions/src/admin-api.ts +0 -125
  26. package/templates/web/js/bridge.js +0 -98
  27. /package/templates/{env.example → shared/env.example} +0 -0
  28. /package/templates/{extensions → shared/extensions}/theme-block/assets/app-block.css +0 -0
  29. /package/templates/{extensions → shared/extensions}/theme-block/assets/app-block.js +0 -0
  30. /package/templates/{extensions → shared/extensions}/theme-block/blocks/app-block.liquid +0 -0
  31. /package/templates/{extensions → shared/extensions}/theme-block/locales/en.default.json +0 -0
  32. /package/templates/{extensions → shared/extensions}/theme-block/shopify.extension.toml +0 -0
  33. /package/templates/{firestore.indexes.json → shared/firestore.indexes.json} +0 -0
  34. /package/templates/{firestore.rules → shared/firestore.rules} +0 -0
  35. /package/templates/{gitignore → shared/gitignore} +0 -0
  36. /package/templates/{functions → ts/functions}/package.json +0 -0
  37. /package/templates/{functions → ts/functions}/src/auth.ts +0 -0
  38. /package/templates/{functions → ts/functions}/src/config.ts +0 -0
  39. /package/templates/{functions → ts/functions}/src/firebase.ts +0 -0
  40. /package/templates/{functions → ts/functions}/src/index.ts +0 -0
  41. /package/templates/{functions → ts/functions}/src/proxy.ts +0 -0
  42. /package/templates/{functions → ts/functions}/src/verify-token.ts +0 -0
  43. /package/templates/{functions → ts/functions}/src/webhooks.ts +0 -0
  44. /package/templates/{functions → ts/functions}/tsconfig.json +0 -0
package/lib/index.js CHANGED
@@ -1,12 +1,19 @@
1
1
  /**
2
- * create-shopify-firebase-app — CLI core
2
+ * create-shopify-firebase-app — CLI core (v2)
3
3
  *
4
- * Orchestrates the entire scaffolding flow:
5
- * 1. Collect project config (interactive prompts or CLI args)
6
- * 2. Scaffold files from templates
7
- * 3. Install dependencies
8
- * 4. Wire up Firebase + Shopify
9
- * 5. Initialize git
4
+ * Full-stack scaffolding for Shopify + Firebase apps.
5
+ *
6
+ * Flow:
7
+ * 1. App type selection (extension-only vs full-stack Firebase)
8
+ * 2. Language selection (TypeScript / JavaScript)
9
+ * 3. Project name + app name
10
+ * 4. Scaffold files (multi-page frontend + backend)
11
+ * 5. Firebase setup (login, create/select project, provision)
12
+ * 6. Shopify app creation (login, create/link app via CLI)
13
+ * 7. Configure URLs + credentials
14
+ * 8. Install dependencies + build
15
+ * 9. Git init
16
+ * 10. Ready to deploy!
10
17
  */
11
18
 
12
19
  import fs from "node:fs";
@@ -37,13 +44,20 @@ const c = {
37
44
  const ok = (msg) => console.log(` ${c.green}✔${c.reset} ${msg}`);
38
45
  const warn = (msg) => console.log(` ${c.yellow}⚠${c.reset} ${msg}`);
39
46
  const info = (msg) => console.log(` ${c.cyan}ℹ${c.reset} ${msg}`);
47
+ const fail = (msg) => console.log(` ${c.red}✘${c.reset} ${msg}`);
40
48
  const section = (title) => {
41
49
  console.log();
42
50
  console.log(` ${c.cyan}===${c.reset} ${c.bold}${title}${c.reset} ${c.cyan}===${c.reset}`);
43
51
  console.log();
44
52
  };
45
53
 
46
- // ─── Check if a CLI tool is available ────────────────────────────────────
54
+ const onCancel = () => {
55
+ console.log("\n Cancelled.\n");
56
+ process.exit(0);
57
+ };
58
+
59
+ // ─── Shell helpers ───────────────────────────────────────────────────────
60
+
47
61
  function hasCommand(cmd) {
48
62
  try {
49
63
  execSync(`${cmd} --version`, { stdio: "ignore" });
@@ -53,19 +67,12 @@ function hasCommand(cmd) {
53
67
  }
54
68
  }
55
69
 
56
- // ─── Run a command with live output ──────────────────────────────────────
57
70
  function exec(cmd, cwd) {
58
71
  return new Promise((resolve, reject) => {
59
- const child = spawn(cmd, {
60
- cwd,
61
- shell: true,
62
- stdio: ["ignore", "pipe", "pipe"],
63
- });
64
-
72
+ const child = spawn(cmd, { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] });
65
73
  let stderr = "";
66
- child.stdout?.on("data", () => {}); // consume but don't print
74
+ child.stdout?.on("data", () => {});
67
75
  child.stderr?.on("data", (d) => (stderr += d.toString()));
68
-
69
76
  child.on("close", (code) => {
70
77
  if (code === 0) resolve();
71
78
  else reject(new Error(`Command failed: ${cmd}\n${stderr}`));
@@ -73,30 +80,16 @@ function exec(cmd, cwd) {
73
80
  });
74
81
  }
75
82
 
76
- // ─── Parse CLI arguments ─────────────────────────────────────────────────
77
- function parseArgs(argv) {
78
- const args = {};
79
- let projectName = null;
80
-
81
- for (let i = 0; i < argv.length; i++) {
82
- const arg = argv[i];
83
- if (arg.startsWith("--")) {
84
- const [key, val] = arg.slice(2).split("=");
85
- args[key] = val ?? true;
86
- } else if (arg.startsWith("-") && arg.length > 1) {
87
- // Single-dash flags: -h, -v
88
- for (const ch of arg.slice(1)) {
89
- args[ch] = true;
90
- }
91
- } else if (!projectName) {
92
- projectName = arg;
93
- }
94
- }
95
-
96
- return { projectName, ...args };
83
+ function execInteractive(cmd, cwd) {
84
+ return new Promise((resolve, reject) => {
85
+ const child = spawn(cmd, { cwd, shell: true, stdio: "inherit" });
86
+ child.on("close", (code) => {
87
+ if (code === 0) resolve();
88
+ else reject(new Error(`Command exited with code ${code}`));
89
+ });
90
+ });
97
91
  }
98
92
 
99
- // ─── Open URL in browser (cross-platform) ───────────────────────────────
100
93
  function openBrowser(url) {
101
94
  const platform = process.platform;
102
95
  try {
@@ -108,7 +101,16 @@ function openBrowser(url) {
108
101
  }
109
102
  }
110
103
 
111
- // ─── List Firebase projects ──────────────────────────────────────────────
104
+ function parseTomlField(tomlPath, field) {
105
+ try {
106
+ const content = fs.readFileSync(tomlPath, "utf8");
107
+ const match = content.match(new RegExp(`${field}\\s*=\\s*"([^"]+)"`));
108
+ return match ? match[1] : null;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
112
114
  function listFirebaseProjects() {
113
115
  try {
114
116
  const output = execSync("firebase projects:list --json", {
@@ -128,88 +130,269 @@ function listFirebaseProjects() {
128
130
  return [];
129
131
  }
130
132
 
131
- // ─── Run an interactive command (user sees prompts) ─────────────────────
132
- function execInteractive(cmd, cwd) {
133
- return new Promise((resolve, reject) => {
134
- const child = spawn(cmd, {
135
- cwd,
136
- shell: true,
137
- stdio: "inherit",
138
- });
139
- child.on("close", (code) => {
140
- if (code === 0) resolve();
141
- else reject(new Error(`Command exited with code ${code}`));
142
- });
143
- });
144
- }
145
-
146
- // ─── Check if logged into Shopify CLI ───────────────────────────────────
147
- function isShopifyLoggedIn() {
133
+ async function createFirebaseProject(projectId, displayName) {
148
134
  try {
149
- // `shopify app config link --help` doesn't need auth,
150
- // but `shopify app info` does — use a quick check
151
- const out = execSync("shopify auth login --help", {
152
- encoding: "utf8",
153
- stdio: ["ignore", "pipe", "ignore"],
154
- });
135
+ await exec(`firebase projects:create "${projectId}" --display-name "${displayName}"`);
155
136
  return true;
156
137
  } catch {
157
138
  return false;
158
139
  }
159
140
  }
160
141
 
161
- // ─── Parse TOML file for client_id ──────────────────────────────────────
162
- function parseTomlClientId(tomlPath) {
163
- try {
164
- const content = fs.readFileSync(tomlPath, "utf8");
165
- const match = content.match(/client_id\s*=\s*"([^"]+)"/);
166
- return match ? match[1] : null;
167
- } catch {
168
- return null;
142
+ // ─── Parse CLI arguments ─────────────────────────────────────────────────
143
+ function parseArgs(argv) {
144
+ const args = {};
145
+ let projectName = null;
146
+
147
+ for (let i = 0; i < argv.length; i++) {
148
+ const arg = argv[i];
149
+ if (arg.startsWith("--")) {
150
+ const [key, val] = arg.slice(2).split("=");
151
+ args[key] = val ?? true;
152
+ } else if (arg.startsWith("-") && arg.length > 1) {
153
+ for (const ch of arg.slice(1)) args[ch] = true;
154
+ } else if (!projectName) {
155
+ projectName = arg;
156
+ }
169
157
  }
158
+
159
+ return { projectName, ...args };
170
160
  }
171
161
 
172
- // ─── Create Firebase project ─────────────────────────────────────────────
173
- async function createFirebaseProject(projectId, displayName) {
174
- try {
175
- await exec(`firebase projects:create "${projectId}" --display-name "${displayName}"`);
176
- return true;
177
- } catch {
178
- return false;
162
+ // ─── File helpers ────────────────────────────────────────────────────────
163
+ function copyDirSync(src, dest) {
164
+ fs.mkdirSync(dest, { recursive: true });
165
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
166
+ const srcPath = path.join(src, entry.name);
167
+ const destPath = path.join(dest, entry.name);
168
+ if (entry.isDirectory()) copyDirSync(srcPath, destPath);
169
+ else fs.copyFileSync(srcPath, destPath);
179
170
  }
180
171
  }
181
172
 
182
- // ─── Interactive prompts ─────────────────────────────────────────────────
183
- async function getConfig(args) {
184
- // Check if running non-interactively
185
- const isCI =
186
- args["api-key"] && args["api-secret"] && args["project-id"];
173
+ function countFiles(dir) {
174
+ let count = 0;
175
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
176
+ if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
177
+ else count++;
178
+ }
179
+ return count;
180
+ }
187
181
 
188
- if (isCI) {
189
- return {
190
- projectName: args.projectName || "my-shopify-app",
191
- appName: args["app-name"] || args.projectName || "My Shopify App",
192
- apiKey: args["api-key"],
193
- apiSecret: args["api-secret"],
194
- scopes: args.scopes || "read_products",
195
- projectId: args["project-id"],
196
- appUrl: `https://${args["project-id"]}.web.app`,
197
- };
198
- }
199
-
200
- const onCancel = () => {
201
- console.log("\n Cancelled.\n");
202
- process.exit(0);
182
+ function substituteVars(filePath, vars) {
183
+ if (!fs.existsSync(filePath)) return;
184
+ let content = fs.readFileSync(filePath, "utf8");
185
+ for (const [key, val] of Object.entries(vars)) {
186
+ content = content.replaceAll(key, val);
187
+ }
188
+ fs.writeFileSync(filePath, content);
189
+ }
190
+
191
+ // ─── Scaffold ────────────────────────────────────────────────────────────
192
+ function scaffold(outputDir, config) {
193
+ // 1. Copy shared files (firebase.json, firestore, gitignore, extensions)
194
+ copyDirSync(path.join(TEMPLATES_DIR, "shared"), outputDir);
195
+
196
+ // 2. Copy web frontend (multi-page with App Bridge + Polaris)
197
+ copyDirSync(path.join(TEMPLATES_DIR, "web"), path.join(outputDir, "web"));
198
+
199
+ // 3. Copy functions backend (TS or JS based on language choice)
200
+ const lang = config.language === "javascript" ? "js" : "ts";
201
+ copyDirSync(
202
+ path.join(TEMPLATES_DIR, lang, "functions"),
203
+ path.join(outputDir, "functions"),
204
+ );
205
+
206
+ // 4. Copy shopify.app.toml
207
+ fs.copyFileSync(
208
+ path.join(TEMPLATES_DIR, "shopify.app.toml"),
209
+ path.join(outputDir, "shopify.app.toml"),
210
+ );
211
+
212
+ // 5. Rename dotfiles (npm strips leading dots on publish)
213
+ const renames = [
214
+ ["gitignore", ".gitignore"],
215
+ ["env.example", ".env.example"],
216
+ ];
217
+ for (const [from, to] of renames) {
218
+ const src = path.join(outputDir, from);
219
+ const dest = path.join(outputDir, to);
220
+ if (fs.existsSync(src)) fs.renameSync(src, dest);
221
+ }
222
+
223
+ // 6. Variable substitution
224
+ const vars = {
225
+ "{{APP_NAME}}": config.appName,
226
+ "{{API_KEY}}": config.apiKey || "",
227
+ "{{API_SECRET}}": config.apiSecret || "",
228
+ "{{SCOPES}}": config.scopes,
229
+ "{{PROJECT_ID}}": config.projectId || "",
230
+ "{{APP_URL}}": config.appUrl || "",
231
+ };
232
+
233
+ const templateFiles = [
234
+ "shopify.app.toml",
235
+ "web/index.html",
236
+ "web/products.html",
237
+ "web/settings.html",
238
+ "web/polaris.html",
239
+ ];
240
+
241
+ for (const relPath of templateFiles) {
242
+ substituteVars(path.join(outputDir, relPath), vars);
243
+ }
244
+
245
+ // 7. Generate functions/.env
246
+ const envContent = [
247
+ `SHOPIFY_API_KEY=${config.apiKey || ""}`,
248
+ `SHOPIFY_API_SECRET=${config.apiSecret || ""}`,
249
+ `SCOPES=${config.scopes}`,
250
+ `APP_URL=${config.appUrl || ""}`,
251
+ "",
252
+ ].join("\n");
253
+ fs.writeFileSync(path.join(outputDir, "functions", ".env"), envContent);
254
+
255
+ // 8. Generate .firebaserc
256
+ if (config.projectId) {
257
+ const firebaserc = JSON.stringify(
258
+ { projects: { default: config.projectId } },
259
+ null,
260
+ 2,
261
+ );
262
+ fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
263
+ }
264
+
265
+ // 9. Generate root package.json
266
+ const rootPkg = JSON.stringify(
267
+ { name: config.projectName, private: true },
268
+ null,
269
+ 2,
270
+ );
271
+ fs.writeFileSync(path.join(outputDir, "package.json"), rootPkg + "\n");
272
+
273
+ return countFiles(outputDir);
274
+ }
275
+
276
+ // ─── Update credentials after Shopify app creation ───────────────────────
277
+ function updateCredentials(outputDir, config) {
278
+ const vars = {
279
+ "{{API_KEY}}": config.apiKey || "",
280
+ "{{API_SECRET}}": config.apiSecret || "",
281
+ "{{APP_URL}}": config.appUrl || "",
203
282
  };
204
283
 
284
+ // Update shopify.app.toml
285
+ substituteVars(path.join(outputDir, "shopify.app.toml"), vars);
286
+
287
+ // Update functions/.env
288
+ const envContent = [
289
+ `SHOPIFY_API_KEY=${config.apiKey || ""}`,
290
+ `SHOPIFY_API_SECRET=${config.apiSecret || ""}`,
291
+ `SCOPES=${config.scopes}`,
292
+ `APP_URL=${config.appUrl || ""}`,
293
+ "",
294
+ ].join("\n");
295
+ fs.writeFileSync(path.join(outputDir, "functions", ".env"), envContent);
296
+ }
297
+
298
+ // ═══════════════════════════════════════════════════════════════════════════
299
+ // ─── MAIN FLOW ───────────────────────────────────────────────────────────
300
+ // ═══════════════════════════════════════════════════════════════════════════
301
+
302
+ export async function run(argv) {
303
+ const args = parseArgs(argv);
304
+
305
+ // ── Handle flags ──────────────────────────────────────────────────
306
+ if (args.help || args.h) { printHelp(); return; }
307
+ if (args.version || args.v) {
308
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
309
+ console.log(pkg.version);
310
+ return;
311
+ }
312
+
313
+ // ── Handle --distribute ───────────────────────────────────────────
314
+ if (args.distribute) {
315
+ await distributeFlow();
316
+ return;
317
+ }
318
+
319
+ // ── CI / non-interactive mode ─────────────────────────────────────
320
+ const isCI = args["api-key"] && args["api-secret"] && args["project-id"];
321
+ if (isCI) {
322
+ await runCI(args);
323
+ return;
324
+ }
325
+
326
+ // ═══════════════════════════════════════════════════════════════════
327
+ // ── Interactive flow — guided project wizard ────
328
+ // ═══════════════════════════════════════════════════════════════════
329
+
205
330
  // ── Banner ────────────────────────────────────────────────────────
206
331
  console.log();
207
332
  console.log(` ${c.green}${c.bold}🛍️ + 🔥${c.reset} ${c.bold}create-shopify-firebase-app${c.reset}`);
208
- console.log(` ${c.dim}Serverless Shopify apps free until you scale${c.reset}`);
333
+ console.log(` ${c.dim}Build Shopify apps for free serverless, zero-framework${c.reset}`);
334
+
335
+ // ═══════════════════════════════════════════════════════════════════
336
+ section("Choose Your Template");
337
+
338
+ const { appTemplate } = await prompts({
339
+ type: "select",
340
+ name: "appTemplate",
341
+ message: "What would you like to create?",
342
+ choices: [
343
+ {
344
+ title: `${c.bold}Shopify + Firebase app${c.reset} ${c.dim}(full-stack serverless)${c.reset}`,
345
+ description: "Dashboard, product search, settings, Polaris components — ready to deploy",
346
+ value: "firebase",
347
+ },
348
+ {
349
+ title: `Extension-only app ${c.dim}(Shopify CLI)${c.reset}`,
350
+ description: "Theme extensions, checkout extensions — no backend needed",
351
+ value: "extension",
352
+ },
353
+ ],
354
+ }, { onCancel });
355
+
356
+ // ── Extension-only: delegate to Shopify CLI ───────────────────────
357
+ if (appTemplate === "extension") {
358
+ console.log();
359
+ if (!hasCommand("shopify")) {
360
+ info("Installing Shopify CLI...");
361
+ try {
362
+ await exec("npm install -g @shopify/cli");
363
+ ok("Shopify CLI installed");
364
+ } catch {
365
+ fail("Could not install Shopify CLI");
366
+ info("Install manually: npm i -g @shopify/cli");
367
+ info("Then run: shopify app init");
368
+ return;
369
+ }
370
+ }
371
+ info("Launching Shopify CLI...");
372
+ console.log();
373
+ try {
374
+ await execInteractive("shopify app init");
375
+ } catch {
376
+ warn("Shopify CLI exited");
377
+ }
378
+ return;
379
+ }
209
380
 
210
381
  // ═══════════════════════════════════════════════════════════════════
211
- section("App Configuration");
382
+ section("Project Setup");
212
383
 
384
+ // ── Language ──────────────────────────────────────────────────────
385
+ const { language } = await prompts({
386
+ type: "select",
387
+ name: "language",
388
+ message: "Language for Cloud Functions",
389
+ choices: [
390
+ { title: `TypeScript ${c.dim}(recommended)${c.reset}`, value: "typescript" },
391
+ { title: "JavaScript", value: "javascript" },
392
+ ],
393
+ }, { onCancel });
394
+
395
+ // ── Project name ──────────────────────────────────────────────────
213
396
  let projectName = args.projectName;
214
397
  if (!projectName) {
215
398
  const res = await prompts({
@@ -226,136 +409,15 @@ async function getConfig(args) {
226
409
  projectName = res.projectName;
227
410
  }
228
411
 
412
+ // ── App name ──────────────────────────────────────────────────────
229
413
  const { appName } = await prompts({
230
414
  type: "text",
231
415
  name: "appName",
232
416
  message: "App name (shown in Shopify admin)",
233
- initial: projectName || "My Shopify App",
234
- }, { onCancel });
235
-
236
- const { appType } = await prompts({
237
- type: "select",
238
- name: "appType",
239
- message: "What kind of app are you building?",
240
- choices: [
241
- { title: "Public app — list on the Shopify App Store", value: "public" },
242
- { title: "Custom app — built for a single store", value: "custom" },
243
- ],
244
- }, { onCancel });
245
-
246
- // ═══════════════════════════════════════════════════════════════════
247
- section("Shopify Setup");
248
-
249
- const hasShopifyCli = hasCommand("shopify");
250
-
251
- // Build choices based on what's available
252
- const shopifyChoices = [];
253
- if (hasShopifyCli) {
254
- shopifyChoices.push(
255
- { title: "Create a new app via Shopify CLI", value: "cli-create" },
256
- { title: "Link an existing app via Shopify CLI", value: "cli-link" },
257
- );
258
- }
259
- shopifyChoices.push(
260
- { title: `Enter credentials manually ${c.dim}(Client ID + Secret)${c.reset}`, value: "manual" },
261
- );
262
-
263
- const { shopifySetup } = await prompts({
264
- type: "select",
265
- name: "shopifySetup",
266
- message: "How would you like to connect your Shopify app?",
267
- choices: shopifyChoices,
417
+ initial: projectName.replace(/[-_.]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
268
418
  }, { onCancel });
269
419
 
270
- let apiKey, apiSecret;
271
-
272
- if (shopifySetup === "cli-create" || shopifySetup === "cli-link") {
273
- // ── Use Shopify CLI to create/link the app ────────────────────
274
- // We need a temp directory with a shopify.app.toml for the CLI to work
275
- const tmpDir = path.resolve(process.cwd(), `__shopify_setup_${Date.now()}`);
276
- fs.mkdirSync(tmpDir, { recursive: true });
277
-
278
- try {
279
- // Ensure logged in
280
- console.log();
281
- info("Logging into Shopify CLI...");
282
- info("A browser window will open — sign in to your Partner account.");
283
- console.log();
284
- await execInteractive("shopify auth login", tmpDir);
285
- ok("Logged into Shopify");
286
-
287
- // Run config link — it handles both create and link interactively
288
- console.log();
289
- if (shopifySetup === "cli-create") {
290
- info("Creating a new Shopify app...");
291
- info(`Select ${c.bold}"Create a new app"${c.reset} when prompted.`);
292
- } else {
293
- info("Linking an existing Shopify app...");
294
- info("Select your app from the list when prompted.");
295
- }
296
- console.log();
297
- await execInteractive("shopify app config link", tmpDir);
298
-
299
- // Parse the generated TOML to get client_id
300
- const tomlFiles = fs.readdirSync(tmpDir).filter((f) => f.endsWith(".toml"));
301
- let parsedKey = null;
302
- for (const f of tomlFiles) {
303
- parsedKey = parseTomlClientId(path.join(tmpDir, f));
304
- if (parsedKey) break;
305
- }
306
-
307
- if (parsedKey) {
308
- apiKey = parsedKey;
309
- ok(`Client ID: ${c.cyan}${apiKey}${c.reset}`);
310
- } else {
311
- warn("Could not read Client ID from TOML");
312
- const res = await prompts({
313
- type: "text",
314
- name: "apiKey",
315
- message: "Paste your Client ID (API Key)",
316
- validate: (v) => (v.trim() ? true : "Required"),
317
- }, { onCancel });
318
- apiKey = res.apiKey;
319
- }
320
-
321
- // API Secret is never in the TOML — always need to ask
322
- console.log();
323
- info("The API Secret is not stored in config files for security.");
324
- info(`Find it at: ${c.cyan}https://partners.shopify.com${c.reset} → Apps → your app → Client credentials`);
325
- console.log();
326
- const { secret } = await prompts({
327
- type: "password",
328
- name: "secret",
329
- message: "Paste your Client Secret (API Secret)",
330
- validate: (v) => (v.trim() ? true : "Required — find it in Partner Dashboard → Apps → Client credentials"),
331
- }, { onCancel });
332
- apiSecret = secret;
333
-
334
- } finally {
335
- // Clean up temp directory
336
- fs.rmSync(tmpDir, { recursive: true, force: true });
337
- }
338
- } else {
339
- // ── Manual entry ──────────────────────────────────────────────
340
- const creds = await prompts([
341
- {
342
- type: "text",
343
- name: "apiKey",
344
- message: `Shopify API Key ${c.dim}(Client ID)${c.reset}`,
345
- validate: (v) => (v.trim() ? true : "Required"),
346
- },
347
- {
348
- type: "password",
349
- name: "apiSecret",
350
- message: "Shopify API Secret",
351
- validate: (v) => (v.trim() ? true : "Required"),
352
- },
353
- ], { onCancel });
354
- apiKey = creds.apiKey;
355
- apiSecret = creds.apiSecret;
356
- }
357
-
358
- // Scope presets (like Shopify CLI template selection)
420
+ // ── API scopes ────────────────────────────────────────────────────
359
421
  const { scopeChoice } = await prompts({
360
422
  type: "select",
361
423
  name: "scopeChoice",
@@ -382,34 +444,97 @@ async function getConfig(args) {
382
444
  scopes = scopeChoice;
383
445
  }
384
446
 
447
+ // ── Check for directory conflict ──────────────────────────────────
448
+ const outputDir = path.resolve(process.cwd(), projectName);
449
+ if (fs.existsSync(outputDir)) {
450
+ const { overwrite } = await prompts({
451
+ type: "confirm",
452
+ name: "overwrite",
453
+ message: `Directory "${projectName}" already exists. Overwrite?`,
454
+ initial: false,
455
+ }, { onCancel });
456
+ if (!overwrite) { console.log("\n Cancelled.\n"); process.exit(0); }
457
+ fs.rmSync(outputDir, { recursive: true, force: true });
458
+ }
459
+
460
+ // ═══════════════════════════════════════════════════════════════════
461
+ section("Scaffolding");
462
+
463
+ // Build initial config (credentials filled in later after Shopify app creation)
464
+ const config = {
465
+ projectName,
466
+ appName,
467
+ language,
468
+ scopes,
469
+ apiKey: "",
470
+ apiSecret: "",
471
+ projectId: "",
472
+ appUrl: "",
473
+ };
474
+
475
+ info("Creating project files...");
476
+ const fileCount = scaffold(outputDir, config);
477
+ ok(`Created ${fileCount} files in ${c.cyan}${projectName}/${c.reset}`);
478
+
479
+ if (language === "typescript") {
480
+ info(`${c.dim}Backend: TypeScript (functions/src/*.ts)${c.reset}`);
481
+ } else {
482
+ info(`${c.dim}Backend: JavaScript (functions/src/*.js)${c.reset}`);
483
+ }
484
+ info(`${c.dim}Frontend: 4 pages — Dashboard, Products, Settings, Components${c.reset}`);
485
+
385
486
  // ═══════════════════════════════════════════════════════════════════
386
487
  section("Firebase Setup");
387
488
 
388
- let projectId;
389
- const hasFirebase = hasCommand("firebase");
489
+ // ── Ensure Firebase CLI ───────────────────────────────────────────
490
+ if (!hasCommand("firebase")) {
491
+ info("Firebase CLI not found — installing...");
492
+ try {
493
+ await exec("npm install -g firebase-tools");
494
+ ok("Firebase CLI installed");
495
+ } catch {
496
+ warn("Could not install Firebase CLI automatically");
497
+ info("Install manually: npm i -g firebase-tools");
498
+ }
499
+ }
390
500
 
391
- if (hasFirebase) {
392
- info("Fetching your Firebase projects...");
501
+ if (hasCommand("firebase")) {
502
+ // ── Firebase login ──────────────────────────────────────────────
503
+ info("Checking Firebase authentication...");
504
+ const projects = listFirebaseProjects();
505
+ if (projects.length > 0) {
506
+ ok("Firebase authenticated");
507
+ } else {
508
+ info("Opening browser for Firebase login...");
509
+ try {
510
+ await execInteractive("firebase login");
511
+ ok("Logged into Firebase");
512
+ } catch {
513
+ warn("Firebase login failed — run 'firebase login' manually later");
514
+ }
515
+ }
393
516
 
394
- const choices = [
517
+ // ── Project selection ───────────────────────────────────────────
518
+ const freshProjects = listFirebaseProjects();
519
+ const fbChoices = [
395
520
  { title: `${c.cyan}[create a new project]${c.reset}`, value: "__create__" },
396
521
  ];
397
- const projects = listFirebaseProjects();
398
- if (projects.length > 0) {
399
- for (const p of projects) {
400
- choices.push({
522
+
523
+ if (freshProjects.length > 0) {
524
+ for (const p of freshProjects) {
525
+ fbChoices.push({
401
526
  title: `${p.displayName} ${c.dim}(${p.projectId})${c.reset}`,
402
527
  value: p.projectId,
403
528
  });
404
529
  }
405
530
  }
406
- choices.push({ title: `${c.dim}[enter project ID manually]${c.reset}`, value: "__manual__" });
531
+ fbChoices.push({ title: `${c.dim}[enter project ID manually]${c.reset}`, value: "__manual__" });
407
532
 
408
533
  const { firebaseChoice } = await prompts({
409
534
  type: "select",
410
535
  name: "firebaseChoice",
411
536
  message: "Select a Firebase project",
412
- choices,
537
+ choices: fbChoices,
413
538
  }, { onCancel });
414
539
 
415
540
  if (firebaseChoice === "__create__") {
@@ -417,7 +542,7 @@ async function getConfig(args) {
417
542
  type: "text",
418
543
  name: "newProjectId",
419
544
  message: "New project ID",
420
- initial: projectName,
545
+ initial: projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30),
421
546
  validate: (v) => {
422
547
  if (!v.trim()) return "Required";
423
548
  if (!/^[a-z0-9][a-z0-9-]*$/.test(v)) return "Only lowercase letters, numbers, and hyphens";
@@ -430,17 +555,16 @@ async function getConfig(args) {
430
555
  const created = await createFirebaseProject(newProjectId, appName);
431
556
  if (created) {
432
557
  ok(`Project created: ${c.cyan}${newProjectId}${c.reset}`);
433
- projectId = newProjectId;
558
+ config.projectId = newProjectId;
434
559
  } else {
435
560
  warn("Could not create project automatically");
436
- info("Create one at https://console.firebase.google.com");
437
561
  const { manualId } = await prompts({
438
562
  type: "text",
439
563
  name: "manualId",
440
564
  message: "Firebase Project ID",
441
565
  validate: (v) => (v.trim() ? true : "Required"),
442
566
  }, { onCancel });
443
- projectId = manualId;
567
+ config.projectId = manualId;
444
568
  }
445
569
  } else if (firebaseChoice === "__manual__") {
446
570
  const { manualId } = await prompts({
@@ -449,190 +573,265 @@ async function getConfig(args) {
449
573
  message: "Firebase Project ID",
450
574
  validate: (v) => (v.trim() ? true : "Required"),
451
575
  }, { onCancel });
452
- projectId = manualId;
576
+ config.projectId = manualId;
453
577
  } else {
454
- projectId = firebaseChoice;
455
- ok(`Using project: ${c.cyan}${projectId}${c.reset}`);
578
+ config.projectId = firebaseChoice;
579
+ ok(`Using project: ${c.cyan}${config.projectId}${c.reset}`);
456
580
  }
457
- } else {
458
- // No Firebase CLI — manual setup
459
- const { firebaseSetup } = await prompts({
460
- type: "select",
461
- name: "firebaseSetup",
462
- message: "How would you like to set up Firebase?",
463
- choices: [
464
- { title: "Create a new project — opens Firebase Console", value: "create" },
465
- { title: "Enter project ID manually", value: "manual" },
466
- ],
467
- }, { onCancel });
468
581
 
469
- if (firebaseSetup === "create") {
470
- console.log();
471
- info("Opening Firebase Console...");
472
- info("Create a new project and note the Project ID");
473
- console.log();
474
- openBrowser("https://console.firebase.google.com");
475
- }
582
+ config.appUrl = `https://${config.projectId}.web.app`;
476
583
 
584
+ // ── Write .firebaserc now that we have projectId ────────────────
585
+ const firebaserc = JSON.stringify({ projects: { default: config.projectId } }, null, 2);
586
+ fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
587
+
588
+ // ── Provision Firebase services ─────────────────────────────────
589
+ await provisionFirebase(config, {
590
+ skipProvision: !!args["skip-provision"],
591
+ firestoreRegion: args["firestore-region"],
592
+ nonInteractive: false,
593
+ cwd: outputDir,
594
+ });
595
+ } else {
596
+ // No Firebase CLI available
477
597
  const { manualId } = await prompts({
478
598
  type: "text",
479
599
  name: "manualId",
480
- message: "Firebase Project ID",
600
+ message: "Firebase Project ID (create at console.firebase.google.com)",
481
601
  validate: (v) => (v.trim() ? true : "Required"),
482
602
  }, { onCancel });
483
- projectId = manualId;
484
- }
603
+ config.projectId = manualId;
604
+ config.appUrl = `https://${config.projectId}.web.app`;
485
605
 
486
- return {
487
- projectName,
488
- appName: appName || projectName,
489
- appType,
490
- apiKey,
491
- apiSecret,
492
- scopes: scopes || "read_products",
493
- projectId,
494
- appUrl: `https://${projectId}.web.app`,
495
- };
496
- }
606
+ const firebaserc = JSON.stringify({ projects: { default: config.projectId } }, null, 2);
607
+ fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
608
+ }
497
609
 
498
- // ─── Scaffold files from templates ───────────────────────────────────────
499
- function scaffold(outputDir, config) {
500
- // Recursively copy templates directory
501
- copyDirSync(TEMPLATES_DIR, outputDir);
610
+ // ═══════════════════════════════════════════════════════════════════
611
+ section("Shopify App Setup");
502
612
 
503
- // Rename dotfiles (npm strips leading dots on publish)
504
- const renames = [
505
- ["gitignore", ".gitignore"],
506
- ["env.example", ".env.example"],
507
- ];
508
- for (const [from, to] of renames) {
509
- const src = path.join(outputDir, from);
510
- const dest = path.join(outputDir, to);
511
- if (fs.existsSync(src)) {
512
- fs.renameSync(src, dest);
613
+ // ── Ensure Shopify CLI ────────────────────────────────────────────
614
+ if (!hasCommand("shopify")) {
615
+ info("Shopify CLI not found — installing...");
616
+ try {
617
+ await exec("npm install -g @shopify/cli");
618
+ ok("Shopify CLI installed");
619
+ } catch {
620
+ warn("Could not install Shopify CLI");
513
621
  }
514
622
  }
515
623
 
516
- // Variable substitution map
517
- const vars = {
518
- "{{APP_NAME}}": config.appName,
519
- "{{API_KEY}}": config.apiKey,
520
- "{{API_SECRET}}": config.apiSecret,
521
- "{{SCOPES}}": config.scopes,
522
- "{{PROJECT_ID}}": config.projectId,
523
- "{{APP_URL}}": config.appUrl,
524
- };
624
+ if (hasCommand("shopify")) {
625
+ const { shopifyAction } = await prompts({
626
+ type: "select",
627
+ name: "shopifyAction",
628
+ message: "Shopify app setup",
629
+ choices: [
630
+ { title: "Create a new app + link it (recommended)", value: "create" },
631
+ { title: "Link an existing app", value: "link" },
632
+ { title: "Skip — I'll configure manually later", value: "skip" },
633
+ ],
634
+ }, { onCancel });
525
635
 
526
- // Files that need variable substitution
527
- const templateFiles = [
528
- "shopify.app.toml",
529
- "web/index.html",
530
- ];
636
+ if (shopifyAction !== "skip") {
637
+ // ── Login ──────────────────────────────────────────────────
638
+ console.log();
639
+ info("Logging into Shopify...");
640
+ info("A browser window will open — sign in to your Partner account.");
641
+ console.log();
642
+ try {
643
+ await execInteractive("shopify auth login");
644
+ ok("Logged into Shopify");
645
+ } catch {
646
+ warn("Shopify login failed — continuing with manual setup");
647
+ }
531
648
 
532
- for (const relPath of templateFiles) {
533
- const filePath = path.join(outputDir, relPath);
534
- if (!fs.existsSync(filePath)) continue;
535
- let content = fs.readFileSync(filePath, "utf8");
536
- for (const [key, val] of Object.entries(vars)) {
537
- content = content.replaceAll(key, val);
649
+ // ── Create / Link app via Shopify CLI ──────────────────────
650
+ // Must `cd` into project dir — Shopify CLI uses process.cwd(),
651
+ // not spawn's cwd option, for TOML generation.
652
+ console.log();
653
+ if (shopifyAction === "create") {
654
+ info(`Creating a new Shopify app: ${c.cyan}${appName}${c.reset}`);
655
+ info(`Select ${c.bold}"Create a new app"${c.reset} when prompted by the CLI.`);
656
+ } else {
657
+ info("Select your existing app from the list.");
658
+ }
659
+ console.log();
660
+
661
+ // Use `cd` to ensure Shopify CLI writes files in the project directory
662
+ const cdCmd = process.platform === "win32"
663
+ ? `cd /d "${outputDir}" && shopify app config link`
664
+ : `cd "${outputDir}" && shopify app config link`;
665
+
666
+ try {
667
+ await execInteractive(cdCmd);
668
+ } catch {
669
+ // Shopify CLI may exit non-zero even after creating/linking the app
670
+ // (e.g. "directory doesn't have a package.json" warning)
671
+ warn("Shopify CLI exited with a warning");
672
+ }
673
+
674
+ // ── Parse client_id from any TOML (runs regardless of exit code) ──
675
+ // Check project dir first, then parent dir (Shopify CLI fallback location)
676
+ const dirsToCheck = [outputDir, path.dirname(outputDir)];
677
+ for (const dir of dirsToCheck) {
678
+ if (config.apiKey) break;
679
+ try {
680
+ const tomlFiles = fs.readdirSync(dir).filter((f) => f.endsWith(".toml"));
681
+ for (const f of tomlFiles) {
682
+ const filePath = path.join(dir, f);
683
+ const clientId = parseTomlField(filePath, "client_id");
684
+ if (clientId && clientId !== "{{API_KEY}}" && clientId.length > 5) {
685
+ config.apiKey = clientId;
686
+ ok(`Client ID: ${c.cyan}${config.apiKey}${c.reset}`);
687
+
688
+ // If TOML was in parent dir (Shopify CLI bug), move it to project dir
689
+ if (dir !== outputDir) {
690
+ const destPath = path.join(outputDir, f);
691
+ try {
692
+ // Don't overwrite our template — just read the client_id
693
+ if (!fs.existsSync(destPath)) {
694
+ fs.renameSync(filePath, destPath);
695
+ } else {
696
+ // Clean up the stray TOML from parent dir
697
+ fs.unlinkSync(filePath);
698
+ }
699
+ info(`${c.dim}Picked up credentials from Shopify CLI${c.reset}`);
700
+ } catch {}
701
+ }
702
+ break;
703
+ }
704
+ }
705
+ } catch {}
706
+ }
707
+
708
+ if (!config.apiKey) {
709
+ console.log();
710
+ info("Could not read Client ID from Shopify CLI output.");
711
+ const res = await prompts({
712
+ type: "text",
713
+ name: "apiKey",
714
+ message: "Paste your Client ID (from Partner Dashboard → Apps)",
715
+ validate: (v) => (v.trim() ? true : "Required"),
716
+ }, { onCancel });
717
+ config.apiKey = res.apiKey;
718
+ }
538
719
  }
539
- fs.writeFileSync(filePath, content);
540
- }
541
720
 
542
- // Generate functions/.env (secrets not a template to avoid leaks)
543
- const envContent = [
544
- `SHOPIFY_API_KEY=${config.apiKey}`,
545
- `SHOPIFY_API_SECRET=${config.apiSecret}`,
546
- `SCOPES=${config.scopes}`,
547
- `APP_URL=${config.appUrl}`,
548
- "",
549
- ].join("\n");
550
- fs.writeFileSync(path.join(outputDir, "functions", ".env"), envContent);
721
+ // ── Get API Secret (never in TOML) ──────────────────────────────
722
+ if (!config.apiKey) {
723
+ console.log();
724
+ info(`Find credentials at: ${c.cyan}https://partners.shopify.com${c.reset} → Apps → Client credentials`);
725
+ const res = await prompts({
726
+ type: "text",
727
+ name: "apiKey",
728
+ message: `Client ID ${c.dim}(API Key)${c.reset}`,
729
+ validate: (v) => (v.trim() ? true : "Required"),
730
+ }, { onCancel });
731
+ config.apiKey = res.apiKey;
732
+ }
551
733
 
552
- // Generate .firebaserc
553
- const firebaserc = JSON.stringify(
554
- { projects: { default: config.projectId } },
555
- null,
556
- 2,
557
- );
558
- fs.writeFileSync(path.join(outputDir, ".firebaserc"), firebaserc + "\n");
734
+ console.log();
735
+ info("The API Secret is not stored in config files for security.");
736
+ info(`Find it at: ${c.cyan}https://partners.shopify.com${c.reset} your app → Client credentials`);
737
+ console.log();
738
+ const { apiSecret } = await prompts({
739
+ type: "password",
740
+ name: "apiSecret",
741
+ message: "Client Secret (API Secret)",
742
+ validate: (v) => (v.trim() ? true : "Required"),
743
+ }, { onCancel });
744
+ config.apiSecret = apiSecret;
559
745
 
560
- // Generate root package.json (required by Shopify CLI for `shopify app deploy`)
561
- const rootPkg = JSON.stringify(
562
- { name: config.name, private: true },
563
- null,
564
- 2,
565
- );
566
- fs.writeFileSync(path.join(outputDir, "package.json"), rootPkg + "\n");
746
+ } else {
747
+ // No Shopify CLI — full manual entry
748
+ info(`Enter your Shopify app credentials (from ${c.cyan}partners.shopify.com${c.reset})`);
749
+ const creds = await prompts([
750
+ {
751
+ type: "text",
752
+ name: "apiKey",
753
+ message: `Client ID ${c.dim}(API Key)${c.reset}`,
754
+ validate: (v) => (v.trim() ? true : "Required"),
755
+ },
756
+ {
757
+ type: "password",
758
+ name: "apiSecret",
759
+ message: "Client Secret (API Secret)",
760
+ validate: (v) => (v.trim() ? true : "Required"),
761
+ },
762
+ ], { onCancel });
763
+ config.apiKey = creds.apiKey;
764
+ config.apiSecret = creds.apiSecret;
765
+ }
567
766
 
568
- // Count files
569
- let count = 0;
570
- countFiles(outputDir, () => count++);
571
- return count;
572
- }
767
+ // ── Write final credentials to files ──────────────────────────────
768
+ updateCredentials(outputDir, config);
573
769
 
574
- function copyDirSync(src, dest) {
575
- fs.mkdirSync(dest, { recursive: true });
576
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
577
- const srcPath = path.join(src, entry.name);
578
- const destPath = path.join(dest, entry.name);
579
- if (entry.isDirectory()) {
580
- copyDirSync(srcPath, destPath);
581
- } else {
582
- fs.copyFileSync(srcPath, destPath);
583
- }
770
+ // ═══════════════════════════════════════════════════════════════════
771
+ section("Installing & Building");
772
+
773
+ // ── npm install ───────────────────────────────────────────────────
774
+ info("Installing dependencies...");
775
+ const functionsDir = path.join(outputDir, "functions");
776
+ try {
777
+ await exec("npm install", functionsDir);
778
+ ok("Dependencies installed");
779
+ } catch {
780
+ warn(`npm install failed — run manually: cd ${projectName}/functions && npm install`);
584
781
  }
585
- }
586
782
 
587
- function countFiles(dir, cb) {
588
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
589
- if (entry.isDirectory()) {
590
- countFiles(path.join(dir, entry.name), cb);
591
- } else {
592
- cb();
783
+ // ── TypeScript build ──────────────────────────────────────────────
784
+ if (language === "typescript") {
785
+ info("Building TypeScript...");
786
+ try {
787
+ await exec("npm run build", functionsDir);
788
+ ok("TypeScript compiled successfully");
789
+ } catch {
790
+ warn("Build failed — run manually: cd functions && npm run build");
593
791
  }
594
792
  }
595
- }
596
793
 
597
- // ─── Main flow ───────────────────────────────────────────────────────────
598
- export async function run(argv) {
599
- const args = parseArgs(argv);
794
+ // ═══════════════════════════════════════════════════════════════════
795
+ section("Finishing Up");
600
796
 
601
- // Show help
602
- if (args.help || args.h) {
603
- printHelp();
604
- return;
797
+ // ── Git init ──────────────────────────────────────────────────────
798
+ info("Initializing git...");
799
+ if (hasCommand("git")) {
800
+ try {
801
+ await exec("git init", outputDir);
802
+ await exec("git add -A", outputDir);
803
+ await exec('git commit -m "Initial scaffold from create-shopify-firebase-app"', outputDir);
804
+ ok("Git repository initialized");
805
+ } catch {
806
+ warn("Git init failed — initialize manually if needed");
807
+ }
808
+ } else {
809
+ warn("Git not found — skipping");
605
810
  }
606
811
 
607
- // Show version
608
- if (args.version || args.v) {
609
- const pkg = JSON.parse(
610
- fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
611
- );
612
- console.log(pkg.version);
613
- return;
614
- }
812
+ // ═══════════════════════════════════════════════════════════════════
813
+ printSuccess(config);
814
+ }
815
+
816
+ // ─── CI / non-interactive mode ──────────────────────────────────────────
817
+ async function runCI(args) {
818
+ const config = {
819
+ projectName: args.projectName || "my-shopify-app",
820
+ appName: args["app-name"] || args.projectName || "My Shopify App",
821
+ language: args.language === "javascript" ? "javascript" : "typescript",
822
+ apiKey: args["api-key"],
823
+ apiSecret: args["api-secret"],
824
+ scopes: args.scopes || "read_products",
825
+ projectId: args["project-id"],
826
+ appUrl: `https://${args["project-id"]}.web.app`,
827
+ };
615
828
 
616
- // Collect config
617
- const config = await getConfig(args);
618
829
  const outputDir = path.resolve(process.cwd(), config.projectName);
619
830
 
620
- // Check if directory exists
621
831
  if (fs.existsSync(outputDir)) {
622
- const { overwrite } = await prompts({
623
- type: "confirm",
624
- name: "overwrite",
625
- message: `Directory "${config.projectName}" already exists. Overwrite?`,
626
- initial: false,
627
- });
628
- if (!overwrite) {
629
- console.log("\n Cancelled.\n");
630
- process.exit(0);
631
- }
632
832
  fs.rmSync(outputDir, { recursive: true, force: true });
633
833
  }
634
834
 
635
- // ═══════════════════════════════════════════════════════════════════
636
835
  section("Setting Up");
637
836
 
638
837
  info("Scaffolding project...");
@@ -644,69 +843,111 @@ export async function run(argv) {
644
843
  try {
645
844
  await exec("npm install", functionsDir);
646
845
  ok("Dependencies installed");
647
- } catch (e) {
648
- warn(`npm install failed — run manually: cd ${config.projectName}/functions && npm install`);
649
- }
650
-
651
- info("Building TypeScript...");
652
- try {
653
- await exec("npm run build", functionsDir);
654
- ok("TypeScript compiled successfully");
655
- } catch (e) {
656
- warn("Build failed — run manually: cd functions && npm run build");
846
+ } catch {
847
+ warn("npm install failed");
657
848
  }
658
849
 
659
- info("Setting up Firebase...");
660
- if (!hasCommand("firebase")) {
661
- info("Firebase CLI not found — installing globally...");
850
+ if (config.language === "typescript") {
851
+ info("Building TypeScript...");
662
852
  try {
663
- await exec("npm install -g firebase-tools");
664
- ok("Firebase CLI installed");
665
- } catch (e) {
666
- warn("Could not install Firebase CLI automatically");
667
- info("Install manually: npm i -g firebase-tools");
668
- info(`Then run: cd ${config.projectName} && firebase use ${config.projectId}`);
853
+ await exec("npm run build", functionsDir);
854
+ ok("TypeScript compiled");
855
+ } catch {
856
+ warn("Build failed");
669
857
  }
670
858
  }
671
- if (hasCommand("firebase")) {
672
- const isCI = args["api-key"] && args["api-secret"] && args["project-id"];
859
+
860
+ if (hasCommand("firebase") && !args["skip-provision"]) {
861
+ info("Setting up Firebase...");
673
862
  await provisionFirebase(config, {
674
863
  skipProvision: !!args["skip-provision"],
675
864
  firestoreRegion: args["firestore-region"],
676
- nonInteractive: isCI,
865
+ nonInteractive: true,
677
866
  cwd: outputDir,
678
867
  });
679
868
  }
680
869
 
681
- info("Checking Shopify CLI...");
682
- if (hasCommand("shopify")) {
683
- ok("Shopify CLI detected");
684
- } else {
685
- info("Shopify CLI not found — installing globally...");
686
- try {
687
- await exec("npm install -g @shopify/cli");
688
- ok("Shopify CLI installed");
689
- } catch (e) {
690
- warn("Could not install Shopify CLI automatically");
691
- info("Install manually: npm i -g @shopify/cli");
692
- }
693
- }
694
-
695
- info("Initializing git...");
696
870
  if (hasCommand("git")) {
871
+ info("Initializing git...");
697
872
  try {
698
873
  await exec("git init", outputDir);
699
874
  await exec("git add -A", outputDir);
700
875
  await exec('git commit -m "Initial scaffold from create-shopify-firebase-app"', outputDir);
701
- ok("Git repository initialized with first commit");
702
- } catch {
703
- warn("Git init failed — initialize manually if needed");
876
+ ok("Git initialized");
877
+ } catch {}
878
+ }
879
+
880
+ printSuccess(config);
881
+ }
882
+
883
+ // ─── Distribution flow ──────────────────────────────────────────────────
884
+ async function distributeFlow() {
885
+ console.log();
886
+ console.log(` ${c.green}${c.bold}🛍️ + 🔥${c.reset} ${c.bold}App Distribution${c.reset}`);
887
+
888
+ const tomlPath = path.resolve(process.cwd(), "shopify.app.toml");
889
+
890
+ if (!fs.existsSync(tomlPath)) {
891
+ // No app configured — offer to set one up
892
+ console.log();
893
+ fail("No shopify.app.toml found in the current directory.");
894
+ info("Run this command from your app's root directory.");
895
+ console.log();
896
+
897
+ if (hasCommand("shopify")) {
898
+ const { shouldLink } = await prompts({
899
+ type: "confirm",
900
+ name: "shouldLink",
901
+ message: "Would you like to link a Shopify app now?",
902
+ initial: true,
903
+ }, { onCancel });
904
+
905
+ if (shouldLink) {
906
+ try {
907
+ await execInteractive("shopify auth login");
908
+ await execInteractive("shopify app config link");
909
+ ok("App linked successfully");
910
+ } catch {
911
+ fail("Could not link app");
912
+ return;
913
+ }
914
+ } else {
915
+ return;
916
+ }
917
+ } else {
918
+ info("Install Shopify CLI: npm i -g @shopify/cli");
919
+ return;
704
920
  }
921
+ }
922
+
923
+ const clientId = parseTomlField(tomlPath, "client_id");
924
+ const appName = parseTomlField(tomlPath, "name");
925
+
926
+ section("Distribution Checklist");
927
+
928
+ console.log(` ${c.bold}App: ${c.cyan}${appName || "Unknown"}${c.reset}`);
929
+ if (clientId) console.log(` ${c.bold}Client ID: ${c.dim}${clientId}${c.reset}`);
930
+ console.log();
931
+
932
+ console.log(` ${c.bold}Before submitting to the App Store:${c.reset}`);
933
+ console.log();
934
+ console.log(` ${c.cyan}1.${c.reset} Deploy your app: ${c.cyan}firebase deploy${c.reset}`);
935
+ console.log(` ${c.cyan}2.${c.reset} Test on a development store`);
936
+ console.log(` ${c.cyan}3.${c.reset} Add your privacy policy URL`);
937
+ console.log(` ${c.cyan}4.${c.reset} Add your app listing details (description, screenshots)`);
938
+ console.log(` ${c.cyan}5.${c.reset} Submit for review`);
939
+ console.log();
940
+
941
+ if (clientId) {
942
+ info("Opening Partner Dashboard → Distribution...");
943
+ openBrowser(`https://partners.shopify.com/apps/${clientId}/distribution`);
705
944
  } else {
706
- warn("Git not found skipping");
945
+ info(`Open: ${c.cyan}https://partners.shopify.com${c.reset} Apps → your app → Distribution`);
707
946
  }
708
947
 
709
- printSuccess(config);
948
+ console.log();
949
+ console.log(` ${c.dim}Docs: https://shopify.dev/docs/apps/launch${c.reset}`);
950
+ console.log();
710
951
  }
711
952
 
712
953
  // ─── Success output ──────────────────────────────────────────────────────
@@ -714,7 +955,15 @@ function printSuccess(config) {
714
955
  console.log();
715
956
  console.log(` ${c.green}${c.bold}✔ All done!${c.reset} Your Shopify + Firebase app is ready.`);
716
957
  console.log();
717
- console.log(` ${c.bold}Next steps:${c.reset}`);
958
+ console.log(` ${c.bold}Your app includes:${c.reset}`);
959
+ console.log(` ${c.green}✔${c.reset} Dashboard — store info + quick stats`);
960
+ console.log(` ${c.green}✔${c.reset} Products — search + detail view`);
961
+ console.log(` ${c.green}✔${c.reset} Settings — form with Firestore persistence`);
962
+ console.log(` ${c.green}✔${c.reset} Components — Polaris reference with copy-paste code`);
963
+ console.log(` ${c.green}✔${c.reset} App Bridge — navigation, toasts, modals, resource picker`);
964
+ console.log(` ${c.green}✔${c.reset} 4 Cloud Functions — auth, api, webhooks, proxy`);
965
+ console.log();
966
+ console.log(` ${c.bold}Deploy:${c.reset}`);
718
967
  console.log();
719
968
  console.log(` ${c.cyan}cd ${config.projectName}${c.reset}`);
720
969
  console.log(` ${c.cyan}firebase deploy${c.reset}`);
@@ -723,11 +972,12 @@ function printSuccess(config) {
723
972
  console.log();
724
973
  console.log(` ${c.cyan}${config.appUrl}/auth?shop=YOUR-STORE.myshopify.com${c.reset}`);
725
974
  console.log();
726
- console.log(` ${c.bold}Or develop locally:${c.reset}`);
975
+ console.log(` ${c.bold}Go live:${c.reset}`);
727
976
  console.log();
728
- console.log(` ${c.cyan}shopify app dev${c.reset}`);
977
+ console.log(` ${c.cyan}npx create-shopify-firebase-app --distribute${c.reset}`);
729
978
  console.log();
730
979
  console.log(` ${c.dim}─────────────────────────────────────────${c.reset}`);
980
+ console.log(` ${c.dim}Language: ${config.language}${c.reset}`);
731
981
  console.log(` ${c.dim}App URL: ${config.appUrl}${c.reset}`);
732
982
  console.log(` ${c.dim}Firebase: ${config.projectId}${c.reset}`);
733
983
  console.log(` ${c.dim}Scopes: ${config.scopes}${c.reset}`);
@@ -739,8 +989,8 @@ function printHelp() {
739
989
  console.log(`
740
990
  ${c.bold}create-shopify-firebase-app${c.reset}
741
991
 
742
- Create Shopify apps powered by Firebase.
743
- Serverless, lightweight, zero-framework.
992
+ Build Shopify apps for free. Serverless, zero-framework.
993
+ The easiest way to build Shopify apps on Firebase.
744
994
 
745
995
  ${c.bold}Usage:${c.reset}
746
996
 
@@ -748,49 +998,47 @@ function printHelp() {
748
998
 
749
999
  ${c.bold}Options:${c.reset}
750
1000
 
1001
+ --help, -h Show this help
1002
+ --version, -v Show version
1003
+ --distribute Open distribution dashboard for your app
1004
+
1005
+ ${c.bold}CI / non-interactive:${c.reset}
1006
+
751
1007
  --api-key=KEY Shopify API Key (client_id)
752
1008
  --api-secret=SECRET Shopify API Secret
753
1009
  --project-id=ID Firebase Project ID
754
1010
  --scopes=SCOPES API scopes (default: read_products)
1011
+ --language=LANG typescript or javascript (default: typescript)
755
1012
  --app-name=NAME App name shown in Shopify admin
756
- --help, -h Show this help
757
- --version, -v Show version
758
-
759
- ${c.bold}Firebase provisioning:${c.reset}
760
-
761
1013
  --skip-provision Skip Firebase service provisioning
762
- --firestore-region=LOC Firestore region (e.g. asia-south1, us-central1)
1014
+ --firestore-region=LOC Firestore region (e.g. us-central1)
763
1015
 
764
1016
  ${c.bold}Examples:${c.reset}
765
1017
 
766
- ${c.dim}# Interactive mode (prompts for config + provisions Firebase)${c.reset}
1018
+ ${c.dim}# Interactive guided wizard${c.reset}
767
1019
  npx create-shopify-firebase-app
768
1020
 
769
1021
  ${c.dim}# With project name${c.reset}
770
1022
  npx create-shopify-firebase-app my-app
771
1023
 
772
- ${c.dim}# Non-interactive (CI/CD)${c.reset}
1024
+ ${c.dim}# CI / non-interactive${c.reset}
773
1025
  npx create-shopify-firebase-app my-app \\
774
- --api-key=abc123 \\
775
- --api-secret=secret \\
1026
+ --api-key=abc123 --api-secret=secret \\
776
1027
  --project-id=my-firebase-project
777
1028
 
778
- ${c.dim}# With Firebase provisioning in CI${c.reset}
779
- npx create-shopify-firebase-app my-app \\
780
- --api-key=abc123 \\
781
- --api-secret=secret \\
782
- --project-id=my-firebase-project \\
783
- --firestore-region=asia-south1
1029
+ ${c.dim}# Go live open distribution page${c.reset}
1030
+ npx create-shopify-firebase-app --distribute
784
1031
 
785
1032
  ${c.bold}What you get:${c.reset}
786
1033
 
787
- Firebase v2 Cloud Functions (4 independent, auto-scaling functions)
788
- Shopify API 2026-01 (latest) + OAuth, webhooks, GDPR
789
- Firestore for sessions and app data
790
- App Bridge embedded admin dashboard (vanilla HTML/JS)
791
- Theme App Extension for storefront UI
792
- Firebase Hosting ($0/monthfree for up to 25K installed stores)
793
- Auto-installs Firebase CLI + Shopify CLI if missing
794
- ✔ Auto-provisioning: Firestore, Web App, Hosting (interactive)
1034
+ 4 pages Dashboard, Products, Settings, Polaris Components
1035
+ App Bridge embedded admin with navigation, toasts, modals
1036
+ Firebase v2 Cloud Functions 4 independent, auto-scaling
1037
+ Shopify API 2026-01 OAuth, webhooks, GDPR
1038
+ Firestore sessions, settings, app data
1039
+ TypeScript or JavaScriptyour choice
1040
+ ✔ Firebase Hosting $0/month for up to 25K installed stores
1041
+ ✔ Auto-installs Firebase CLI + Shopify CLI
1042
+ ✔ Distribution helper — go live in minutes
795
1043
  `);
796
1044
  }