epicshop 6.67.0 → 6.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import '@epic-web/workshop-utils/init-env';
3
+ import { PACKAGE_MANAGERS, getPackageManager, isPackageManagerConfigured, setPackageManager, } from '@epic-web/workshop-utils/workshops.server';
3
4
  import chalk from 'chalk';
4
5
  import { matchSorter } from 'match-sorter';
5
6
  import yargs from 'yargs';
6
7
  import { hideBin } from 'yargs/helpers';
7
- import { assertCanPrompt } from "./utils/cli-runtime.js";
8
+ import { assertCanPrompt, hasTty, isCiEnvironment, } from "./utils/cli-runtime.js";
9
+ import { detectRuntimePackageManager } from "./utils/package-manager.js";
8
10
  import { initCliSentry } from "./utils/sentry-cli.js";
9
11
  // Check for --help on start command before yargs parses
10
12
  // (yargs exits before command handler when help is requested)
@@ -28,6 +30,43 @@ function formatHelp(helpText) {
28
30
  .replace(/--[\w-]+/g, (match) => chalk.yellow(match))
29
31
  .replace(/-\w(?=\s|,)/g, (match) => chalk.yellow(match));
30
32
  }
33
+ async function maybePromptToUpdatePackageManager(parsedArgs) {
34
+ if (parsedArgs.includes('--help') || parsedArgs.includes('-h'))
35
+ return;
36
+ if (parsedArgs.includes('--silent') || parsedArgs.includes('-s'))
37
+ return;
38
+ if (parsedArgs[0] === 'config')
39
+ return;
40
+ if (isCiEnvironment() || !hasTty())
41
+ return;
42
+ const runtimeManager = detectRuntimePackageManager();
43
+ if (!runtimeManager)
44
+ return;
45
+ const isConfigured = await isPackageManagerConfigured();
46
+ if (!isConfigured)
47
+ return;
48
+ const configuredManager = await getPackageManager();
49
+ if (configuredManager === runtimeManager)
50
+ return;
51
+ const { confirm } = await import('@inquirer/prompts');
52
+ const shouldUpdate = await confirm({
53
+ message: `You ran epicshop with ${runtimeManager}, but your default package manager is ${configuredManager}. Update the default?`,
54
+ default: true,
55
+ });
56
+ if (shouldUpdate) {
57
+ await setPackageManager(runtimeManager);
58
+ console.log(chalk.green(`✅ Default package manager updated to ${runtimeManager}.`));
59
+ }
60
+ }
61
+ try {
62
+ await maybePromptToUpdatePackageManager(args);
63
+ }
64
+ catch (error) {
65
+ if (error.message === 'USER_QUIT') {
66
+ process.exit(0);
67
+ }
68
+ // Silently ignore other errors during package manager prompt to avoid disrupting CLI startup
69
+ }
31
70
  // Set up yargs CLI
32
71
  const cli = yargs(args)
33
72
  .scriptName('epicshop')
@@ -138,6 +177,22 @@ const cli = yargs(args)
138
177
  if (!result.success) {
139
178
  process.exit(1);
140
179
  }
180
+ })
181
+ .command('setup', 'Install workshop dependencies (uses configured package manager)', (yargs) => {
182
+ return yargs
183
+ .option('silent', {
184
+ alias: 's',
185
+ type: 'boolean',
186
+ description: 'Run without output logs',
187
+ default: false,
188
+ })
189
+ .example('$0 setup', 'Install workshop dependencies');
190
+ }, async (argv) => {
191
+ const { setup } = await import("./commands/setup.js");
192
+ const result = await setup({ silent: argv.silent });
193
+ if (!result.success) {
194
+ process.exit(1);
195
+ }
141
196
  })
