@@ -5,6 +5,9 @@ import { checkSubprocess, isWindows, runSubprocessCapture } from '../../lib';
55import type { AgentCoreProjectSpec , TargetLanguage } from '../../schema' ;
66import { detectContainerRuntime } from './detect' ;
77import { AWS_CLI_MIN_VERSION , NODE_MIN_VERSION , formatSemVer , parseSemVer , semVerGte } from './versions' ;
8+ import { stat } from 'node:fs/promises' ;
9+ import { homedir } from 'node:os' ;
10+ import { join } from 'node:path' ;
811
912/**
1013 * Result of a version check.
@@ -150,6 +153,66 @@ export function formatVersionError(result: VersionCheckResult): string {
150153 return `${ result . binary } ${ result . current } is below minimum required version ${ result . required } ` ;
151154}
152155
156+ /**
157+ * Result of checking npm cache directory ownership.
158+ */
159+ export interface NpmCacheCheckResult {
160+ /** true when the cache dir doesn't exist or is owned by the current user. */
161+ satisfied : boolean ;
162+ /** Owner of ~/.npm, or null if the directory doesn't exist. */
163+ owner : string | null ;
164+ /** The cache directory path that was checked. */
165+ cacheDir : string ;
166+ }
167+
168+ /**
169+ * Check that the npm cache directory (~/.npm) is owned by the current user.
170+ *
171+ * A previous `sudo npm install` can leave root-owned files in the cache,
172+ * causing EACCES errors on subsequent `npm install` runs.
173+ * Skipped on Windows where file ownership semantics differ.
174+ */
175+ export async function checkNpmCacheOwnership ( ) : Promise < NpmCacheCheckResult > {
176+ const cacheDir = join ( homedir ( ) , '.npm' ) ;
177+
178+ // Skip on Windows - file ownership model is different
179+ if ( isWindows ) {
180+ return { satisfied : true , owner : null , cacheDir } ;
181+ }
182+
183+ try {
184+ const stats = await stat ( cacheDir ) ;
185+ const currentUid = process . getuid ?.( ) ;
186+ if ( currentUid === undefined ) {
187+ // getuid not available (e.g. some non-POSIX runtimes) — skip check
188+ return { satisfied : true , owner : null , cacheDir } ;
189+ }
190+
191+ if ( stats . uid !== currentUid ) {
192+ // Resolve owner name for the error message
193+ const ownerResult = await runSubprocessCapture ( 'id' , [ '-un' , String ( stats . uid ) ] ) ;
194+ const owner = ownerResult . code === 0 ? ownerResult . stdout . trim ( ) : `uid=${ stats . uid } ` ;
195+ return { satisfied : false , owner, cacheDir } ;
196+ }
197+
198+ return { satisfied : true , owner : null , cacheDir } ;
199+ } catch {
200+ // Directory doesn't exist yet — not a problem
201+ return { satisfied : true , owner : null , cacheDir } ;
202+ }
203+ }
204+
205+ /**
206+ * Format an npm cache ownership failure as a user-friendly error message.
207+ */
208+ export function formatNpmCacheError ( result : NpmCacheCheckResult ) : string {
209+ return (
210+ `npm cache directory (${ result . cacheDir } ) is owned by '${ result . owner } ' instead of the current user. ` +
211+ `This was likely caused by a previous 'sudo npm install'. ` +
212+ `Fix: sudo chown -R $(whoami) ${ result . cacheDir } `
213+ ) ;
214+ }
215+
153216/**
154217 * Check if the project has any Python CodeZip agents that require uv.
155218 */
@@ -171,6 +234,7 @@ export interface DependencyCheckResult {
171234 passed : boolean ;
172235 nodeCheck : VersionCheckResult ;
173236 uvCheck : VersionCheckResult | null ;
237+ npmCacheCheck : NpmCacheCheckResult ;
174238 containerRuntimeAvailable : boolean ;
175239 errors : string [ ] ;
176240}
@@ -198,6 +262,12 @@ export async function checkDependencyVersions(projectSpec: AgentCoreProjectSpec)
198262 }
199263 }
200264
265+ // Check npm cache ownership (root-owned cache causes EACCES on npm install)
266+ const npmCacheCheck = await checkNpmCacheOwnership ( ) ;
267+ if ( ! npmCacheCheck . satisfied ) {
268+ errors . push ( formatNpmCacheError ( npmCacheCheck ) ) ;
269+ }
270+
201271 // Check container runtime only if there are Container agents (warn only, not error)
202272 let containerRuntimeAvailable = true ;
203273 if ( requiresContainerRuntime ( projectSpec ) ) {
@@ -213,6 +283,7 @@ export async function checkDependencyVersions(projectSpec: AgentCoreProjectSpec)
213283 passed : errors . length === 0 ,
214284 nodeCheck,
215285 uvCheck,
286+ npmCacheCheck,
216287 containerRuntimeAvailable,
217288 errors,
218289 } ;
@@ -291,6 +362,14 @@ export async function checkCreateDependencies(
291362 errors . push ( "'npm' is required. Install Node.js from https://nodejs.org/" ) ;
292363 }
293364
365+ // Check npm cache ownership (root-owned cache causes EACCES on npm install)
366+ if ( npmAvailable ) {
367+ const npmCacheCheck = await checkNpmCacheOwnership ( ) ;
368+ if ( ! npmCacheCheck . satisfied ) {
369+ errors . push ( formatNpmCacheError ( npmCacheCheck ) ) ;
370+ }
371+ }
372+
294373 // Check aws (warn if missing)
295374 const awsAvailable = await checkBinaryAvailable ( 'aws' ) ;
296375 checks . push ( {
0 commit comments