Skip to content
Merged
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
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@
"typescript": "6.0.2"
},
"engines": {
"node": ">=24"
"node": ">=24.4.0"
}
}
145 changes: 69 additions & 76 deletions cli/src/npm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import crypto from 'node:crypto'
import process from 'node:process'
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
import { mkdtempDisposable, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import * as v from 'valibot'
Expand Down Expand Up @@ -569,91 +569,84 @@ export async function packageInit(
): Promise<NpmExecResult> {
validatePackageName(name)

// Create a temporary directory
const tempDir = await mkdtemp(join(tmpdir(), 'npmx-init-'))

try {
// Determine access type based on whether it's a scoped package
const isScoped = name.startsWith('@')
const access = isScoped ? 'public' : undefined

// Create minimal package.json
const packageJson = {
name,
version: '0.0.0',
description: `Placeholder for ${name}`,
main: 'index.js',
scripts: {},
keywords: [],
author: author ? `${author} (https://www.npmjs.com/~${author})` : '',
license: 'UNLICENSED',
private: false,
...(access && { publishConfig: { access } }),
}
// Let Node clean up the temp directory automatically when this scope exits.
await using tempDir = await mkdtempDisposable(join(tmpdir(), 'npmx-init-'))

// Determine access type based on whether it's a scoped package
const isScoped = name.startsWith('@')
const access = isScoped ? 'public' : undefined

// Create minimal package.json
const packageJson = {
name,
version: '0.0.0',
description: `Placeholder for ${name}`,
main: 'index.js',
scripts: {},
keywords: [],
author: author ? `${author} (https://www.npmjs.com/~${author})` : '',
license: 'UNLICENSED',
private: false,
...(access && { publishConfig: { access } }),
}

await writeFile(join(tempDir, 'package.json'), JSON.stringify(packageJson, null, 2))
await writeFile(join(tempDir.path, 'package.json'), JSON.stringify(packageJson, null, 2))

// Create empty index.js
await writeFile(join(tempDir, 'index.js'), '// Placeholder\n')
// Create empty index.js
await writeFile(join(tempDir.path, 'index.js'), '// Placeholder\n')

// Build npm publish args
const args = ['publish']
if (access) {
args.push('--access', access)
}
// Build npm publish args
const args = ['publish']
if (access) {
args.push('--access', access)
}

// Run npm publish from the temp directory
const npmArgs = otp ? [...args, '--otp', otp] : args
// 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(' ')}`
logCommand(`${displayCmd} (in temp dir for ${name})`)
// Log the command being run (hide OTP value for security)
const displayCmd = otp ? `npm ${args.join(' ')} --otp ******` : `npm ${args.join(' ')}`
logCommand(`${displayCmd} (in temp dir for ${name})`)

try {
const { stdout, stderr } = await execFileAsync('npm', npmArgs, {
timeout: 60000,
cwd: tempDir,
env: createNpmEnv(),
shell: process.platform === 'win32',
})
try {
const { stdout, stderr } = await execFileAsync('npm', npmArgs, {
timeout: 60000,
cwd: tempDir.path,
env: createNpmEnv(),
shell: process.platform === 'win32',
})

logSuccess(`Published ${name}@0.0.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)
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')
}
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,
}
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,
}
} finally {
// Clean up temp directory
await rm(tempDir, { recursive: true, force: true }).catch(() => {
// Ignore cleanup errors
})
}
}
Loading