π File detail
utils/plugins/parseMarketplaceInput.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes parseMarketplaceInput β mainly functions, hooks, or classes. Dependencies touch Node OS/process metadata and Node path helpers. It composes internal code from errors, fsOperations, and schemas (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { homedir } from 'os' import { resolve } from 'path' import { getErrnoCode } from '../errors.js' import { getFsImplementation } from '../fsOperations.js' import type { MarketplaceSource } from './schemas.js'
π€ Exports (heuristic)
parseMarketplaceInput
π External import roots
Package roots from from "β¦" (relative paths omitted).
ospath
π₯οΈ Source preview
import { homedir } from 'os'
import { resolve } from 'path'
import { getErrnoCode } from '../errors.js'
import { getFsImplementation } from '../fsOperations.js'
import type { MarketplaceSource } from './schemas.js'
/**
* Parses a marketplace input string and returns the appropriate marketplace source type.
* Handles various input formats:
* - Git SSH URLs (user@host:path or user@host:path.git)
* - Standard: git@github.com:owner/repo.git
* - GitHub Enterprise SSH certificates: org-123456@github.com:owner/repo.git
* - Custom usernames: deploy@gitlab.com:group/project.git
* - Self-hosted: user@192.168.10.123:path/to/repo
* - HTTP/HTTPS URLs
* - GitHub shorthand (owner/repo)
* - Local file paths (.json files)
* - Local directory paths
*
* @param input The marketplace source input string
* @returns MarketplaceSource object, error object, or null if format is unrecognized
*/
export async function parseMarketplaceInput(
input: string,
): Promise<MarketplaceSource | { error: string } | null> {
const trimmed = input.trim()
const fs = getFsImplementation()
// Handle git SSH URLs with any valid username (not just 'git')
// Supports: user@host:path, user@host:path.git, and with #ref suffix
// Username can contain: alphanumeric, dots, underscores, hyphens
const sshMatch = trimmed.match(
/^([a-zA-Z0-9._-]+@[^:]+:.+?(?:\.git)?)(#(.+))?$/,
)
if (sshMatch?.[1]) {
const url = sshMatch[1]
const ref = sshMatch[3]
return ref ? { source: 'git', url, ref } : { source: 'git', url }
}
// Handle URLs
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
// Extract fragment (ref) from URL if present
const fragmentMatch = trimmed.match(/^([^#]+)(#(.+))?$/)
const urlWithoutFragment = fragmentMatch?.[1] || trimmed
const ref = fragmentMatch?.[3]
// When user explicitly provides an HTTPS/HTTP URL that looks like a git
// repo, use the git source type so we clone rather than fetch-as-JSON.
// The .git suffix is a GitHub/GitLab/Bitbucket convention. Azure DevOps
// uses /_git/ in the path with NO suffix (appending .git breaks ADO:
// TF401019 "repo does not exist"). Without this check, an ADO URL falls
// through to source:'url' below, which tries to fetch it as a raw
// marketplace.json β the HTML response parses as "expected object,
// received string". (gh-31256 / CC-299)
if (
urlWithoutFragment.endsWith('.git') ||
urlWithoutFragment.includes('/_git/')
) {
return ref
? { source: 'git', url: urlWithoutFragment, ref }
: { source: 'git', url: urlWithoutFragment }
}
// Parse URL to check hostname
let url: URL
try {
url = new URL(urlWithoutFragment)
} catch (_err) {
// Not a valid URL for parsing, treat as generic URL
// new URL() throws TypeError for invalid URLs
return { source: 'url', url: urlWithoutFragment }
}
if (url.hostname === 'github.com' || url.hostname === 'www.github.com') {
const match = url.pathname.match(/^\/([^/]+\/[^/]+?)(\/|\.git|$)/)
if (match?.[1]) {
// User explicitly provided HTTPS URL - keep it as HTTPS via 'git' type
// Add .git suffix if not present for proper git clone
const gitUrl = urlWithoutFragment.endsWith('.git')
? urlWithoutFragment
: `${urlWithoutFragment}.git`
return ref
? { source: 'git', url: gitUrl, ref }
: { source: 'git', url: gitUrl }
}
}
return { source: 'url', url: urlWithoutFragment }
}
// Handle local paths
// On Windows, also recognize backslash-relative (.\, ..\) and drive letter paths (C:\)
// These are Windows-only because backslashes are valid filename chars on Unix
const isWindows = process.platform === 'win32'
const isWindowsPath =
isWindows &&
(trimmed.startsWith('.\\') ||
trimmed.startsWith('..\\') ||
/^[a-zA-Z]:[/\\]/.test(trimmed))
if (
trimmed.startsWith('./') ||
trimmed.startsWith('../') ||
trimmed.startsWith('/') ||
trimmed.startsWith('~') ||
isWindowsPath
) {
const resolvedPath = resolve(
trimmed.startsWith('~') ? trimmed.replace(/^~/, homedir()) : trimmed,
)
// Stat the path to determine if it's a file or directory. Swallow all stat
// errors (ENOENT, EACCES, EPERM, etc.) and return an error result instead
// of throwing β matches the old existsSync behavior which never threw.
let stats
try {
stats = await fs.stat(resolvedPath)
} catch (e: unknown) {
const code = getErrnoCode(e)
return {
error:
code === 'ENOENT'
? `Path does not exist: ${resolvedPath}`
: `Cannot access path: ${resolvedPath} (${code ?? e})`,
}
}
if (stats.isFile()) {
if (resolvedPath.endsWith('.json')) {
return { source: 'file', path: resolvedPath }
} else {
return {
error: `File path must point to a .json file (marketplace.json), but got: ${resolvedPath}`,
}
}
} else if (stats.isDirectory()) {
return { source: 'directory', path: resolvedPath }
} else {
return {
error: `Path is neither a file nor a directory: ${resolvedPath}`,
}
}
}
// Handle GitHub shorthand (owner/repo, owner/repo#ref, or owner/repo@ref)
// Accept both # and @ as ref separators β the display formatter uses @, so users
// naturally type @ when copying from error messages or managed settings.
if (trimmed.includes('/') && !trimmed.startsWith('@')) {
if (trimmed.includes(':')) {
return null
}
// Extract ref if present (either #ref or @ref)
const fragmentMatch = trimmed.match(/^([^#@]+)(?:[#@](.+))?$/)
const repo = fragmentMatch?.[1] || trimmed
const ref = fragmentMatch?.[2]
// Assume it's a GitHub repo
return ref ? { source: 'github', repo, ref } : { source: 'github', repo }
}
// NPM packages not yet implemented
// Returning null for unrecognized input
return null
}