@lpm-registry/cli 0.2.0

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 (54) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +15 -0
  3. package/README.md +406 -0
  4. package/bin/lpm.js +334 -0
  5. package/index.d.ts +131 -0
  6. package/index.js +31 -0
  7. package/lib/api.js +324 -0
  8. package/lib/commands/add.js +1217 -0
  9. package/lib/commands/audit.js +283 -0
  10. package/lib/commands/cache.js +209 -0
  11. package/lib/commands/check-name.js +112 -0
  12. package/lib/commands/config.js +174 -0
  13. package/lib/commands/doctor.js +142 -0
  14. package/lib/commands/info.js +215 -0
  15. package/lib/commands/init.js +146 -0
  16. package/lib/commands/install.js +217 -0
  17. package/lib/commands/login.js +547 -0
  18. package/lib/commands/logout.js +94 -0
  19. package/lib/commands/marketplace-compare.js +164 -0
  20. package/lib/commands/marketplace-earnings.js +89 -0
  21. package/lib/commands/mcp-setup.js +363 -0
  22. package/lib/commands/open.js +82 -0
  23. package/lib/commands/outdated.js +291 -0
  24. package/lib/commands/pool-stats.js +100 -0
  25. package/lib/commands/publish.js +707 -0
  26. package/lib/commands/quality.js +211 -0
  27. package/lib/commands/remove.js +82 -0
  28. package/lib/commands/run.js +14 -0
  29. package/lib/commands/search.js +143 -0
  30. package/lib/commands/setup.js +92 -0
  31. package/lib/commands/skills.js +863 -0
  32. package/lib/commands/token-rotate.js +25 -0
  33. package/lib/commands/whoami.js +129 -0
  34. package/lib/config.js +240 -0
  35. package/lib/constants.js +190 -0
  36. package/lib/ecosystem.js +501 -0
  37. package/lib/editors.js +215 -0
  38. package/lib/import-rewriter.js +364 -0
  39. package/lib/install-targets/mcp-server.js +245 -0
  40. package/lib/install-targets/vscode-extension.js +178 -0
  41. package/lib/install-targets.js +82 -0
  42. package/lib/integrity.js +179 -0
  43. package/lib/lpm-config-prompts.js +102 -0
  44. package/lib/lpm-config.js +408 -0
  45. package/lib/project-utils.js +152 -0
  46. package/lib/quality/checks.js +654 -0
  47. package/lib/quality/display.js +139 -0
  48. package/lib/quality/score.js +115 -0
  49. package/lib/quality/swift-checks.js +447 -0
  50. package/lib/safe-path.js +180 -0
  51. package/lib/secure-store.js +288 -0
  52. package/lib/swift-project.js +637 -0
  53. package/lib/ui.js +40 -0
  54. package/package.json +74 -0
