@swell/cli 2.3.4 → 2.4.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.
@@ -8,6 +8,7 @@ import * as path from 'node:path';
8
8
  import ora from 'ora';
9
9
  import { ConfigType, allConfigFilesInDir, appConfigFromFile, } from '../../lib/apps/index.js';
10
10
  import { bundleFunction } from '../../lib/bundle.js';
11
+ import { detectPackageManager, getExecSpawnArgs, } from '../../lib/package-manager.js';
11
12
  import style from '../../lib/style.js';
12
13
  import { PushAppCommand } from '../../push-app-command.js';
13
14
  export default class AppDev extends PushAppCommand {
@@ -345,8 +346,11 @@ ENVIRONMENT = "development"
345
346
  const configPath = path.join(this.tmpDir, `${functionName}.toml`);
346
347
  await fs.promises.writeFile(configPath, wranglerConfig);
347
348
  functionStatus.set(functionName, 'starting');
348
- // Start wrangler process in background
349
- const wranglerProcess = spawn('npx', [
349
+ // Start wrangler process in background using detected package manager
350
+ const pm = detectPackageManager(this.appPath);
351
+ const { program, prefixArgs } = getExecSpawnArgs(pm);
352
+ const wranglerProcess = spawn(program, [
353
+ ...prefixArgs,
350
354
  'wrangler',
351
355
  'dev',
352
356
  `--config=${configPath}`,
@@ -1,4 +1,5 @@
1
1
  import { Flags } from '@oclif/core';
2
+ import { getProjectCommands, } from '../../../lib/apps/index.js';
2
3
  import { default as localConfig } from '../../../lib/config.js';
3
4
  import style from '../../../lib/style.js';
4
5
  import { PushAppCommand } from '../../../push-app-command.js';
@@ -69,9 +70,11 @@ export default class AppFrontendDev extends PushAppCommand {
69
70
  if (!projectType) {
70
71
  return;
71
72
  }
73
+ // Get commands transformed for the detected package manager
74
+ const { devCommand } = getProjectCommands(this.appPath, projectType);
72
75
  const currentStore = localConfig.getDefaultStore();
73
76
  let serverReadyDetected = false;
74
- await this.execFrontend(projectType.devCommand.replace('${PORT}', String(serverPort)), (string) => {
77
+ await this.execFrontend(devCommand.replace('${PORT}', String(serverPort)), (string) => {
75
78
  // Framework-specific output handling and ready detection
76
79
  switch (projectType.slug) {
77
80
  case 'nextjs': {
@@ -127,8 +130,8 @@ export default class AppFrontendDev extends PushAppCommand {
127
130
  }
128
131
  // 3. READY DETECTION: Detect server ready, show our message
129
132
  if (!serverReadyDetected &&
130
- (/application bundle generation complete/.test(string) ||
131
- /watch mode enabled/.test(string))) {
133
+ (/application bundle generation complete/i.test(string) ||
134
+ /watch mode enabled/i.test(string))) {
132
135
  serverReadyDetected = true;
133
136
  this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
134
137
  const sessionId = localConfig.getSessionId(currentStore);
@@ -70,8 +70,8 @@ export default class CreateApp extends CreateAppCommand {
70
70
  pkg: Flags.string({
71
71
  char: 'p',
72
72
  default: 'npm',
73
- description: 'Package manager: npm | yarn | none',
74
- options: ['npm', 'yarn', 'none'],
73
+ description: 'Package manager: npm | yarn | pnpm | bun | none',
74
+ options: ['npm', 'yarn', 'pnpm', 'bun', 'none'],
75
75
  }),
76
76
  type: Flags.string({
77
77
  char: 't',
@@ -18,8 +18,8 @@ export default class CreateFrontend extends CreateAppCommand {
18
18
  pkg: Flags.string({
19
19
  char: 'p',
20
20
  default: 'npm',
21
- description: `use npm or yarn to install default dependencies, or none to disable`,
22
- options: ['npm', 'yarn', 'none'],
21
+ description: `Package manager: npm | yarn | pnpm | bun | none`,
22
+ options: ['npm', 'yarn', 'pnpm', 'bun', 'none'],
23
23
  }),
24
24
  };
25
25
  async run() {
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import { AppCommand } from '../../app-command.js';
3
3
  import { toAppId } from '../../lib/create/index.js';
4
4
  import { createTestsScaffold, } from '../../lib/create/tests.js';
5
+ import { detectPackageManager, getPackageManagerCommands, } from '../../lib/package-manager.js';
5
6
  export default class CreateTests extends AppCommand {
6
7
  static description = 'Initialize a vitest + Cloudflare Workers test setup that reuses swell-cli authentication.';
7
8
  static examples = [
@@ -66,10 +67,12 @@ export default class CreateTests extends AppCommand {
66
67
  this.warn(warning);
67
68
  }
68
69
  if (hasChanges) {
70
+ const pm = detectPackageManager(this.appPath);
71
+ const commands = getPackageManagerCommands(pm);
69
72
  this.log('');
70
73
  this.log('Next steps:');
71
- this.log(' 1. npm install');
72
- this.log(' 2. npm test');
74
+ this.log(` 1. ${commands.install}`);
75
+ this.log(` 2. ${commands.run('test')}`);
73
76
  }
74
77
  this.log('');
75
78
  }
@@ -4,6 +4,7 @@ import { execSync, spawn } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import ora from 'ora';
6
6
  import { default as localConfig } from '../../lib/config.js';
7
+ import { findLockFile, getPackageManager } from '../../lib/package-manager.js';
7
8
  import style from '../../lib/style.js';
8
9
  import { ThemeSync } from '../../lib/theme-sync.js';
9
10
  import { PushAppCommand } from '../../push-app-command.js';
@@ -74,11 +75,10 @@ export default class AppThemeDev extends PushAppCommand {
74
75
  async findAndInstallDependencies() {
75
76
  const spinner = ora('Locating package.json...').start();
76
77
  try {
77
- const packageJsonLockPath = await findUp('package-lock.json', {
78
- type: 'file',
79
- });
80
- if (packageJsonLockPath) {
81
- spinner.succeed('package-lock.json found. Skipping npm install.');
78
+ // Check for any package manager lock file
79
+ const lockFile = findLockFile(process.cwd());
80
+ if (lockFile) {
81
+ spinner.succeed(`${lockFile.name} found. Skipping dependency install.`);
82
82
  return null;
83
83
  }
84
84
  const packageJsonPath = await findUp('package.json', { type: 'file' });
@@ -88,7 +88,8 @@ export default class AppThemeDev extends PushAppCommand {
88
88
  }
89
89
  spinner.succeed(`Found package.json at ${packageJsonPath}`);
90
90
  spinner.start('Installing dependencies...');
91
- execSync('npm install', { stdio: 'inherit' });
91
+ const { commands } = getPackageManager(process.cwd());
92
+ execSync(commands.install, { stdio: 'inherit' });
92
93
  spinner.succeed('Dependencies installed');
93
94
  return packageJsonPath;
94
95
  }
@@ -109,7 +110,9 @@ export default class AppThemeDev extends PushAppCommand {
109
110
  console.warn('"bundle" script not found in package.json');
110
111
  return;
111
112
  }
112
- const childProcess = spawn('npm', ['run', 'bundle'], {
113
+ const { commands } = getPackageManager(process.cwd());
114
+ const runCommand = commands.run('bundle').split(' ');
115
+ const childProcess = spawn(runCommand[0], runCommand.slice(1), {
113
116
  env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: 'true' },
114
117
  shell: true,
115
118
  stdio: 'pipe',
@@ -1,3 +1,4 @@
1
+ import { PackageManager } from './lib/package-manager.js';
1
2
  import { SwellCommand } from './swell-command.js';
2
3
  export declare abstract class CreateAppCommand extends SwellCommand {
3
4
  protected commandExample: string;
@@ -13,6 +14,11 @@ export declare abstract class CreateAppCommand extends SwellCommand {
13
14
  addAllowedHostsToAngular(configPath: string): Promise<void>;
14
15
  addAllowedHostsToAstro(configPath: string): Promise<void>;
15
16
  addAllowedHostsToNuxt(configPath: string): Promise<void>;
17
+ /**
18
+ * Add vite allowedHosts configuration to a framework config file.
19
+ * Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
20
+ */
21
+ private addViteAllowedHosts;
16
22
  createAppConfigFolders(swellConfig: any): Promise<void>;
17
23
  createFrontendApp(swellConfig: any, flags: any, directCreate?: boolean, kind?: string): Promise<boolean>;
18
24
  createStorefrontApp(swellConfig: any, flags: any, directCreate?: boolean): Promise<boolean>;
@@ -29,7 +35,7 @@ export declare abstract class CreateAppCommand extends SwellCommand {
29
35
  name: string;
30
36
  slug: string;
31
37
  };
32
- setupPackage(name: string, config: any, pkg: string): Promise<void>;
33
- tryPackageSetup(name: string, config: any, pkg: string): Promise<void>;
38
+ setupPackage(name: string, config: any, pkg: PackageManager): Promise<void>;
39
+ tryPackageSetup(name: string, config: any, pkg: PackageManager): Promise<void>;
34
40
  private addFrontendAllowedHosts;
35
41
  }
@@ -9,9 +9,39 @@ import { promisify } from 'node:util';
9
9
  import ora from 'ora';
10
10
  import Api from './lib/api.js';
11
11
  import { FrontendProjectTypes, getFrontendProjectValidValues, getAllConfigPaths, getFrontendProjectSlugs, writeFile, writeJsonFile, } from './lib/apps/index.js';
12
+ import { getPackageManagerCommands, transformCreateCommand, } from './lib/package-manager.js';
12
13
  import style from './lib/style.js';
13
14
  import { SwellCommand } from './swell-command.js';
14
15
  const execAsync = promisify(exec);
16
+ /** Allowed hosts for tunnel providers used in local development */
17
+ const TUNNEL_ALLOWED_HOSTS = ['.ngrok.app', '.loca.lt', '.trycloudflare.com'];
18
+ /**
19
+ * Find the closing brace position for a config function call (e.g., defineConfig, defineNuxtConfig).
20
+ * Uses brace counting to find the matching `}` for the opening `{`.
21
+ * @returns Position of the closing `}`, or -1 if not found
22
+ */
23
+ function findConfigClosingBrace(content, functionName) {
24
+ const pattern = new RegExp(`${functionName}\\s*\\(\\s*\\{`);
25
+ const match = content.match(pattern);
26
+ if (!match || match.index === undefined) {
27
+ return -1;
28
+ }
29
+ const startPos = match.index + match[0].length;
30
+ let depth = 1;
31
+ for (let i = startPos; i < content.length && depth > 0; i++) {
32
+ const char = content[i];
33
+ if (char === '{') {
34
+ depth++;
35
+ }
36
+ else if (char === '}') {
37
+ depth--;
38
+ if (depth === 0) {
39
+ return i;
40
+ }
41
+ }
42
+ }
43
+ return -1;
44
+ }
15
45
  export class CreateAppCommand extends SwellCommand {
16
46
  // Command name used in error message examples; override in subclasses
17
47
  commandExample = 'swell create app';
@@ -76,51 +106,24 @@ export class CreateAppCommand extends SwellCommand {
76
106
  if (project.architect.serve.options.allowedHosts) {
77
107
  return;
78
108
  }
79
- project.architect.serve.options.allowedHosts = ['.ngrok.app', '.loca.lt'];
109
+ project.architect.serve.options.allowedHosts = TUNNEL_ALLOWED_HOSTS;
80
110
  await fs.writeFile(angularJsonPath, JSON.stringify(config, null, 2), 'utf8');
81
111
  }
82
112
  async addAllowedHostsToAstro(configPath) {
83
- const astroConfigPath = path.join(configPath, 'frontend', 'astro.config.mjs');
84
- let content;
85
- try {
86
- content = await fs.readFile(astroConfigPath, 'utf8');
87
- }
88
- catch {
89
- return;
90
- }
91
- if (content.includes('allowedHosts')) {
92
- return;
93
- }
94
- const lines = content.split('\n');
95
- let insertIndex = -1;
96
- for (const [i, line] of lines.entries()) {
97
- if (line.includes('defineConfig({')) {
98
- insertIndex = i + 1;
99
- break;
100
- }
101
- }
102
- if (insertIndex === -1) {
103
- return;
104
- }
105
- // Detect indentation from next line or use default
106
- const nextLine = lines[insertIndex];
107
- const indentMatch = nextLine?.match(/^(\s+)/);
108
- const baseIndent = indentMatch ? indentMatch[1] : ' ';
109
- const viteConfig = [
110
- `${baseIndent}vite: {`,
111
- `${baseIndent} server: {`,
112
- `${baseIndent} allowedHosts: ['.ngrok.app', '.loca.lt'],`,
113
- `${baseIndent} },`,
114
- `${baseIndent}},`,
115
- ];
116
- lines.splice(insertIndex, 0, ...viteConfig);
117
- await fs.writeFile(astroConfigPath, lines.join('\n'), 'utf8');
113
+ return this.addViteAllowedHosts(configPath, 'astro.config.mjs', 'defineConfig');
118
114
  }
119
115
  async addAllowedHostsToNuxt(configPath) {
120
- const nuxtConfigPath = path.join(configPath, 'frontend', 'nuxt.config.ts');
116
+ return this.addViteAllowedHosts(configPath, 'nuxt.config.ts', 'defineNuxtConfig');
117
+ }
118
+ /**
119
+ * Add vite allowedHosts configuration to a framework config file.
120
+ * Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
121
+ */
122
+ async addViteAllowedHosts(configPath, configFile, functionName) {
123
+ const configFilePath = path.join(configPath, 'frontend', configFile);
121
124
  let content;
122
125
  try {
123
- content = await fs.readFile(nuxtConfigPath, 'utf8');
126
+ content = await fs.readFile(configFilePath, 'utf8');
124
127
  }
125
128
  catch {
126
129
  return;
@@ -128,29 +131,30 @@ export class CreateAppCommand extends SwellCommand {
128
131
  if (content.includes('allowedHosts')) {
129
132
  return;
130
133
  }
131
- const lines = content.split('\n');
132
- let insertIndex = -1;
133
- for (const [i, line] of lines.entries()) {
134
- if (line.includes('defineNuxtConfig({')) {
135
- insertIndex = i + 1;
136
- break;
137
- }
134
+ const viteConfig = ` vite: {
135
+ server: {
136
+ allowedHosts: ${JSON.stringify(TUNNEL_ALLOWED_HOSTS)},
137
+ },
138
+ },`;
139
+ // Handle empty config: functionName({})
140
+ const emptyConfig = `${functionName}({})`;
141
+ if (content.includes(emptyConfig)) {
142
+ content = content.replace(emptyConfig, `${functionName}({\n${viteConfig}\n})`);
143
+ await fs.writeFile(configFilePath, content, 'utf8');
144
+ return;
138
145
  }
139
- if (insertIndex === -1) {
146
+ // Find the closing brace of the config function using brace counting
147
+ const closingPos = findConfigClosingBrace(content, functionName);
148
+ if (closingPos === -1) {
140
149
  return;
141
150
  }
142
- const nextLine = lines[insertIndex];
143
- const indentMatch = nextLine?.match(/^(\s+)/);
144
- const baseIndent = indentMatch ? indentMatch[1] : ' ';
145
- const viteConfig = [
146
- `${baseIndent}vite: {`,
147
- `${baseIndent} server: {`,
148
- `${baseIndent} allowedHosts: ['.ngrok.app', '.loca.lt'],`,
149
- `${baseIndent} },`,
150
- `${baseIndent}},`,
151
- ];
152
- lines.splice(insertIndex, 0, ...viteConfig);
153
- await fs.writeFile(nuxtConfigPath, lines.join('\n'), 'utf8');
151
+ const before = content.slice(0, closingPos);
152
+ const after = content.slice(closingPos);
153
+ // Ensure proper comma before vite config
154
+ const needsComma = before.trimEnd().slice(-1) !== ',';
155
+ const separator = needsComma ? ',\n' : '\n';
156
+ content = before.trimEnd() + separator + viteConfig + '\n' + after;
157
+ await fs.writeFile(configFilePath, content, 'utf8');
154
158
  }
155
159
  async createAppConfigFolders(swellConfig) {
156
160
  for (const type of getAllConfigPaths(swellConfig.get('type'))) {
@@ -188,19 +192,63 @@ export class CreateAppCommand extends SwellCommand {
188
192
  }));
189
193
  if (frameworkType && frameworkType !== 'none') {
190
194
  const projectType = this.getProjectType(frameworkType);
195
+ // Determine package manager - 'none' is not valid for frontend scaffolding
196
+ let pkg = (flags.pkg || 'npm');
197
+ if (flags.pkg === 'none') {
198
+ this.log(`\n${style.dim('Note: --pkg none is not available for frontend scaffolding, using npm.')}`);
199
+ pkg = 'npm';
200
+ // Ensure root package.json exists (skipped when --pkg none)
201
+ const rootPkgPath = path.join(configPath, 'package.json');
202
+ try {
203
+ await fs.access(rootPkgPath);
204
+ }
205
+ catch {
206
+ // Root package.json doesn't exist, create it
207
+ await this.setupPackage(swellConfig.get('id'), swellConfig, pkg);
208
+ }
209
+ }
191
210
  this.log();
211
+ // Check for placeholder frontend/package.json and remove it before C3 scaffolding
212
+ const frontendPath = path.join(configPath, 'frontend');
213
+ const frontendPkgPath = path.join(frontendPath, 'package.json');
214
+ try {
215
+ const content = await fs.readFile(frontendPkgPath, 'utf8');
216
+ const frontendPkg = JSON.parse(content);
217
+ // Only remove if it matches our placeholder signature
218
+ if (frontendPkg.version === '0.0.0' &&
219
+ frontendPkg.name === 'frontend' &&
220
+ frontendPkg.private === true) {
221
+ await fs.unlink(frontendPkgPath);
222
+ }
223
+ }
224
+ catch {
225
+ // No package.json or can't read, that's fine
226
+ }
227
+ // Check if frontend folder exists and has files (user-created content)
228
+ try {
229
+ const files = await fs.readdir(frontendPath);
230
+ if (files.length > 0) {
231
+ spinner.fail('frontend/ folder is not empty. Please remove existing files before scaffolding.');
232
+ return false;
233
+ }
234
+ }
235
+ catch {
236
+ // Folder doesn't exist, that's fine - C3 will create it
237
+ }
192
238
  spinner.start(`Creating ${projectType?.name} frontend app (this may take a while)...`);
193
239
  try {
194
240
  await execAsync(`mkdir -p frontend`, {
195
241
  cwd: configPath,
196
242
  });
197
- await execAsync(projectType.installCommand, {
243
+ // Transform install command for the selected package manager
244
+ const installCommand = transformCreateCommand(projectType.installCommand, pkg);
245
+ await execAsync(installCommand, {
198
246
  cwd: configPath,
199
247
  });
200
248
  // Use this command to debug output, i.e.e when command becomes non-responsive
201
249
  /* await this.execWithStdio(
202
250
  configPath,
203
- projectType.installCommand,
251
+ installCommand,
204
252
  ); */
205
253
  }
206
254
  catch (error) {
@@ -210,25 +258,23 @@ export class CreateAppCommand extends SwellCommand {
210
258
  return false;
211
259
  }
212
260
  // Ensure frontend package.json has correct name for workspace
213
- const frontendPkgPath = path.join(configPath, 'frontend', 'package.json');
214
261
  try {
215
262
  const pkgContent = await fs.readFile(frontendPkgPath, 'utf8');
216
- const pkg = JSON.parse(pkgContent);
217
- if (pkg.name !== 'frontend') {
218
- pkg.name = 'frontend';
219
- await fs.writeFile(frontendPkgPath, JSON.stringify(pkg, null, 2), 'utf8');
263
+ const frontendPkgJson = JSON.parse(pkgContent);
264
+ if (frontendPkgJson.name !== 'frontend') {
265
+ frontendPkgJson.name = 'frontend';
266
+ await fs.writeFile(frontendPkgPath, JSON.stringify(frontendPkgJson, null, 2), 'utf8');
220
267
  }
221
268
  }
222
269
  catch {
223
270
  // Ignore if package.json doesn't exist or can't be read
224
271
  }
225
272
  await this.addFrontendAllowedHosts(projectType, configPath);
226
- // Re-run npm install at root to properly initialize workspace structure
227
- // (removes frontend/package-lock.json, hoists dependencies, creates root lock file)
273
+ // Run install at root to hoist dependencies to workspace
228
274
  spinner.start('Initializing workspace...');
229
275
  try {
230
- const execAsync = promisify(exec);
231
- await execAsync('npm install', { cwd: configPath });
276
+ const { install } = getPackageManagerCommands(pkg);
277
+ await execAsync(install, { cwd: configPath });
232
278
  spinner.succeed('Workspace initialized');
233
279
  }
234
280
  catch {
@@ -375,19 +421,22 @@ export class CreateAppCommand extends SwellCommand {
375
421
  const configPath = path.dirname(config.path);
376
422
  const packageJson = {
377
423
  description: config.get('description'),
378
- // Include workspace even if frontend doesn't exist yet
379
- // npm tolerates missing workspace directories without errors
380
- workspaces: ['frontend'],
381
424
  devDependencies: {
382
425
  '@swell/app-types': '^1.0.5',
383
426
  typescript: '^5.9.3',
384
427
  },
385
428
  name,
429
+ // Required for yarn workspaces, good practice for all package managers
430
+ private: true,
386
431
  scripts: {
387
432
  typecheck: '([ -z "$(find functions -name \'*.ts\' 2>/dev/null | head -1)" ] || tsc --noEmit) && ([ ! -f test/tsconfig.json ] || tsc --noEmit -p test) && ([ ! -f frontend/tsconfig.json ] || tsc --noEmit -p frontend)',
388
433
  },
389
434
  version: config.get('version'),
390
435
  };
436
+ // pnpm uses pnpm-workspace.yaml instead of workspaces field in package.json
437
+ if (pkg !== 'pnpm') {
438
+ packageJson.workspaces = ['frontend'];
439
+ }
391
440
  const tsConfig = {
392
441
  compilerOptions: {
393
442
  lib: ['esnext', 'webworker'],
@@ -401,15 +450,24 @@ export class CreateAppCommand extends SwellCommand {
401
450
  await writeJsonFile(path.join(configPath, 'package.json'), packageJson);
402
451
  await writeJsonFile(path.join(configPath, 'tsconfig.json'), tsConfig);
403
452
  await writeFile(path.join(configPath, '.gitignore'), `node_modules`);
404
- try {
405
- const packageManager = await this.findPackageManager(pkg);
406
- await execAsync(`${packageManager} install`, {
407
- cwd: configPath,
453
+ // Create pnpm-workspace.yaml for pnpm (required for workspace support)
454
+ if (pkg === 'pnpm') {
455
+ await writeFile(path.join(configPath, 'pnpm-workspace.yaml'), 'packages:\n - frontend\n');
456
+ }
457
+ // Create placeholder frontend/package.json for bun and yarn
458
+ // These package managers require workspace directories to exist with a package.json
459
+ if (pkg === 'bun' || pkg === 'yarn') {
460
+ const frontendPath = path.join(configPath, 'frontend');
461
+ await fs.mkdir(frontendPath, { recursive: true });
462
+ await writeJsonFile(path.join(frontendPath, 'package.json'), {
463
+ name: 'frontend',
464
+ private: true,
465
+ version: '0.0.0',
408
466
  });
409
467
  }
410
- catch (error) {
411
- this.error(error.message);
412
- }
468
+ // Install root dependencies using selected package manager
469
+ const { install } = getPackageManagerCommands(pkg);
470
+ await execAsync(install, { cwd: configPath });
413
471
  }
414
472
  async tryPackageSetup(name, config, pkg) {
415
473
  try {
@@ -131,6 +131,15 @@ export declare function getFrontendProjectSlugs(withNone?: boolean, withLegacy?:
131
131
  export declare function getFrontendProjectValidValues(withNone?: boolean, withLegacy?: boolean): string;
132
132
  export declare function getAppSlugId(app: App): string | undefined;
133
133
  export declare function getFrontendProjectType(appPath: string): FrontendProjectType | undefined;
134
+ /**
135
+ * Get project commands transformed for the detected package manager.
136
+ * Detects the package manager from lock files in appPath and transforms
137
+ * npm-style commands to the equivalent for that package manager.
138
+ */
139
+ export declare function getProjectCommands(appPath: string, projectType: FrontendProjectType): {
140
+ buildCommand?: string;
141
+ devCommand: string;
142
+ };
134
143
  export declare function getConfigTypeFromPath(path: string): ConfigType | undefined;
135
144
  export declare function getConfigTypeKeyFromValue(value: string): string | undefined;
136
145
  export declare function filePathExists(filePath: string): boolean;
@@ -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) {
@@ -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
@@ -812,7 +812,7 @@
812
812
  },
813
813
  "pkg": {
814
814
  "char": "p",
815
- "description": "Package manager: npm | yarn | none",
815
+ "description": "Package manager: npm | yarn | pnpm | bun | none",
816
816
  "name": "pkg",
817
817
  "default": "npm",
818
818
  "hasDynamicHelp": false,
@@ -820,6 +820,8 @@
820
820
  "options": [
821
821
  "npm",
822
822
  "yarn",
823
+ "pnpm",
824
+ "bun",
823
825
  "none"
824
826
  ],
825
827
  "type": "option"
@@ -1368,7 +1370,7 @@
1368
1370
  },
1369
1371
  "pkg": {
1370
1372
  "char": "p",
1371
- "description": "Package manager: npm | yarn | none",
1373
+ "description": "Package manager: npm | yarn | pnpm | bun | none",
1372
1374
  "name": "pkg",
1373
1375
  "default": "npm",
1374
1376
  "hasDynamicHelp": false,
@@ -1376,6 +1378,8 @@
1376
1378
  "options": [
1377
1379
  "npm",
1378
1380
  "yarn",
1381
+ "pnpm",
1382
+ "bun",
1379
1383
  "none"
1380
1384
  ],
1381
1385
  "type": "option"
@@ -1635,7 +1639,7 @@
1635
1639
  },
1636
1640
  "pkg": {
1637
1641
  "char": "p",
1638
- "description": "use npm or yarn to install default dependencies, or none to disable",
1642
+ "description": "Package manager: npm | yarn | pnpm | bun | none",
1639
1643
  "name": "pkg",
1640
1644
  "default": "npm",
1641
1645
  "hasDynamicHelp": false,
@@ -1643,6 +1647,8 @@
1643
1647
  "options": [
1644
1648
  "npm",
1645
1649
  "yarn",
1650
+ "pnpm",
1651
+ "bun",
1646
1652
  "none"
1647
1653
  ],
1648
1654
  "type": "option"
@@ -2511,7 +2517,7 @@
2511
2517
  },
2512
2518
  "pkg": {
2513
2519
  "char": "p",
2514
- "description": "Package manager: npm | yarn | none",
2520
+ "description": "Package manager: npm | yarn | pnpm | bun | none",
2515
2521
  "name": "pkg",
2516
2522
  "default": "npm",
2517
2523
  "hasDynamicHelp": false,
@@ -2519,6 +2525,8 @@
2519
2525
  "options": [
2520
2526
  "npm",
2521
2527
  "yarn",
2528
+ "pnpm",
2529
+ "bun",
2522
2530
  "none"
2523
2531
  ],
2524
2532
  "type": "option"
@@ -2876,5 +2884,5 @@
2876
2884
  ]
2877
2885
  }
2878
2886
  },
2879
- "version": "2.3.4"
2887
+ "version": "2.4.0"
2880
2888
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [