Skip to content

Commit 0110989

Browse files
rexxarsclaude
andauthored
fix: allow running sanity debug outside of project context (#678)
* fix: allow running `sanity debug` outside of project context * fix: clean up gatherProjectConfig and remove dead code - Remove unnecessary try/catch and async from gatherProjectConfig (only does synchronous property access) - Remove unused findSanityModulesVersions mocks from no-project tests - Fix Promise.resolve() returning void instead of undefined Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: display project config errors as messages, not formatted objects When projectConfig is an Error (e.g. missing projectId), show the error message in red instead of dumping a formatted Error object. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: show clear messages for missing project/config in debug output - "No project found" when outside a project directory - "No CLI configuration file found" when in a project but config is invalid Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: distinguish invalid config from missing config in debug output - "CLI configuration error: <message>" when config exists but is invalid - "No CLI configuration file found" as defensive fallback - "No project found" when outside a project directory Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2afef5a commit 0110989

File tree

4 files changed

+100
-38
lines changed

4 files changed

+100
-38
lines changed

packages/@sanity/cli/src/actions/debug/gatherDebugInfo.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export async function gatherDebugInfo(options: DebugInfoOptions): Promise<DebugI
1919
const [auth, globalConfig, projectConfigResult, versions] = await Promise.all([
2020
gatherAuthInfo(includeSecrets),
2121
gatherGlobalConfig(),
22-
gatherProjectConfig(cliConfig),
22+
cliConfig ? gatherProjectConfig(cliConfig) : undefined,
2323
gatherVersionsInfo(projectRoot),
2424
])
2525

@@ -52,21 +52,21 @@ function gatherGlobalConfig(): Record<string, unknown> {
5252
return getUserConfig().all
5353
}
5454

55-
async function gatherProjectConfig(cliConfig: CliConfig): Promise<CliConfig | Error> {
56-
try {
57-
const config = cliConfig
55+
function gatherProjectConfig(cliConfig: CliConfig): CliConfig | Error {
56+
if (!cliConfig.api?.projectId) {
57+
return new Error('Missing required "api.projectId" key')
58+
}
5859

59-
if (!config.api?.projectId) {
60-
return new Error('Missing required "api.projectId" key')
61-
}
60+
return cliConfig
61+
}
6262

63-
return config
64-
} catch (error) {
65-
return error instanceof Error ? error : new Error('Failed to load project config')
63+
async function gatherVersionsInfo(
64+
projectRoot: ProjectRootResult | undefined,
65+
): Promise<ModuleVersionResult[] | undefined> {
66+
if (!projectRoot) {
67+
return undefined
6668
}
67-
}
6869

69-
async function gatherVersionsInfo(projectRoot: ProjectRootResult): Promise<ModuleVersionResult[]> {
7070
try {
7171
return await findSanityModulesVersions({cwd: projectRoot.directory})
7272
} catch {
@@ -75,7 +75,7 @@ async function gatherVersionsInfo(projectRoot: ProjectRootResult): Promise<Modul
7575
}
7676

7777
async function gatherUserInfo(
78-
projectConfig: CliConfig | Error,
78+
projectConfig: CliConfig | Error | undefined,
7979
hasToken: boolean,
8080
): Promise<Error | UserInfo | null> {
8181
if (!hasToken) {
@@ -88,7 +88,7 @@ async function gatherUserInfo(
8888
* Otherwise, get the user for the global client
8989
*/
9090
const userInfo =
91-
projectConfig instanceof Error || !projectConfig.api?.projectId
91+
!projectConfig || projectConfig instanceof Error || !projectConfig.api?.projectId
9292
? await getCliUser()
9393
: await getProjectUser(projectConfig.api.projectId)
9494

@@ -103,11 +103,11 @@ async function gatherUserInfo(
103103
}
104104

105105
async function gatherProjectInfo(
106-
projectConfig: CliConfig | Error,
106+
projectConfig: CliConfig | Error | undefined,
107107
hasToken: boolean,
108108
user: Error | UserInfo | null,
109109
): Promise<Error | ProjectInfo | null> {
110-
if (!hasToken || projectConfig instanceof Error) {
110+
if (!hasToken || !projectConfig || projectConfig instanceof Error) {
111111
return null
112112
}
113113

packages/@sanity/cli/src/actions/debug/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import {type CliConfig, type ProjectRootResult} from '@sanity/cli-core'
33
import {type ModuleVersionResult} from '../versions/types.js'
44

55
export interface DebugInfoOptions {
6-
cliConfig: CliConfig
6+
cliConfig: CliConfig | undefined
77
includeSecrets: boolean
8-
projectRoot: ProjectRootResult
8+
projectRoot: ProjectRootResult | undefined
99
}
1010

1111
export interface UserInfo {
@@ -30,7 +30,7 @@ export interface DebugInfo {
3030
auth: AuthInfo
3131
globalConfig: Record<string, unknown>
3232
project: Error | ProjectInfo | null
33-
projectConfig: CliConfig | Error
33+
projectConfig: CliConfig | Error | undefined
3434
user: Error | UserInfo | null
35-
versions: ModuleVersionResult[]
35+
versions: ModuleVersionResult[] | undefined
3636
}

packages/@sanity/cli/src/commands/__tests__/debug.test.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {getCliToken, getCliUserConfig} from '@sanity/cli-core'
1+
import {getCliToken, getCliUserConfig, ProjectRootNotFoundError} from '@sanity/cli-core'
22
import {mockApi, testCommand} from '@sanity/cli-test'
33
import nock from 'nock'
44
import {afterEach, describe, expect, test, vi} from 'vitest'
@@ -290,39 +290,37 @@ describe('#debug', () => {
290290
expect(stdout).toContain("Roles: [ 'administrator' ]")
291291
})
292292

293-
test('handles case when no project config is present', async () => {
294-
// Mock authentication
293+
test('shows config error when project config exists but is invalid', async () => {
295294
vi.mocked(getCliToken).mockResolvedValue('mock-auth-token')
296295
vi.mocked(getCliUserConfig).mockImplementation(async (key: string) => {
297296
if (key === 'authToken') return 'mock-auth-token'
298297
return undefined
299298
})
300299
vi.mocked(findSanityModulesVersions).mockResolvedValue([])
301300

302-
// Mock the /me API endpoint to return user info
303-
// Uses global API host since there's no projectId
301+
// Mock the /me API endpoint (uses global API host since there's no projectId)
304302
mockApi({apiVersion: USERS_API_VERSION, uri: '/users/me'}).reply(200, {
305303
email: 'test@example.com',
306304
id: 'user123',
307305
name: 'Test User',
308306
})
309307

310-
// No project API mock needed since no valid projectId
311-
312-
const {stdout} = await testCommand(Debug, [], {
308+
const {error, stdout} = await testCommand(Debug, [], {
313309
mocks: {
314310
...defaultMocks,
315311
cliConfig: {
316312
api: {
317-
// No projectId - this will cause project config to be invalid
313+
// No projectId - config exists but is invalid
318314
},
319315
},
320316
},
321317
})
322318

319+
if (error) throw error
323320
expect(stdout).toContain('Global config')
324-
expect(stdout).toContain('Project config')
321+
expect(stdout).toContain('CLI configuration error:')
325322
expect(stdout).toContain('Missing required "api.projectId" key')
323+
expect(stdout).not.toContain('Project config')
326324
})
327325

328326
test('handles case when no versions are present', async () => {
@@ -559,6 +557,60 @@ describe('#debug', () => {
559557
expect(stdout).toContain("Roles: [ '<none>' ]")
560558
})
561559

560+
test('works outside a project directory (no project root)', async () => {
561+
vi.mocked(getCliToken).mockResolvedValue('mock-auth-token')
562+
vi.mocked(getCliUserConfig).mockImplementation(async (key: string) => {
563+
if (key === 'authToken') return 'mock-auth-token'
564+
return undefined
565+
})
566+
567+
// Mock the /me API endpoint (uses global API host since no project)
568+
mockApi({apiVersion: USERS_API_VERSION, uri: '/users/me'}).reply(200, {
569+
email: 'test@example.com',
570+
id: 'user123',
571+
name: 'Test User',
572+
})
573+
574+
const {error, stdout} = await testCommand(Debug, [], {
575+
mocks: {
576+
cliConfigError: new ProjectRootNotFoundError('No project root found'),
577+
token: 'mock-auth-token',
578+
},
579+
})
580+
581+
if (error) throw error
582+
expect(stdout).toContain('User:')
583+
expect(stdout).toContain("Email: 'test@example.com'")
584+
expect(stdout).toContain('Authentication:')
585+
expect(stdout).toContain('Global config')
586+
expect(stdout).toContain('No project found')
587+
// Should NOT contain project-specific sections
588+
expect(stdout).not.toContain('Project:')
589+
expect(stdout).not.toContain('Project config')
590+
expect(stdout).not.toContain('Package versions:')
591+
})
592+
593+
test('works outside a project directory when not logged in', async () => {
594+
vi.mocked(getCliToken).mockResolvedValue(undefined)
595+
vi.mocked(getCliUserConfig).mockImplementation(async () => undefined)
596+
597+
const {error, stdout} = await testCommand(Debug, [], {
598+
mocks: {
599+
cliConfigError: new ProjectRootNotFoundError('No project root found'),
600+
token: undefined,
601+
},
602+
})
603+
604+
if (error) throw error
605+
expect(stdout).toContain('User:')
606+
expect(stdout).toContain('Not logged in')
607+
expect(stdout).toContain('Global config')
608+
expect(stdout).toContain('No project found')
609+
expect(stdout).not.toContain('Authentication:')
610+
expect(stdout).not.toContain('Project:')
611+
expect(stdout).not.toContain('Package versions:')
612+
})
613+
562614
test('handles project member with no roles gracefully', async () => {
563615
// Mock authentication
564616
vi.mocked(getCliToken).mockResolvedValue('mock-auth-token')

packages/@sanity/cli/src/commands/debug.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'node:path'
22
import {styleText} from 'node:util'
33

44
import {Flags} from '@oclif/core'
5-
import {SanityCommand} from '@sanity/cli-core'
5+
import {ProjectRootNotFoundError, SanityCommand} from '@sanity/cli-core'
66
import omit from 'lodash-es/omit.js'
77
import padStart from 'lodash-es/padStart.js'
88

@@ -30,8 +30,14 @@ export class Debug extends SanityCommand<typeof Debug> {
3030
const {flags} = this
3131

3232
try {
33-
const projectRoot = await this.getProjectRoot()
34-
const cliConfig = await this.getCliConfig()
33+
let projectRoot
34+
try {
35+
projectRoot = await this.getProjectRoot()
36+
} catch (err) {
37+
if (!(err instanceof ProjectRootNotFoundError)) throw err
38+
}
39+
40+
const cliConfig = projectRoot ? await this.getCliConfig() : undefined
3541

3642
const {auth, globalConfig, project, projectConfig, user, versions} = await gatherDebugInfo({
3743
cliConfig,
@@ -80,14 +86,18 @@ export class Debug extends SanityCommand<typeof Debug> {
8086
const globalCfg = omit(globalConfig, ['authType', 'authToken'])
8187
this.log(` ${formatObject(globalCfg).replaceAll('\n', '\n ')}\n`)
8288

83-
// Project configuration (projectDir/sanity.json)
84-
if (projectConfig) {
85-
const configLocation = projectConfig
86-
? ` (${styleText('yellow', path.relative(process.cwd(), projectRoot.path))})`
87-
: ''
89+
// Project configuration (projectDir/sanity.cli.ts)
90+
if (!projectRoot) {
91+
this.log('No project found\n')
92+
} else if (projectConfig instanceof Error) {
93+
this.log(`CLI configuration error: ${styleText('red', projectConfig.message)}\n`)
94+
} else if (projectConfig) {
95+
const configLocation = ` (${styleText('yellow', path.relative(process.cwd(), projectRoot.path))})`
8896

8997
this.log(`Project config${configLocation}:`)
9098
this.log(` ${formatObject(projectConfig).replaceAll('\n', '\n ')}`)
99+
} else {
100+
this.log('No CLI configuration file found\n')
91101
}
92102

93103
// Print installed package versions

0 commit comments

Comments
 (0)