@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +15 -0
- package/README.md +406 -0
- package/bin/lpm.js +334 -0
- package/index.d.ts +131 -0
- package/index.js +31 -0
- package/lib/api.js +324 -0
- package/lib/commands/add.js +1217 -0
- package/lib/commands/audit.js +283 -0
- package/lib/commands/cache.js +209 -0
- package/lib/commands/check-name.js +112 -0
- package/lib/commands/config.js +174 -0
- package/lib/commands/doctor.js +142 -0
- package/lib/commands/info.js +215 -0
- package/lib/commands/init.js +146 -0
- package/lib/commands/install.js +217 -0
- package/lib/commands/login.js +547 -0
- package/lib/commands/logout.js +94 -0
- package/lib/commands/marketplace-compare.js +164 -0
- package/lib/commands/marketplace-earnings.js +89 -0
- package/lib/commands/mcp-setup.js +363 -0
- package/lib/commands/open.js +82 -0
- package/lib/commands/outdated.js +291 -0
- package/lib/commands/pool-stats.js +100 -0
- package/lib/commands/publish.js +707 -0
- package/lib/commands/quality.js +211 -0
- package/lib/commands/remove.js +82 -0
- package/lib/commands/run.js +14 -0
- package/lib/commands/search.js +143 -0
- package/lib/commands/setup.js +92 -0
- package/lib/commands/skills.js +863 -0
- package/lib/commands/token-rotate.js +25 -0
- package/lib/commands/whoami.js +129 -0
- package/lib/config.js +240 -0
- package/lib/constants.js +190 -0
- package/lib/ecosystem.js +501 -0
- package/lib/editors.js +215 -0
- package/lib/import-rewriter.js +364 -0
- package/lib/install-targets/mcp-server.js +245 -0
- package/lib/install-targets/vscode-extension.js +178 -0
- package/lib/install-targets.js +82 -0
- package/lib/integrity.js +179 -0
- package/lib/lpm-config-prompts.js +102 -0
- package/lib/lpm-config.js +408 -0
- package/lib/project-utils.js +152 -0
- package/lib/quality/checks.js +654 -0
- package/lib/quality/display.js +139 -0
- package/lib/quality/score.js +115 -0
- package/lib/quality/swift-checks.js +447 -0
- package/lib/safe-path.js +180 -0
- package/lib/secure-store.js +288 -0
- package/lib/swift-project.js +637 -0
- package/lib/ui.js +40 -0
- 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
|
+
}
|