Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 19 additions & 45 deletions cli/src/npm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ const AUTH_URL_TIMEOUT_MS = 90_000
export interface ExecNpmOptions {
otp?: string
silent?: boolean
/** Working directory for the npm command. */
cwd?: string
/** When true, use PTY-based interactive execution instead of execFile. */
interactive?: boolean
/** When true, npm opens auth URLs in the user's browser.
Expand Down Expand Up @@ -214,6 +216,7 @@ async function execNpmInteractive(
name: 'xterm-256color',
cols: 120,
rows: 30,
cwd: options.cwd,
env,
})

Expand Down Expand Up @@ -337,6 +340,7 @@ async function execNpm(args: string[], options: ExecNpmOptions = {}): Promise<Np
const { command, args: processArgs } = resolveNpmProcessCommand(npmArgs)
const { stdout, stderr } = await execFileAsync(command, processArgs, {
timeout: 60000,
cwd: options.cwd,
env: createNpmEnv(),
})

Expand Down Expand Up @@ -559,12 +563,12 @@ export async function listUserPackages(user: string): Promise<NpmExecResult> {
* Creates a minimal package.json in a temp directory and publishes it.
* @param name Package name to claim
* @param author npm username of the publisher (for author field)
* @param otp Optional OTP for 2FA
* @param options Execution options (otp, interactive, etc.)
*/
export async function packageInit(
name: string,
author?: string,
otp?: string,
options?: ExecNpmOptions,
): Promise<NpmExecResult> {
validatePackageName(name)

Expand Down Expand Up @@ -600,52 +604,22 @@ export async function packageInit(
args.push('--access', access)
}

// Run npm publish from the temp directory
const npmArgs = otp ? [...args, '--otp', otp] : args

// Log the command being run (hide OTP value for security)
const displayCmd = otp ? `npm ${args.join(' ')} --otp ******` : `npm ${args.join(' ')}`
const displayCmd = options?.otp
? ['npm', ...args, '--otp', '******'].join(' ')
: ['npm', ...args].join(' ')
logCommand(`${displayCmd} (in temp dir for ${name})`)

try {
const { command, args: processArgs } = resolveNpmProcessCommand(npmArgs)
const { stdout, stderr } = await execFileAsync(command, processArgs, {
timeout: 60000,
cwd: tempDir.path,
env: createNpmEnv(),
})
const result = await execNpm(args, { ...options, cwd: tempDir.path, silent: true })

if (result.exitCode === 0) {
logSuccess(`Published ${name}@0.0.0`)

return {
stdout: stdout.trim(),
stderr: filterNpmWarnings(stderr),
exitCode: 0,
}
} catch (error) {
const err = error as { stdout?: string; stderr?: string; code?: number }
const stderr = err.stderr?.trim() ?? String(error)
const requiresOtp = detectOtpRequired(stderr)
const authFailure = detectAuthFailure(stderr)

if (requiresOtp) {
logError('OTP required')
} else if (authFailure) {
logError('Authentication required - please run "npm login" and restart the connector')
} else {
logError(filterNpmWarnings(stderr).split('\n')[0] || 'Command failed')
}

return {
stdout: err.stdout?.trim() ?? '',
stderr: requiresOtp
? 'This operation requires a one-time password (OTP).'
: authFailure
? 'Authentication failed. Please run "npm login" and restart the connector.'
: filterNpmWarnings(stderr),
exitCode: err.code ?? 1,
requiresOtp,
authFailure,
}
} else if (result.requiresOtp) {
logError('OTP required')
} else if (result.authFailure) {
logError('Authentication required - please run "npm login" and restart the connector')
} else {
logError(result.stderr.split('\n')[0] || 'Command failed')
}

return result
}
4 changes: 1 addition & 3 deletions cli/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,9 +809,7 @@ async function executeOperation(
result = await ownerRemove(params.user, params.pkg, execOptions)
break
case 'package:init':
// package:init has its own special execution path (temp dir + publish)
// and does not support interactive mode
result = await packageInit(params.name, params.author, options.otp)
result = await packageInit(params.name, params.author, execOptions)
break
default:
return {
Expand Down
Loading