epicshop 6.67.0 → 6.69.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
@@ -138,6 +138,22 @@ const cli = yargs(args)
138
138
  if (!result.success) {
139
139
  process.exit(1);
140
140
  }
141
+ })
142
+ .command('setup', 'Install workshop dependencies (uses configured package manager)', (yargs) => {
143
+ return yargs
144
+ .option('silent', {
145
+ alias: 's',
146
+ type: 'boolean',
147
+ description: 'Run without output logs',
148
+ default: false,
149
+ })
150
+ .example('$0 setup', 'Install workshop dependencies');
151
+ }, async (argv) => {
152
+ const { setup } = await import("./commands/setup.js");
153
+ const result = await setup({ silent: argv.silent });
154
+ if (!result.success) {
155
+ process.exit(1);
156
+ }
141
157
  })
142
158
  .command('add [repo-name] [destination]', 'Add a workshop by cloning from epicweb-dev GitHub org', (yargs) => {
143
159
  return yargs
@@ -1131,6 +1147,10 @@ try {
1131
1147
  name: `${chalk.green('add')} - Add a workshop`,
1132
1148
  value: 'add',
1133
1149
  description: 'Clone a workshop from epicweb-dev GitHub org',
1150
+ }, {
1151
+ name: `${chalk.green('setup')} - Install dependencies`,
1152
+ value: 'setup',
1153
+ description: 'Install workshop dependencies (uses configured manager)',
1134
1154
  }, {
1135
1155
  name: `${chalk.green('remove')} - Remove a workshop`,
1136
1156
  value: 'remove',
@@ -1239,6 +1259,13 @@ try {
1239
1259
  process.exit(1);
1240
1260
  break;
1241
1261
  }
1262
+ case 'setup': {
1263
+ const { setup } = await import("./commands/setup.js");
1264
+ const result = await setup({});
1265
+ if (!result.success)
1266
+ process.exit(1);
1267
+ break;
1268
+ }
1242
1269
  case 'remove': {
1243
1270
  const { findWorkshopRoot, remove } = await import("./commands/workshops.js");
1244
1271
  const workshopRoot = await findWorkshopRoot();
@@ -0,0 +1,18 @@
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
+ /**
11
+ * Install workshop dependencies in the current directory.
12
+ * Must be run from within a workshop directory (containing package.json).
13
+ *
14
+ * Automatically detects and uses the package manager based on how epicshop was
15
+ * invoked (e.g., pnpm dlx epicshop setup uses pnpm, bunx epicshop setup uses bun).
16
+ * This is handled by pkgmgr, which detects the runtime package manager.
17
+ */
18
+ export declare function setup(options?: SetupOptions): Promise<SetupResult>;
@@ -0,0 +1,91 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getErrorMessage } from '@epic-web/workshop-utils/utils';
4
+ import chalk from 'chalk';
5
+ import { runCommand, } from "../utils/command-runner.js";
6
+ function isPackageJson(value) {
7
+ if (!value || typeof value !== 'object')
8
+ return false;
9
+ if (!('scripts' in value))
10
+ return true;
11
+ const scriptsValue = value.scripts;
12
+ if (scriptsValue === undefined)
13
+ return true;
14
+ if (!scriptsValue || typeof scriptsValue !== 'object')
15
+ return false;
16
+ return Object.values(scriptsValue).every((script) => typeof script === 'string');
17
+ }
18
+ function formatCommandResultError(result, fallbackMessage) {
19
+ return {
20
+ success: false,
21
+ message: result.message ?? fallbackMessage,
22
+ error: result.error,
23
+ };
24
+ }
25
+ /**
26
+ * Install workshop dependencies in the current directory.
27
+ * Must be run from within a workshop directory (containing package.json).
28
+ *
29
+ * Automatically detects and uses the package manager based on how epicshop was
30
+ * invoked (e.g., pnpm dlx epicshop setup uses pnpm, bunx epicshop setup uses bun).
31
+ * This is handled by pkgmgr, which detects the runtime package manager.
32
+ */
33
+ export async function setup(options = {}) {
34
+ const { silent = false } = options;
35
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
36
+ const packageJsonPath = path.join(cwd, 'package.json');
37
+ try {
38
+ await fs.promises.access(packageJsonPath);
39
+ }
40
+ catch {
41
+ const message = `package.json not found at ${packageJsonPath}`;
42
+ if (!silent) {
43
+ console.error(chalk.red(`❌ ${message}`));
44
+ }
45
+ return { success: false, message, error: new Error(message) };
46
+ }
47
+ let scripts;
48
+ try {
49
+ const parsed = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8'));
50
+ if (isPackageJson(parsed)) {
51
+ scripts = parsed.scripts;
52
+ }
53
+ }
54
+ catch (error) {
55
+ const message = getErrorMessage(error, 'Failed to read package.json');
56
+ if (!silent) {
57
+ console.error(chalk.red(`❌ ${message}`));
58
+ }
59
+ return { success: false, message, error: error };
60
+ }
61
+ if (!silent) {
62
+ console.log(chalk.cyan(`📦 Installing dependencies using ${chalk.bold('pkgmgr')}...`));
63
+ console.log(chalk.gray(` pkgmgr automatically detects your package manager (npm, pnpm, yarn, or bun)`));
64
+ console.log(chalk.gray(` Running: pkgmgr install`));
65
+ }
66
+ const installResult = await runCommand('pkgmgr', ['install'], {
67
+ cwd,
68
+ silent,
69
+ });
70
+ if (!installResult.success) {
71
+ return formatCommandResultError(installResult, 'Failed to install dependencies');
72
+ }
73
+ const hasCustomSetup = Boolean(scripts?.['setup:custom']);
74
+ if (hasCustomSetup) {
75
+ if (!silent) {
76
+ console.log(chalk.cyan(`🔧 Running npm run setup:custom...`));
77
+ }
78
+ const customResult = await runCommand('npm', ['run', 'setup:custom'], {
79
+ cwd,
80
+ silent,
81
+ });
82
+ if (!customResult.success) {
83
+ return formatCommandResultError(customResult, 'Failed to run setup:custom');
84
+ }
85
+ }
86
+ const message = 'Workshop setup complete';
87
+ if (!silent) {
88
+ console.log(chalk.green(`✅ ${message}`));
89
+ }
90
+ return { success: true, message };
91
+ }
@@ -6,12 +6,12 @@ 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';
10
9
  import chalk from 'chalk';
11
- import { execa } from 'execa';
12
10
  import { matchSorter, rankings } from 'match-sorter';
13
11
  import ora from 'ora';
14
12
  import { assertCanPrompt, isCiEnvironment } from "../utils/cli-runtime.js";
13
+ import { runCommand, runCommandInteractive } from "../utils/command-runner.js";
14
+ import { setup } from "./setup.js";
15
15
  const GITHUB_ORG = 'epicweb-dev';
16
16
  const TUTORIAL_REPO = 'epicshop-tutorial';
17
17
  const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
@@ -343,14 +343,7 @@ async function addSingleWorkshop(repoName, options) {
343
343
  error: cloneResult.error,
344
344
  };
345
345
  }
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
- });
346
+ const setupResult = await setup({ cwd: workshopPath, silent });
354
347
  if (!setupResult.success) {
355
348
  // Clean up the cloned directory on setup failure
356
349
  if (!silent) {
@@ -364,7 +357,7 @@ async function addSingleWorkshop(repoName, options) {
364
357
  }
365
358
  return {
366
359
  success: false,
367
- message: `Failed to run setup: ${setupResult.message}`,
360
+ message: `Failed to set up workshop: ${setupResult.message}`,
368
361
  error: setupResult.error,
369
362
  };
370
363
  }
@@ -794,12 +787,13 @@ export async function list({ silent = false, } = {}) {
794
787
  });