@@ -0,0 +1,637 @@
1
+ /**
2
+ * Swift project utilities for `lpm add`.
3
+ *
4
+ * Handles SPM target detection, Xcode local package scaffolding,
5
+ * and automatic Xcode project linking.
6
+ *
7
+ * Each installed LPM package gets its own Swift target/module so users
8
+ * can `import Charts`, `import Networking`, etc. instead of one monolithic module.
9
+ */
10
+
11
+ import { exec } from "node:child_process"
12
+ import fs from "node:fs"
13
+ import path from "node:path"
14
+ import { promisify } from "node:util"
15
+ import { project as XcodeProject } from "xcode"
16
+
17
+ const execAsync = promisify(exec)
18
+
19
+ // ─── SPM Target Detection ────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Get the list of non-test targets from the current SPM package.
23
+ * Uses `swift package dump-package` to read the manifest.
24
+ *
25
+ * @returns {Promise<string[]>} Array of target names (excluding test targets)
26
+ */
27
+ export async function getSpmTargets() {
28
+ try {
29
+ const { stdout } = await execAsync("swift package dump-package", {
30
+ timeout: 15000,
31
+ })
32
+ const manifest = JSON.parse(stdout)
33
+ const targets = (manifest.targets || [])
34
+ .filter(t => t.type !== "test")
35
+ .map(t => t.name)
36
+ return targets
37
+ } catch {
38
+ return []
39
+ }
40
+ }
41
+
42
+ // ─── Source Package Parsing ──────────────────────────────────────────────────
43
+
44
+ /**
45
+ * Parse a Package.swift to extract the first library product name.
46
+ * This is the module name users will `import` in their code.
47
+ *
48
+ * Uses regex since we can't run `swift package dump-package` on an
49
+ * extracted tarball (it's not in a valid SPM project context).
50
+ *
51
+ * @param {string} packageSwiftPath - Path to the Package.swift file
52
+ * @returns {string|null} The product/target name, or null if not found
53
+ */
54
+ export function parseSwiftTargetName(packageSwiftPath) {
55
+ if (!fs.existsSync(packageSwiftPath)) return null
56
+
57
+ const content = fs.readFileSync(packageSwiftPath, "utf-8")
58
+
59
+ // Match .library(name: "Foo", ...) — the product name is the import name
60
+ const libraryMatch = content.match(/\.library\(\s*name:\s*"([^"]+)"/)
61
+ if (libraryMatch) return libraryMatch[1]
62
+
63
+ // Fallback: match .target(name: "Foo", ...) excluding test targets
64
+ const targetMatches = [...content.matchAll(/\.target\(\s*name:\s*"([^"]+)"/g)]
65
+ const testTargets = [
66
+ ...content.matchAll(/\.testTarget\(\s*name:\s*"([^"]+)"/g),
67
+ ].map(m => m[1])
68
+
69
+ for (const match of targetMatches) {
70
+ if (!testTargets.includes(match[1])) {
71
+ return match[1]
72
+ }
73
+ }
74
+
75
+ return null
76
+ }
77
+
78
+ /**
79
+ * Parse the platforms block from a Package.swift file.
80
+ *
81
+ * @param {string} packageSwiftPath - Path to the Package.swift file
82
+ * @returns {string[]|null} Array of platform strings, or null if not found
83
+ */
84
+ export function parsePlatforms(packageSwiftPath) {
85
+ if (!fs.existsSync(packageSwiftPath)) return null
86
+
87
+ const content = fs.readFileSync(packageSwiftPath, "utf-8")
88
+
89
+ // Match platforms: [ ... ] block
90
+ const platformsMatch = content.match(/platforms:\s*\[([\s\S]*?)\]/)
91
+ if (!platformsMatch) return null
92
+
93
+ const entries = [...platformsMatch[1].matchAll(/\.\w+\(\.\w+\)/g)]
94
+ return entries.map(m => m[0])
95
+ }
96
+
97
+ // ─── Target Name Conflict Resolution ─────────────────────────────────────────
98
+
99
+ /**
100
+ * Derive a scoped target name from an lpm package reference.
101
+ * e.g., "@lpm.dev/user2.haptic" → "User2Haptic"
102
+ *
103
+ * @param {string} lpmPackageName - Full lpm package name (e.g., "@lpm.dev/user2.haptic")
104
+ * @returns {string} PascalCase scoped name
105
+ */
106
+ export function scopedTargetName(lpmPackageName) {
107
+ // Extract "user2.haptic" from "@lpm.dev/user2.haptic"
108
+ const shortName = lpmPackageName.replace(/^@lpm\.dev\//, "")
109
+
110
+ // Convert "user2.haptic" → "User2Haptic"
111
+ return shortName
112
+ .split(/[.\-_]/)
113
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
114
+ .join("")
115
+ }
116
+
117
+ /**
118
+ * Resolve the final target name for an LPM package, handling conflicts.
119
+ *
120
+ * Strategy (Option C):
121
+ * - Default to the original target name from the source Package.swift (e.g., "Haptic")
122
+ * - If that name already exists in the local manifest, auto-scope using the lpm package name
123
+ *
124
+ * @param {string} originalTarget - Target name from source Package.swift
125
+ * @param {string} lpmPackageName - Full lpm package name (e.g., "@lpm.dev/user2.haptic")
126
+ * @param {string} manifestPath - Path to local LPMComponents Package.swift
127
+ * @returns {{ targetName: string, wasScoped: boolean }}
128
+ */
129
+ export function resolveTargetName(
130
+ originalTarget,
131
+ lpmPackageName,
132
+ manifestPath,
133
+ ) {
134
+ if (!fs.existsSync(manifestPath)) {
135
+ // No manifest yet — no conflict possible
136
+ return { targetName: originalTarget, wasScoped: false }
137
+ }
138
+
139
+ const content = fs.readFileSync(manifestPath, "utf-8")
140
+
141
+ if (!content.includes(`name: "${originalTarget}"`)) {
142
+ // Target name not yet taken
143
+ return { targetName: originalTarget, wasScoped: false }
144
+ }
145
+
146
+ // Conflict: scope the name
147
+ const scoped = scopedTargetName(lpmPackageName)
148
+ return { targetName: scoped, wasScoped: true }
149
+ }
150
+
151
+ // ─── Local Package Scaffolding ───────────────────────────────────────────────
152
+
153
+ /**
154
+ * Ensure the local LPMComponents SPM package exists for Xcode projects.
155
+ * Creates the package structure on first run. Each installed package gets
156
+ * its own target so it can be imported by its original module name.
157
+ *
158
+ * Structure:
159
+ * Packages/LPMComponents/
160
+ * ├── Package.swift (multi-target manifest)
161
+ * └── Sources/
162
+ * ├── Charts/ (from acme.swift-charts)
163
+ * ├── Networking/ (from acme.networking)
164
+ * └── ...
165
+ *
166
+ * @param {string} [targetName] - The Swift target/module name for the package being installed
167
+ * @param {string[]} [platforms] - Platform requirements from the source package
168
+ * @returns {{ created: boolean, installPath: string, targetName: string|null }}
169
+ */
170
+ export function ensureXcodeLocalPackage(targetName, platforms) {
171
+ const cwd = process.cwd()
172
+ const pkgDir = path.join(cwd, "Packages", "LPMComponents")
173
+ const manifestPath = path.join(pkgDir, "Package.swift")
174
+
175
+ const effectiveTarget = targetName || "LPMComponents"
176
+ const sourcesDir = path.join(pkgDir, "Sources", effectiveTarget)
177
+
178
+ if (fs.existsSync(manifestPath)) {
179
+ // Package.swift already exists — add the new target if not present
180
+ if (targetName) {
181
+ addTargetToManifest(manifestPath, targetName, platforms)
182
+ }
183
+
184
+ fs.mkdirSync(sourcesDir, { recursive: true })
185
+ return {
186
+ created: false,
187
+ installPath: sourcesDir,
188
+ targetName: effectiveTarget,
189
+ }
190
+ }
191
+
192
+ // First-time creation
193
+ fs.mkdirSync(sourcesDir, { recursive: true })
194
+
195
+ if (targetName) {
196
+ const platformsStr =
197
+ platforms?.length > 0
198
+ ? `\n platforms: [\n ${platforms.join(",\n ")},\n ],`
199
+ : "\n platforms: [\n .iOS(.v16),\n .macOS(.v13),\n ],"
200
+
201
+ const packageSwift = `// swift-tools-version: 5.9
202
+ // Managed by lpm — do not edit manually.
203
+ // Each \`lpm add\` of a Swift package adds a new target below.
204
+
205
+ import PackageDescription
206
+
207
+ let package = Package(
208
+ name: "LPMComponents",${platformsStr}
209
+ products: [
210
+ .library(name: "${targetName}", targets: ["${targetName}"]),
211
+ ],
212
+ targets: [
213
+ .target(name: "${targetName}", path: "Sources/${targetName}"),
214
+ ]
215
+ )
216
+ `
217
+ fs.writeFileSync(manifestPath, packageSwift)
218
+ } else {
219
+ // Legacy single-target manifest (backwards compatibility)
220
+ const packageSwift = `// swift-tools-version: 5.9
221
+ import PackageDescription
222
+
223
+ let package = Package(
224
+ name: "LPMComponents",
225
+ platforms: [
226
+ .iOS(.v16),
227
+ .macOS(.v13),
228
+ ],
229
+ products: [
230
+ .library(name: "LPMComponents", targets: ["LPMComponents"]),
231
+ ],
232
+ targets: [
233
+ .target(name: "LPMComponents"),
234
+ ]
235
+ )
236
+ `
237
+ fs.writeFileSync(manifestPath, packageSwift)
238
+
239
+ // Write placeholder source file (SPM requires at least one source file)
240
+ const placeholderPath = path.join(sourcesDir, "LPMComponents.swift")
241
+ if (!fs.existsSync(placeholderPath)) {
242
+ fs.writeFileSync(
243
+ placeholderPath,
244
+ "// LPM Components — files added via `lpm add`\n",
245
+ )
246
+ }
247
+ }
248
+
249
+ return {
250
+ created: true,
251
+ installPath: sourcesDir,
252
+ targetName: effectiveTarget,
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Add a new target to an existing LPMComponents Package.swift.
258
+ * Parses the manifest, checks if the target already exists, and adds it if not.
259
+ *
260
+ * @param {string} manifestPath - Path to Package.swift
261
+ * @param {string} targetName - Target name to add
262
+ * @param {string[]} [platforms] - Platform requirements (used to merge)
263
+ */
264
+ function addTargetToManifest(manifestPath, targetName, platforms) {
265
+ let content = fs.readFileSync(manifestPath, "utf-8")
266
+
267
+ // Check if target already exists
268
+ if (content.includes(`name: "${targetName}"`)) {
269
+ return
270
+ }
271
+
272
+ // Add product entry: find last .library() line and add after it
273
+ const productEntry = `.library(name: "${targetName}", targets: ["${targetName}"])`
274
+ content = content.replace(
275
+ /(products:\s*\[[\s\S]*?)(^\s*\],)/m,
276
+ (_match, before, closing) => {
277
+ const lines = before.split("\n")
278
+ const lastLibraryIdx = lines.findLastIndex(l => l.includes(".library("))
279
+ if (lastLibraryIdx >= 0) {
280
+ const indent = lines[lastLibraryIdx].match(/^(\s*)/)[1]
281
+ lines.splice(lastLibraryIdx + 1, 0, `${indent}${productEntry},`)
282
+ }
283
+ return lines.join("\n") + closing
284
+ },
285
+ )
286
+
287
+ // Add target entry: find last .target() line and add after it
288
+ const targetEntry = `.target(name: "${targetName}", path: "Sources/${targetName}")`
289
+ content = content.replace(
290
+ /(targets:\s*\[[\s\S]*?)(^\s*\]\s*\))/m,
291
+ (_match, before, closing) => {
292
+ const lines = before.split("\n")
293
+ const lastTargetIdx = lines.findLastIndex(l => l.includes(".target("))
294
+ if (lastTargetIdx >= 0) {
295
+ const indent = lines[lastTargetIdx].match(/^(\s*)/)[1]
296
+ lines.splice(lastTargetIdx + 1, 0, `${indent}${targetEntry},`)
297
+ }
298
+ return lines.join("\n") + closing
299
+ },
300
+ )
301
+
302
+ // Merge platforms if the source package requires ones not yet listed
303
+ if (platforms?.length) {
304
+ for (const platform of platforms) {
305
+ const platformName = platform.split("(")[0]
306
+ if (!content.includes(platformName)) {
307
+ content = content.replace(
308
+ /(platforms:\s*\[[\s\S]*?)(^\s*\],)/m,
309
+ (_match, before, closing) => {
310
+ const lines = before.split("\n")
311
+ const lastPlatformIdx = lines.findLastIndex(l =>
312
+ l.match(/^\s+\.\w+\(/),
313
+ )
314
+ if (lastPlatformIdx >= 0) {
315
+ const indent = lines[lastPlatformIdx].match(/^(\s*)/)[1]
316
+ lines.splice(lastPlatformIdx + 1, 0, `${indent}${platform},`)
317
+ }
318
+ return lines.join("\n") + closing
319
+ },
320
+ )
321
+ }
322
+ }
323
+ }
324
+
325
+ fs.writeFileSync(manifestPath, content)
326
+ }
327
+
328
+ // ─── Xcode Auto-Linking ─────────────────────────────────────────────────────
329
+
330
+ /**
331
+ * Find the .xcodeproj directory in the given project root.
332
+ *
333
+ * @param {string} projectRoot - The project root directory
334
+ * @returns {string|null} Path to project.pbxproj, or null if not found
335
+ */
336
+ function findPbxprojPath(projectRoot) {
337
+ try {
338
+ const entries = fs.readdirSync(projectRoot)
339
+ const xcodeproj = entries.find(e => e.endsWith(".xcodeproj"))
340
+ if (!xcodeproj) return null
341
+ const pbxproj = path.join(projectRoot, xcodeproj, "project.pbxproj")
342
+ return fs.existsSync(pbxproj) ? pbxproj : null
343
+ } catch {
344
+ return null
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Auto-link the LPMComponents local package to the Xcode project.
350
+ *
351
+ * Uses the `xcode` library (Apache Cordova's pbxproj parser/writer) to
352
+ * safely manipulate the project file as a parsed object tree, avoiding
353
+ * fragile regex-based string surgery.
354
+ *
355
+ * Adds:
356
+ * 1. XCLocalSwiftPackageReference → points to Packages/LPMComponents
357
+ * 2. XCSwiftPackageProductDependency → for each product (target)
358
+ * 3. PBXBuildFile → registers in frameworks build phase
359
+ * 4. References in PBXProject.packageReferences and PBXNativeTarget.packageProductDependencies
360
+ *
361
+ * @param {string} [productName] - The product name to link (e.g., "Haptic")
362
+ * @returns {{ success: boolean, message: string }}
363
+ */
364
+ export function autoLinkXcodePackage(productName) {
365
+ const cwd = process.cwd()
366
+ const pbxprojPath = findPbxprojPath(cwd)
367
+
368
+ if (!pbxprojPath) {
369
+ return { success: false, message: "Could not find .xcodeproj" }
370
+ }
371
+
372
+ const proj = new XcodeProject(pbxprojPath)
373
+ proj.parseSync()
374
+
375
+ const objects = proj.hash.project.objects
376
+ const localPkgRelPath = "Packages/LPMComponents"
377
+ const effectiveProduct = productName || "LPMComponents"
378
+
379
+ // Check if local package reference already exists
380
+ const existingPkgRef = findExistingLocalPkgRef(objects, localPkgRelPath)
381
+
382
+ if (existingPkgRef) {
383
+ // Package is already linked — just add new product dependency if needed
384
+ if (productName && !findExistingProductDep(objects, productName)) {
385
+ addProductDependency(proj, objects, productName)
386
+ fs.writeFileSync(pbxprojPath, proj.writeSync())
387
+ return {
388
+ success: true,
389
+ message: `Linked ${productName} to your Xcode target`,
390
+ }
391
+ }
392
+ return { success: true, message: "Already linked" }
393
+ }
394
+
395
+ // Generate UUIDs for new entries
396
+ const pkgRefUUID = proj.generateUuid()
397
+ const productDepUUID = proj.generateUuid()
398
+ const buildFileUUID = proj.generateUuid()
399
+
400
+ // 1. Add XCLocalSwiftPackageReference
401
+ if (!objects.XCLocalSwiftPackageReference) {
402
+ objects.XCLocalSwiftPackageReference = {}
403
+ }
404
+ objects.XCLocalSwiftPackageReference[pkgRefUUID] = {
405
+ isa: "XCLocalSwiftPackageReference",
406
+ relativePath: localPkgRelPath,
407
+ }
408
+ objects.XCLocalSwiftPackageReference[`${pkgRefUUID}_comment`] =
409
+ `XCLocalSwiftPackageReference "${localPkgRelPath}"`
410
+
411
+ // 2. Add XCSwiftPackageProductDependency
412
+ if (!objects.XCSwiftPackageProductDependency) {
413
+ objects.XCSwiftPackageProductDependency = {}
414
+ }
415
+ objects.XCSwiftPackageProductDependency[productDepUUID] = {
416
+ isa: "XCSwiftPackageProductDependency",
417
+ productName: effectiveProduct,
418
+ }
419
+ objects.XCSwiftPackageProductDependency[`${productDepUUID}_comment`] =
420
+ effectiveProduct
421
+
422
+ // 3. Add PBXBuildFile with productRef
423
+ if (!objects.PBXBuildFile) {
424
+ objects.PBXBuildFile = {}
425
+ }
426
+ objects.PBXBuildFile[buildFileUUID] = {
427
+ isa: "PBXBuildFile",
428
+ productRef: productDepUUID,
429
+ productRef_comment: effectiveProduct,
430
+ }
431
+ objects.PBXBuildFile[`${buildFileUUID}_comment`] =
432
+ `${effectiveProduct} in Frameworks`
433
+
434
+ // 4. Add to PBXFrameworksBuildPhase files
435
+ for (const key in objects.PBXFrameworksBuildPhase) {
436
+ if (key.endsWith("_comment")) continue
437
+ const phase = objects.PBXFrameworksBuildPhase[key]
438
+ if (phase?.files) {
439
+ phase.files.push({
440
+ value: buildFileUUID,
441
+ comment: `${effectiveProduct} in Frameworks`,
442
+ })
443
+ }
444
+ }
445
+
446
+ // 5. Add packageReferences to PBXProject
447
+ for (const key in objects.PBXProject) {
448
+ if (key.endsWith("_comment")) continue
449
+ const project = objects.PBXProject[key]
450
+ if (!project?.isa) continue
451
+ if (!project.packageReferences) project.packageReferences = []
452
+ project.packageReferences.push({
453
+ value: pkgRefUUID,
454
+ comment: `XCLocalSwiftPackageReference "${localPkgRelPath}"`,
455
+ })
456
+ }
457
+
458
+ // 6. Add packageProductDependencies to PBXNativeTarget
459
+ for (const key in objects.PBXNativeTarget) {
460
+ if (key.endsWith("_comment")) continue
461
+ const target = objects.PBXNativeTarget[key]
462
+ if (!target?.isa) continue
463
+ if (!target.packageProductDependencies) {
464
+ target.packageProductDependencies = []
465
+ }
466
+ target.packageProductDependencies.push({
467
+ value: productDepUUID,
468
+ comment: effectiveProduct,
469
+ })
470
+ }
471
+
472
+ fs.writeFileSync(pbxprojPath, proj.writeSync())
473
+
474
+ return {
475
+ success: true,
476
+ message: `Linked ${effectiveProduct} to your Xcode project`,
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Find an existing XCLocalSwiftPackageReference for the given path.
482
+ *
483
+ * @param {object} objects - The parsed pbxproj objects
484
+ * @param {string} relativePath - The local package path to search for
485
+ * @returns {string|null} UUID of existing reference, or null
486
+ */
487
+ function findExistingLocalPkgRef(objects, relativePath) {
488
+ const section = objects.XCLocalSwiftPackageReference
489
+ if (!section) return null
490
+ for (const key in section) {
491
+ if (key.endsWith("_comment")) continue
492
+ if (section[key]?.relativePath === relativePath) return key
493
+ }
494
+ return null
495
+ }
496
+
497
+ /**
498
+ * Find an existing XCSwiftPackageProductDependency for the given product.
499
+ *
500
+ * @param {object} objects - The parsed pbxproj objects
501
+ * @param {string} productName - The product name to search for
502
+ * @returns {string|null} UUID of existing dependency, or null
503
+ */
504
+ function findExistingProductDep(objects, productName) {
505
+ const section = objects.XCSwiftPackageProductDependency
506
+ if (!section) return null
507
+ for (const key in section) {
508
+ if (key.endsWith("_comment")) continue
509
+ if (section[key]?.productName === productName) return key
510
+ }
511
+ return null
512
+ }
513
+
514
+ /**
515
+ * Add a product dependency for a new target to an already-linked local package.
516
+ *
517
+ * @param {object} proj - The parsed XcodeProject instance
518
+ * @param {object} objects - The parsed pbxproj objects
519
+ * @param {string} productName - The product to add
520
+ */
521
+ function addProductDependency(proj, objects, productName) {
522
+ const productDepUUID = proj.generateUuid()
523
+ const buildFileUUID = proj.generateUuid()
524
+
525
+ // Add XCSwiftPackageProductDependency
526
+ if (!objects.XCSwiftPackageProductDependency) {
527
+ objects.XCSwiftPackageProductDependency = {}
528
+ }
529
+ objects.XCSwiftPackageProductDependency[productDepUUID] = {
530
+ isa: "XCSwiftPackageProductDependency",
531
+ productName,
532
+ }
533
+ objects.XCSwiftPackageProductDependency[`${productDepUUID}_comment`] =
534
+ productName
535
+
536
+ // Add PBXBuildFile with productRef
537
+ if (!objects.PBXBuildFile) {
538
+ objects.PBXBuildFile = {}
539
+ }
540
+ objects.PBXBuildFile[buildFileUUID] = {
541
+ isa: "PBXBuildFile",
542
+ productRef: productDepUUID,
543
+ productRef_comment: productName,
544
+ }
545
+ objects.PBXBuildFile[`${buildFileUUID}_comment`] =
546
+ `${productName} in Frameworks`
547
+
548
+ // Add to frameworks build phase
549
+ for (const key in objects.PBXFrameworksBuildPhase) {
550
+ if (key.endsWith("_comment")) continue
551
+ const phase = objects.PBXFrameworksBuildPhase[key]
552
+ if (phase?.files) {
553
+ phase.files.push({
554
+ value: buildFileUUID,
555
+ comment: `${productName} in Frameworks`,
556
+ })
557
+ }
558
+ }
559
+
560
+ // Add to packageProductDependencies
561
+ for (const key in objects.PBXNativeTarget) {
562
+ if (key.endsWith("_comment")) continue
563
+ const target = objects.PBXNativeTarget[key]
564
+ if (!target?.isa) continue
565
+ if (!target.packageProductDependencies) {
566
+ target.packageProductDependencies = []
567
+ }
568
+ target.packageProductDependencies.push({
569
+ value: productDepUUID,
570
+ comment: productName,
571
+ })
572
+ }
573
+ }
574
+
575
+ // ─── CLI Output ──────────────────────────────────────────────────────────────
576
+
577
+ /**
578
+ * Print one-time Xcode setup instructions.
579
+ * Only shown when auto-linking is not available (no .xcodeproj found).
580
+ *
581
+ * @param {Function} log - Logging function (e.g., console.log)
582
+ * @param {string} [targetName] - The module name the user should import
583
+ */
584
+ export function printXcodeSetupInstructions(log, targetName) {
585
+ log("")
586
+ log(" To use LPM components in your Xcode project:")
587
+ log(" 1. In Xcode: File → Add Package Dependencies…")
588
+ log(' 2. Click "Add Local…"')
589
+ log(" 3. Select the Packages/LPMComponents directory")
590
+ log(" 4. Add LPMComponents to your app target")
591
+ log("")
592
+ if (targetName) {
593
+ log(` Then import in your Swift code:`)
594
+ log(` import ${targetName}`)
595
+ log("")
596
+ }
597
+ log(" This is a one-time setup. Future `lpm add` commands")
598
+ log(" will add new targets to the same package automatically.")
599
+ }
600
+
601
+ /**
602
+ * Print Swift dependency instructions for the consumer.
603
+ *
604
+ * @param {object} versionData - Package version data from registry
605
+ * @param {Function} log - Logging function
606
+ */
607
+ export function printSwiftDependencyInstructions(versionData, log) {
608
+ const meta = versionData?.versionMeta || versionData?.meta || {}
609
+ const swiftManifest = meta.swiftManifest || meta._swiftManifest
610
+ if (!swiftManifest?.dependencies?.length) return
611
+
612
+ const externalDeps = swiftManifest.dependencies.filter(
613
+ d => d.type === "sourceControl" && !d.location?.includes("lpm.dev"),
614
+ )
615
+ const lpmDeps = swiftManifest.dependencies.filter(
616
+ d => d.type === "sourceControl" && d.location?.includes("lpm.dev"),
617
+ )
618
+
619
+ if (externalDeps.length > 0) {
620
+ log("")
621
+ log(" This package depends on external Swift packages.")
622
+ log(" Add these to your Package.swift dependencies:")
623
+ log("")
624
+ for (const dep of externalDeps) {
625
+ const version = dep.requirement?.range?.[0]?.lowerBound || "1.0.0"
626
+ log(` .package(url: "${dep.location}", from: "${version}"),`)
627
+ }
628
+ }
629
+
630
+ if (lpmDeps.length > 0) {
631
+ log("")
632
+ log(" This package also uses LPM dependencies:")
633
+ for (const dep of lpmDeps) {
634
+ log(` lpm add ${dep.identity}`)
635
+ }
636
+ }
637
+ }
package/lib/ui.js ADDED
@@ -0,0 +1,40 @@
1
+ import boxen from "boxen"
2
+ import chalk from "chalk"
3
+ import ora from "ora"
4
+
5
+ export const printHeader = () => {
6
+ console.log(chalk.dim("Licensed Package Manager CLI\n"))
7
+ }
8
+
9
+ export const printUpdateNotice = _pkg => {
10
+ // This will be handled by update-notifier in the main entry point,
11
+ // but we can have a custom one if needed.
12
+ }
13
+
14
+ export const createSpinner = text => {
15
+ return ora({
16
+ text,
17
+ color: "cyan",
18
+ spinner: "dots",
19
+ })
20
+ }
21
+
22
+ export const log = {
23
+ success: msg => console.log(chalk.green(`✔ ${msg}`)),
24
+ error: msg => console.log(chalk.red(`✖ ${msg}`)),
25
+ info: msg => console.log(chalk.blue(`ℹ ${msg}`)),
26
+ warn: msg => console.log(chalk.yellow(`⚠ ${msg}`)),
27
+ dim: msg => console.log(chalk.dim(msg)),
28
+ box: (msg, title) => {
29
+ console.log(
30
+ boxen(msg, {
31
+ padding: 1,
32
+ margin: 1,
33
+ borderStyle: "round",
34
+ borderColor: "cyan",
35
+ title: title,
36
+ titleAlignment: "center",
37
+ }),
38
+ )
39
+ },
40
+ }