@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,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LPM Package Config System
|
|
3
|
+
*
|
|
4
|
+
* Handles lpm.config.json parsing, validation, condition evaluation,
|
|
5
|
+
* file filtering, and dependency resolution
|
|
6
|
+
* for the `lpm add` command.
|
|
7
|
+
*
|
|
8
|
+
* Pure logic module — no I/O beyond file reads, no prompts.
|
|
9
|
+
*
|
|
10
|
+
* @module cli/lib/lpm-config
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs"
|
|
14
|
+
import path from "node:path"
|
|
15
|
+
|
|
16
|
+
/** Maximum lpm.config.json file size (1 MB) */
|
|
17
|
+
const MAX_CONFIG_FILE_SIZE = 1024 * 1024
|
|
18
|
+
|
|
19
|
+
/** Maximum number of file rules allowed */
|
|
20
|
+
const MAX_FILE_RULES = 1000
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a package reference that may include version and URL query params.
|
|
24
|
+
*
|
|
25
|
+
* Supports:
|
|
26
|
+
* @lpm.dev/owner.pkg
|
|
27
|
+
* @lpm.dev/owner.pkg@1.0.0
|
|
28
|
+
* @lpm.dev/owner.pkg?component=dialog&styling=panda
|
|
29
|
+
* @lpm.dev/owner.pkg@1.0.0?component=dialog,button&styling=panda
|
|
30
|
+
*
|
|
31
|
+
* @param {string} ref - Package reference string
|
|
32
|
+
* @returns {{ name: string, version: string, inlineConfig: Record<string, string>, providedParams: Set<string> }}
|
|
33
|
+
*/
|
|
34
|
+
export function parseLpmPackageReference(ref) {
|
|
35
|
+
// Split query string first
|
|
36
|
+
const queryIndex = ref.indexOf("?")
|
|
37
|
+
let packagePart = ref
|
|
38
|
+
let queryString = ""
|
|
39
|
+
|
|
40
|
+
if (queryIndex !== -1) {
|
|
41
|
+
packagePart = ref.substring(0, queryIndex)
|
|
42
|
+
queryString = ref.substring(queryIndex + 1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Extract version from package part
|
|
46
|
+
let name = packagePart
|
|
47
|
+
let version = "latest"
|
|
48
|
+
|
|
49
|
+
// Find version separator: last @ after position 0 (position 0 is the scope @)
|
|
50
|
+
const lastAt = packagePart.lastIndexOf("@")
|
|
51
|
+
if (lastAt > 0) {
|
|
52
|
+
name = packagePart.substring(0, lastAt)
|
|
53
|
+
version = packagePart.substring(lastAt + 1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse query params
|
|
57
|
+
const inlineConfig = {}
|
|
58
|
+
const providedParams = new Set()
|
|
59
|
+
|
|
60
|
+
if (queryString) {
|
|
61
|
+
const params = new URLSearchParams(queryString)
|
|
62
|
+
for (const [key, value] of params.entries()) {
|
|
63
|
+
inlineConfig[key] = value
|
|
64
|
+
providedParams.add(key)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { name, version, inlineConfig, providedParams }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read and validate lpm.config.json from an extracted package directory.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} extractDir - Path to extracted tarball directory
|
|
75
|
+
* @returns {import('./lpm-config.js').LpmConfig | null} Parsed config or null if not found
|
|
76
|
+
*/
|
|
77
|
+
export function readLpmConfig(extractDir) {
|
|
78
|
+
const configPath = path.join(extractDir, "lpm.config.json")
|
|
79
|
+
|
|
80
|
+
if (!fs.existsSync(configPath)) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const stat = fs.statSync(configPath)
|
|
85
|
+
if (stat.size > MAX_CONFIG_FILE_SIZE) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`lpm.config.json exceeds maximum size of ${MAX_CONFIG_FILE_SIZE / 1024 / 1024}MB`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const raw = fs.readFileSync(configPath, "utf-8")
|
|
92
|
+
const config = JSON.parse(raw)
|
|
93
|
+
|
|
94
|
+
const errors = validateLpmConfig(config)
|
|
95
|
+
if (errors.length > 0) {
|
|
96
|
+
throw new Error(`Invalid lpm.config.json:\n ${errors.join("\n ")}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return config
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate a parsed lpm.config.json object.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} config - Parsed lpm.config.json
|
|
106
|
+
* @returns {string[]} Array of error messages (empty if valid)
|
|
107
|
+
*/
|
|
108
|
+
export function validateLpmConfig(config) {
|
|
109
|
+
const errors = []
|
|
110
|
+
|
|
111
|
+
// Validate file rules
|
|
112
|
+
if (config.files) {
|
|
113
|
+
if (!Array.isArray(config.files)) {
|
|
114
|
+
errors.push("'files' must be an array")
|
|
115
|
+
} else {
|
|
116
|
+
if (config.files.length > MAX_FILE_RULES) {
|
|
117
|
+
errors.push(
|
|
118
|
+
`Too many file rules (${config.files.length}). Maximum is ${MAX_FILE_RULES}.`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < config.files.length; i++) {
|
|
123
|
+
const rule = config.files[i]
|
|
124
|
+
|
|
125
|
+
if (rule.src) {
|
|
126
|
+
const normalizedSrc = rule.src.replace(/\\/g, "/")
|
|
127
|
+
if (normalizedSrc.includes("..")) {
|
|
128
|
+
errors.push(
|
|
129
|
+
`files[${i}].src contains path traversal: "${rule.src}"`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
if (normalizedSrc.startsWith("/")) {
|
|
133
|
+
errors.push(
|
|
134
|
+
`files[${i}].src must be a relative path: "${rule.src}"`,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (rule.dest) {
|
|
140
|
+
const normalizedDest = rule.dest.replace(/\\/g, "/")
|
|
141
|
+
if (normalizedDest.includes("..")) {
|
|
142
|
+
errors.push(
|
|
143
|
+
`files[${i}].dest contains path traversal: "${rule.dest}"`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
if (normalizedDest.startsWith("/")) {
|
|
147
|
+
errors.push(
|
|
148
|
+
`files[${i}].dest must be a relative path: "${rule.dest}"`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
rule.include &&
|
|
155
|
+
!["always", "never", "when"].includes(rule.include)
|
|
156
|
+
) {
|
|
157
|
+
errors.push(
|
|
158
|
+
`files[${i}].include must be "always", "never", or "when". Got: "${rule.include}"`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (rule.include === "when" && !rule.condition) {
|
|
163
|
+
errors.push(`files[${i}] has include "when" but no condition object`)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate configSchema
|
|
170
|
+
if (config.configSchema) {
|
|
171
|
+
if (
|
|
172
|
+
typeof config.configSchema !== "object" ||
|
|
173
|
+
Array.isArray(config.configSchema)
|
|
174
|
+
) {
|
|
175
|
+
errors.push("'configSchema' must be an object")
|
|
176
|
+
} else {
|
|
177
|
+
for (const [key, entry] of Object.entries(config.configSchema)) {
|
|
178
|
+
if (!entry.type) {
|
|
179
|
+
errors.push(`configSchema.${key} is missing 'type'`)
|
|
180
|
+
} else if (!["select", "boolean"].includes(entry.type)) {
|
|
181
|
+
errors.push(
|
|
182
|
+
`configSchema.${key}.type must be "select" or "boolean". Got: "${entry.type}"`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (entry.type === "select") {
|
|
187
|
+
if (
|
|
188
|
+
!entry.options ||
|
|
189
|
+
!Array.isArray(entry.options) ||
|
|
190
|
+
entry.options.length === 0
|
|
191
|
+
) {
|
|
192
|
+
errors.push(
|
|
193
|
+
`configSchema.${key} is type "select" but has no options`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate importAlias
|
|
202
|
+
if (config.importAlias !== undefined) {
|
|
203
|
+
if (typeof config.importAlias !== "string") {
|
|
204
|
+
errors.push('\'importAlias\' must be a string (e.g., "@/", "~/")')
|
|
205
|
+
} else if (!config.importAlias.endsWith("/")) {
|
|
206
|
+
errors.push(
|
|
207
|
+
'\'importAlias\' must end with "/" (e.g., "@/", "~/", "@src/")',
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return errors
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Evaluate whether a file rule should be included based on config and provided params.
|
|
217
|
+
*
|
|
218
|
+
* Implements "include all by default": if a condition key was NOT explicitly
|
|
219
|
+
* provided by the user, the file is included regardless of its condition value.
|
|
220
|
+
*
|
|
221
|
+
* @param {object} fileRule - File rule from lpm.config.json
|
|
222
|
+
* @param {Record<string, string>} mergedConfig - Merged configuration values
|
|
223
|
+
* @param {Set<string>} providedParams - Set of parameter keys explicitly provided by the user
|
|
224
|
+
* @returns {boolean} Whether the file should be included
|
|
225
|
+
*/
|
|
226
|
+
export function evaluateCondition(fileRule, mergedConfig, providedParams) {
|
|
227
|
+
if (!fileRule.include || fileRule.include === "always") return true
|
|
228
|
+
if (fileRule.include === "never") return false
|
|
229
|
+
|
|
230
|
+
if (fileRule.include === "when" && fileRule.condition) {
|
|
231
|
+
// All condition entries must match (AND logic)
|
|
232
|
+
for (const [conditionKey, conditionValue] of Object.entries(
|
|
233
|
+
fileRule.condition,
|
|
234
|
+
)) {
|
|
235
|
+
// "Include all by default": if param was NOT provided, skip this check (include the file)
|
|
236
|
+
if (!providedParams.has(conditionKey)) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const configValue = mergedConfig[conditionKey]
|
|
241
|
+
|
|
242
|
+
// Handle comma-separated multi-select values
|
|
243
|
+
if (typeof configValue === "string" && configValue.includes(",")) {
|
|
244
|
+
const selectedValues = configValue.split(",").map(v => v.trim())
|
|
245
|
+
if (!selectedValues.includes(String(conditionValue))) {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// Single value or boolean comparison
|
|
250
|
+
if (String(configValue) !== String(conditionValue)) {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Default: include
|
|
260
|
+
return true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Filter file rules based on merged config and provided params.
|
|
265
|
+
*
|
|
266
|
+
* @param {object[]} files - Array of file rules from lpm.config.json
|
|
267
|
+
* @param {Record<string, string>} mergedConfig - Merged configuration values
|
|
268
|
+
* @param {Set<string>} providedParams - Set of parameter keys explicitly provided
|
|
269
|
+
* @returns {object[]} Filtered file rules that should be included
|
|
270
|
+
*/
|
|
271
|
+
export function filterFiles(files, mergedConfig, providedParams) {
|
|
272
|
+
return files.filter(fileRule =>
|
|
273
|
+
evaluateCondition(fileRule, mergedConfig, providedParams),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Resolve conditional dependencies based on config choices.
|
|
279
|
+
*
|
|
280
|
+
* @param {Record<string, Record<string, string[]>>} depConfig - Dependencies config from lpm.config.json
|
|
281
|
+
* @param {Record<string, string>} mergedConfig - Merged configuration values
|
|
282
|
+
* @returns {{ npm: string[], lpm: string[] }} Separated npm and LPM dependencies
|
|
283
|
+
*/
|
|
284
|
+
export function resolveConditionalDependencies(depConfig, mergedConfig) {
|
|
285
|
+
const allDeps = new Set()
|
|
286
|
+
|
|
287
|
+
for (const [configKey, depMap] of Object.entries(depConfig)) {
|
|
288
|
+
const selectedValue = mergedConfig[configKey]
|
|
289
|
+
if (!selectedValue) continue
|
|
290
|
+
|
|
291
|
+
// Handle comma-separated values
|
|
292
|
+
const values =
|
|
293
|
+
typeof selectedValue === "string" && selectedValue.includes(",")
|
|
294
|
+
? selectedValue.split(",").map(v => v.trim())
|
|
295
|
+
: [selectedValue]
|
|
296
|
+
|
|
297
|
+
for (const value of values) {
|
|
298
|
+
const deps = depMap[value]
|
|
299
|
+
if (Array.isArray(deps)) {
|
|
300
|
+
for (const dep of deps) {
|
|
301
|
+
allDeps.add(dep)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const npm = []
|
|
308
|
+
const lpm = []
|
|
309
|
+
|
|
310
|
+
for (const dep of allDeps) {
|
|
311
|
+
if (dep.startsWith("@lpm.dev/")) {
|
|
312
|
+
lpm.push(dep)
|
|
313
|
+
} else {
|
|
314
|
+
npm.push(dep)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { npm, lpm }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Expand glob-like src patterns to actual file paths.
|
|
323
|
+
*
|
|
324
|
+
* Supports simple patterns:
|
|
325
|
+
* - Exact paths: "lib/utils.js"
|
|
326
|
+
* - Directory wildcards: "components/dialog/**"
|
|
327
|
+
*
|
|
328
|
+
* @param {string} srcPattern - Source pattern from file rule
|
|
329
|
+
* @param {string} extractDir - Extracted tarball directory
|
|
330
|
+
* @returns {string[]} Array of matching file paths (relative to extractDir)
|
|
331
|
+
*/
|
|
332
|
+
export function expandSrcGlob(srcPattern, extractDir) {
|
|
333
|
+
// If pattern doesn't contain *, it's an exact path
|
|
334
|
+
if (!srcPattern.includes("*")) {
|
|
335
|
+
const fullPath = path.join(extractDir, srcPattern)
|
|
336
|
+
if (fs.existsSync(fullPath)) {
|
|
337
|
+
return [srcPattern]
|
|
338
|
+
}
|
|
339
|
+
return []
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Handle ** (directory wildcard)
|
|
343
|
+
if (srcPattern.endsWith("/**")) {
|
|
344
|
+
const baseDir = srcPattern.slice(0, -3) // Remove /**
|
|
345
|
+
const fullBaseDir = path.join(extractDir, baseDir)
|
|
346
|
+
|
|
347
|
+
if (
|
|
348
|
+
!fs.existsSync(fullBaseDir) ||
|
|
349
|
+
!fs.statSync(fullBaseDir).isDirectory()
|
|
350
|
+
) {
|
|
351
|
+
return []
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const results = []
|
|
355
|
+
const collectFiles = (dir, relativeTo) => {
|
|
356
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
357
|
+
for (const entry of entries) {
|
|
358
|
+
const fullEntryPath = path.join(dir, entry.name)
|
|
359
|
+
const relPath = path.relative(extractDir, fullEntryPath)
|
|
360
|
+
|
|
361
|
+
if (entry.isDirectory()) {
|
|
362
|
+
collectFiles(fullEntryPath, relativeTo)
|
|
363
|
+
} else {
|
|
364
|
+
results.push(relPath)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
collectFiles(fullBaseDir, extractDir)
|
|
370
|
+
return results
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Handle dir/*.ext patterns (single-directory wildcard)
|
|
374
|
+
const lastSlash = srcPattern.lastIndexOf("/")
|
|
375
|
+
const dirPart = lastSlash >= 0 ? srcPattern.slice(0, lastSlash) : "."
|
|
376
|
+
const filePart = lastSlash >= 0 ? srcPattern.slice(lastSlash + 1) : srcPattern
|
|
377
|
+
|
|
378
|
+
if (filePart.includes("*")) {
|
|
379
|
+
const fullDir = path.join(extractDir, dirPart)
|
|
380
|
+
if (!fs.existsSync(fullDir) || !fs.statSync(fullDir).isDirectory()) {
|
|
381
|
+
return []
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Convert glob pattern to regex: *.mdc → /^.*\.mdc$/
|
|
385
|
+
const escaped = filePart
|
|
386
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
387
|
+
.replace(/\*/g, ".*")
|
|
388
|
+
const regex = new RegExp(`^${escaped}$`)
|
|
389
|
+
|
|
390
|
+
const entries = fs.readdirSync(fullDir, { withFileTypes: true })
|
|
391
|
+
const results = []
|
|
392
|
+
for (const entry of entries) {
|
|
393
|
+
if (entry.isFile() && regex.test(entry.name)) {
|
|
394
|
+
results.push(
|
|
395
|
+
dirPart === "." ? entry.name : path.join(dirPart, entry.name),
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return results
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// For other patterns, treat as exact path
|
|
403
|
+
const fullPath = path.join(extractDir, srcPattern)
|
|
404
|
+
if (fs.existsSync(fullPath)) {
|
|
405
|
+
return [srcPattern]
|
|
406
|
+
}
|
|
407
|
+
return []
|
|
408
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect the consumer project type.
|
|
6
|
+
* Returns a framework string for JS projects, or a Swift project type.
|
|
7
|
+
* Priority: Package.swift → .xcodeproj → .xcworkspace → JS frameworks → unknown
|
|
8
|
+
*/
|
|
9
|
+
export function detectFramework() {
|
|
10
|
+
const cwd = process.cwd()
|
|
11
|
+
|
|
12
|
+
// Check for Swift projects first
|
|
13
|
+
if (fs.existsSync(path.join(cwd, "Package.swift"))) return "swift-spm"
|
|
14
|
+
|
|
15
|
+
// Check for Xcode projects (without Package.swift = app project)
|
|
16
|
+
const entries = fs.readdirSync(cwd)
|
|
17
|
+
if (entries.some(e => e.endsWith(".xcodeproj"))) return "swift-xcode"
|
|
18
|
+
if (entries.some(e => e.endsWith(".xcworkspace"))) return "swift-xcode"
|
|
19
|
+
|
|
20
|
+
// JS frameworks
|
|
21
|
+
const pkgPath = path.join(cwd, "package.json")
|
|
22
|
+
if (!fs.existsSync(pkgPath)) return "unknown"
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
|
26
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
27
|
+
|
|
28
|
+
if (deps.next) {
|
|
29
|
+
if (fs.existsSync(path.join(cwd, "app"))) return "next-app"
|
|
30
|
+
return "next-pages"
|
|
31
|
+
}
|
|
32
|
+
if (deps["@remix-run/react"]) return "remix"
|
|
33
|
+
if (deps.vite) return "vite"
|
|
34
|
+
} catch (_e) {
|
|
35
|
+
return "unknown"
|
|
36
|
+
}
|
|
37
|
+
return "unknown"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if the detected framework is a Swift project.
|
|
42
|
+
* @param {string} framework
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function isSwiftProject(framework) {
|
|
46
|
+
return framework === "swift-spm" || framework === "swift-xcode"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getDefaultPath(framework, swiftTarget) {
|
|
50
|
+
switch (framework) {
|
|
51
|
+
case "swift-spm":
|
|
52
|
+
return swiftTarget ? `Sources/${swiftTarget}` : "Sources"
|
|
53
|
+
case "swift-xcode":
|
|
54
|
+
// Target-specific path is set in add.js via ensureXcodeLocalPackage().
|
|
55
|
+
// This default is a fallback only used when no target name is known.
|
|
56
|
+
return swiftTarget
|
|
57
|
+
? `Packages/LPMComponents/Sources/${swiftTarget}`
|
|
58
|
+
: "Packages/LPMComponents/Sources/LPMComponents"
|
|
59
|
+
case "next-app":
|
|
60
|
+
if (fs.existsSync(path.join(process.cwd(), "components"))) {
|
|
61
|
+
return "components"
|
|
62
|
+
}
|
|
63
|
+
return "src/components"
|
|
64
|
+
case "next-pages":
|
|
65
|
+
case "vite":
|
|
66
|
+
case "remix":
|
|
67
|
+
return "src/components"
|
|
68
|
+
default:
|
|
69
|
+
return "components"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getProjectAliases() {
|
|
74
|
+
const cwd = process.cwd()
|
|
75
|
+
const tsConfigPath = path.join(cwd, "tsconfig.json")
|
|
76
|
+
const jsConfigPath = path.join(cwd, "jsconfig.json")
|
|
77
|
+
|
|
78
|
+
let configPath
|
|
79
|
+
if (fs.existsSync(tsConfigPath)) configPath = tsConfigPath
|
|
80
|
+
else if (fs.existsSync(jsConfigPath)) configPath = jsConfigPath
|
|
81
|
+
|
|
82
|
+
if (!configPath) return {}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Simple JSON parse (might fail with comments)
|
|
86
|
+
const content = fs.readFileSync(configPath, "utf-8")
|
|
87
|
+
// Basic comment stripping
|
|
88
|
+
const jsonContent = content.replace(
|
|
89
|
+
/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm,
|
|
90
|
+
"$1",
|
|
91
|
+
)
|
|
92
|
+
const config = JSON.parse(jsonContent)
|
|
93
|
+
return config.compilerOptions?.paths || {}
|
|
94
|
+
} catch (_e) {
|
|
95
|
+
return {}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getUserImportPrefix() {
|
|
100
|
+
const aliases = getProjectAliases()
|
|
101
|
+
// Look for an alias that points to ./src or ./
|
|
102
|
+
for (const [alias, paths] of Object.entries(aliases)) {
|
|
103
|
+
if (
|
|
104
|
+
Array.isArray(paths) &&
|
|
105
|
+
paths.some(p => p.startsWith("./src") || p.startsWith("src"))
|
|
106
|
+
) {
|
|
107
|
+
return alias.replace("/*", "")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return "@" // Default fallback
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the import alias that maps to a given directory.
|
|
115
|
+
*
|
|
116
|
+
* Given a target directory like "src/components/design-system" and aliases
|
|
117
|
+
* like { "@/*": ["./src/*"] }, returns "@/components/design-system".
|
|
118
|
+
*
|
|
119
|
+
* @param {string} targetDirRelative - Target directory relative to project root
|
|
120
|
+
* @param {Record<string, string[]>} aliases - Parsed tsconfig/jsconfig paths
|
|
121
|
+
* @returns {string | null} The resolved alias path, or null if no alias covers this directory
|
|
122
|
+
*/
|
|
123
|
+
export function resolveAliasForDirectory(targetDirRelative, aliases) {
|
|
124
|
+
const normalized = targetDirRelative
|
|
125
|
+
.replace(/\\/g, "/")
|
|
126
|
+
.replace(/^\.\//, "")
|
|
127
|
+
.replace(/\/$/, "")
|
|
128
|
+
|
|
129
|
+
for (const [aliasPattern, aliasPaths] of Object.entries(aliases)) {
|
|
130
|
+
if (!aliasPattern.endsWith("/*")) continue
|
|
131
|
+
if (!Array.isArray(aliasPaths)) continue
|
|
132
|
+
|
|
133
|
+
const aliasPrefix = aliasPattern.slice(0, -2) // "@/*" → "@"
|
|
134
|
+
|
|
135
|
+
for (const aliasPath of aliasPaths) {
|
|
136
|
+
const mappedDir = aliasPath
|
|
137
|
+
.replace(/^\.\//, "")
|
|
138
|
+
.replace(/\/\*$/, "")
|
|
139
|
+
.replace(/\/$/, "")
|
|
140
|
+
|
|
141
|
+
if (normalized.startsWith(`${mappedDir}/`)) {
|
|
142
|
+
const remainder = normalized.slice(mappedDir.length + 1)
|
|
143
|
+
return `${aliasPrefix}/${remainder}`
|
|
144
|
+
}
|
|
145
|
+
if (normalized === mappedDir) {
|
|
146
|
+
return aliasPrefix
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null
|
|
152
|
+
}
|