diff --git a/.github/workflows/windows-recovery.yml b/.github/workflows/windows-recovery.yml index 3350f66f66b..6ab2fc56d30 100644 --- a/.github/workflows/windows-recovery.yml +++ b/.github/workflows/windows-recovery.yml @@ -32,6 +32,8 @@ name: Windows recovery # regression this lane has caught that no other lane could have. That second # group is generated rather than curated, because a hand-picked list cannot be # shown to be complete and this one has to be. +# The CLI project-root parser is also included: node:path.isAbsolute has native +# Windows behavior without a win32 literal in the caller. # # What is deliberately absent is the portable rest of those workspaces. Every # source directory in the closure used to be here, which put this lane on 118 of @@ -65,7 +67,17 @@ on: # `process.platform === 'win32'`. Generated, not curated: # `scripts/windows-package-source-closure.test.mjs` recomputes this set # from the dist tests the steps run and fails on any difference in either - # direction. + # direction, including the CLI's native path parser described above. + - 'packages/cli/src/__tests__/runtime-host-service-manager.test.ts' + - 'packages/cli/src/runtime-host-cli-installation.ts' + - 'packages/cli/src/runtime-host-cli.ts' + - 'packages/cli/src/runtime-host-lifecycle-transaction.ts' + - 'packages/cli/src/runtime-host-managed-deployment.ts' + - 'packages/cli/src/runtime-host-package-deployment.ts' + - 'packages/cli/src/runtime-host-peer-artifact.ts' + - 'packages/cli/src/runtime-host-service-management-command.ts' + - 'packages/cli/src/runtime-host-update-command.ts' + - 'packages/cli/src/runtime-host-windows-task-launcher-artifact.ts' - 'packages/core/src/diagnostic-log.ts' - 'packages/runtime-host/src/__tests__/control-endpoint.test.ts' - 'packages/runtime-host/src/__tests__/fixtures/endpoint-hygiene.ts' @@ -132,6 +144,7 @@ on: - 'packages/storage/src/session-bundle-file-service.ts' - 'packages/storage/src/sqlite-long-term-memory-store.ts' - 'packages/storage/src/stable-storage.ts' + - 'packages/storage/src/workspace-root.ts' - '.github/workflows/windows-recovery.yml' # Unfiltered on purpose: required_status_checks is `strict: false`, so a pull # request goes green against a stale base and only the merged result proves @@ -199,6 +212,14 @@ jobs: exit 1 } + # The remaining service-manager tests exercise POSIX service backends. + - name: Verify native Runtime Host project-root parsing + shell: pwsh + run: >- + node.exe --test + --test-name-pattern="uses native Host paths consistently|parses the bounded managed service" + packages/cli/dist/__tests__/runtime-host-service-manager.test.js + - name: Verify root initialization replacement race shell: pwsh run: | diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts index a72a25fd0ca..1a3b3536988 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts @@ -28,6 +28,7 @@ import { RUNTIME_HOST_OPERATOR_PEER_WEBRTC_STUN_CAPABILITY, runtimeHostAccessCredentialFingerprint, type RuntimeHostServiceManagementFrame, + type RuntimeHostOperatorCommand, } from '@maka/runtime-host/operator'; import { createDesktopRuntimeHostManagement } from '../runtime-host-management.js'; import { createDesktopRuntimeHostPeerMeshManagement } from '../runtime-host-peer-mesh-management.js'; @@ -793,7 +794,12 @@ test('publishes update progress and waits for the managed profile to reconnect', ); }); -test('configures Project roots with CAS and reconnects only after a committed cutover', async () => { +for (const platform of ['posix', 'win32'] as const) { + test(`configures ${platform} Project roots with CAS and reconnects only after a committed cutover`, () => + assertConfiguresProjectRoots(platform)); +} + +async function assertConfiguresProjectRoots(platform: 'posix' | 'win32'): Promise { const handlers = new Map unknown>(); const profile = { id: 'office', @@ -810,7 +816,9 @@ test('configures Project roots with CAS and reconnects only after a committed cu const service = { id: 'b'.repeat(64), rootPath: '/srv/maka', - operator: OPERATOR, + operator: platform === 'posix' ? OPERATOR : { + kind: 'node' as const, platform, nodePath: 'C:\\node.exe', modulePath: 'C:\\Maka\\operator.mjs', + }, }; const fingerprint = `sha256:${'c'.repeat(64)}`; const inputs: DesktopRuntimeHostSshManagementInput[] = []; @@ -859,7 +867,11 @@ test('configures Project roots with CAS and reconnects only after a committed cu }); const configure = handlers.get('runtime-host-management:configure-project-directories'); assert.ok(configure); - const roots = [{ label: 'Work', path: '/srv/work ' }]; + const roots = [{ label: 'Work', path: platform === 'win32' ? 'C:\\work ' : '/srv/work ' }]; + if (platform === 'posix') { + await assert.rejects(async () => configure({}, profile.id, [{ label: 'Work', path: 'C:\\work' }], fingerprint, false), /Project directory is invalid/u); + assert.equal(inputs.length, 0); + } const blocked = await configure({}, profile.id, roots, fingerprint, false); assert.equal( (blocked as { configuration: { kind: string } }).configuration.kind, @@ -899,7 +911,7 @@ test('configures Project roots with CAS and reconnects only after a committed cu 'The Runtime Host change was applied, but Desktop could not reconnect: authentication required', }, ); -}); +} test('manages one Host update policy and reconciles it through the bound operator', async () => { const handlers = new Map unknown>(); @@ -1431,7 +1443,7 @@ function managedSshBinding() { function managedBinding< Profile, - Service extends { readonly id: string; readonly rootPath: string; readonly operator: typeof OPERATOR }, + Service extends { readonly id: string; readonly rootPath: string; readonly operator: RuntimeHostOperatorCommand }, State extends 'active' | 'uninstalling' | 'cleanup_pending', >(profile: Profile, service: Service, state: State) { return { diff --git a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts index 7f21b797ebc..dbb74ec469c 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts @@ -34,6 +34,51 @@ const OPERATOR = { modulePath: '/home/operator/.local/share/maka/operator.mjs', }; +test('checks SSH project directories against the probed Host platform before setup', async () => { + const roots = [{ label: 'Work', path: 'C:\\projects' }]; + for (const platform of ['win32', 'linux'] as const) { + let setupCalls = 0; + const harness = createHarness({ + resolveSshNodeIdentity: async () => ({ platform, architecture: 'x64' }), + profiles: { addAndEnableVerified: async (input) => ({ profileId: input.profile.id }) }, + runSetup: async (input) => { + setupCalls++; + assert.equal(input.remotePlatform, 'win32'); + assert.deepEqual(input.projectDirectoryRoots, roots); + return { + serviceId: 'b'.repeat(64), deploymentId: '00000000-0000-4000-8000-000000000001', + rootPath: 'C:\\Maka', rootId: 'a'.repeat(64), + operator: { kind: 'node', platform: 'win32', nodePath: 'C:\\node.exe', modulePath: 'C:\\Maka\\operator.mjs' }, + endpoint: 'ws://127.0.0.1:7443/runtime-host', credential: 'secret-access-token', + }; + }, + }); + try { + const result = await harness.invoke('runtime-host-onboarding:start', { + kind: 'ssh', name: 'Lab', destination: 'operator@example.com', projectDirectoryRoots: roots, + }); + assert.equal((result as { kind: string }).kind, platform === 'win32' ? 'complete' : 'failed'); + assert.equal(setupCalls, platform === 'win32' ? 1 : 0); + if (platform === 'linux') assert.match(JSON.stringify(result), /Project directory is invalid/u); + } finally { + await harness.onboarding.close(); + } + } +}); + +test('rejects Windows directories for WSL before starting setup', async () => { + const harness = createHarness(); + try { + const result = await harness.invoke('runtime-host-onboarding:start', { + kind: 'wsl', distribution: 'Ubuntu', projectDirectoryRoots: [{ label: 'Work', path: 'C:\\work' }], + }); + assert.equal((result as { kind: string }).kind, 'failed'); + assert.match(JSON.stringify(result), /Project directory is invalid/u); + } finally { + await harness.onboarding.close(); + } +}); + test('persists a verified on-demand SSH profile without endpoint or credential projection', async () => { let setupInput: unknown; let saved: diff --git a/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts b/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts index 58597427bcc..c1081c9c4e1 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts @@ -39,7 +39,26 @@ test('preserves opaque remote paths while normalizing labels', () => { test('rejects paths that are not absolute on the remote POSIX Host', () => { for (const path of ['srv/work', 'C:\\work']) { const roots = [{ label: 'Work', path }]; - assert.equal(projectDirectoryRootsValid(roots), false); - assert.throws(() => requireProjectDirectoryRoots(roots), /Project directory is invalid/u); + assert.equal(projectDirectoryRootsValid(roots, 'posix'), false); + assert.throws(() => requireProjectDirectoryRoots(roots, 'posix'), /Project directory is invalid/u); + } +}); + +test('validates absolute paths for the target Host rather than the Desktop platform', () => { + for (const path of ['C:\\work', 'C:/work', '\\\\server\\share\\work', '/work']) { + const roots = [{ label: 'Work', path }]; + assert.equal(projectDirectoryRootsValid(roots, 'win32'), true, path); + assert.deepEqual(requireProjectDirectoryRoots(roots, 'win32'), roots); + assert.equal(projectDirectoryRootsValid(roots), true, path); + } + for (const platform of ['posix', 'win32', undefined] as const) { + for (const path of ['work', 'C:work', '', '/work\nother']) { + const roots = [{ label: 'Work', path }]; + assert.equal(projectDirectoryRootsValid(roots, platform), false, path); + assert.throws(() => requireProjectDirectoryRoots(roots, platform), /invalid/u); + } + const duplicates = [{ label: 'Work', path: '/one' }, { label: ' Work ', path: '/two' }]; + assert.equal(projectDirectoryRootsValid(duplicates, platform), false); + assert.throws(() => requireProjectDirectoryRoots(duplicates, platform), /unique/u); } }); diff --git a/apps/desktop/src/main/runtime-host-local-management.ts b/apps/desktop/src/main/runtime-host-local-management.ts index c6a1f90090e..904fef8766d 100644 --- a/apps/desktop/src/main/runtime-host-local-management.ts +++ b/apps/desktop/src/main/runtime-host-local-management.ts @@ -44,6 +44,7 @@ export function createDesktopRuntimeHostLocalManagement(input: { }): DesktopRuntimeHostManagementProvider { return { profileId: LOCAL_RUNTIME_HOST_PROFILE.id, + platform: process.platform === 'win32' ? 'win32' : 'posix', accessManagementAvailable: false, run: (action, allowInterruptActiveTasks) => { const execute = (target: DesktopRuntimeHostLocalManagementTarget) => diff --git a/apps/desktop/src/main/runtime-host-management-provider.ts b/apps/desktop/src/main/runtime-host-management-provider.ts index 67bf941bc52..95344efb4ce 100644 --- a/apps/desktop/src/main/runtime-host-management-provider.ts +++ b/apps/desktop/src/main/runtime-host-management-provider.ts @@ -33,6 +33,7 @@ export type DesktopRuntimeHostManagementTerminalFrame = Exclude< export interface DesktopRuntimeHostManagementProvider { readonly profileId: string; + readonly platform: 'posix' | 'win32'; readonly accessManagementAvailable: boolean; run( action: Exclude, diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts index a7fa881c887..295958bfd42 100644 --- a/apps/desktop/src/main/runtime-host-management.ts +++ b/apps/desktop/src/main/runtime-host-management.ts @@ -721,10 +721,10 @@ export function createDesktopRuntimeHostManagement(input: { if (provider) { const previousHostEpoch = provider.currentHostEpoch(); execute = () => provider.configureProjectDirectories( - roots, - expectedConfigFingerprintValue, - allowInterruptActiveTasksValue, - ); + requireProjectDirectoryRoots(roots, provider.platform), + expectedConfigFingerprintValue, + allowInterruptActiveTasksValue, + ); reconnect = () => provider.awaitUpdatedConnection(previousHostEpoch, true); } else { const { managed, expectedTarget } = await activeManagedTarget(profileId); @@ -735,7 +735,7 @@ export function createDesktopRuntimeHostManagement(input: { operator: managed.profile.operator, action: 'configure', expectedTarget, - projectDirectoryRoots: roots, + projectDirectoryRoots: requireProjectDirectoryRoots(roots, 'posix'), expectedConfigFingerprint: expectedConfigFingerprintValue, ...(allowInterruptActiveTasksValue ? { allowInterruptActiveTasks: true } : {}), }); @@ -747,7 +747,7 @@ export function createDesktopRuntimeHostManagement(input: { operator: managed.control.operator, action: 'configure', expectedTarget, - projectDirectoryRoots: roots, + projectDirectoryRoots: requireProjectDirectoryRoots(roots, managed.control.operator.kind === 'node' ? managed.control.operator.platform : 'posix'), expectedConfigFingerprint: expectedConfigFingerprintValue, ...(allowInterruptActiveTasksValue ? { allowInterruptActiveTasks: true } : {}), }); diff --git a/apps/desktop/src/main/runtime-host-onboarding.ts b/apps/desktop/src/main/runtime-host-onboarding.ts index 14dac44db25..80d39cf1c2b 100644 --- a/apps/desktop/src/main/runtime-host-onboarding.ts +++ b/apps/desktop/src/main/runtime-host-onboarding.ts @@ -141,6 +141,10 @@ export function createDesktopRuntimeHostOnboarding(input: { return await runWsl(request, setupPackage, signal); } const nodeIdentity = await resolveSshNodeIdentity(request, signal); + const remotePlatform = nodeIdentity.platform === 'win32' ? 'win32' : 'posix'; + const projectDirectoryRoots = request.projectDirectoryRoots === undefined + ? undefined + : requireProjectDirectoryRoots(request.projectDirectoryRoots, remotePlatform); const peerTarget = input.setupPackageMode === 'development' ? runtimeHostPeerTargetFromNode(nodeIdentity.platform, nodeIdentity.architecture) : 'none'; @@ -166,11 +170,11 @@ export function createDesktopRuntimeHostOnboarding(input: { destination: request.destination, ...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }), setupPackage, - remotePlatform: nodeIdentity.platform === 'win32' ? 'win32' : 'posix', + remotePlatform, lifecycle, principalId: `desktop:${input.clientInstanceId}`, - ...(request.projectDirectoryRoots - ? { projectDirectoryRoots: request.projectDirectoryRoots } + ...(projectDirectoryRoots + ? { projectDirectoryRoots } : {}), signal, }, @@ -348,7 +352,7 @@ function requireOnboardingInput(value: unknown): DesktopRuntimeHostOnboardingInp } const roots = input.projectDirectoryRoots === undefined ? undefined - : requireProjectDirectoryRoots(input.projectDirectoryRoots); + : requireProjectDirectoryRoots(input.projectDirectoryRoots, input.kind === 'wsl' ? 'posix' : undefined); if (input.kind === 'wsl') { if ( typeof input.distribution !== 'string' || diff --git a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx index 564bf0e3a48..a472d7469ba 100644 --- a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx +++ b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx @@ -642,7 +642,7 @@ export function RuntimeHostManagementDialog(props: { const automaticUpdatesAvailable = updatePolicy?.schedulingState === 'ready'; const directoryRoots = directoryPolicyEdit?.draft ?? []; const normalizedDirectoryRoots = canonicalProjectDirectoryRoots(directoryRoots); - const directoryRootsAreValid = projectDirectoryRootsValid(directoryRoots); + const directoryRootsAreValid = projectDirectoryRootsValid(directoryRoots, service?.platform === 'win32' ? 'win32' : 'posix'); const directoryRootsDirty = directoryPolicyEdit !== undefined && JSON.stringify(normalizedDirectoryRoots) !== JSON.stringify(directoryPolicyEdit.baseline.roots); diff --git a/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx index f989d6dac53..ef6edfab2fb 100644 --- a/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx +++ b/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx @@ -89,7 +89,7 @@ export function RuntimeHostOnboardingDialog(props: { (targetKind === 'wsl' ? distribution.trim().length > 0 : destination.trim().length > 0 && validOptionalPort(sshPort)) && - projectDirectoryRootsValid(projectDirectoryRoots); + projectDirectoryRootsValid(projectDirectoryRoots, targetKind === 'wsl' ? 'posix' : undefined); async function start(): Promise { const roots = projectDirectoryRoots.length === 0 diff --git a/apps/desktop/src/shared/runtime-host-project-directory-policy.ts b/apps/desktop/src/shared/runtime-host-project-directory-policy.ts index aedb4df26a3..4289c17c228 100644 --- a/apps/desktop/src/shared/runtime-host-project-directory-policy.ts +++ b/apps/desktop/src/shared/runtime-host-project-directory-policy.ts @@ -20,12 +20,22 @@ import { canonicalProjectDirectoryRootSpec, PROJECT_DIRECTORY_MAX_ROOTS, - projectDirectoryPosixRootSpecValid, + projectDirectoryRootSpecValid, type ProjectDirectoryRootSpec, } from '@maka/runtime-host/protocol'; export type RuntimeHostProjectDirectoryRootInput = ProjectDirectoryRootSpec; +// Undefined is only a preliminary check while SSH onboarding has not probed +// the target yet. Main validates again with the discovered operator platform. +type DirectoryPlatform = 'posix' | 'win32'; + +function directoryRootValid(root: ProjectDirectoryRootSpec, platform?: DirectoryPlatform): boolean { + if (!projectDirectoryRootSpecValid(root)) return false; + if (root.path.startsWith('/')) return true; + return platform !== 'posix' && (/^[A-Za-z]:[\\/]/u.test(root.path) || root.path.startsWith('\\')); +} + export function canonicalProjectDirectoryRoots( roots: readonly RuntimeHostProjectDirectoryRootInput[], ): readonly RuntimeHostProjectDirectoryRootInput[] { @@ -34,17 +44,19 @@ export function canonicalProjectDirectoryRoots( export function projectDirectoryRootsValid( roots: readonly RuntimeHostProjectDirectoryRootInput[], + platform?: DirectoryPlatform, ): boolean { if (roots.length > PROJECT_DIRECTORY_MAX_ROOTS) return false; const canonical = canonicalProjectDirectoryRoots(roots); return ( - canonical.every(projectDirectoryPosixRootSpecValid) && + canonical.every((root) => directoryRootValid(root, platform)) && new Set(canonical.map(({ label }) => label)).size === canonical.length ); } export function requireProjectDirectoryRoots( value: unknown, + platform?: DirectoryPlatform, ): readonly RuntimeHostProjectDirectoryRootInput[] { if (!Array.isArray(value) || value.length > PROJECT_DIRECTORY_MAX_ROOTS) { throw new Error('Runtime Host Project directory policy is invalid'); @@ -62,7 +74,7 @@ export function requireProjectDirectoryRoots( throw new Error('Runtime Host Project directory is invalid'); } const canonical = canonicalProjectDirectoryRootSpec({ label: root.label, path: root.path }); - if (!projectDirectoryPosixRootSpecValid(canonical)) { + if (!directoryRootValid(canonical, platform)) { throw new Error('Runtime Host Project directory is invalid'); } return canonical; diff --git a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts index 5ad9084e66a..39aa10a1ef6 100644 --- a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts +++ b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts @@ -87,6 +87,65 @@ import { } from '../runtime-host-systemd-service.js'; describe('managed Runtime Host service', () => { + it('uses native Host paths consistently for serve and managed service roots', () => { + const paths = + process.platform === 'win32' + ? ['C:\\projects', 'C:/projects', '\\\\server\\share\\projects'] + : ['/srv/projects']; + for (const path of paths) { + for (const [option, value] of [ + ['--project-root', `Work=${path}`], + ['--project-root-json', JSON.stringify({ label: 'Work', path })], + ]) { + assert.ok(option && value); + assert.deepEqual(parseRuntimeHostCommand(['serve', option, value]), { + kind: 'runtime-host-serve', + json: false, + projectDirectoryRoots: [{ label: 'Work', path }], + }); + for (const action of ['install', 'configure']) { + const target = + action === 'configure' + ? [ + '--expected-service-id', + 'b'.repeat(64), + '--expected-root-path', + path, + '--expected-root-id', + 'a'.repeat(64), + '--expected-config-fingerprint', + `sha256:${'c'.repeat(64)}`, + ] + : []; + const result = parseRuntimeHostCommand(['service', action, option, value, ...target]); + assert.equal(result.kind, 'runtime-host-service-manage', JSON.stringify(result)); + if (result.kind !== 'runtime-host-service-manage') + assert.fail('expected managed command'); + assert.deepEqual(result.projectDirectoryRoots, [{ label: 'Work', path }]); + } + } + } + for (const path of [ + 'relative', + 'C:relative', + ...(process.platform === 'win32' ? [] : ['C:\\projects']), + ]) { + assert.equal( + parseRuntimeHostCommand(['service', 'install', '--project-root', `Work=${path}`]).kind, + 'error', + ); + assert.equal( + parseRuntimeHostCommand([ + 'service', + 'install', + '--project-root-json', + JSON.stringify({ label: 'Work', path }), + ]).kind, + 'error', + ); + } + }); + it('parses the bounded managed service command surface', () => { const nativeProjectRoot = process.platform === 'win32' ? 'C:\\projects' : '/srv/projects'; assert.deepEqual( diff --git a/packages/cli/src/runtime-host-cli.ts b/packages/cli/src/runtime-host-cli.ts index 3348bb9bc63..e9fb5aecfea 100644 --- a/packages/cli/src/runtime-host-cli.ts +++ b/packages/cli/src/runtime-host-cli.ts @@ -34,7 +34,6 @@ import { canonicalProjectDirectoryRootSpec, isCanonicalRuntimeHostWebSocketPath, PROJECT_DIRECTORY_MAX_ROOTS, - projectDirectoryPosixRootSpecValid, projectDirectoryRootSpecValid, } from '@maka/runtime-host/protocol'; import type { RuntimeHostManagedServiceTarget } from './runtime-host-service-manager.js'; @@ -1552,9 +1551,7 @@ function parseManagedServiceOptions( } projectDirectoryPolicySpecified = true; const root = - argument === '--project-root' - ? parseProjectRoot(parsed, 'posix') - : parseProjectRootJson(parsed, 'posix'); + argument === '--project-root' ? parseProjectRoot(parsed) : parseProjectRootJson(parsed); if ('kind' in root) return root; if (projectDirectoryRoots.length >= PROJECT_DIRECTORY_MAX_ROOTS) { return error( @@ -2086,9 +2083,7 @@ function parseServeCommand(argv: string[]): RuntimeHostCliCommand { const parsed = optionValue(argv, index, argument); if (typeof parsed !== 'string') return parsed; const root = - argument === '--project-root' - ? parseProjectRoot(parsed, 'native') - : parseProjectRootJson(parsed, 'native'); + argument === '--project-root' ? parseProjectRoot(parsed) : parseProjectRootJson(parsed); if ('kind' in root) return root; if (projectDirectoryRoots.length >= PROJECT_DIRECTORY_MAX_ROOTS) { return error(`--project-root may be provided at most ${PROJECT_DIRECTORY_MAX_ROOTS} times`); @@ -2277,10 +2272,7 @@ function parseServeCommand(argv: string[]): RuntimeHostCliCommand { }; } -function parseProjectRoot( - value: string, - pathKind: 'native' | 'posix', -): { label: string; path: string } | RuntimeHostCliError { +function parseProjectRoot(value: string): { label: string; path: string } | RuntimeHostCliError { const separator = value.indexOf('='); if (separator <= 0 || separator === value.length - 1) { return error('--project-root must use