@swell/cli 2.3.4 → 2.4.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 (40) hide show
  1. package/dist/commands/api/delete.d.ts +1 -0
  2. package/dist/commands/api/delete.js +8 -1
  3. package/dist/commands/api/get.d.ts +1 -0
  4. package/dist/commands/api/get.js +9 -2
  5. package/dist/commands/api/index.js +4 -2
  6. package/dist/commands/api/post.d.ts +1 -0
  7. package/dist/commands/api/post.js +9 -2
  8. package/dist/commands/api/put.d.ts +1 -0
  9. package/dist/commands/api/put.js +9 -2
  10. package/dist/commands/app/dev.js +6 -2
  11. package/dist/commands/app/frontend/dev.js +6 -3
  12. package/dist/commands/create/app.js +2 -2
  13. package/dist/commands/create/frontend.js +2 -2
  14. package/dist/commands/create/tests.js +5 -2
  15. package/dist/commands/inspect/models.d.ts +0 -1
  16. package/dist/commands/inspect/models.js +2 -15
  17. package/dist/commands/theme/dev.js +10 -7
  18. package/dist/create-app-command.d.ts +8 -2
  19. package/dist/create-app-command.js +137 -79
  20. package/dist/env/local.d.ts +1 -0
  21. package/dist/env/local.js +1 -0
  22. package/dist/env/production.d.ts +1 -0
  23. package/dist/env/production.js +1 -0
  24. package/dist/env/review.d.ts +1 -0
  25. package/dist/env/review.js +1 -0
  26. package/dist/env/staging.d.ts +1 -0
  27. package/dist/env/staging.js +1 -0
  28. package/dist/lib/api.d.ts +5 -2
  29. package/dist/lib/api.js +60 -9
  30. package/dist/lib/apps/index.d.ts +11 -0
  31. package/dist/lib/apps/index.js +45 -46
  32. package/dist/lib/constants.d.ts +1 -0
  33. package/dist/lib/constants.js +5 -0
  34. package/dist/lib/create/tests.js +1 -1
  35. package/dist/lib/package-manager.d.ts +86 -0
  36. package/dist/lib/package-manager.js +186 -0
  37. package/dist/push-app-command.js +13 -6
  38. package/dist/swell-api-command.js +8 -2
  39. package/oclif.manifest.json +76 -15
  40. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { pluralize, titleize } from 'inflection';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
5
  import { toAppId } from '../create/index.js';
6
+ import { detectPackageManager, transformCommand } from '../package-manager.js';
6
7
  import { AppConfig } from './app-config.js';
7
8
  export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
8
9
  export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js';
@@ -113,54 +114,42 @@ export var ConfigInputFields;
113
114
  ConfigInputFields["VALUES"] = "values";
114
115
  ConfigInputFields["VERSION"] = "version";
115
116
  })(ConfigInputFields || (ConfigInputFields = {}));