795
788
  },
796
789
  });
790
+ const startCommand = 'npm run start';
797
791
  // Show actions for selected workshop
798
792
  const actionChoices = [
799
793
  {
800
794
  name: 'Start workshop',
801
795
  value: 'start',
802
- description: 'Run npm start in the workshop directory',
796
+ description: `Run ${startCommand} in the workshop directory`,
803
797
  },
804
798
  {
805
799
  name: 'Open in editor',
@@ -1084,8 +1078,8 @@ export async function startWorkshop(options = {}) {
1084
1078
  console.log(chalk.cyan(`🚀 Starting ${chalk.bold(workshopToStart.title)}...`));
1085
1079
  console.log(chalk.gray(` Path: ${workshopToStart.path}\n`));
1086
1080
  }
1087
- // Run npm start in the workshop directory
1088
- const startResult = await runCommandInteractive('npm', ['start'], {
1081
+ // Run start script in the workshop directory
1082
+ const startResult = await runCommandInteractive('npm', ['run', 'start'], {
1089
1083
  cwd: workshopToStart.path,
1090
1084
  });
1091
1085
  if (!startResult.success) {
@@ -1282,14 +1276,20 @@ export async function config(options = {}) {
1282
1276
  return { success: true, message: 'Cancelled' };
1283
1277
  }
1284
1278
  }
1279
+ // Handle CLI flags for setting config values
1280
+ const messages = [];
1285
1281
  if (options.reposDir) {
1286
1282
  // Set the repos directory directly via CLI flag
1287
1283
  const resolvedPath = path.resolve(options.reposDir);
1288
1284
  await setReposDirectory(resolvedPath);
1289
1285
  const message = `Repos directory set to: ${resolvedPath}`;
1286
+ messages.push(message);
1290
1287
  if (!silent)
1291
1288
  console.log(chalk.green(`✅ ${message}`));
1292
- return { success: true, message };
1289
+ }
1290
+ // If either option was set, return now
1291
+ if (messages.length > 0) {
1292
+ return { success: true, message: messages.join('; ') };
1293
1293
  }
1294
1294
  if (silent) {
1295
1295
  // In silent mode, just return current config
@@ -1719,7 +1719,7 @@ async function promptAndSetupAccessibleWorkshops() {
1719
1719
  const { workshopExists } = await import('@epic-web/workshop-utils/workshops.server');
1720
1720
  console.log(chalk.bold.cyan('\n📚 Workshop Setup\n'));
1721
1721
  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' +
1722
+ console.log(chalk.gray(` This will clone each workshop repo into your workshops directory and run setup.\n` +
1723
1723
  ' (If something fails, we’ll keep going and you can retry later with `npx epicshop add`.)\n'));
1724
1724
  assertCanPrompt({
1725
1725
  reason: 'select workshops to set up',
@@ -2000,12 +2000,7 @@ async function ensureTutorialAndStart() {
2000
2000
  error: cloneResult.error,
2001
2001
  };
2002
2002
  }
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
- });
2003
+ const setupResult = await setup({ cwd: workshopPath, silent: false });
2009
2004
  if (!setupResult.success) {
2010
2005
  // Clean up on failure
2011
2006
  console.log(chalk.yellow(`🧹 Cleaning up cloned directory...`));
@@ -2015,10 +2010,10 @@ async function ensureTutorialAndStart() {
2015
2010
  catch {
2016
2011
  // Ignore cleanup errors
2017
2012
  }
2018
- console.error(chalk.red(`❌ Failed to run setup: ${setupResult.message}`));
2013
+ console.error(chalk.red(`❌ Failed to set up workshop: ${setupResult.message}`));
2019
2014
  return {
2020
2015
  success: false,
2021
- message: `Failed to run setup: ${setupResult.message}`,
2016
+ message: `Failed to set up workshop: ${setupResult.message}`,
2022
2017
  error: setupResult.error,
2023
2018
  };
2024
2019
  }
@@ -2034,7 +2029,7 @@ async function ensureTutorialAndStart() {
2034
2029
  await promptToStartTutorial(workshopTitle);
2035
2030
  console.log(chalk.cyan(`\n🚀 Starting ${chalk.bold(workshopTitle)}...\n`));
2036
2031
  // Start the workshop
2037
- const startResult = await runCommandInteractive('npm', ['start'], {
2032
+ const startResult = await runCommandInteractive('npm', ['run', 'start'], {
2038
2033
  cwd: workshopPath,
2039
2034
  });
2040
2035
  if (!startResult.success) {
@@ -2200,40 +2195,3 @@ async function directoryExists(dirPath) {
2200
2195
  return false;
2201
2196
  }
2202
2197
  }
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
+ }
@@ -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.69.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.69.0",
97
103
  "@inquirer/prompts": "^8.2.0",
98
104
  "@sentry/node": "^10.35.0",
99
105
  "chalk": "^5.6.2",