epicshop 6.66.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,13 +1,17 @@
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";
10
+ import { initCliSentry } from "./utils/sentry-cli.js";
8
11
  // Check for --help on start command before yargs parses
9
12
  // (yargs exits before command handler when help is requested)
10
13
  const args = hideBin(process.argv);
14
+ const cliSentry = initCliSentry(args);
11
15
  if ((args.includes('--help') || args.includes('-h')) &&
12
16
  (args.length === 0 || args[0] === 'start')) {
13
17
  const { displayHelp } = await import("./commands/start.js");
@@ -26,6 +30,43 @@ function formatHelp(helpText) {
26
30
  .replace(/--[\w-]+/g, (match) => chalk.yellow(match))
27
31
  .replace(/-\w(?=\s|,)/g, (match) => chalk.yellow(match));
28
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
+ }
29
70
  // Set up yargs CLI
30
71
  const cli = yargs(args)
31
72
  .scriptName('epicshop')
@@ -34,6 +75,9 @@ const cli = yargs(args)
34
75
  .alias('h', 'help')
35
76
  .version(false)
36
77
  .showHelpOnFail(true)
78
+ .middleware((argv) => {
79
+ cliSentry.setCommandContextFromArgv(argv);
80
+ })
37
81
  .command('start [workshop]', 'Start a workshop (auto-detects if inside a workshop directory)', (yargs) => {
38
82
  return yargs
39
83
  .positional('workshop', {
@@ -133,6 +177,22 @@ const cli = yargs(args)
133
177
  if (!result.success) {
134
178
  process.exit(1);
135
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
+ }
136
196
  })
137
197
  .command('add [repo-name] [destination]', 'Add a workshop by cloning from epicweb-dev GitHub org', (yargs) => {
138
198
  return yargs
@@ -263,6 +323,11 @@ const cli = yargs(args)
263
323
  .option('repos-dir', {
264
324
  type: 'string',
265
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',
266
331
  })
267
332
  .option('silent', {
268
333
  alias: 's',
@@ -278,6 +343,7 @@ const cli = yargs(args)
278
343
  const result = await config({
279
344
  subcommand: argv.subcommand === 'reset' ? 'reset' : undefined,
280
345
  reposDir: argv.reposDir,
346
+ packageManager: argv.packageManager,
281
347
  silent: argv.silent,
282
348
  });
283
349
  if (!result.success) {
@@ -663,6 +729,12 @@ const cli = yargs(args)
663
729
  throw error;
664
730
  }
665
731
  }
732
+ if (!argv.subcommand) {
733
+ cliSentry.setCommandContext({
734
+ command: 'auth',
735
+ subcommand,
736
+ });
737
+ }
666
738
  let result;
667
739
  switch (subcommand) {
668
740
  case 'status':
@@ -1120,6 +1192,10 @@ try {
1120
1192
  name: `${chalk.green('add')} - Add a workshop`,
1121
1193
  value: 'add',
1122
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)',
1123
1199
  }, {
1124
1200
  name: `${chalk.green('remove')} - Remove a workshop`,
1125
1201
  value: 'remove',
@@ -1172,6 +1248,7 @@ try {
1172
1248
  });
1173
1249
  },
1174
1250
  });
1251
+ cliSentry.setCommandContext({ command: subcommand, interactive: true });
1175
1252
  switch (subcommand) {
1176
1253
  case 'start': {
1177
1254
  const { findWorkshopRoot, startWorkshop } = await import("./commands/workshops.js");
@@ -1227,6 +1304,13 @@ try {
1227
1304
  process.exit(1);
1228
1305
  break;
1229
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
+ }
1230
1314
  case 'remove': {
1231
1315
  const { findWorkshopRoot, remove } = await import("./commands/workshops.js");
1232
1316
  const workshopRoot = await findWorkshopRoot();
@@ -1421,6 +1505,10 @@ try {
1421
1505
  });
1422
1506
  },
1423
1507
  });
1508
+ cliSentry.setCommandContext({
1509
+ command: 'auth',
1510
+ subcommand: authSubcommand,
1511
+ });
1424
1512
  let authResult;
1425
1513
  switch (authSubcommand) {
1426
1514
  case 'status':
@@ -1532,6 +1620,8 @@ catch (error) {
1532
1620
  if (error.message === 'USER_QUIT') {
1533
1621
  process.exit(0);
1534
1622
  }
1623
+ cliSentry.captureException(error);
1624
+ await cliSentry.flush();
1535
1625
  console.error(chalk.red('❌ Error:'), error);
1536
1626
  process.exit(1);
1537
1627
  }
@@ -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
+ }
@@ -0,0 +1,18 @@
1
+ type CliCommandContext = {
2
+ command?: string;
3
+ subcommand?: string;
4
+ flags?: string[];
5
+ interactive?: boolean;
6
+ help?: boolean;
7
+ };
8
+ type CliSentry = {
9
+ enabled: boolean;
10
+ setCommandContext: (update: Partial<CliCommandContext>) => void;
11
+ setCommandContextFromArgv: (argv: {
12
+ _: Array<string | number>;
13
+ }) => void;
14
+ captureException: (error: unknown) => void;
15
+ flush: (timeoutMs?: number) => Promise<boolean>;
16
+ };
17
+ export declare function initCliSentry(args: string[]): CliSentry;
18
+ export {};
@@ -0,0 +1,135 @@
1
+ import { getEnv } from '@epic-web/workshop-utils/init-env';
2
+ import * as Sentry from '@sentry/node';
3
+ const HELP_FLAGS = new Set(['--help', '-h']);
4
+ function extractFlags(args) {
5
+ const flags = new Set();
6
+ for (let i = 0; i < args.length; i += 1) {
7
+ const arg = args[i];
8
+ if (!arg || arg === '-' || !arg.startsWith('-')) {
9
+ continue;
10
+ }
11
+ if (arg === '--') {
12
+ break;
13
+ }
14
+ if (arg.startsWith('--')) {
15
+ const [flag] = arg.split('=');
16
+ if (flag)
17
+ flags.add(flag);
18
+ if (!arg.includes('=') && args[i + 1] && !args[i + 1]?.startsWith('-')) {
19
+ i += 1;
20
+ }
21
+ }
22
+ else {
23
+ flags.add(arg);
24
+ if (arg.length === 2 && args[i + 1] && !args[i + 1]?.startsWith('-')) {
25
+ i += 1;
26
+ }
27
+ }
28
+ }
29
+ return Array.from(flags).sort();
30
+ }
31
+ function deriveCommandContext(args) {
32
+ const interactive = args.length === 0;
33
+ const doubleDashIndex = args.indexOf('--');
34
+ const argsBeforeDoubleDash = doubleDashIndex >= 0 ? args.slice(0, doubleDashIndex) : args;
35
+ const help = argsBeforeDoubleDash.some((arg) => HELP_FLAGS.has(arg));
36
+ const flags = extractFlags(args);
37
+ let command;
38
+ const firstNonFlagIndex = args.findIndex((arg) => !arg.startsWith('-'));
39
+ if (firstNonFlagIndex >= 0) {
40
+ command = args[firstNonFlagIndex];
41
+ }
42
+ if (!command) {
43
+ if (help) {
44
+ command = 'help';
45
+ }
46
+ else if (interactive) {
47
+ command = 'chooser';
48
+ }
49
+ }
50
+ return {
51
+ command,
52
+ flags,
53
+ interactive,
54
+ help: help || command === 'help',
55
+ };
56
+ }
57
+ function deriveCommandContextFromArgv(args, argv) {
58
+ const baseContext = deriveCommandContext(args);
59
+ const segments = argv._?.map(String).filter(Boolean) ?? [];
60
+ const command = segments[0] ?? baseContext.command;
61
+ const hasSubcommand = Object.prototype.hasOwnProperty.call(argv, 'subcommand');
62
+ const subcommand = hasSubcommand && typeof argv.subcommand === 'string'
63
+ ? argv.subcommand
64
+ : baseContext.subcommand;
65
+ const help = Boolean(argv.help ?? argv.h ?? baseContext.help);
66
+ return {
67
+ ...baseContext,
68
+ command,
69
+ subcommand,
70
+ help,
71
+ };
72
+ }
73
+ function applyCommandContext(context) {
74
+ Sentry.setTag('cli.command', context.command ?? 'unknown');
75
+ Sentry.setTag('cli.subcommand', context.subcommand ?? 'none');
76
+ if (context.interactive !== undefined) {
77
+ Sentry.setTag('cli.interactive', context.interactive ? 'true' : 'false');
78
+ }
79
+ if (context.help !== undefined) {
80
+ Sentry.setTag('cli.help', context.help ? 'true' : 'false');
81
+ }
82
+ Sentry.setContext('cli', {
83
+ command: context.command ?? 'unknown',
84
+ subcommand: context.subcommand ?? null,
85
+ flags: context.flags ?? [],
86
+ interactive: context.interactive ?? false,
87
+ help: context.help ?? false,
88
+ stdin_tty: Boolean(process.stdin.isTTY),
89
+ stdout_tty: Boolean(process.stdout.isTTY),
90
+ });
91
+ }
92
+ export function initCliSentry(args) {
93
+ const env = getEnv();
94
+ const enabled = env.EPICSHOP_IS_PUBLISHED && Boolean(env.SENTRY_DSN);
95
+ if (!enabled) {
96
+ return {
97
+ enabled,
98
+ setCommandContext: () => { },
99
+ setCommandContextFromArgv: () => { },
100
+ captureException: () => { },
101
+ flush: async () => true,
102
+ };
103
+ }
104
+ Sentry.init({
105
+ dsn: env.SENTRY_DSN,
106
+ sendDefaultPii: false,
107
+ environment: env.EPICSHOP_IS_PUBLISHED ? 'production' : 'development',
108
+ tracesSampleRate: 1,
109
+ });
110
+ Sentry.setTags({
111
+ epicshop_github_repo: env.EPICSHOP_GITHUB_REPO || 'unknown',
112
+ epicshop_workshop_instance_id: env.EPICSHOP_WORKSHOP_INSTANCE_ID || 'unknown',
113
+ epicshop_app_version: env.EPICSHOP_APP_VERSION || 'unknown',
114
+ epicshop_published: env.EPICSHOP_IS_PUBLISHED ? 'true' : 'false',
115
+ });
116
+ let currentContext = deriveCommandContext(args);
117
+ applyCommandContext(currentContext);
118
+ return {
119
+ enabled,
120
+ setCommandContext(update) {
121
+ currentContext = { ...currentContext, ...update };
122
+ applyCommandContext(currentContext);
123
+ },
124
+ setCommandContextFromArgv(argv) {
125
+ currentContext = deriveCommandContextFromArgv(args, argv);
126
+ applyCommandContext(currentContext);
127
+ },
128
+ captureException(error) {
129
+ Sentry.captureException(error);
130
+ },
131
+ flush(timeoutMs = 2000) {
132
+ return Sentry.flush(timeoutMs);
133
+ },
134
+ };
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicshop",
3
- "version": "6.66.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,15 +99,16 @@
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.66.0",
102
+ "@epic-web/workshop-utils": "6.68.0",
97
103
  "@inquirer/prompts": "^8.2.0",
104
+ "@sentry/node": "^10.35.0",
98
105
  "chalk": "^5.6.2",
99
106
  "close-with-grace": "^2.4.0",
100
107
  "execa": "^9.6.1",
101
108
  "get-port": "^7.1.0",
102
109
  "match-sorter": "^8.2.0",
103
- "openid-client": "^6.8.1",
104
110
  "open": "^11.0.0",
111
+ "openid-client": "^6.8.1",
105
112
  "ora": "^9.0.0",
106
113
  "yargs": "^18.0.0"
107
114
  },
@@ -109,8 +116,8 @@
109
116
  "@epic-web/config": "^1.21.3",
110
117
  "@types/node": "^25.0.9",
111
118
  "@types/yargs": "^17.0.35",
112
- "zshy": "^0.7.0",
113
- "vitest": "^4.0.17"
119
+ "vitest": "^4.0.17",
120
+ "zshy": "^0.7.0"
114
121
  },
115
122
  "repository": {
116
123
  "type": "git",