116
- // Legacy apps (pre-workspace) - Astro only (Proxima, Sunrise)
117
- // Uses direct commands without workspace prefix
118
- const LegacyFrontendProjectTypes = [
117
+ // Frontend project types - all use direct commands run from frontend/ directory
118
+ export const FrontendProjectTypes = [
119
119
  {
120
120
  buildCommand: 'npx astro build',
121
121
  devCommand: 'npx astro dev --port ${PORT}',
122
- mainPackage: 'astro',
123
- name: 'Astro',
124
- slug: 'astro',
125
- },
126
- ];
127
- // Modern apps (workspace-based) - All frameworks
128
- export const FrontendProjectTypes = [
129
- {
130
- buildCommand: 'npm exec --workspace=frontend -- astro build',
131
- devCommand: 'npm exec --workspace=frontend -- astro dev --port ${PORT}',
132
122
  installCommand: 'npm create cloudflare@latest -- frontend --framework=astro --deploy=false --git=false -- --no-git --yes --skip-houston --typescript strict',
133
123
  mainPackage: 'astro',
134
124
  name: 'Astro',
135
125
  slug: 'astro',
136
126
  },
137
127
  {
138
- buildCommand: 'npm exec --workspace=frontend -- ng build',
139
- devCommand: 'npm exec --workspace=frontend -- ng serve --port ${PORT}',
128
+ buildCommand: 'npx ng build',
129
+ devCommand: 'npx ng serve --port ${PORT}',
140
130
  installCommand: 'npm create cloudflare@latest -- frontend --framework=angular --deploy=false --git=false -- --style=sass --zoneless --ai-config=none',
141
131
  mainPackage: '@angular/core',
142
132
  name: 'Angular',
143
133
  slug: 'angular',
144
134
  },
145
135
  {
146
- devCommand: 'npm exec --workspace=frontend -- wrangler dev --port ${PORT}',
136
+ devCommand: 'npx wrangler dev --port ${PORT}',
147
137
  installCommand: 'npm create cloudflare@latest -- frontend --framework=hono --deploy=false --git=false',
148
138
  mainPackage: 'hono',
149
139
  name: 'Hono',
150
140
  slug: 'hono',
151
141
  },
152
142
  {
153
- buildCommand: 'npm exec --workspace=frontend -- nuxt build',
154
- devCommand: 'npm exec --workspace=frontend -- nuxt dev --port ${PORT}',
143
+ buildCommand: 'npx nuxt build',
144
+ devCommand: 'npx nuxt dev --port ${PORT}',
155
145
  installCommand: 'npm create cloudflare@latest -- frontend --framework=nuxt --deploy=false --git=false -- --no-modules -f',
156
146
  mainPackage: 'nuxt',
157
147
  name: 'Nuxt',
158
148
  slug: 'nuxt',
159
149
  },
160
150
  {
161
- buildCommand: 'npm exec --workspace=frontend -- opennextjs-cloudflare build',
162
- // devCommand: 'npm exec --workspace=frontend -- opennextjs-cloudflare build && npm exec --workspace=frontend -- opennextjs-cloudflare preview --port=${PORT}',
163
- devCommand: 'npm exec --workspace=frontend -- next dev --turbopack --port ${PORT}',
151
+ buildCommand: 'npx opennextjs-cloudflare build',
152
+ devCommand: 'npx next dev --turbopack --port ${PORT}',
164
153
  installCommand: 'npm create cloudflare@latest -- frontend --framework=next --deploy=false --git=false -- --typescript --use-npm --src-dir --app --eslint --import-alias "@/*" --tailwind --turbopack',
165
154
  mainPackage: 'next',
166
155
  name: 'Next.js',
@@ -183,28 +172,8 @@ export function getFrontendProjectValidValues(withNone = true, withLegacy = true
183
172
  export function getAppSlugId(app) {
184
173
  return toAppId(app.private_id) || app.public_id || app.id;
185
174
  }
186
- function hasWorkspaceStructure(appPath) {
187
- // Check if root package.json has workspaces field including 'frontend'
188
- const rootPkgPath = path.join(appPath, 'package.json');
189
- if (!filePathExists(rootPkgPath)) {
190
- return false;
191
- }
192
- try {
193
- const content = fs.readFileSync(rootPkgPath, 'utf8');
194
- const pkg = JSON.parse(content);
195
- return Array.isArray(pkg.workspaces) && pkg.workspaces.includes('frontend');
196
- }
197
- catch {
198
- return false;
199
- }
200
- }
201
175
  export function getFrontendProjectType(appPath) {
202
- // Detect workspace structure and select appropriate config
203
- const hasWorkspace = hasWorkspaceStructure(appPath);
204
- const projectTypes = hasWorkspace
205
- ? FrontendProjectTypes
206
- : LegacyFrontendProjectTypes;
207
- // Try frontend/package.json first (new workspace structure), then root package.json (old structure)
176
+ // Try frontend/package.json first (workspace structure), then root package.json (legacy)
208
177
  const pkgPaths = [
209
178
  path.join(appPath, 'frontend', 'package.json'),
210
179
  path.join(appPath, 'package.json'),
@@ -216,15 +185,16 @@ export function getFrontendProjectType(appPath) {
216
185
  try {
217
186
  const content = fs.readFileSync(pkgPath, 'utf8');
218
187
  const pkg = JSON.parse(content);
219
- for (const projectType of projectTypes) {
188
+ for (const projectType of FrontendProjectTypes) {
220
189
  if (pkg.dependencies?.[projectType.mainPackage] ||
221
190
  pkg.devDependencies?.[projectType.mainPackage]) {
222
191
  // Create a copy to avoid mutating the original
223
192
  const detectedType = { ...projectType };
224
193
  // If buildCommand not explicitly set in framework definition, detect it
225
- detectedType.buildCommand ||=
226
- // Check if package.json has a "build" script, if not set to empty string (no build needed)
227
- pkg.scripts?.build ? 'npm run build' : '';
194
+ // Check if package.json has a "build" script, if not set to empty string (no build needed)
195
+ detectedType.buildCommand ||= pkg.scripts?.build
196
+ ? 'npm run build'
197
+ : '';
228
198
  return detectedType;
229
199
  }
230
200
  }
@@ -236,6 +206,20 @@ export function getFrontendProjectType(appPath) {
236
206
  }
237
207
  return undefined;
238
208
  }
209
+ /**
210
+ * Get project commands transformed for the detected package manager.
211
+ * Detects the package manager from lock files in appPath and transforms
212
+ * npm-style commands to the equivalent for that package manager.
213
+ */
214
+ export function getProjectCommands(appPath, projectType) {
215
+ const pm = detectPackageManager(appPath);
216
+ return {
217
+ buildCommand: projectType.buildCommand
218
+ ? transformCommand(projectType.buildCommand, pm)
219
+ : undefined,
220
+ devCommand: transformCommand(projectType.devCommand, pm),
221
+ };
222
+ }
239
223
  export function getConfigTypeFromPath(path) {
240
224
  for (const type in AllConfigPaths) {
241
225
  if (AllConfigPaths[type] === path) {
@@ -377,3 +361,18 @@ export function batchAppFilesByType(configs) {
377
361
  // Add base files if config types are not specified
378
362
  return batches;
379
363
  }
364
+ export function hasAppContext() {
365
+ return fs.existsSync('swell.json');
366
+ }
367
+ export async function getCurrentAppSlugId() {
368
+ if (!hasAppContext()) {
369
+ return;
370
+ }
371
+ try {
372
+ const appConfig = JSON.parse(await fs.promises.readFile('swell.json', 'utf8'));
373
+ return appConfig.id;
374
+ }
375
+ catch {
376
+ // noop
377
+ }
378
+ }
@@ -2,6 +2,7 @@ export declare const API_BASE_URL: any;
2
2
  export declare const LOCAL_PROXY_PROVIDER: any;
3
3
  export declare const SCHEMAS_BASE_URL: any;
4
4
  export declare function getAdminApiBaseUrl(storeId: string): any;
5
+ export declare function getFrontendApiBaseUrl(storeId: string): any;
5
6
  export declare function getLoginHost(storeId?: string): any;
6
7
  export declare function getAppFrontendHost(storeId: string, installedAppId: string): any;
7
8
  export declare function getStorefrontFrontendHost(storefrontSlug: string, branchId?: string): any;
@@ -5,6 +5,8 @@ import { env } from './config.js';
5
5
  export const API_BASE_URL = env.get('API_BASE_URL') || defaultEnv.API_BASE_URL;
6
6
  // url for admin api calls
7
7
  const ADMIN_API_BASE_URL = env.get('ADMIN_API_BASE_URL') || defaultEnv.ADMIN_API_BASE_URL;
8
+ // url for store api calls
9
+ const FRONTEND_API_BASE_URL = env.get('FRONTEND_API_BASE_URL') || defaultEnv.FRONTEND_API_BASE_URL;
8
10
  // url host for admin login
9
11
  const LOGIN_HOST = env.get('LOGIN_HOST') || defaultEnv.LOGIN_HOST;
10
12
  // url host for app frontend
@@ -19,6 +21,9 @@ export const SCHEMAS_BASE_URL = env.get('SCHEMAS_BASE_URL') || defaultEnv.SCHEMA
19
21
  export function getAdminApiBaseUrl(storeId) {
20
22
  return ADMIN_API_BASE_URL.replace('${STORE_ID}', storeId);
21
23
  }
24
+ export function getFrontendApiBaseUrl(storeId) {
25
+ return FRONTEND_API_BASE_URL.replace('${STORE_ID}', storeId);
26
+ }
22
27
  export function getLoginHost(storeId) {
23
28
  return LOGIN_HOST.replace('${STORE_ID}', storeId || 'login');
24
29
  }
@@ -4,7 +4,7 @@ import { filePathExists, writeFile } from '../apps/index.js';
4
4
  import { envDtsTemplate, integrationTestTemplate, mockRequestTemplate, setupGlobalsTemplate, swellClientTemplate, tsconfigTemplate, unitTestTemplate, vitestConfigTemplate, } from './tests/templates/index.js';
5
5
  const TEST_DEV_DEPENDENCIES = {
6
6
  '@cloudflare/vitest-pool-workers': 'latest',
7
- vitest: 'latest',
7
+ vitest: '3.2.x',
8
8
  '@swell/app-types': 'latest',
9
9
  };
10
10
  async function writeTextFile(filePath, contents, overwrite) {
@@ -0,0 +1,86 @@
1
+ export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
2
+ export interface PackageManagerCommands {
3
+ /** Install command (e.g., 'npm install') */
4
+ install: string;
5
+ /** Run a script (e.g., 'npm run build' for script 'build') */
6
+ run: (script: string) => string;
7
+ /** Execute command in workspace (e.g., 'npm exec --workspace=frontend -- astro build') */
8
+ workspaceExec: (workspace: string, command: string) => string;
9
+ /** Run a script in a workspace (e.g., 'npm run build --workspace=frontend') */
10
+ workspaceRun: (workspace: string, script: string) => string;
11
+ /** Execute a binary (e.g., 'npx wrangler') */
12
+ exec: (command: string) => string;
13
+ }
14
+ /**
15
+ * Find lock file in directory or parent directories
16
+ * @returns Lock file name and path, or null if not found
17
+ */
18
+ export declare function findLockFile(directory: string): {
19
+ name: string;
20
+ packageManager: PackageManager;
21
+ path: string;
22
+ } | null;
23
+ /**
24
+ * Detect package manager from lock file in directory tree
25
+ * @returns Detected package manager, defaults to 'npm' if no lock file found
26
+ */
27
+ export declare function detectPackageManager(directory: string): PackageManager;
28
+ /**
29
+ * Get command templates for a package manager
30
+ */
31
+ export declare function getPackageManagerCommands(pm: PackageManager): PackageManagerCommands;
32
+ /**
33
+ * Detect package manager and get its commands
34
+ */
35
+ export declare function getPackageManager(directory: string): {
36
+ commands: PackageManagerCommands;
37
+ lockFile: {
38
+ name: string;
39
+ path: string;
40
+ } | null;
41
+ name: PackageManager;
42
+ };
43
+ /**
44
+ * Transform an npm command to the equivalent for the detected package manager
45
+ * Handles common patterns:
46
+ * - 'npm run X' -> 'yarn run X' / 'pnpm run X' / 'bun run X'
47
+ * - 'npm run X --workspace=Y' -> 'yarn workspace Y run X' / 'pnpm --filter Y run X' / 'bun run --filter Y X'
48
+ * - 'npm exec --workspace=X -- Y' -> equivalent workspace command
49
+ * - 'npx X' -> 'yarn dlx X' / 'pnpm dlx X' / 'bunx X'
50
+ */
51
+ export declare function transformCommand(command: string, pm: PackageManager): string;
52
+ /**
53
+ * Transform npm create commands to the equivalent for another package manager.
54
+ * Handles 'npm create X@version -- args' -> 'pm create X@version args'
55
+ * Also transforms --use-npm to --use-{pm} for CLIs that support it (e.g., Next.js)
56
+ * For yarn, removes @latest suffix since yarn classic (v1) may not handle it properly.
57
+ *
58
+ * @example
59
+ * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=hono', 'bun')
60
+ * // Returns: 'bun create cloudflare@latest frontend --framework=hono'
61
+ *
62
+ * @example
63
+ * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=next --use-npm', 'pnpm')
64
+ * // Returns: 'pnpm create cloudflare@latest frontend --framework=next --use-pnpm'
65
+ *
66
+ * @example
67
+ * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=astro', 'yarn')
68
+ * // Returns: 'yarn create cloudflare -- frontend --framework=astro'
69
+ */
70
+ export declare function transformCreateCommand(command: string, pm: PackageManager): string;
71
+ /**
72
+ * Get spawn arguments for executing a package binary.
73
+ * Returns the program and any prefix args needed before the command args.
74
+ * Useful for node's spawn() which needs program and args separately.
75
+ *
76
+ * Uses npx for most package managers (universally available via Node.js).
77
+ * Uses bunx for bun projects (performance benefit from Bun runtime).
78
+ *
79
+ * @example
80
+ * const { program, prefixArgs } = getExecSpawnArgs('npm');
81
+ * spawn(program, [...prefixArgs, 'wrangler', 'dev', '--port=3000']);
82
+ */
83
+ export declare function getExecSpawnArgs(pm: PackageManager): {
84
+ prefixArgs: string[];
85
+ program: string;
86
+ };
@@ -0,0 +1,186 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ /** Lock file to package manager mapping (priority order: bun > pnpm > yarn > npm) */
4
+ const LOCK_FILES = [
5
+ ['bun.lockb', 'bun'],
6
+ ['bun.lock', 'bun'],
7
+ ['pnpm-lock.yaml', 'pnpm'],
8
+ ['yarn.lock', 'yarn'],
9
+ ['package-lock.json', 'npm'],
10
+ ];
11
+ /** Command templates for each package manager */
12
+ const COMMANDS = {
13
+ npm: {
14
+ install: 'npm install',
15
+ run: (script) => `npm run ${script}`,
16
+ workspaceExec: (workspace, command) => `npm exec --workspace=${workspace} -- ${command}`,
17
+ workspaceRun: (workspace, script) => `npm run ${script} --workspace=${workspace}`,
18
+ exec: (command) => `npx ${command}`,
19
+ },
20
+ yarn: {
21
+ install: 'yarn install',
22
+ run: (script) => `yarn run ${script}`,
23
+ workspaceExec: (workspace, command) => `yarn workspace ${workspace} ${command}`,
24
+ workspaceRun: (workspace, script) => `yarn workspace ${workspace} run ${script}`,
25
+ exec: (command) => `yarn dlx ${command}`,
26
+ },
27
+ pnpm: {
28
+ install: 'pnpm install',
29
+ run: (script) => `pnpm run ${script}`,
30
+ workspaceExec: (workspace, command) => `pnpm --filter ${workspace} exec ${command}`,
31
+ workspaceRun: (workspace, script) => `pnpm --filter ${workspace} run ${script}`,
32
+ exec: (command) => `pnpm dlx ${command}`,
33
+ },
34
+ bun: {
35
+ install: 'bun install',
36
+ run: (script) => `bun run ${script}`,
37
+ // Use --cwd for executing binaries in a workspace (--filter is for scripts only)
38
+ workspaceExec: (workspace, command) => `bun --cwd ${workspace} x ${command}`,
39
+ workspaceRun: (workspace, script) => `bun run --filter ${workspace} ${script}`,
40
+ exec: (command) => `bunx ${command}`,
41
+ },
42
+ };
43
+ /**
44
+ * Find lock file in directory or parent directories
45
+ * @returns Lock file name and path, or null if not found
46
+ */
47
+ export function findLockFile(directory) {
48
+ let currentDir = path.resolve(directory);
49
+ const { root } = path.parse(currentDir);
50
+ while (currentDir !== root) {
51
+ for (const [lockFile, pm] of LOCK_FILES) {
52
+ const lockPath = path.join(currentDir, lockFile);
53
+ if (fs.existsSync(lockPath)) {
54
+ return { name: lockFile, packageManager: pm, path: lockPath };
55
+ }
56
+ }
57
+ currentDir = path.dirname(currentDir);
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Detect package manager from lock file in directory tree
63
+ * @returns Detected package manager, defaults to 'npm' if no lock file found
64
+ */
65
+ export function detectPackageManager(directory) {
66
+ const lockFile = findLockFile(directory);
67
+ return lockFile?.packageManager ?? 'npm';
68
+ }
69
+ /**
70
+ * Get command templates for a package manager
71
+ */
72
+ export function getPackageManagerCommands(pm) {
73
+ return COMMANDS[pm];
74
+ }
75
+ /**
76
+ * Detect package manager and get its commands
77
+ */
78
+ export function getPackageManager(directory) {
79
+ const lockFile = findLockFile(directory);
80
+ const name = lockFile?.packageManager ?? 'npm';
81
+ return {
82
+ commands: COMMANDS[name],
83
+ lockFile: lockFile ? { name: lockFile.name, path: lockFile.path } : null,
84
+ name,
85
+ };
86
+ }
87
+ /**
88
+ * Transform an npm command to the equivalent for the detected package manager
89
+ * Handles common patterns:
90
+ * - 'npm run X' -> 'yarn run X' / 'pnpm run X' / 'bun run X'
91
+ * - 'npm run X --workspace=Y' -> 'yarn workspace Y run X' / 'pnpm --filter Y run X' / 'bun run --filter Y X'
92
+ * - 'npm exec --workspace=X -- Y' -> equivalent workspace command
93
+ * - 'npx X' -> 'yarn dlx X' / 'pnpm dlx X' / 'bunx X'
94
+ */
95
+ export function transformCommand(command, pm) {
96
+ if (pm === 'npm') {
97
+ return command;
98
+ }
99
+ const commands = COMMANDS[pm];
100
+ // Transform 'npm exec --workspace=X -- Y'
101
+ const workspaceMatch = command.match(/^npm exec --workspace=(\S+) -- (.+)$/);
102
+ if (workspaceMatch) {
103
+ return commands.workspaceExec(workspaceMatch[1], workspaceMatch[2]);
104
+ }
105
+ // Transform 'npm run X --workspace=Y'
106
+ const workspaceRunMatch = command.match(/^npm run (\S+) --workspace=(\S+)$/);
107
+ if (workspaceRunMatch) {
108
+ return commands.workspaceRun(workspaceRunMatch[2], workspaceRunMatch[1]);
109
+ }
110
+ // Transform 'npm run X' (with optional flags)
111
+ const runMatch = command.match(/^npm run (\S.*)$/);
112
+ if (runMatch) {
113
+ return commands.run(runMatch[1]);
114
+ }
115
+ // Transform 'npx X' to 'bunx X' for bun (performance benefit from Bun runtime)
116
+ // For other package managers, keep npx as-is (universally available via Node.js)
117
+ if (pm === 'bun') {
118
+ const npxMatch = command.match(/^npx (.+)$/);
119
+ if (npxMatch) {
120
+ return `bunx ${npxMatch[1]}`;
121
+ }
122
+ }
123
+ // Warn if command looks like npm but wasn't transformed
124
+ if (command.startsWith('npm ')) {
125
+ console.warn(`Warning: Command "${command}" was not transformed for ${pm}.`);
126
+ }
127
+ return command;
128
+ }
129
+ /**
130
+ * Transform npm create commands to the equivalent for another package manager.
131
+ * Handles 'npm create X@version -- args' -> 'pm create X@version args'
132
+ * Also transforms --use-npm to --use-{pm} for CLIs that support it (e.g., Next.js)
133
+ * For yarn, removes @latest suffix since yarn classic (v1) may not handle it properly.
134
+ *
135
+ * @example
136
+ * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=hono', 'bun')
137
+ * // Returns: 'bun create cloudflare@latest frontend --framework=hono'
138
+ *
139
+ * @example
140
+ * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=next --use-npm', 'pnpm')
141
+ * // Returns: 'pnpm create cloudflare@latest frontend --framework=next --use-pnpm'
142
+ *
143
+ * @example
144
+ * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=astro', 'yarn')
145
+ * // Returns: 'yarn create cloudflare -- frontend --framework=astro'
146
+ */
147
+ export function transformCreateCommand(command, pm) {
148
+ if (pm === 'npm') {
149
+ return command;
150
+ }
151
+ // Match 'npm create X@version -- args' or 'npm create X -- args'
152
+ const createMatch = command.match(/^npm create (\S+) -- (.+)$/);
153
+ if (createMatch) {
154
+ let [, packageWithVersion, args] = createMatch;
155
+ // Replace --use-npm with --use-{pm} for Next.js and similar CLIs
156
+ const transformedArgs = args.replace(/--use-npm\b/, `--use-${pm}`);
157
+ // For yarn classic (v1), remove @latest since yarn create fetches latest by default
158
+ // and may not properly handle version specifiers. Also add -- separator for args.
159
+ if (pm === 'yarn') {
160
+ packageWithVersion = packageWithVersion.replace(/@latest$/, '');
161
+ return `yarn create ${packageWithVersion} -- ${transformedArgs}`;
162
+ }
163
+ return `${pm} create ${packageWithVersion} ${transformedArgs}`;
164
+ }
165
+ return command;
166
+ }
167
+ /**
168
+ * Get spawn arguments for executing a package binary.
169
+ * Returns the program and any prefix args needed before the command args.
170
+ * Useful for node's spawn() which needs program and args separately.
171
+ *
172
+ * Uses npx for most package managers (universally available via Node.js).
173
+ * Uses bunx for bun projects (performance benefit from Bun runtime).
174
+ *
175
+ * @example
176
+ * const { program, prefixArgs } = getExecSpawnArgs('npm');
177
+ * spawn(program, [...prefixArgs, 'wrangler', 'dev', '--port=3000']);
178
+ */
179
+ export function getExecSpawnArgs(pm) {
180
+ // Use bunx for bun (performance benefit), npx for everything else
181
+ // npx is universally available via Node.js and avoids yarn dlx v1/v2 issues
182
+ if (pm === 'bun') {
183
+ return { prefixArgs: [], program: 'bunx' };
184
+ }
185
+ return { prefixArgs: [], program: 'npx' };
186
+ }
@@ -6,10 +6,11 @@ import * as path from 'node:path';
6
6
  import Stream from 'node:stream';
7
7
  import ora from 'ora';
8
8
  import { default as swellConfig } from './lib/app-config.js';
9
- import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
9
+ import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
10
10
  import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
11
11
  import { default as localConfig } from './lib/config.js';
12
12
  import { toAppId } from './lib/create/index.js';
13
+ import { detectPackageManager, transformCommand, } from './lib/package-manager.js';
13
14
  import { getProxyUrl } from './lib/proxy.js';
14
15
  import { selectEnvironmentId } from './lib/stores.js';
15
16
  import style from './lib/style.js';
@@ -109,12 +110,14 @@ export class PushAppCommand extends RemoteAppCommand {
109
110
  this.showFrontendMigrationError();
110
111
  return; // unreachable but helps TypeScript narrow the type
111
112
  }
112
- if (!projectType.buildCommand) {
113
+ // Get commands transformed for the detected package manager
114
+ const { buildCommand } = getProjectCommands(this.appPath, projectType);
115
+ if (!buildCommand) {
113
116
  // No build command needed for this framework
114
117
  return;
115
118
  }
116
119
  this.log(`Building ${projectType.name} frontend...\n`);
117
- await this.execFrontend(projectType.buildCommand);
120
+ await this.execFrontend(buildCommand);
118
121
  }
119
122
  async chooseAppToPull(query) {
120
123
  const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
@@ -332,7 +335,8 @@ export class PushAppCommand extends RemoteAppCommand {
332
335
  }
333
336
  }
334
337
  async execFrontend(command, onOutput) {
335
- return this.exec(command, this.frontendPath, onOutput);
338
+ // Run from frontend directory where wrangler.toml and framework configs live
339
+ return this.exec(command, this.frontendPath || path.join(this.appPath, 'frontend'), onOutput);
336
340
  }
337
341
  async getAllAppStorefronts(params) {
338
342
  const query = Object.entries(params || {})
@@ -794,12 +798,15 @@ export class PushAppCommand extends RemoteAppCommand {
794
798
  let pagesProjectError = false;
795
799
  let outputBuffer = '';
796
800
  this.log(`\nDeploying to Cloudflare...\n`);
801
+ // Get the wrangler deploy command for the detected package manager
802
+ const pm = detectPackageManager(this.appPath);
803
+ const deployCommand = transformCommand('npx wrangler deploy', pm);
797
804
  try {
798
- await this.execFrontend(`npx wrangler deploy`, (string) => {
805
+ await this.execFrontend(deployCommand, (string) => {
799
806
  // Accumulate output for URL parsing after command completes
800
807
  outputBuffer += string;
801
808
  // Suppress workers.dev URL from output (users should use swell domain)
802
- if (string.match(/^\s*https:\/\/\S+\.workers\.dev\s*$/m)) {
809
+ if (/^\s*https:\/\/\S+\.workers\.dev\s*$/m.test(string)) {
803
810
  return false;
804
811
  }
805
812
  // Check for Pages project error
@@ -29,10 +29,14 @@ export class SwellApiCommand extends SwellCommand {
29
29
  const parsedInput = await super.parse(options, argv);
30
30
  const { args, flags } = parsedInput;
31
31
  const { path: requestPath } = args;
32
- const { live, body } = flags;
32
+ const { live, api, body } = flags;
33
+ const isFrontendAPI = api === 'frontend';
33
34
  if (!live) {
34
35
  await this.api.setEnv('test');
35
36
  }
37
+ if (isFrontendAPI) {
38
+ await this.api.setPublicKey();
39
+ }
36
40
  if (!requestPath.startsWith('/')) {
37
41
  throw new Error('Path must start with a forward slash (/)');
38
42
  }
@@ -53,7 +57,9 @@ export class SwellApiCommand extends SwellCommand {
53
57
  const queryParams = this.parseQueryString(queryString);
54
58
  return this.buildFunctionCallRequest(parsedInput, functionId, processedBody, queryParams);
55
59
  }
56
- const paths = { adminPath: `/data${requestPath}` };
60
+ const paths = isFrontendAPI
61
+ ? { frontendPath: requestPath }
62
+ : { adminPath: `/data${requestPath}` };
57
63
  return {
58
64
  ...parsedInput,
59
65
  paths,