@lpm-registry/cli 0.58.0 → 0.59.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -44
- package/bin/lpm.js +2 -354
- package/bin/lpx.js +4 -0
- package/bin/native.js +154 -0
- package/package.json +42 -82
- package/scripts/install-binary.js +219 -550
- package/CHANGELOG.md +0 -197
- package/LICENSE +0 -15
- package/index.d.ts +0 -131
- package/index.js +0 -31
- package/lib/api.js +0 -398
- package/lib/commands/add.js +0 -1217
- package/lib/commands/audit.js +0 -283
- package/lib/commands/cache.js +0 -209
- package/lib/commands/check-name.js +0 -112
- package/lib/commands/config.js +0 -186
- package/lib/commands/doctor.js +0 -140
- package/lib/commands/info.js +0 -215
- package/lib/commands/init.js +0 -146
- package/lib/commands/install.js +0 -264
- package/lib/commands/login.js +0 -595
- package/lib/commands/logout.js +0 -94
- package/lib/commands/marketplace-compare.js +0 -164
- package/lib/commands/marketplace-earnings.js +0 -89
- package/lib/commands/mcp-setup.js +0 -363
- package/lib/commands/npmrc.js +0 -178
- package/lib/commands/open.js +0 -82
- package/lib/commands/outdated.js +0 -291
- package/lib/commands/pool-stats.js +0 -100
- package/lib/commands/publish.js +0 -949
- package/lib/commands/quality.js +0 -211
- package/lib/commands/remove.js +0 -82
- package/lib/commands/run.js +0 -14
- package/lib/commands/search.js +0 -143
- package/lib/commands/setup.js +0 -221
- package/lib/commands/skills.js +0 -898
- package/lib/commands/token-rotate.js +0 -25
- package/lib/commands/uninstall.js +0 -107
- package/lib/commands/whoami.js +0 -129
- package/lib/config.js +0 -328
- package/lib/constants.js +0 -190
- package/lib/ecosystem.js +0 -501
- package/lib/editor-skills.js +0 -321
- package/lib/editors.js +0 -215
- package/lib/import-rewriter.js +0 -364
- package/lib/install-targets/mcp-server.js +0 -245
- package/lib/install-targets/vscode-extension.js +0 -178
- package/lib/install-targets.js +0 -82
- package/lib/integrity.js +0 -179
- package/lib/lpm-config-prompts.js +0 -102
- package/lib/lpm-config.js +0 -408
- package/lib/oidc.js +0 -166
- package/lib/project-utils.js +0 -152
- package/lib/quality/checks.js +0 -687
- package/lib/quality/display.js +0 -155
- package/lib/quality/score.js +0 -118
- package/lib/quality/swift-checks.js +0 -447
- package/lib/safe-path.js +0 -180
- package/lib/secure-store.js +0 -321
- package/lib/swift-project.js +0 -637
- package/lib/ui.js +0 -40
- package/scripts/__tests__/install-binary.test.js +0 -346
package/lib/constants.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Constants Configuration
|
|
3
|
-
* Centralized configuration for all CLI behavior.
|
|
4
|
-
*
|
|
5
|
-
* @module cli/lib/constants
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// ============================================================================
|
|
9
|
-
// Network Configuration
|
|
10
|
-
// ============================================================================
|
|
11
|
-
|
|
12
|
-
/** Maximum number of retry attempts for failed requests */
|
|
13
|
-
export const MAX_RETRIES = 3
|
|
14
|
-
|
|
15
|
-
/** Request timeout in milliseconds (30 seconds) */
|
|
16
|
-
export const REQUEST_TIMEOUT_MS = 30_000
|
|
17
|
-
|
|
18
|
-
/** Base delay for exponential backoff in milliseconds */
|
|
19
|
-
export const RETRY_BASE_DELAY_MS = 1_000
|
|
20
|
-
|
|
21
|
-
/** Maximum delay between retries in milliseconds */
|
|
22
|
-
export const RETRY_MAX_DELAY_MS = 10_000
|
|
23
|
-
|
|
24
|
-
/** Multiplier for exponential backoff */
|
|
25
|
-
export const RETRY_BACKOFF_MULTIPLIER = 2
|
|
26
|
-
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// Cache Configuration
|
|
29
|
-
// ============================================================================
|
|
30
|
-
|
|
31
|
-
/** Cache directory name (relative to user's home) */
|
|
32
|
-
export const CACHE_DIR_NAME = ".lpm-cache"
|
|
33
|
-
|
|
34
|
-
/** Maximum cache size in bytes (500 MB) */
|
|
35
|
-
export const MAX_CACHE_SIZE_BYTES = 500 * 1024 * 1024
|
|
36
|
-
|
|
37
|
-
/** Cache entry TTL in milliseconds (7 days) */
|
|
38
|
-
export const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
39
|
-
|
|
40
|
-
// ============================================================================
|
|
41
|
-
// Security Configuration
|
|
42
|
-
// ============================================================================
|
|
43
|
-
|
|
44
|
-
/** Keytar service name for credential storage */
|
|
45
|
-
export const KEYTAR_SERVICE_NAME = "lpm-cli"
|
|
46
|
-
|
|
47
|
-
/** Keytar account name for token storage */
|
|
48
|
-
export const KEYTAR_ACCOUNT_NAME = "auth-token"
|
|
49
|
-
|
|
50
|
-
/** Token scopes that allow publishing */
|
|
51
|
-
export const PUBLISH_SCOPES = ["publish", "write", "full"]
|
|
52
|
-
|
|
53
|
-
/** Token scopes that allow reading */
|
|
54
|
-
export const READ_SCOPES = ["read", "publish", "write", "full"]
|
|
55
|
-
|
|
56
|
-
// ============================================================================
|
|
57
|
-
// API Configuration
|
|
58
|
-
// ============================================================================
|
|
59
|
-
|
|
60
|
-
/** Default registry URL */
|
|
61
|
-
export const DEFAULT_REGISTRY_URL = "https://lpm.dev"
|
|
62
|
-
|
|
63
|
-
/** API version prefix */
|
|
64
|
-
export const API_VERSION = "v1"
|
|
65
|
-
|
|
66
|
-
/** HTTP status codes that trigger retries */
|
|
67
|
-
export const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504]
|
|
68
|
-
|
|
69
|
-
/** HTTP status codes that indicate rate limiting */
|
|
70
|
-
export const RATE_LIMIT_STATUS_CODES = [429]
|
|
71
|
-
|
|
72
|
-
// ============================================================================
|
|
73
|
-
// CLI Configuration
|
|
74
|
-
// ============================================================================
|
|
75
|
-
|
|
76
|
-
/** CLI name for display purposes */
|
|
77
|
-
export const CLI_NAME = "lpm"
|
|
78
|
-
|
|
79
|
-
/** Default pagination limit for list commands */
|
|
80
|
-
export const DEFAULT_PAGE_LIMIT = 20
|
|
81
|
-
|
|
82
|
-
/** Maximum pagination limit */
|
|
83
|
-
export const MAX_PAGE_LIMIT = 100
|
|
84
|
-
|
|
85
|
-
// ============================================================================
|
|
86
|
-
// File System Configuration
|
|
87
|
-
// ============================================================================
|
|
88
|
-
|
|
89
|
-
/** Default source directory for component extraction */
|
|
90
|
-
export const DEFAULT_COMPONENTS_DIR = "components"
|
|
91
|
-
|
|
92
|
-
/** Allowed file extensions for source code extraction */
|
|
93
|
-
export const ALLOWED_SOURCE_EXTENSIONS = [
|
|
94
|
-
".js",
|
|
95
|
-
".jsx",
|
|
96
|
-
".ts",
|
|
97
|
-
".tsx",
|
|
98
|
-
".css",
|
|
99
|
-
".scss",
|
|
100
|
-
".json",
|
|
101
|
-
".md",
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
/** Maximum file size for source extraction (10 MB) */
|
|
105
|
-
export const MAX_SOURCE_FILE_SIZE_BYTES = 10 * 1024 * 1024
|
|
106
|
-
|
|
107
|
-
// ============================================================================
|
|
108
|
-
// Integrity Verification
|
|
109
|
-
// ============================================================================
|
|
110
|
-
|
|
111
|
-
/** Default hash algorithm for tarball verification */
|
|
112
|
-
export const DEFAULT_HASH_ALGORITHM = "sha512"
|
|
113
|
-
|
|
114
|
-
/** Supported hash algorithms */
|
|
115
|
-
export const SUPPORTED_HASH_ALGORITHMS = ["sha256", "sha384", "sha512"]
|
|
116
|
-
|
|
117
|
-
// ============================================================================
|
|
118
|
-
// Spinner Messages
|
|
119
|
-
// ============================================================================
|
|
120
|
-
|
|
121
|
-
export const SPINNER_MESSAGES = {
|
|
122
|
-
authenticating: "Authenticating...",
|
|
123
|
-
downloading: "Downloading package...",
|
|
124
|
-
extracting: "Extracting files...",
|
|
125
|
-
publishing: "Publishing package...",
|
|
126
|
-
verifying: "Verifying integrity...",
|
|
127
|
-
retrying: (attempt, max) => `Retrying (${attempt}/${max})...`,
|
|
128
|
-
rateLimited: seconds => `Rate limited. Waiting ${seconds}s...`,
|
|
129
|
-
readingConfig: "Reading package configuration...",
|
|
130
|
-
filteringFiles: "Filtering files based on configuration...",
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ============================================================================
|
|
134
|
-
// Error Messages
|
|
135
|
-
// ============================================================================
|
|
136
|
-
|
|
137
|
-
export const ERROR_MESSAGES = {
|
|
138
|
-
notAuthenticated: "Not authenticated. Run `lpm login` first.",
|
|
139
|
-
tokenExpired: "Token expired. Run `lpm login` to refresh.",
|
|
140
|
-
tokenMissingScope: scope =>
|
|
141
|
-
`Token missing required scope: ${scope}. Run \`lpm token-rotate --scope ${scope}\` to fix.`,
|
|
142
|
-
networkError: "Network error. Check your connection and try again.",
|
|
143
|
-
rateLimited: "Rate limited. Please wait and try again.",
|
|
144
|
-
integrityMismatch:
|
|
145
|
-
"Package integrity check failed. Download may be corrupted.",
|
|
146
|
-
pathTraversal: "Invalid path: path traversal detected.",
|
|
147
|
-
timeout:
|
|
148
|
-
"Request timed out. Try again or increase timeout with `lpm config set timeout <ms>`.",
|
|
149
|
-
invalidLpmConfig: "Invalid lpm.config.json: ",
|
|
150
|
-
invalidConfigValue: (key, value, allowed) =>
|
|
151
|
-
`Invalid value "${value}" for "${key}". Allowed: ${allowed.join(", ")}`,
|
|
152
|
-
missingRequiredConfig: key =>
|
|
153
|
-
`Required config parameter "${key}" not provided. Use ?${key}=value in the package URL.`,
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ============================================================================
|
|
157
|
-
// Warning Messages
|
|
158
|
-
// ============================================================================
|
|
159
|
-
|
|
160
|
-
export const WARNING_MESSAGES = {
|
|
161
|
-
usernameNotSet: "Your personal username is not set.",
|
|
162
|
-
usernameNotSetHint: registryUrl =>
|
|
163
|
-
`Set it to publish packages under your personal owner:\n ${registryUrl}/dashboard/settings`,
|
|
164
|
-
ownerMismatch: owner =>
|
|
165
|
-
`Package owner "@lpm.dev/${owner}" doesn't match your available owners.`,
|
|
166
|
-
// Legacy - kept for backward compatibility
|
|
167
|
-
scopeMismatch: scope =>
|
|
168
|
-
`Package owner "@lpm.dev/${scope}" doesn't match your available owners.`,
|
|
169
|
-
noOrganizations: "You have no organizations.",
|
|
170
|
-
createOrgHint: registryUrl =>
|
|
171
|
-
`Create one at: ${registryUrl}/dashboard/orgs/new`,
|
|
172
|
-
ownerFixHint:
|
|
173
|
-
'Either:\n 1. Set your username/org slug to match the package owner\n 2. Change package.json "name" to use @lpm.dev/YOUR_OWNER.package-name',
|
|
174
|
-
// Legacy - kept for backward compatibility
|
|
175
|
-
scopeFixHint:
|
|
176
|
-
'Either:\n 1. Set your username/org slug to match the package owner\n 2. Change package.json "name" to use @lpm.dev/YOUR_OWNER.package-name',
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ============================================================================
|
|
180
|
-
// Success Messages
|
|
181
|
-
// ============================================================================
|
|
182
|
-
|
|
183
|
-
export const SUCCESS_MESSAGES = {
|
|
184
|
-
// Updated for new format: @lpm.dev/owner.package-name
|
|
185
|
-
// owner = username or org slug, pkgName = package name (without owner prefix)
|
|
186
|
-
publishPersonal: (registryUrl, owner, pkgName, version) =>
|
|
187
|
-
`Successfully published @lpm.dev/${owner}.${pkgName}@${version}\n ${registryUrl}/${owner}.${pkgName}`,
|
|
188
|
-
publishOrg: (registryUrl, owner, pkgName, version) =>
|
|
189
|
-
`Successfully published @lpm.dev/${owner}.${pkgName}@${version}\n ${registryUrl}/${owner}.${pkgName}`,
|
|
190
|
-
}
|
package/lib/ecosystem.js
DELETED
|
@@ -1,501 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ecosystem Detection & Non-JS Packaging Utilities
|
|
3
|
-
*
|
|
4
|
-
* Detects project ecosystem (JS, Swift, Rust, Python, Ruby) from manifest files,
|
|
5
|
-
* reads ecosystem-specific manifests, and creates tarballs without npm.
|
|
6
|
-
*
|
|
7
|
-
* @module cli/lib/ecosystem
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { exec } from "node:child_process"
|
|
11
|
-
import fs from "node:fs"
|
|
12
|
-
import path from "node:path"
|
|
13
|
-
import { promisify } from "node:util"
|
|
14
|
-
import * as tar from "tar"
|
|
15
|
-
|
|
16
|
-
const execAsync = promisify(exec)
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Ecosystem manifest files in detection priority order.
|
|
20
|
-
* First match wins.
|
|
21
|
-
*/
|
|
22
|
-
const ECOSYSTEM_MANIFESTS = [
|
|
23
|
-
{ file: "Package.swift", ecosystem: "swift" },
|
|
24
|
-
{ file: "Cargo.toml", ecosystem: "rust" },
|
|
25
|
-
{ file: "pyproject.toml", ecosystem: "python" },
|
|
26
|
-
{ file: "package.json", ecosystem: "js" },
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Files/directories to always skip when creating tarballs.
|
|
31
|
-
* Keyed by ecosystem.
|
|
32
|
-
*/
|
|
33
|
-
const ECOSYSTEM_SKIP_PATTERNS = {
|
|
34
|
-
swift: [
|
|
35
|
-
".build/",
|
|
36
|
-
"DerivedData/",
|
|
37
|
-
"Pods/",
|
|
38
|
-
".swiftpm/",
|
|
39
|
-
"*.xcworkspace/",
|
|
40
|
-
"xcuserdata/",
|
|
41
|
-
".DS_Store",
|
|
42
|
-
],
|
|
43
|
-
rust: ["target/", ".DS_Store"],
|
|
44
|
-
python: [
|
|
45
|
-
"__pycache__/",
|
|
46
|
-
"*.pyc",
|
|
47
|
-
".venv/",
|
|
48
|
-
"venv/",
|
|
49
|
-
"dist/",
|
|
50
|
-
"*.egg-info/",
|
|
51
|
-
".DS_Store",
|
|
52
|
-
],
|
|
53
|
-
ruby: [".bundle/", "vendor/bundle/", "*.gem", ".DS_Store"],
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Files to always include in tarball for each ecosystem.
|
|
58
|
-
*/
|
|
59
|
-
const _ECOSYSTEM_REQUIRED_FILES = {
|
|
60
|
-
swift: ["Package.swift", "lpm.config.json"],
|
|
61
|
-
rust: ["Cargo.toml", "lpm.config.json"],
|
|
62
|
-
python: ["pyproject.toml", "lpm.config.json"],
|
|
63
|
-
ruby: ["Gemfile", "lpm.config.json"],
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Detect the ecosystem of the current project.
|
|
68
|
-
* Checks for manifest files in priority order.
|
|
69
|
-
*
|
|
70
|
-
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
71
|
-
* @returns {{ ecosystem: string, manifestFile: string }}
|
|
72
|
-
*/
|
|
73
|
-
export function detectEcosystem(cwd = process.cwd()) {
|
|
74
|
-
for (const { file, ecosystem } of ECOSYSTEM_MANIFESTS) {
|
|
75
|
-
const filePath = path.resolve(cwd, file)
|
|
76
|
-
if (fs.existsSync(filePath)) {
|
|
77
|
-
return { ecosystem, manifestFile: file }
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// No manifest found — caller should handle this
|
|
81
|
-
return { ecosystem: null, manifestFile: null }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Read and parse the Swift Package.swift manifest using `swift package dump-package`.
|
|
86
|
-
* Returns structured data about the Swift package.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} [cwd] - Working directory
|
|
89
|
-
* @returns {Promise<object>} Parsed Swift manifest JSON
|
|
90
|
-
* @throws {Error} If swift CLI is not available or manifest is invalid
|
|
91
|
-
*/
|
|
92
|
-
export async function readSwiftManifest(cwd = process.cwd()) {
|
|
93
|
-
try {
|
|
94
|
-
const { stdout } = await execAsync("swift package dump-package", {
|
|
95
|
-
cwd,
|
|
96
|
-
timeout: 30_000,
|
|
97
|
-
})
|
|
98
|
-
const manifest = JSON.parse(stdout)
|
|
99
|
-
return manifest
|
|
100
|
-
} catch (err) {
|
|
101
|
-
if (err.code === "ENOENT" || err.message.includes("not found")) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
"Swift toolchain not found. Install Xcode or Swift from swift.org.",
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
throw new Error(`Failed to read Package.swift: ${err.message}`)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Extract structured metadata from a Swift manifest for storage in versionMeta.
|
|
112
|
-
*
|
|
113
|
-
* @param {object} manifest - Parsed output from `swift package dump-package`
|
|
114
|
-
* @returns {object} Structured Swift metadata
|
|
115
|
-
*/
|
|
116
|
-
export function extractSwiftMetadata(manifest) {
|
|
117
|
-
const metadata = {
|
|
118
|
-
toolsVersion: manifest.toolsVersion?._version || null,
|
|
119
|
-
platforms: [],
|
|
120
|
-
products: [],
|
|
121
|
-
targets: [],
|
|
122
|
-
dependencies: [],
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Extract platform declarations
|
|
126
|
-
if (manifest.platforms) {
|
|
127
|
-
metadata.platforms = manifest.platforms.map(p => ({
|
|
128
|
-
name: p.platformName,
|
|
129
|
-
version: p.version,
|
|
130
|
-
}))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Extract products (libraries, executables)
|
|
134
|
-
if (manifest.products) {
|
|
135
|
-
metadata.products = manifest.products.map(p => ({
|
|
136
|
-
name: p.name,
|
|
137
|
-
type: p.type ? Object.keys(p.type)[0] : "library",
|
|
138
|
-
targets: p.targets || [],
|
|
139
|
-
}))
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Extract targets
|
|
143
|
-
if (manifest.targets) {
|
|
144
|
-
metadata.targets = manifest.targets.map(t => ({
|
|
145
|
-
name: t.name,
|
|
146
|
-
type: t.type || "regular",
|
|
147
|
-
dependencies: (t.dependencies || []).map(d => {
|
|
148
|
-
if (d.byName) return { type: "byName", name: d.byName[0] }
|
|
149
|
-
if (d.product) return { type: "product", name: d.product[0] }
|
|
150
|
-
return d
|
|
151
|
-
}),
|
|
152
|
-
}))
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Extract dependencies (external packages)
|
|
156
|
-
if (manifest.dependencies) {
|
|
157
|
-
metadata.dependencies = manifest.dependencies.map(dep => {
|
|
158
|
-
if (dep.sourceControl) {
|
|
159
|
-
const sc = dep.sourceControl[0]
|
|
160
|
-
return {
|
|
161
|
-
type: "sourceControl",
|
|
162
|
-
identity: sc.identity,
|
|
163
|
-
location: sc.location?.remote?.[0] || null,
|
|
164
|
-
requirement: sc.requirement || null,
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (dep.fileSystem) {
|
|
168
|
-
const fs = dep.fileSystem[0]
|
|
169
|
-
return {
|
|
170
|
-
type: "fileSystem",
|
|
171
|
-
identity: fs.identity,
|
|
172
|
-
path: fs.path,
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return dep
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return metadata
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Map Swift dependencies to LPM dependency tree format.
|
|
184
|
-
*
|
|
185
|
-
* @param {object} swiftMetadata - Output from extractSwiftMetadata
|
|
186
|
-
* @returns {{ lpm: Array, external: Array }}
|
|
187
|
-
*/
|
|
188
|
-
export function mapSwiftDependencies(swiftMetadata) {
|
|
189
|
-
const lpm = []
|
|
190
|
-
const external = []
|
|
191
|
-
|
|
192
|
-
for (const dep of swiftMetadata.dependencies) {
|
|
193
|
-
if (dep.type === "sourceControl") {
|
|
194
|
-
// Check if it's an LPM package (lpm.dev URL) — for future use
|
|
195
|
-
const isLpm = dep.location?.includes("lpm.dev")
|
|
196
|
-
|
|
197
|
-
if (isLpm) {
|
|
198
|
-
lpm.push({
|
|
199
|
-
name: dep.identity,
|
|
200
|
-
location: dep.location,
|
|
201
|
-
requirement: dep.requirement,
|
|
202
|
-
})
|
|
203
|
-
} else {
|
|
204
|
-
external.push({
|
|
205
|
-
name: dep.identity,
|
|
206
|
-
location: dep.location,
|
|
207
|
-
requirement: dep.requirement,
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// Skip fileSystem dependencies (local only)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return { lpm, external }
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Parse an XCFramework Info.plist to extract platform slices.
|
|
219
|
-
* Handles the XML plist format without external dependencies.
|
|
220
|
-
*
|
|
221
|
-
* @param {string} plistPath - Path to Info.plist
|
|
222
|
-
* @returns {{ slices: Array<{ identifier: string, platform: string, variant: string|null, architectures: string[] }>, formatVersion: string|null }}
|
|
223
|
-
*/
|
|
224
|
-
/**
|
|
225
|
-
* Extract the content of an outer <array>...</array> after a given <key>,
|
|
226
|
-
* correctly handling nested <array> tags inside (e.g. SupportedArchitectures).
|
|
227
|
-
*
|
|
228
|
-
* @param {string} xml - Full plist XML string
|
|
229
|
-
* @param {string} keyName - The plist key name (e.g. "AvailableLibraries")
|
|
230
|
-
* @returns {string|null} The inner content of the matched array, or null
|
|
231
|
-
*/
|
|
232
|
-
function extractOuterArray(xml, keyName) {
|
|
233
|
-
const keyPattern = `<key>${keyName}</key>`
|
|
234
|
-
const keyIdx = xml.indexOf(keyPattern)
|
|
235
|
-
if (keyIdx === -1) return null
|
|
236
|
-
|
|
237
|
-
const afterKey = xml.substring(keyIdx + keyPattern.length)
|
|
238
|
-
const arrayTagIdx = afterKey.indexOf("<array>")
|
|
239
|
-
if (arrayTagIdx === -1) return null
|
|
240
|
-
|
|
241
|
-
const startContent = arrayTagIdx + "<array>".length
|
|
242
|
-
let depth = 1
|
|
243
|
-
let i = startContent
|
|
244
|
-
|
|
245
|
-
while (i < afterKey.length && depth > 0) {
|
|
246
|
-
if (afterKey.substring(i, i + 7) === "<array>") {
|
|
247
|
-
depth++
|
|
248
|
-
i += 7
|
|
249
|
-
} else if (afterKey.substring(i, i + 8) === "</array>") {
|
|
250
|
-
depth--
|
|
251
|
-
if (depth === 0) return afterKey.substring(startContent, i)
|
|
252
|
-
i += 8
|
|
253
|
-
} else {
|
|
254
|
-
i++
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return null
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function parseXCFrameworkPlist(plistPath) {
|
|
262
|
-
const xml = fs.readFileSync(plistPath, "utf-8")
|
|
263
|
-
const slices = []
|
|
264
|
-
let formatVersion = null
|
|
265
|
-
|
|
266
|
-
// Extract XCFrameworkFormatVersion
|
|
267
|
-
const fvMatch = xml.match(
|
|
268
|
-
/<key>XCFrameworkFormatVersion<\/key>\s*<string>([^<]+)<\/string>/,
|
|
269
|
-
)
|
|
270
|
-
if (fvMatch) formatVersion = fvMatch[1]
|
|
271
|
-
|
|
272
|
-
// Extract AvailableLibraries array (handles nested <array> for architectures)
|
|
273
|
-
const libsContent = extractOuterArray(xml, "AvailableLibraries")
|
|
274
|
-
if (!libsContent) return { slices, formatVersion }
|
|
275
|
-
|
|
276
|
-
// Split into individual <dict> entries
|
|
277
|
-
const dictBlocks = libsContent.match(/<dict>[\s\S]*?<\/dict>/g) || []
|
|
278
|
-
|
|
279
|
-
for (const block of dictBlocks) {
|
|
280
|
-
const id = block.match(
|
|
281
|
-
/<key>LibraryIdentifier<\/key>\s*<string>([^<]+)<\/string>/,
|
|
282
|
-
)
|
|
283
|
-
const platform = block.match(
|
|
284
|
-
/<key>SupportedPlatform<\/key>\s*<string>([^<]+)<\/string>/,
|
|
285
|
-
)
|
|
286
|
-
const variant = block.match(
|
|
287
|
-
/<key>SupportedPlatformVariant<\/key>\s*<string>([^<]+)<\/string>/,
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
// Extract architectures array
|
|
291
|
-
const archMatch = block.match(
|
|
292
|
-
/<key>SupportedArchitectures<\/key>\s*<array>([\s\S]*?)<\/array>/,
|
|
293
|
-
)
|
|
294
|
-
const architectures = archMatch
|
|
295
|
-
? (archMatch[1].match(/<string>([^<]+)<\/string>/g) || []).map(s =>
|
|
296
|
-
s.replace(/<\/?string>/g, ""),
|
|
297
|
-
)
|
|
298
|
-
: []
|
|
299
|
-
|
|
300
|
-
if (id && platform) {
|
|
301
|
-
slices.push({
|
|
302
|
-
identifier: id[1],
|
|
303
|
-
platform: platform[1],
|
|
304
|
-
variant: variant ? variant[1] : null,
|
|
305
|
-
architectures,
|
|
306
|
-
})
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return { slices, formatVersion }
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Detect XCFramework directories in the project.
|
|
315
|
-
* If found and Info.plist exists, parses platform slices.
|
|
316
|
-
*
|
|
317
|
-
* @param {string} [cwd] - Working directory
|
|
318
|
-
* @returns {{ found: boolean, name: string|null, path: string|null, hasInfoPlist: boolean, slices: Array, formatVersion: string|null }}
|
|
319
|
-
*/
|
|
320
|
-
export function detectXCFramework(cwd = process.cwd()) {
|
|
321
|
-
const entries = fs.readdirSync(cwd, { withFileTypes: true })
|
|
322
|
-
|
|
323
|
-
for (const entry of entries) {
|
|
324
|
-
if (entry.isDirectory() && entry.name.endsWith(".xcframework")) {
|
|
325
|
-
const xcfPath = path.join(cwd, entry.name)
|
|
326
|
-
const infoPlistPath = path.join(xcfPath, "Info.plist")
|
|
327
|
-
const hasInfoPlist = fs.existsSync(infoPlistPath)
|
|
328
|
-
|
|
329
|
-
if (hasInfoPlist) {
|
|
330
|
-
const { slices, formatVersion } = parseXCFrameworkPlist(infoPlistPath)
|
|
331
|
-
return {
|
|
332
|
-
found: true,
|
|
333
|
-
name: entry.name,
|
|
334
|
-
path: xcfPath,
|
|
335
|
-
hasInfoPlist: true,
|
|
336
|
-
slices,
|
|
337
|
-
formatVersion,
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
found: true,
|
|
343
|
-
name: entry.name,
|
|
344
|
-
path: xcfPath,
|
|
345
|
-
hasInfoPlist: false,
|
|
346
|
-
slices: [],
|
|
347
|
-
formatVersion: null,
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
found: false,
|
|
354
|
-
name: null,
|
|
355
|
-
path: null,
|
|
356
|
-
hasInfoPlist: false,
|
|
357
|
-
slices: [],
|
|
358
|
-
formatVersion: null,
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Recursively collect files in a directory, respecting skip patterns and .gitignore.
|
|
364
|
-
*
|
|
365
|
-
* @param {string} dir - Directory to scan
|
|
366
|
-
* @param {string} baseDir - Base directory for relative paths
|
|
367
|
-
* @param {string[]} skipPatterns - Patterns to skip
|
|
368
|
-
* @returns {Array<{ path: string, size: number }>}
|
|
369
|
-
*/
|
|
370
|
-
function collectFilesRecursive(dir, baseDir, skipPatterns) {
|
|
371
|
-
const results = []
|
|
372
|
-
let entries
|
|
373
|
-
try {
|
|
374
|
-
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
375
|
-
} catch {
|
|
376
|
-
return results
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
for (const entry of entries) {
|
|
380
|
-
const fullPath = path.join(dir, entry.name)
|
|
381
|
-
const relativePath = path.relative(baseDir, fullPath)
|
|
382
|
-
|
|
383
|
-
// Check skip patterns
|
|
384
|
-
const shouldSkip = skipPatterns.some(pattern => {
|
|
385
|
-
if (pattern.endsWith("/")) {
|
|
386
|
-
// Directory pattern
|
|
387
|
-
const dirName = pattern.slice(0, -1)
|
|
388
|
-
return entry.name === dirName || relativePath.startsWith(`${dirName}/`)
|
|
389
|
-
}
|
|
390
|
-
if (pattern.startsWith("*.")) {
|
|
391
|
-
// Extension pattern
|
|
392
|
-
return entry.name.endsWith(pattern.slice(1))
|
|
393
|
-
}
|
|
394
|
-
return entry.name === pattern
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
if (shouldSkip) continue
|
|
398
|
-
|
|
399
|
-
if (entry.isDirectory()) {
|
|
400
|
-
results.push(...collectFilesRecursive(fullPath, baseDir, skipPatterns))
|
|
401
|
-
} else if (entry.isFile()) {
|
|
402
|
-
try {
|
|
403
|
-
const stats = fs.statSync(fullPath)
|
|
404
|
-
results.push({
|
|
405
|
-
path: relativePath,
|
|
406
|
-
size: stats.size,
|
|
407
|
-
})
|
|
408
|
-
} catch {
|
|
409
|
-
// Skip files we can't stat
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return results
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Collect all files that should be included in the tarball for a non-JS ecosystem.
|
|
419
|
-
*
|
|
420
|
-
* @param {string} ecosystem - The ecosystem identifier
|
|
421
|
-
* @param {string} [cwd] - Working directory
|
|
422
|
-
* @returns {Array<{ path: string, size: number }>}
|
|
423
|
-
*/
|
|
424
|
-
export function collectPackageFiles(ecosystem, cwd = process.cwd()) {
|
|
425
|
-
const skipPatterns = [
|
|
426
|
-
// Universal skips
|
|
427
|
-
".git/",
|
|
428
|
-
"node_modules/",
|
|
429
|
-
".DS_Store",
|
|
430
|
-
// Ecosystem-specific skips
|
|
431
|
-
...(ECOSYSTEM_SKIP_PATTERNS[ecosystem] || []),
|
|
432
|
-
]
|
|
433
|
-
|
|
434
|
-
// Also read .gitignore patterns if available
|
|
435
|
-
const gitignorePath = path.join(cwd, ".gitignore")
|
|
436
|
-
if (fs.existsSync(gitignorePath)) {
|
|
437
|
-
try {
|
|
438
|
-
const gitignore = fs.readFileSync(gitignorePath, "utf8")
|
|
439
|
-
const patterns = gitignore
|
|
440
|
-
.split("\n")
|
|
441
|
-
.map(line => line.trim())
|
|
442
|
-
.filter(line => line && !line.startsWith("#"))
|
|
443
|
-
skipPatterns.push(...patterns)
|
|
444
|
-
} catch {
|
|
445
|
-
// Ignore .gitignore read errors
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return collectFilesRecursive(cwd, cwd, skipPatterns)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Create a tarball (.tgz) from a list of files using the `tar` command.
|
|
454
|
-
* Used for non-JS ecosystems where npm pack is not available.
|
|
455
|
-
*
|
|
456
|
-
* @param {string} ecosystem - Ecosystem identifier
|
|
457
|
-
* @param {string} name - Package name (for tarball filename)
|
|
458
|
-
* @param {string} version - Package version
|
|
459
|
-
* @param {string} [cwd] - Working directory
|
|
460
|
-
* @returns {Promise<{ tarballPath: string, files: Array<{ path: string, size: number }>, unpackedSize: number, fileCount: number }>}
|
|
461
|
-
*/
|
|
462
|
-
export async function createEcosystemTarball(
|
|
463
|
-
ecosystem,
|
|
464
|
-
name,
|
|
465
|
-
version,
|
|
466
|
-
cwd = process.cwd(),
|
|
467
|
-
) {
|
|
468
|
-
const files = collectPackageFiles(ecosystem, cwd)
|
|
469
|
-
|
|
470
|
-
if (files.length === 0) {
|
|
471
|
-
throw new Error("No files found to package. Check your project directory.")
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Calculate unpacked size
|
|
475
|
-
const unpackedSize = files.reduce((sum, f) => sum + f.size, 0)
|
|
476
|
-
|
|
477
|
-
// Generate tarball filename (sanitize name for filesystem)
|
|
478
|
-
const safeName = name.replace(/[/@]/g, "-").replace(/^-/, "")
|
|
479
|
-
const tarballFilename = `${safeName}-${version}.tgz`
|
|
480
|
-
const tarballPath = path.resolve(cwd, tarballFilename)
|
|
481
|
-
|
|
482
|
-
// Create tarball using node-tar with package/ prefix.
|
|
483
|
-
// This matches npm pack convention so `lpm add` can use strip:1
|
|
484
|
-
// to extract files at the correct paths.
|
|
485
|
-
await tar.create(
|
|
486
|
-
{
|
|
487
|
-
gzip: true,
|
|
488
|
-
file: tarballPath,
|
|
489
|
-
cwd,
|
|
490
|
-
prefix: "package",
|
|
491
|
-
},
|
|
492
|
-
files.map(f => f.path),
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
return {
|
|
496
|
-
tarballPath,
|
|
497
|
-
files,
|
|
498
|
-
unpackedSize,
|
|
499
|
-
fileCount: files.length,
|
|
500
|
-
}
|
|
501
|
-
}
|