@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.
Files changed (62) hide show
  1. package/README.md +25 -44
  2. package/bin/lpm.js +2 -354
  3. package/bin/lpx.js +4 -0
  4. package/bin/native.js +154 -0
  5. package/package.json +42 -82
  6. package/scripts/install-binary.js +219 -550
  7. package/CHANGELOG.md +0 -197
  8. package/LICENSE +0 -15
  9. package/index.d.ts +0 -131
  10. package/index.js +0 -31
  11. package/lib/api.js +0 -398
  12. package/lib/commands/add.js +0 -1217
  13. package/lib/commands/audit.js +0 -283
  14. package/lib/commands/cache.js +0 -209
  15. package/lib/commands/check-name.js +0 -112
  16. package/lib/commands/config.js +0 -186
  17. package/lib/commands/doctor.js +0 -140
  18. package/lib/commands/info.js +0 -215
  19. package/lib/commands/init.js +0 -146
  20. package/lib/commands/install.js +0 -264
  21. package/lib/commands/login.js +0 -595
  22. package/lib/commands/logout.js +0 -94
  23. package/lib/commands/marketplace-compare.js +0 -164
  24. package/lib/commands/marketplace-earnings.js +0 -89
  25. package/lib/commands/mcp-setup.js +0 -363
  26. package/lib/commands/npmrc.js +0 -178
  27. package/lib/commands/open.js +0 -82
  28. package/lib/commands/outdated.js +0 -291
  29. package/lib/commands/pool-stats.js +0 -100
  30. package/lib/commands/publish.js +0 -949
  31. package/lib/commands/quality.js +0 -211
  32. package/lib/commands/remove.js +0 -82
  33. package/lib/commands/run.js +0 -14
  34. package/lib/commands/search.js +0 -143
  35. package/lib/commands/setup.js +0 -221
  36. package/lib/commands/skills.js +0 -898
  37. package/lib/commands/token-rotate.js +0 -25
  38. package/lib/commands/uninstall.js +0 -107
  39. package/lib/commands/whoami.js +0 -129
  40. package/lib/config.js +0 -328
  41. package/lib/constants.js +0 -190
  42. package/lib/ecosystem.js +0 -501
  43. package/lib/editor-skills.js +0 -321
  44. package/lib/editors.js +0 -215
  45. package/lib/import-rewriter.js +0 -364
  46. package/lib/install-targets/mcp-server.js +0 -245
  47. package/lib/install-targets/vscode-extension.js +0 -178
  48. package/lib/install-targets.js +0 -82
  49. package/lib/integrity.js +0 -179
  50. package/lib/lpm-config-prompts.js +0 -102
  51. package/lib/lpm-config.js +0 -408
  52. package/lib/oidc.js +0 -166
  53. package/lib/project-utils.js +0 -152
  54. package/lib/quality/checks.js +0 -687
  55. package/lib/quality/display.js +0 -155
  56. package/lib/quality/score.js +0 -118
  57. package/lib/quality/swift-checks.js +0 -447
  58. package/lib/safe-path.js +0 -180
  59. package/lib/secure-store.js +0 -321
  60. package/lib/swift-project.js +0 -637
  61. package/lib/ui.js +0 -40
  62. 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
- }