142
197
  .command('add [repo-name] [destination]', 'Add a workshop by cloning from epicweb-dev GitHub org', (yargs) => {
143
198
  return yargs
@@ -268,6 +323,11 @@ const cli = yargs(args)
268
323
  .option('repos-dir', {
269
324
  type: 'string',
270
325
  description: 'Set the default directory for workshop repos',
326
+ })
327
+ .option('package-manager', {
328
+ type: 'string',
329
+ choices: PACKAGE_MANAGERS,
330
+ description: 'Set the default package manager',
271
331
  })
272
332
  .option('silent', {
273
333
  alias: 's',
@@ -283,6 +343,7 @@ const cli = yargs(args)
283
343
  const result = await config({
284
344
  subcommand: argv.subcommand === 'reset' ? 'reset' : undefined,
285
345
  reposDir: argv.reposDir,
346
+ packageManager: argv.packageManager,
286
347
  silent: argv.silent,
287
348
  });
288
349
  if (!result.success) {
@@ -1131,6 +1192,10 @@ try {
1131
1192
  name: `${chalk.green('add')} - Add a workshop`,
1132
1193
  value: 'add',
1133
1194
  description: 'Clone a workshop from epicweb-dev GitHub org',
1195
+ }, {
1196
+ name: `${chalk.green('setup')} - Install dependencies`,
1197
+ value: 'setup',
1198
+ description: 'Install workshop dependencies (uses configured manager)',
1134
1199
  }, {
1135
1200
  name: `${chalk.green('remove')} - Remove a workshop`,
1136
1201
  value: 'remove',
@@ -1239,6 +1304,13 @@ try {
1239
1304
  process.exit(1);
1240
1305
  break;
1241
1306
  }
1307
+ case 'setup': {
1308
+ const { setup } = await import("./commands/setup.js");
1309
+ const result = await setup({});
1310
+ if (!result.success)
1311
+ process.exit(1);
1312
+ break;
1313
+ }
1242
1314
  case 'remove': {
1243
1315
  const { findWorkshopRoot, remove } = await import("./commands/workshops.js");
1244
1316
  const workshopRoot = await findWorkshopRoot();
@@ -0,0 +1,10 @@
1
+ export type SetupOptions = {
2
+ cwd?: string;
3
+ silent?: boolean;
4
+ };
5
+ export type SetupResult = {
6
+ success: boolean;
7
+ message?: string;
8
+ error?: Error;
9
+ };
10
+ export declare function setup(options?: SetupOptions): Promise<SetupResult>;
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getErrorMessage } from '@epic-web/workshop-utils/utils';
4
+ import { getPackageManager, isPackageManagerConfigured, } from '@epic-web/workshop-utils/workshops.server';
5
+ import chalk from 'chalk';
6
+ import { execa } from 'execa';
7
+ import { resolveCliCommand, runCommand, } from "../utils/command-runner.js";
8
+ import { formatPackageManagerCommand, getPackageManagerInstallArgs, getPackageManagerRunArgs, } from "../utils/package-manager.js";
9
+ function isPackageJson(value) {
10
+ if (!value || typeof value !== 'object')
11
+ return false;
12
+ if (!('scripts' in value))
13
+ return true;
14
+ const scriptsValue = value.scripts;
15
+ if (scriptsValue === undefined)
16
+ return true;
17
+ if (!scriptsValue || typeof scriptsValue !== 'object')
18
+ return false;
19
+ return Object.values(scriptsValue).every((script) => typeof script === 'string');
20
+ }
21
+ async function getPackageManagerVersion(packageManager) {
22
+ try {
23
+ const result = await execa(resolveCliCommand(packageManager), ['--version'], {
24
+ stdio: 'pipe',
25
+ });
26
+ return { success: true, version: result.stdout.trim() };
27
+ }
28
+ catch (error) {
29
+ const message = getErrorMessage(error, 'Failed to check package manager');
30
+ const err = error instanceof Error ? error : new Error(message);
31
+ return { success: false, error: err };
32
+ }
33
+ }
34
+ function isNpmVersionSupported(version) {
35
+ const [majorString, minorString] = version.split('.');
36
+ const major = Number(majorString);
37
+ const minor = Number(minorString);
38
+ if (!Number.isFinite(major) || !Number.isFinite(minor))
39
+ return false;
40
+ return major > 8 || (major === 8 && minor >= 16);
41
+ }
42
+ function formatCommandResultError(result, fallbackMessage) {
43
+ return {
44
+ success: false,
45
+ message: result.message ?? fallbackMessage,
46
+ error: result.error,
47
+ };
48
+ }
49
+ export async function setup(options = {}) {
50
+ const { silent = false } = options;
51
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
52
+ const packageJsonPath = path.join(cwd, 'package.json');
53
+ try {
54
+ await fs.promises.access(packageJsonPath);
55
+ }
56
+ catch {
57
+ const message = `package.json not found at ${packageJsonPath}`;
58
+ if (!silent) {
59
+ console.error(chalk.red(`❌ ${message}`));
60
+ }
61
+ return { success: false, message, error: new Error(message) };
62
+ }
63
+ let scripts;
64
+ try {
65
+ const parsed = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8'));
66
+ if (isPackageJson(parsed)) {
67
+ scripts = parsed.scripts;
68
+ }
69
+ }
70
+ catch (error) {
71
+ const message = getErrorMessage(error, 'Failed to read package.json');
72
+ if (!silent) {
73
+ console.error(chalk.red(`❌ ${message}`));
74
+ }
75
+ return { success: false, message, error: error };
76
+ }
77
+ const packageManager = await getPackageManager();
78
+ const isConfigured = await isPackageManagerConfigured();
79
+ const managerLabel = isConfigured
80
+ ? packageManager
81
+ : `${packageManager} (default)`;
82
+ const versionResult = await getPackageManagerVersion(packageManager);
83
+ if (!versionResult.success || !versionResult.version) {
84
+ const message = `Failed to run ${packageManager} --version. Please ensure ${packageManager} is installed.`;
85
+ if (!silent) {
86
+ console.error(chalk.red(`❌ ${message}`));
87
+ }
88
+ return {
89
+ success: false,
90
+ message,
91
+ error: versionResult.error ?? new Error(message),
92
+ };
93
+ }
94
+ if (packageManager === 'npm' &&
95
+ !isNpmVersionSupported(versionResult.version)) {
96
+ const message = `npm version is ${versionResult.version} which is out of date. Please install npm@8.16.0 or greater.`;
97
+ if (!silent) {
98
+ console.error(chalk.red(`❌ ${message}`));
99
+ }
100
+ return { success: false, message, error: new Error(message) };
101
+ }
102
+ const installArgs = getPackageManagerInstallArgs(packageManager);
103
+ const installCommand = formatPackageManagerCommand(packageManager, installArgs);
104
+ if (!silent) {
105
+ console.log(chalk.cyan(`📦 Installing dependencies using ${chalk.bold(managerLabel)}...`));
106
+ console.log(chalk.gray(` To change this, run: npx epicshop config --package-manager <npm|pnpm|yarn|bun>`));
107
+ console.log(chalk.gray(` Running: ${installCommand}`));
108
+ }
109
+ const installResult = await runCommand(packageManager, installArgs, {
110
+ cwd,
111
+ silent,
112
+ });
113
+ if (!installResult.success) {
114
+ return formatCommandResultError(installResult, 'Failed to install dependencies');
115
+ }
116
+ const hasCustomSetup = Boolean(scripts?.['setup:custom']);
117
+ if (hasCustomSetup) {
118
+ const customArgs = getPackageManagerRunArgs(packageManager, 'setup:custom');
119
+ const customCommand = formatPackageManagerCommand(packageManager, customArgs);
120
+ if (!silent) {
121
+ console.log(chalk.cyan(`🔧 Running ${customCommand}...`));
122
+ }
123
+ const customResult = await runCommand(packageManager, customArgs, {
124
+ cwd,
125
+ silent,
126
+ });
127
+ if (!customResult.success) {
128
+ return formatCommandResultError(customResult, 'Failed to run setup:custom');
129
+ }
130
+ }
131
+ const message = 'Workshop setup complete';
132
+ if (!silent) {
133
+ console.log(chalk.green(`✅ ${message}`));
134
+ }
135
+ return { success: true, message };
136
+ }
@@ -1,4 +1,5 @@
1
1
  import '@epic-web/workshop-utils/init-env';
2
+ import { type PackageManager } from '@epic-web/workshop-utils/workshops.server';
2
3
  /**
3
4
  * Find the workshop root directory by walking up from the current directory
4
5
  * looking for a package.json with an epicshop field.
@@ -27,6 +28,7 @@ export type StartOptions = {
27
28
  };
28
29
  export type ConfigOptions = {
29
30
  reposDir?: string;
31
+ packageManager?: PackageManager;
30
32
  silent?: boolean;
31
33
  subcommand?: 'reset' | 'delete';
32
34
  };
@@ -6,12 +6,14 @@ import { cachified, githubCache } from '@epic-web/workshop-utils/cache.server';
6
6
  import { parseEpicshopConfig } from '@epic-web/workshop-utils/config.server';
7
7
  import { getAuthInfo } from '@epic-web/workshop-utils/db.server';
8
8
  import { userHasAccessToWorkshop } from '@epic-web/workshop-utils/epic-api.server';
9
- import { getErrorMessage } from '@epic-web/workshop-utils/utils';
9
+ import { PACKAGE_MANAGERS, clearPackageManager, getPackageManager, isPackageManagerConfigured, setPackageManager, } from '@epic-web/workshop-utils/workshops.server';
10
10
  import chalk from 'chalk';
11
- import { execa } from 'execa';
12
11
  import { matchSorter, rankings } from 'match-sorter';
13
12
  import ora from 'ora';
14
13
  import { assertCanPrompt, isCiEnvironment } from "../utils/cli-runtime.js";
14
+ import { runCommand, runCommandInteractive } from "../utils/command-runner.js";
15
+ import { formatPackageManagerCommand, getPackageManagerRunArgs, } from "../utils/package-manager.js";
16
+ import { setup } from "./setup.js";
15
17
  const GITHUB_ORG = 'epicweb-dev';
16
18
  const TUTORIAL_REPO = 'epicshop-tutorial';
17
19
  const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
@@ -343,14 +345,7 @@ async function addSingleWorkshop(repoName, options) {
343
345
  error: cloneResult.error,
344
346
  };
345
347
  }
346
- if (!silent) {
347
- console.log(chalk.cyan(`🔧 Running npm run setup...`));
348
- }
349
- // Run npm run setup
350
- const setupResult = await runCommand('npm', ['run', 'setup'], {
351
- cwd: workshopPath,
352
- silent,
353
- });
348
+ const setupResult = await setup({ cwd: workshopPath, silent });
354
349
  if (!setupResult.success) {
355
350
  // Clean up the cloned directory on setup failure
356
351
  if (!silent) {
@@ -364,7 +359,7 @@ async function addSingleWorkshop(repoName, options) {
364
359
  }
365
360
  return {
366
361
  success: false,
367
- message: `Failed to run setup: ${setupResult.message}`,
362
+ message: `Failed to set up workshop: ${setupResult.message}`,
368
363
  error: setupResult.error,
369
364
  };
370
365
  }
@@ -794,12 +789,14 @@ export async function list({ silent = false, } = {}) {
794
789
  });
795
790
  },
796
791
  });
792
+ const packageManager = await getPackageManager();
793
+ const startCommand = formatPackageManagerCommand(packageManager, getPackageManagerRunArgs(packageManager, 'start'));
797
794
  // Show actions for selected workshop
798
795
  const actionChoices = [
799
796
  {
800
797
  name: 'Start workshop',
801
798
  value: 'start',
802
- description: 'Run npm start in the workshop directory',
799
+ description: `Run ${startCommand} in the workshop directory`,
803
800
  },
804
801
  {
805
802
  name: 'Open in editor',
@@ -1084,8 +1081,10 @@ export async function startWorkshop(options = {}) {
1084
1081
  console.log(chalk.cyan(`🚀 Starting ${chalk.bold(workshopToStart.title)}...`));
1085
1082
  console.log(chalk.gray(` Path: ${workshopToStart.path}\n`));
1086
1083
  }
1087
- // Run npm start in the workshop directory
1088
- const startResult = await runCommandInteractive('npm', ['start'], {
1084
+ const packageManager = await getPackageManager();
1085
+ const startArgs = getPackageManagerRunArgs(packageManager, 'start');
1086
+ // Run start script in the workshop directory
1087
+ const startResult = await runCommandInteractive(packageManager, startArgs, {
1089
1088
  cwd: workshopToStart.path,
1090
1089
  });
1091
1090
  if (!startResult.success) {
@@ -1282,14 +1281,27 @@ export async function config(options = {}) {
1282
1281
  return { success: true, message: 'Cancelled' };
1283
1282
  }
1284
1283
  }
1284
+ // Handle CLI flags for setting config values
1285
+ const messages = [];
1285
1286
  if (options.reposDir) {
1286
1287
  // Set the repos directory directly via CLI flag
1287
1288
  const resolvedPath = path.resolve(options.reposDir);
1288
1289
  await setReposDirectory(resolvedPath);
1289
1290
  const message = `Repos directory set to: ${resolvedPath}`;
1291
+ messages.push(message);
1290
1292
  if (!silent)
1291
1293
  console.log(chalk.green(`✅ ${message}`));
1292
- return { success: true, message };
1294
+ }
1295
+ if (options.packageManager) {
1296
+ await setPackageManager(options.packageManager);
1297
+ const message = `Package manager set to: ${options.packageManager}`;
1298
+ messages.push(message);
1299
+ if (!silent)
1300
+ console.log(chalk.green(`✅ ${message}`));
1301
+ }
1302
+ // If either option was set, return now
1303
+ if (messages.length > 0) {
1304
+ return { success: true, message: messages.join('; ') };
1293
1305
  }
1294
1306
  if (silent) {
1295
1307
  // In silent mode, just return current config
@@ -1301,13 +1313,16 @@ export async function config(options = {}) {
1301
1313
  reason: 'select a configuration option',
1302
1314
  hints: [
1303
1315
  'Set repos dir directly: npx epicshop config --repos-dir <path>',
1316
+ 'Set package manager directly: npx epicshop config --package-manager <npm|pnpm|yarn|bun>',
1304
1317
  'Delete config non-interactively: npx epicshop config reset --silent',
1305
1318
  ],
1306
1319
  });
1307
- const { search, confirm } = await import('@inquirer/prompts');
1320
+ const { search, confirm, select } = await import('@inquirer/prompts');
1308
1321
  const reposDir = await getReposDirectory();
1309
1322
  const isConfigured = await isReposDirectoryConfigured();
1310
1323
  const defaultDir = getDefaultReposDir();
1324
+ const packageManager = await getPackageManager();
1325
+ const isPackageManagerSet = await isPackageManagerConfigured();
1311
1326
  // Build config options
1312
1327
  const configOptions = [
1313
1328
  {
@@ -1315,6 +1330,13 @@ export async function config(options = {}) {
1315
1330
  value: 'repos-dir',
1316
1331
  description: isConfigured ? reposDir : `${reposDir} (default)`,
1317
1332
  },
1333
+ {
1334
+ name: `Package manager`,
1335
+ value: 'package-manager',
1336
+ description: isPackageManagerSet
1337
+ ? packageManager
1338
+ : `${packageManager} (default)`,
1339
+ },
1318
1340
  {
1319
1341
  name: `Reset config file`,
1320
1342
  value: 'reset',
@@ -1433,6 +1455,87 @@ export async function config(options = {}) {
1433
1455
  return { success: true, message: 'Cancelled' };
1434
1456
  }
1435
1457
  }
1458
+ if (selectedConfig === 'package-manager') {
1459
+ console.log();
1460
+ console.log(chalk.bold(' Current value:'));
1461
+ if (isPackageManagerSet) {
1462
+ console.log(chalk.white(` ${packageManager}`));
1463
+ }
1464
+ else {
1465
+ console.log(chalk.gray(` ${packageManager} (default, not explicitly set)`));
1466
+ }
1467
+ console.log();
1468
+ const actionChoices = [
1469
+ {
1470
+ name: 'Edit',
1471
+ value: 'edit',
1472
+ description: 'Change the default package manager',
1473
+ },
1474
+ ...(isPackageManagerSet
1475
+ ? [
1476
+ {
1477
+ name: 'Remove',
1478
+ value: 'remove',
1479
+ description: 'Reset to default (npm)',
1480
+ },
1481
+ ]
1482
+ : []),
1483
+ {
1484
+ name: 'Cancel',
1485
+ value: 'cancel',
1486
+ description: 'Go back without changes',
1487
+ },
1488
+ ];
1489
+ const action = await search({
1490
+ message: 'What would you like to do?',
1491
+ source: async (input) => {
1492
+ if (!input)
1493
+ return actionChoices;
1494
+ return matchSorter(actionChoices, input, {
1495
+ keys: ['name', 'value', 'description'],
1496
+ });
1497
+ },
1498
+ });
1499
+ if (action === 'edit') {
1500
+ const selectedManager = await select({
1501
+ message: 'Select a package manager:',
1502
+ choices: PACKAGE_MANAGERS.map((manager) => ({
1503
+ name: manager,
1504
+ value: manager,
1505
+ })),
1506
+ });
1507
+ await setPackageManager(selectedManager);
1508
+ console.log();
1509
+ console.log(chalk.green(`✅ Package manager set to: ${chalk.bold(selectedManager)}`));
1510
+ return {
1511
+ success: true,
1512
+ message: `Package manager set to: ${selectedManager}`,
1513
+ };
1514
+ }
1515
+ else if (action === 'remove') {
1516
+ const shouldRemove = await confirm({
1517
+ message: 'Reset package manager to default (npm)?',
1518
+ default: false,
1519
+ });
1520
+ if (shouldRemove) {
1521
+ await clearPackageManager();
1522
+ console.log();
1523
+ console.log(chalk.green(`✅ Package manager reset to default (npm).`));
1524
+ return {
1525
+ success: true,
1526
+ message: 'Package manager reset to default (npm).',
1527
+ };
1528
+ }
1529
+ else {
1530
+ console.log(chalk.gray('\nNo changes made.'));
1531
+ return { success: true, message: 'Cancelled' };
1532
+ }
1533
+ }
1534
+ else {
1535
+ console.log(chalk.gray('\nNo changes made.'));
1536
+ return { success: true, message: 'Cancelled' };
1537
+ }
1538
+ }
1436
1539
  return { success: true, message: 'Config viewed' };
1437
1540
  }
1438
1541
  catch (error) {
@@ -1719,7 +1822,8 @@ async function promptAndSetupAccessibleWorkshops() {
1719
1822
  const { workshopExists } = await import('@epic-web/workshop-utils/workshops.server');
1720
1823
  console.log(chalk.bold.cyan('\n📚 Workshop Setup\n'));
1721
1824
  console.log(chalk.cyan('🐨 Next, you can select any workshops you’d like me to set up for you.'));
1722
- console.log(chalk.gray(' This will clone each workshop repo into your workshops directory and run `npm run setup`.\n' +
1825
+ const packageManager = await getPackageManager();
1826
+ console.log(chalk.gray(` This will clone each workshop repo into your workshops directory and run setup using ${packageManager}.\n` +
1723
1827
  ' (If something fails, we’ll keep going and you can retry later with `npx epicshop add`.)\n'));
1724
1828
  assertCanPrompt({
1725
1829
  reason: 'select workshops to set up',
@@ -2000,12 +2104,7 @@ async function ensureTutorialAndStart() {
2000
2104
  error: cloneResult.error,
2001
2105
  };
2002
2106
  }
2003
- console.log(chalk.cyan(`\n🔧 Running npm run setup...\n`));
2004
- // Run npm run setup
2005
- const setupResult = await runCommand('npm', ['run', 'setup'], {
2006
- cwd: workshopPath,
2007
- silent: false,
2008
- });
2107
+ const setupResult = await setup({ cwd: workshopPath, silent: false });
2009
2108
  if (!setupResult.success) {
2010
2109
  // Clean up on failure
2011
2110
  console.log(chalk.yellow(`🧹 Cleaning up cloned directory...`));
@@ -2015,10 +2114,10 @@ async function ensureTutorialAndStart() {
2015
2114
  catch {
2016
2115
  // Ignore cleanup errors
2017
2116
  }
2018
- console.error(chalk.red(`❌ Failed to run setup: ${setupResult.message}`));
2117
+ console.error(chalk.red(`❌ Failed to set up workshop: ${setupResult.message}`));
2019
2118
  return {
2020
2119
  success: false,
2021
- message: `Failed to run setup: ${setupResult.message}`,
2120
+ message: `Failed to set up workshop: ${setupResult.message}`,
2022
2121
  error: setupResult.error,
2023
2122
  };
2024
2123
  }
@@ -2033,8 +2132,10 @@ async function ensureTutorialAndStart() {
2033
2132
  }
2034
2133
  await promptToStartTutorial(workshopTitle);
2035
2134
  console.log(chalk.cyan(`\n🚀 Starting ${chalk.bold(workshopTitle)}...\n`));
2135
+ const packageManager = await getPackageManager();
2136
+ const startArgs = getPackageManagerRunArgs(packageManager, 'start');
2036
2137
  // Start the workshop
2037
- const startResult = await runCommandInteractive('npm', ['start'], {
2138
+ const startResult = await runCommandInteractive(packageManager, startArgs, {
2038
2139
  cwd: workshopPath,
2039
2140
  });
2040
2141
  if (!startResult.success) {
@@ -2200,40 +2301,3 @@ async function directoryExists(dirPath) {
2200
2301
  return false;
2201
2302
  }
2202
2303
  }
2203
- function resolveCliCommand(command) {
2204
- // On Windows, package manager binaries are typically shimmed as *.cmd files.
2205
- // Spawning "npm" directly can fail with ENOENT even though "npm.cmd" exists.
2206
- if (process.platform === 'win32' &&
2207
- (command === 'npm' || command === 'npx')) {
2208
- return `${command}.cmd`;
2209
- }
2210
- return command;
2211
- }
2212
- function runCommand(command, args, options) {
2213
- return execa(resolveCliCommand(command), args, {
2214
- cwd: options.cwd,
2215
- stdio: options.silent ? 'pipe' : 'inherit',
2216
- }).then(() => ({ success: true }), (error) => {
2217
- const message = getErrorMessage(error, 'Command failed');
2218
- const err = error instanceof Error ? error : new Error(message);
2219
- return { success: false, message, error: err };
2220
- });
2221
- }
2222
- function runCommandInteractive(command, args, options) {
2223
- return execa(resolveCliCommand(command), args, {
2224
- cwd: options.cwd,
2225
- stdio: 'inherit',
2226
- }).then(() => ({ success: true }), (error) => {
2227
- // If the process was terminated by a signal (e.g. user presses Ctrl+C),
2228
- // treat it as success so we don't show a confusing error message.
2229
- if (error &&
2230
- typeof error === 'object' &&
2231
- 'signal' in error &&
2232
- typeof error.signal === 'string') {
2233
- return { success: true };
2234
- }
2235
- const message = getErrorMessage(error, 'Command failed');
2236
- const err = error instanceof Error ? error : new Error(message);
2237
- return { success: false, message, error: err };
2238
- });
2239
- }
@@ -0,0 +1,13 @@
1
+ export type CommandResult = {
2
+ success: boolean;
3
+ message?: string;
4
+ error?: Error;
5
+ };
6
+ export declare function resolveCliCommand(command: string): string;
7
+ export declare function runCommand(command: string, args: string[], options: {
8
+ cwd: string;
9
+ silent?: boolean;
10
+ }): Promise<CommandResult>;
11
+ export declare function runCommandInteractive(command: string, args: string[], options: {
12
+ cwd: string;
13
+ }): Promise<CommandResult>;
@@ -0,0 +1,41 @@
1
+ import { getErrorMessage } from '@epic-web/workshop-utils/utils';
2
+ import { execa } from 'execa';
3
+ export function resolveCliCommand(command) {
4
+ // On Windows, package manager binaries are typically shimmed as *.cmd files.
5
+ if (process.platform === 'win32' &&
6
+ (command === 'npm' ||
7
+ command === 'npx' ||
8
+ command === 'pnpm' ||
9
+ command === 'yarn')) {
10
+ return `${command}.cmd`;
11
+ }
12
+ return command;
13
+ }
14
+ export function runCommand(command, args, options) {
15
+ return execa(resolveCliCommand(command), args, {
16
+ cwd: options.cwd,
17
+ stdio: options.silent ? 'pipe' : 'inherit',
18
+ }).then(() => ({ success: true }), (error) => {
19
+ const message = getErrorMessage(error, 'Command failed');
20
+ const err = error instanceof Error ? error : new Error(message);
21
+ return { success: false, message, error: err };
22
+ });
23
+ }
24
+ export function runCommandInteractive(command, args, options) {
25
+ return execa(resolveCliCommand(command), args, {
26
+ cwd: options.cwd,
27
+ stdio: 'inherit',
28
+ }).then(() => ({ success: true }), (error) => {
29
+ // If the process was terminated by a signal (e.g. user presses Ctrl+C),
30
+ // treat it as success so we don't show a confusing error message.
31
+ if (error &&
32
+ typeof error === 'object' &&
33
+ 'signal' in error &&
34
+ typeof error.signal === 'string') {
35
+ return { success: true };
36
+ }
37
+ const message = getErrorMessage(error, 'Command failed');
38
+ const err = error instanceof Error ? error : new Error(message);
39
+ return { success: false, message, error: err };
40
+ });
41
+ }
@@ -0,0 +1,5 @@
1
+ import { type PackageManager } from '@epic-web/workshop-utils/workshops.server';
2
+ export declare function detectRuntimePackageManager(): PackageManager | null;
3
+ export declare function getPackageManagerInstallArgs(_packageManager: PackageManager): string[];
4
+ export declare function getPackageManagerRunArgs(_packageManager: PackageManager, script: string): string[];
5
+ export declare function formatPackageManagerCommand(packageManager: PackageManager, args: string[]): string;
@@ -0,0 +1,27 @@
1
+ function detectPackageManager(value) {
2
+ if (!value)
3
+ return null;
4
+ if (value.includes('pnpm'))
5
+ return 'pnpm';
6
+ if (value.includes('yarn'))
7
+ return 'yarn';
8
+ if (value.includes('bun'))
9
+ return 'bun';
10
+ if (value.includes('npm'))
11
+ return 'npm';
12
+ return null;
13
+ }
14
+ export function detectRuntimePackageManager() {
15
+ const userAgent = (process.env.npm_config_user_agent ?? '').toLowerCase();
16
+ const execPath = (process.env.npm_execpath ?? '').toLowerCase();
17
+ return detectPackageManager(userAgent) ?? detectPackageManager(execPath);
18
+ }
19
+ export function getPackageManagerInstallArgs(_packageManager) {
20
+ return ['install'];
21
+ }
22
+ export function getPackageManagerRunArgs(_packageManager, script) {
23
+ return ['run', script];
24
+ }
25
+ export function formatPackageManagerCommand(packageManager, args) {
26
+ return `${packageManager} ${args.join(' ')}`.trim();
27
+ }
@@ -1,5 +1,5 @@
1
- import * as Sentry from '@sentry/node';
2
1
  import { getEnv } from '@epic-web/workshop-utils/init-env';
2
+ import * as Sentry from '@sentry/node';
3
3
  const HELP_FLAGS = new Set(['--help', '-h']);
4
4
  function extractFlags(args) {
5
5
  const flags = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicshop",
3
- "version": "6.67.0",
3
+ "version": "6.68.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -14,6 +14,7 @@
14
14
  "exports": {
15
15
  "./package.json": "./package.json",
16
16
  "./cleanup": "./src/commands/cleanup.ts",
17
+ "./setup": "./src/commands/setup.ts",
17
18
  "./warm": "./src/commands/warm.ts",
18
19
  "./start": "./src/commands/start.ts",
19
20
  "./update": "./src/commands/update.ts",
@@ -33,6 +34,11 @@
33
34
  "types": "./dist/commands/cleanup.d.ts",
34
35
  "default": "./dist/commands/cleanup.js"
35
36
  },
37
+ "./setup": {
38
+ "import": "./dist/commands/setup.js",
39
+ "types": "./dist/commands/setup.d.ts",
40
+ "default": "./dist/commands/setup.js"
41
+ },
36
42
  "./warm": {
37
43
  "import": "./dist/commands/warm.js",
38
44
  "types": "./dist/commands/warm.d.ts",
@@ -93,7 +99,7 @@
93
99
  "build:watch": "nx watch --projects=epicshop -- nx run \\$NX_PROJECT_NAME:build"
94
100
  },
95
101
  "dependencies": {
96
- "@epic-web/workshop-utils": "6.67.0",
102
+ "@epic-web/workshop-utils": "6.68.0",
97
103
  "@inquirer/prompts": "^8.2.0",
98
104
  "@sentry/node": "^10.35.0",
99
105
  "chalk": "^5.6.2",