Open
Conversation
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Removing error detail printing loses unexpected startup error info
- Added conditional error-detail printing when the error message differs from the main failure message in the start command handler.
- ✅ Fixed: Interactive menu start failure produces no error output
- Interactive menu start now prints the failure message (and distinct error detail) before exiting.
Preview (6cacbda73a)
diff --git a/docs/cli.md b/docs/cli.md
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -1326,10 +1326,20 @@
If the CLI can't find the workshop app:
-1. Ensure the workshop app is installed: `npm install -g @epic-web/workshop-app`
-2. Set the `EPICSHOP_APP_LOCATION` environment variable
-3. Use the `--app-location` flag to specify the path
+1. If you are inside a workshop repository, run `npm install` from the workshop
+ root first so its local `epicshop` install can resolve
+ `@epic-web/workshop-app`
+2. If the workshop keeps a local `epicshop` directory (for example `./epicshop`
+ or `./.epicshop`), reinstall that local directory if it is missing or
+ incomplete
+3. Set the `EPICSHOP_APP_LOCATION` environment variable or pass `--app-location`
+ if your workshop app lives in a separate checkout
+4. If you are relying on a global install, run
+ `npm install -g @epic-web/workshop-app`
+When startup fails, the CLI now lists each lookup it attempted so you can see
+which path or installation method was checked.
+
### Port Conflicts
The CLI automatically finds available ports starting from 3742. If you encounter
diff --git a/packages/workshop-cli/src/cli.ts b/packages/workshop-cli/src/cli.ts
--- a/packages/workshop-cli/src/cli.ts
+++ b/packages/workshop-cli/src/cli.ts
@@ -148,12 +148,10 @@
if (!result.success) {
if (!argv.silent) {
- console.error(
- chalk.red(
- `❌ ${result.message || 'Failed to start workshop application'}`,
- ),
- )
- if (result.error) {
+ const message =
+ result.message || 'Failed to start workshop application'
+ console.error(chalk.red(`❌ ${message}`))
+ if (result.error?.message && result.error.message !== message) {
console.error(chalk.red(result.error.message))
}
}
@@ -1804,7 +1802,15 @@
.catch(() => {})
const { start } = await import('./commands/start.js')
const result = await start({})
- if (!result.success) process.exit(1)
+ if (!result.success) {
+ const message =
+ result.message || 'Failed to start workshop application'
+ console.error(chalk.red(`❌ ${message}`))
+ if (result.error?.message && result.error.message !== message) {
+ console.error(chalk.red(result.error.message))
+ }
+ process.exit(1)
+ }
} finally {
process.chdir(originalCwd)
}
diff --git a/packages/workshop-cli/src/commands/start.test.ts b/packages/workshop-cli/src/commands/start.test.ts
--- a/packages/workshop-cli/src/commands/start.test.ts
+++ b/packages/workshop-cli/src/commands/start.test.ts
@@ -5,7 +5,11 @@
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
-import { test } from 'vitest'
+import { expect, test } from 'vitest'
+import {
+ buildWorkshopAppNotFoundMessage,
+ resolveWorkshopAppLocation,
+} from './workshop-app-location.ts'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(__dirname, '..', '..', '..', '..')
@@ -59,6 +63,65 @@
20000,
)
+test('start explains when a workshop repo is missing its local epicshop install (aha)', async () => {
+ const workshopRoot = '/tmp/advanced-react-apis'
+ const resolution = await resolveWorkshopAppLocation(
+ {},
+ {
+ cwd: () => path.join(workshopRoot, 'exercises', '01.problem'),
+ env: {},
+ homedir: () => '/home/tester',
+ readTextFile: async (filePath) => {
+ if (filePath === path.join(workshopRoot, 'package.json')) {
+ return JSON.stringify({
+ name: 'advanced-react-apis',
+ epicshop: { title: 'Advanced React APIs' },
+ scripts: {
+ start: 'npx --prefix ./.epicshop epicshop start',
+ },
+ })
+ }
+ throw Object.assign(
+ new Error(`ENOENT: no such file or directory, open '${filePath}'`),
+ {
+ code: 'ENOENT',
+ },
+ )
+ },
+ accessPath: async (filePath) => {
+ if (filePath === path.join(workshopRoot, 'package.json')) return
+ throw Object.assign(
+ new Error(`ENOENT: no such file or directory, access '${filePath}'`),
+ {
+ code: 'ENOENT',
+ },
+ )
+ },
+ resolveImport: () => {
+ throw new Error('not resolved in test')
+ },
+ runCommand: () => '/global/node_modules',
+ },
+ )
+
+ expect(resolution.appDir).toBeNull()
+ expect(resolution.workshopContext).toEqual({
+ workshopRoot,
+ localCliPrefix: './.epicshop',
+ localCliDir: path.join(workshopRoot, '.epicshop'),
+ localCliDirExists: false,
+ })
+
+ const message = buildWorkshopAppNotFoundMessage(resolution)
+
+ expect(message).toContain('This looks like a workshop repository')
+ expect(message).toContain('Run `npm install` in the workshop root')
+ expect(message).toContain('`./.epicshop`')
+ expect(message).toContain('Lookups attempted:')
+ expect(message).toContain('EPICSHOP_APP_LOCATION: not set')
+ expect(message).toContain('--app-location: not provided')
+})
+
async function createRunnerFixture() {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'epicshop-start-'))
const appDir = path.join(rootDir, 'fake-workshop')
@@ -69,7 +132,7 @@
path.join(appDir, 'package.json'),
JSON.stringify(
{
- name: 'fake-workshop',
+ name: '@epic-web/workshop-app',
version: '0.0.0',
type: 'module',
epicshop: {
diff --git a/packages/workshop-cli/src/commands/start.ts b/packages/workshop-cli/src/commands/start.ts
--- a/packages/workshop-cli/src/commands/start.ts
+++ b/packages/workshop-cli/src/commands/start.ts
@@ -1,17 +1,21 @@
// oxlint-disable-next-line import/order -- must appear first
import { getEnv } from '@epic-web/workshop-utils/init-env'
-import { spawn, type ChildProcess, execSync } from 'node:child_process'
+import { spawn, type ChildProcess } from 'node:child_process'
import crypto from 'node:crypto'
import fs from 'node:fs'
import http from 'node:http'
import os from 'node:os'
import path from 'node:path'
-import { fileURLToPath, pathToFileURL } from 'node:url'
+import { pathToFileURL } from 'node:url'
import chalk from 'chalk'
import closeWithGrace from 'close-with-grace'
import getPort from 'get-port'
import open from 'open'
+import {
+ buildWorkshopAppNotFoundMessage,
+ resolveWorkshopAppLocation,
+} from './workshop-app-location.ts'
export type StartOptions = {
appLocation?: string
@@ -111,30 +115,13 @@
*/
export async function start(options: StartOptions = {}): Promise<StartResult> {
try {
- // Find workshop-app directory using new resolution order
- const appDir = await findWorkshopAppDir(options.appLocation)
+ const resolution = await resolveWorkshopAppLocation({
+ appLocation: options.appLocation,
+ })
+ const appDir = resolution.appDir
if (!appDir) {
- const errorMessage =
- 'Could not locate workshop-app directory. Please ensure the workshop app is installed or specify its location using:\n - Environment variable: EPICSHOP_APP_LOCATION\n - Command line flag: --app-location\n - Global installation: npm install -g @epic-web/workshop-app'
+ const errorMessage = buildWorkshopAppNotFoundMessage(resolution)
- if (!options.silent) {
- console.error(chalk.red('❌ Could not locate workshop-app directory'))
- console.error(
- chalk.yellow(
- 'Please ensure the workshop app is installed or specify its location using:',
- ),
- )
- console.error(
- chalk.yellow(' - Environment variable: EPICSHOP_APP_LOCATION'),
- )
- console.error(chalk.yellow(' - Command line flag: --app-location'))
- console.error(
- chalk.yellow(
- ' - Global installation: npm install -g @epic-web/workshop-app',
- ),
- )
- }
-
return {
success: false,
message: errorMessage,
@@ -642,110 +629,6 @@
})
}
-async function findWorkshopAppDir(
- appLocation?: string,
-): Promise<string | null> {
- // 1. Check process.env.EPICSHOP_APP_LOCATION
- if (process.env.EPICSHOP_APP_LOCATION) {
- const envDir = path.resolve(process.env.EPICSHOP_APP_LOCATION)
- try {
- await fs.promises.access(path.join(envDir, 'package.json'))
- return envDir
- } catch {
- // Continue to next step
- }
- }
-
- // 2. Check command line flag --app-location
- if (appLocation) {
- const flagDir = path.resolve(appLocation)
- try {
- await fs.promises.access(path.join(flagDir, 'package.json'))
- return flagDir
- } catch {
- // Continue to next step
- }
- }
-
- // 3. Node's resolution process
- try {
- const workshopAppPath = import.meta
- .resolve('@epic-web/workshop-app/package.json')
- const packagePath = fileURLToPath(workshopAppPath)
- return path.dirname(packagePath)
- } catch {
- // Continue to next step
- }
-
- // 4. Global installation lookup
- try {
- const globalDir = await findGlobalWorkshopApp()
- if (globalDir) {
- return globalDir
- }
- } catch {
- // Continue to next step
- }
-
- // Fallback for development (when running from a monorepo)
- try {
- const cliPkgPath = import.meta.resolve('epicshop/package.json')
- const cliPkgDir = path.dirname(fileURLToPath(cliPkgPath))
- const relativePath = path.resolve(cliPkgDir, '..', '..', 'workshop-app')
- try {
- await fs.promises.access(path.join(relativePath, 'package.json'))
- return relativePath
- } catch {
- // Continue to final return
- }
- } catch {
- // Continue to final return
- }
-
- return null
-}
-
-async function findGlobalWorkshopApp(): Promise<string | null> {
- // Try to find globally installed workshop app
- try {
- const npmRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim()
- const globalAppPath = path.join(npmRoot, '@epic-web/workshop-app')
- try {
- await fs.promises.access(path.join(globalAppPath, 'package.json'))
- return globalAppPath
- } catch {
- // Continue to common global locations
- }
- } catch {
- // If npm root -g fails, try common global locations
- }
-
- // Try common global locations
- const commonGlobalPaths = [
- path.join(
- os.homedir(),
- '.npm-global/lib/node_modules/@epic-web/workshop-app',
- ),
- path.join(
- os.homedir(),
- '.npm-packages/lib/node_modules/@epic-web/workshop-app',
- ),
- '/usr/local/lib/node_modules/@epic-web/workshop-app',
- '/usr/lib/node_modules/@epic-web/workshop-app',
- ]
-
- for (const globalPath of commonGlobalPaths) {
- try {
- await fs.promises.access(path.join(globalPath, 'package.json'))
- return globalPath
- } catch {
- // Continue to next path
- }
- }
-
- return null
-}
-
async function appIsPublished(appDir: string): Promise<boolean> {
if (process.env.EPICSHOP_IS_PUBLISHED) {
return (
diff --git a/packages/workshop-cli/src/commands/workshop-app-location.ts b/packages/workshop-cli/src/commands/workshop-app-location.ts
new file mode 100644
--- /dev/null
+++ b/packages/workshop-cli/src/commands/workshop-app-location.ts
@@ -1,0 +1,434 @@
+import { execSync } from 'node:child_process'
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+type ReadTextFile = (filePath: string) => Promise<string>
+type AccessPath = (filePath: string) => Promise<void>
+type ResolveImport = (specifier: string) => string
+type RunCommand = (command: string) => string
+
+type PackageJson = {
+ name?: string
+ epicshop?: unknown
+ scripts?: Record<string, string | undefined>
+}
+
+type WorkshopContext = {
+ workshopRoot: string
+ localCliPrefix?: string
+ localCliDir?: string
+ localCliDirExists: boolean
+}
+
+export type WorkshopAppResolutionAttempt = {
+ label: string
+ detail: string
+}
+
+export type WorkshopAppResolution = {
+ appDir: string | null
+ attempts: Array<WorkshopAppResolutionAttempt>
+ workshopContext: WorkshopContext | null
+}
+
+type ResolutionDeps = {
+ env?: Partial<NodeJS.ProcessEnv>
+ cwd?: () => string
+ homedir?: () => string
+ readTextFile?: ReadTextFile
+ accessPath?: AccessPath
+ resolveImport?: ResolveImport
+ runCommand?: RunCommand
+}
+
+export async function resolveWorkshopAppLocation(
+ options: { appLocation?: string } = {},
+ deps: ResolutionDeps = {},
+): Promise<WorkshopAppResolution> {
+ const attempts: Array<WorkshopAppResolutionAttempt> = []
+ const env = deps.env ?? process.env
+ const readTextFile =
+ deps.readTextFile ??
+ (async (filePath: string) => fs.promises.readFile(filePath, 'utf8'))
+ const accessPath =
+ deps.accessPath ??
+ (async (filePath: string) => fs.promises.access(filePath))
+ const resolveImport =
+ deps.resolveImport ??
+ ((specifier: string) => import.meta.resolve(specifier))
+ const runCommand =
+ deps.runCommand ??
+ ((command: string) => execSync(command, { encoding: 'utf-8' }).trim())
+ const cwd = deps.cwd ?? (() => process.cwd())
+ const homedir = deps.homedir ?? (() => os.homedir())
+
+ const workshopContext = await findWorkshopContext(cwd(), {
+ readTextFile,
+ accessPath,
+ })
+
+ const envLocation = env.EPICSHOP_APP_LOCATION?.trim()
+ if (envLocation) {
+ const envDir = path.resolve(envLocation)
+ const envAttempt = await inspectWorkshopAppDir(
+ envDir,
+ 'EPICSHOP_APP_LOCATION',
+ { readTextFile, accessPath },
+ )
+ attempts.push(envAttempt.attempt)
+ if (envAttempt.appDir) {
+ return { appDir: envAttempt.appDir, attempts, workshopContext }
+ }
+ } else {
+ attempts.push({
+ label: 'EPICSHOP_APP_LOCATION',
+ detail: 'not set',
+ })
+ }
+
+ const flagLocation = options.appLocation?.trim()
+ if (flagLocation) {
+ const flagDir = path.resolve(flagLocation)
+ const flagAttempt = await inspectWorkshopAppDir(flagDir, '--app-location', {
+ readTextFile,
+ accessPath,
+ })
+ attempts.push(flagAttempt.attempt)
+ if (flagAttempt.appDir) {
+ return { appDir: flagAttempt.appDir, attempts, workshopContext }
+ }
+ } else {
+ attempts.push({
+ label: '--app-location',
+ detail: 'not provided',
+ })
+ }
+
+ try {
+ const packagePath = toFilePath(
+ resolveImport('@epic-web/workshop-app/package.json'),
+ )
+ const resolvedAttempt = await inspectWorkshopAppDir(
+ path.dirname(packagePath),
+ 'local package resolution',
+ { readTextFile, accessPath },
+ )
+ attempts.push(resolvedAttempt.attempt)
+ if (resolvedAttempt.appDir) {
+ return { appDir: resolvedAttempt.appDir, attempts, workshopContext }
+ }
+ } catch (error) {
+ attempts.push({
+ label: 'local package resolution',
+ detail: `could not resolve @epic-web/workshop-app/package.json (${formatError(
+ error,
+ )})`,
+ })
+ }
+
+ const globalResult = await findGlobalWorkshopApp({
+ homedir,
+ readTextFile,
+ accessPath,
+ runCommand,
+ })
+ attempts.push(...globalResult.attempts)
+ if (globalResult.appDir) {
+ return { appDir: globalResult.appDir, attempts, workshopContext }
+ }
+
+ try {
+ const cliPkgPath = toFilePath(resolveImport('epicshop/package.json'))
+ const cliPkgDir = path.dirname(cliPkgPath)
+ const relativePath = path.resolve(cliPkgDir, '..', '..', 'workshop-app')
+ const fallbackAttempt = await inspectWorkshopAppDir(
+ relativePath,
+ 'monorepo fallback',
+ { readTextFile, accessPath },
+ )
+ attempts.push(fallbackAttempt.attempt)
+ if (fallbackAttempt.appDir) {
+ return { appDir: fallbackAttempt.appDir, attempts, workshopContext }
+ }
+ } catch (error) {
+ attempts.push({
+ label: 'monorepo fallback',
+ detail: `could not resolve epicshop/package.json (${formatError(error)})`,
+ })
+ }
+
+ return { appDir: null, attempts, workshopContext }
+}
+
+export function buildWorkshopAppNotFoundMessage(
+ resolution: WorkshopAppResolution,
+): string {
+ const lines = [
+ 'Could not find `@epic-web/workshop-app`, so `epicshop start` does not know which app to launch.',
+ ]
+
+ if (resolution.workshopContext) {
+ const { workshopRoot, localCliPrefix, localCliDir, localCliDirExists } =
+ resolution.workshopContext
+ lines.push('', `This looks like a workshop repository: ${workshopRoot}`)
+
+ if (localCliPrefix && localCliDir) {
+ lines.push(
+ localCliDirExists
+ ? `Its start script uses a local epicshop install at \`${localCliPrefix}\` (${localCliDir}), but that install could not resolve the workshop app package.`
+ : `Its start script points at a local epicshop install \`${localCliPrefix}\` (${localCliDir}), but that directory does not exist.`,
+ )
+ } else {
+ lines.push(
+ 'That usually means the workshop dependencies in this repository have not been installed yet.',
+ )
+ }
+ }
+
+ lines.push('', 'Lookups attempted:')
+ for (const attempt of resolution.attempts) {
+ lines.push(`- ${attempt.label}: ${attempt.detail}`)
+ }
+
+ lines.push('', 'Try this:')
+ if (resolution.workshopContext) {
+ const { workshopRoot, localCliPrefix } = resolution.workshopContext
+ lines.push(`1. Run \`npm install\` in the workshop root: ${workshopRoot}`)
+ if (localCliPrefix) {
+ lines.push(
+ `2. If that still fails, remove and reinstall the local epicshop directory at \`${localCliPrefix}\`.`,
+ )
+ } else {
+ lines.push(
+ '2. If that still fails, reinstall the workshop dependencies from the workshop root.',
+ )
+ }
+ lines.push(
+ '3. If you keep `@epic-web/workshop-app` in a separate checkout, point to it with `--app-location` or `EPICSHOP_APP_LOCATION`.',
+ )
+ } else {
+ lines.push(
+ '1. Install `@epic-web/workshop-app` where this `epicshop` CLI can resolve it.',
+ )
+ lines.push(
+ '2. Or point to an existing checkout with `--app-location` or `EPICSHOP_APP_LOCATION`.',
+ )
+ lines.push(
+ '3. For a global install, run `npm install -g @epic-web/workshop-app`.',
+ )
+ }
+
+ return lines.join('\n')
+}
+
+async function findWorkshopContext(
+ startDir: string,
+ deps: { readTextFile: ReadTextFile; accessPath: AccessPath },
+): Promise<WorkshopContext | null> {
+ let currentDir = path.resolve(startDir)
+ const root = path.parse(currentDir).root
+
+ while (true) {
+ const packageJsonPath = path.join(currentDir, 'package.json')
+ const packageJson = await readPackageJson(
+ packageJsonPath,
+ deps.readTextFile,
+ )
+ if (packageJson?.epicshop) {
+ const localCliPrefix = getLocalCliPrefix(packageJson)
+ const localCliDir = localCliPrefix
+ ? path.resolve(currentDir, localCliPrefix)
+ : undefined
+
+ return {
+ workshopRoot: currentDir,
+ localCliPrefix,
+ localCliDir,
+ localCliDirExists: localCliDir
+ ? await pathExists(
+ path.join(localCliDir, 'package.json'),
+ deps.accessPath,
+ )
+ : false,
+ }
+ }
+
+ if (currentDir === root) {
+ return null
+ }
+ currentDir = path.dirname(currentDir)
+ }
+}
+
+function getLocalCliPrefix(packageJson: PackageJson): string | undefined {
+ const candidateScripts = [
+ packageJson.scripts?.start,
+ packageJson.scripts?.dev,
+ ]
+ for (const script of candidateScripts) {
+ if (!script) continue
+ const match = script.match(/--prefix\s+(?:"([^"]+)"|'([^']+)'|([^\s]+))/)
+ const value = match?.[1] ?? match?.[2] ?? match?.[3]
+ if (value) {
+ return value
+ }
+ }
+ return undefined
+}
+
+async function inspectWorkshopAppDir(
+ appDir: string,
+ label: string,
+ deps: { readTextFile: ReadTextFile; accessPath: AccessPath },
+): Promise<{ appDir: string | null; attempt: WorkshopAppResolutionAttempt }> {
+ const packageJsonPath = path.join(appDir, 'package.json')
+ try {
+ await deps.accessPath(packageJsonPath)
+ } catch (error) {
+ return {
+ appDir: null,
+ attempt: {
+ label,
+ detail: `checked ${packageJsonPath}, but it was not readable (${formatError(
+ error,
+ )})`,
+ },
+ }
+ }
+
+ const packageJson = await readPackageJson(packageJsonPath, deps.readTextFile)
+ if (!packageJson) {
+ return {
+ appDir: null,
+ attempt: {
+ label,
+ detail: `found ${packageJsonPath}, but it could not be parsed as JSON`,
+ },
+ }
+ }
+
+ if (packageJson.name !== '@epic-web/workshop-app') {
+ const packageName = packageJson.name
+ ? `\`${packageJson.name}\``
+ : 'an unnamed package'
+ return {
+ appDir: null,
+ attempt: {
+ label,
+ detail: `checked ${packageJsonPath}, but it contains ${packageName} instead of \`@epic-web/workshop-app\``,
+ },
+ }
+ }
+
+ return {
+ appDir,
+ attempt: {
+ label,
+ detail: `resolved to ${appDir}`,
+ },
+ }
+}
+
+async function findGlobalWorkshopApp(deps: {
+ homedir: () => string
+ readTextFile: ReadTextFile
+ accessPath: AccessPath
+ runCommand: RunCommand
+}): Promise<WorkshopAppResolution> {
+ const attempts: Array<WorkshopAppResolutionAttempt> = []
+
+ try {
+ const npmRoot = deps.runCommand('npm root -g')
+ const globalAppPath = path.join(npmRoot, '@epic-web/workshop-app')
+ const npmRootAttempt = await inspectWorkshopAppDir(
+ globalAppPath,
+ 'global npm install',
+ {
+ readTextFile: deps.readTextFile,
+ accessPath: deps.accessPath,
+ },
+ )
+ attempts.push(npmRootAttempt.attempt)
+ if (npmRootAttempt.appDir) {
+ return { appDir: npmRootAttempt.appDir, attempts, workshopContext: null }
+ }
+ } catch (error) {
+ attempts.push({
+ label: 'global npm install',
+ detail: `failed to run \`npm root -g\` (${formatError(error)})`,
+ })
+ }
+
+ const commonGlobalPaths = [
+ path.join(
+ deps.homedir(),
+ '.npm-global/lib/node_modules/@epic-web/workshop-app',
+ ),
+ path.join(
+ deps.homedir(),
+ '.npm-packages/lib/node_modules/@epic-web/workshop-app',
+ ),
+ '/usr/local/lib/node_modules/@epic-web/workshop-app',
+ '/usr/lib/node_modules/@epic-web/workshop-app',
+ ]
+
+ for (const globalPath of commonGlobalPaths) {
+ const globalAttempt = await inspectWorkshopAppDir(
+ globalPath,
+ 'common global install paths',
+ {
+ readTextFile: deps.readTextFile,
+ accessPath: deps.accessPath,
+ },
+ )
+ if (globalAttempt.appDir) {
+ attempts.push(globalAttempt.attempt)
+ return { appDir: globalAttempt.appDir, attempts, workshopContext: null }
+ }
+ }
+
+ attempts.push({
+ label: 'common global install paths',
+ detail: `did not find the package in: ${commonGlobalPaths.join(', ')}`,
+ })
+
+ return { appDir: null, attempts, workshopContext: null }
+}
+
+async function readPackageJson(
+ packageJsonPath: string,
+ readTextFile: ReadTextFile,
+): Promise<PackageJson | null> {
+ try {
+ return JSON.parse(await readTextFile(packageJsonPath)) as PackageJson
+ } catch {
+ return null
+ }
+}
+
+async function pathExists(
+ filePath: string,
+ accessPath: AccessPath,
+): Promise<boolean> {
+ try {
+ await accessPath(filePath)
+ return true
+ } catch {
+ return false
+ }
+}
+
+function toFilePath(resolvedPath: string): string {
+ return resolvedPath.startsWith('file:')
+ ? fileURLToPath(resolvedPath)
+ : resolvedPath
+}
+
+function formatError(error: unknown): string {
+ if (error instanceof Error && error.message) {
+ return error.message
+ }
+ return String(error)
+}You can send follow-ups to this agent here.
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
epicshop startwhen@epic-web/workshop-appcannot be resolvedTesting
npm run typecheck -- --projects epicshopnpm run test -- packages/workshop-cli/src/commands/start.test.tsnpm run build -- --projects epicshophelpful-startup-message-demo.log
To show artifacts inline, enable in settings.