@lpm-registry/cli 0.58.0 → 0.59.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -44
- package/bin/lpm.js +2 -354
- package/bin/lpx.js +4 -0
- package/bin/native.js +154 -0
- package/package.json +42 -82
- package/scripts/install-binary.js +219 -550
- package/CHANGELOG.md +0 -197
- package/LICENSE +0 -15
- package/index.d.ts +0 -131
- package/index.js +0 -31
- package/lib/api.js +0 -398
- package/lib/commands/add.js +0 -1217
- package/lib/commands/audit.js +0 -283
- package/lib/commands/cache.js +0 -209
- package/lib/commands/check-name.js +0 -112
- package/lib/commands/config.js +0 -186
- package/lib/commands/doctor.js +0 -140
- package/lib/commands/info.js +0 -215
- package/lib/commands/init.js +0 -146
- package/lib/commands/install.js +0 -264
- package/lib/commands/login.js +0 -595
- package/lib/commands/logout.js +0 -94
- package/lib/commands/marketplace-compare.js +0 -164
- package/lib/commands/marketplace-earnings.js +0 -89
- package/lib/commands/mcp-setup.js +0 -363
- package/lib/commands/npmrc.js +0 -178
- package/lib/commands/open.js +0 -82
- package/lib/commands/outdated.js +0 -291
- package/lib/commands/pool-stats.js +0 -100
- package/lib/commands/publish.js +0 -949
- package/lib/commands/quality.js +0 -211
- package/lib/commands/remove.js +0 -82
- package/lib/commands/run.js +0 -14
- package/lib/commands/search.js +0 -143
- package/lib/commands/setup.js +0 -221
- package/lib/commands/skills.js +0 -898
- package/lib/commands/token-rotate.js +0 -25
- package/lib/commands/uninstall.js +0 -107
- package/lib/commands/whoami.js +0 -129
- package/lib/config.js +0 -328
- package/lib/constants.js +0 -190
- package/lib/ecosystem.js +0 -501
- package/lib/editor-skills.js +0 -321
- package/lib/editors.js +0 -215
- package/lib/import-rewriter.js +0 -364
- package/lib/install-targets/mcp-server.js +0 -245
- package/lib/install-targets/vscode-extension.js +0 -178
- package/lib/install-targets.js +0 -82
- package/lib/integrity.js +0 -179
- package/lib/lpm-config-prompts.js +0 -102
- package/lib/lpm-config.js +0 -408
- package/lib/oidc.js +0 -166
- package/lib/project-utils.js +0 -152
- package/lib/quality/checks.js +0 -687
- package/lib/quality/display.js +0 -155
- package/lib/quality/score.js +0 -118
- package/lib/quality/swift-checks.js +0 -447
- package/lib/safe-path.js +0 -180
- package/lib/secure-store.js +0 -321
- package/lib/swift-project.js +0 -637
- package/lib/ui.js +0 -40
- package/scripts/__tests__/install-binary.test.js +0 -346
package/lib/commands/publish.js
DELETED
|
@@ -1,949 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process"
|
|
2
|
-
import { createHash } from "node:crypto"
|
|
3
|
-
import fs from "node:fs"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import { promisify } from "node:util"
|
|
6
|
-
import * as p from "@clack/prompts"
|
|
7
|
-
import { request, verifyTokenScope } from "../api.js"
|
|
8
|
-
import {
|
|
9
|
-
clearTransientToken,
|
|
10
|
-
getRegistryUrl,
|
|
11
|
-
setTransientToken,
|
|
12
|
-
} from "../config.js"
|
|
13
|
-
import { SUCCESS_MESSAGES, WARNING_MESSAGES } from "../constants.js"
|
|
14
|
-
import {
|
|
15
|
-
createEcosystemTarball,
|
|
16
|
-
detectEcosystem,
|
|
17
|
-
detectXCFramework,
|
|
18
|
-
extractSwiftMetadata,
|
|
19
|
-
readSwiftManifest,
|
|
20
|
-
} from "../ecosystem.js"
|
|
21
|
-
import { generateIntegrity } from "../integrity.js"
|
|
22
|
-
import {
|
|
23
|
-
exchangeGitLabOidcToken,
|
|
24
|
-
exchangeOidcToken,
|
|
25
|
-
isGitHubActionsWithOidc,
|
|
26
|
-
isGitLabCiWithOidc,
|
|
27
|
-
} from "../oidc.js"
|
|
28
|
-
import { displayQualityReport } from "../quality/display.js"
|
|
29
|
-
import { runQualityChecks } from "../quality/score.js"
|
|
30
|
-
import { createSpinner, log, printHeader } from "../ui.js"
|
|
31
|
-
|
|
32
|
-
const execAsync = promisify(exec)
|
|
33
|
-
const readFileAsync = promisify(fs.readFile)
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Parse package name in the @lpm.dev/owner.package format
|
|
37
|
-
* @returns {{ owner: string, pkgName: string } | { error: string }}
|
|
38
|
-
*/
|
|
39
|
-
function parsePackageName(name) {
|
|
40
|
-
// New format: @lpm.dev/owner.package-name
|
|
41
|
-
if (name.startsWith("@lpm.dev/")) {
|
|
42
|
-
const nameWithOwner = name.replace("@lpm.dev/", "")
|
|
43
|
-
const dotIndex = nameWithOwner.indexOf(".")
|
|
44
|
-
if (dotIndex === -1) {
|
|
45
|
-
return { error: "Invalid format. Expected @lpm.dev/owner.package-name" }
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
owner: nameWithOwner.substring(0, dotIndex),
|
|
49
|
-
pkgName: nameWithOwner.substring(dotIndex + 1),
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Legacy format: @scope/package-name
|
|
54
|
-
if (name.startsWith("@")) {
|
|
55
|
-
const match = name.match(/^@([^/]+)\/(.+)$/)
|
|
56
|
-
if (match) {
|
|
57
|
-
return {
|
|
58
|
-
owner: match[1],
|
|
59
|
-
pkgName: match[2],
|
|
60
|
-
isLegacy: true,
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
error: "Invalid package name. Use @lpm.dev/owner.package-name format",
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Run the interactive init flow for non-JS packages.
|
|
72
|
-
* Generates a minimal package.json with name, version, and description.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} ecosystem - Detected ecosystem
|
|
75
|
-
* @returns {Promise<object>} Generated package.json contents
|
|
76
|
-
*/
|
|
77
|
-
async function initNonJsPackage(ecosystem) {
|
|
78
|
-
log.info(`Detected ${ecosystem} project without package.json.`)
|
|
79
|
-
log.info("Creating package metadata for LPM registry...")
|
|
80
|
-
console.log("")
|
|
81
|
-
|
|
82
|
-
// Get username from auth to auto-prefix owner
|
|
83
|
-
const whoamiResponse = await request("/-/whoami")
|
|
84
|
-
if (!whoamiResponse.ok) {
|
|
85
|
-
throw new Error("Could not determine your username. Run `lpm login` first.")
|
|
86
|
-
}
|
|
87
|
-
const whoami = await whoamiResponse.json()
|
|
88
|
-
|
|
89
|
-
// Build available owners list
|
|
90
|
-
const availableOwners = []
|
|
91
|
-
if (whoami.profile_username) {
|
|
92
|
-
availableOwners.push({
|
|
93
|
-
value: whoami.profile_username,
|
|
94
|
-
label: `${whoami.profile_username} (personal)`,
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
for (const org of whoami.organizations || []) {
|
|
98
|
-
availableOwners.push({
|
|
99
|
-
value: org.slug,
|
|
100
|
-
label: `${org.slug} (organization)`,
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (availableOwners.length === 0) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
"No available owners. Set your username at the dashboard or create an organization.",
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Select owner
|
|
111
|
-
let owner
|
|
112
|
-
if (availableOwners.length === 1) {
|
|
113
|
-
owner = availableOwners[0].value
|
|
114
|
-
} else {
|
|
115
|
-
const selected = await p.select({
|
|
116
|
-
message: "Publish under which owner?",
|
|
117
|
-
options: availableOwners,
|
|
118
|
-
})
|
|
119
|
-
if (p.isCancel(selected)) {
|
|
120
|
-
p.cancel("Cancelled.")
|
|
121
|
-
process.exit(0)
|
|
122
|
-
}
|
|
123
|
-
owner = selected
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Suggest package name from directory name
|
|
127
|
-
const dirName = path
|
|
128
|
-
.basename(process.cwd())
|
|
129
|
-
.toLowerCase()
|
|
130
|
-
.replace(/[^a-z0-9-]/g, "-")
|
|
131
|
-
const packageNameInput = await p.text({
|
|
132
|
-
message: "Package name",
|
|
133
|
-
placeholder: dirName,
|
|
134
|
-
defaultValue: dirName,
|
|
135
|
-
validate: value => {
|
|
136
|
-
if (!value) return "Package name is required"
|
|
137
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(value))
|
|
138
|
-
return "Must start with a letter/number and contain only lowercase letters, numbers, and hyphens"
|
|
139
|
-
},
|
|
140
|
-
})
|
|
141
|
-
if (p.isCancel(packageNameInput)) {
|
|
142
|
-
p.cancel("Cancelled.")
|
|
143
|
-
process.exit(0)
|
|
144
|
-
}
|
|
145
|
-
const packageName = packageNameInput || dirName
|
|
146
|
-
|
|
147
|
-
const versionInput = await p.text({
|
|
148
|
-
message: "Version",
|
|
149
|
-
placeholder: "1.0.0",
|
|
150
|
-
defaultValue: "1.0.0",
|
|
151
|
-
validate: value => {
|
|
152
|
-
if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(value))
|
|
153
|
-
return "Must be valid semver (e.g. 1.0.0)"
|
|
154
|
-
},
|
|
155
|
-
})
|
|
156
|
-
if (p.isCancel(versionInput)) {
|
|
157
|
-
p.cancel("Cancelled.")
|
|
158
|
-
process.exit(0)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const description = await p.text({
|
|
162
|
-
message: "Description (optional)",
|
|
163
|
-
placeholder: "",
|
|
164
|
-
})
|
|
165
|
-
if (p.isCancel(description)) {
|
|
166
|
-
p.cancel("Cancelled.")
|
|
167
|
-
process.exit(0)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const fullName = `@lpm.dev/${owner}.${packageName}`
|
|
171
|
-
const pkg = {
|
|
172
|
-
name: fullName,
|
|
173
|
-
version: versionInput || "1.0.0",
|
|
174
|
-
}
|
|
175
|
-
if (description) {
|
|
176
|
-
pkg.description = description
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Write package.json
|
|
180
|
-
const packageJsonPath = path.resolve(process.cwd(), "package.json")
|
|
181
|
-
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
182
|
-
log.success(`Created package.json for ${fullName}`)
|
|
183
|
-
console.log("")
|
|
184
|
-
|
|
185
|
-
return pkg
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Read README from the current directory.
|
|
190
|
-
* Searches for common README filenames.
|
|
191
|
-
*
|
|
192
|
-
* @returns {string|null}
|
|
193
|
-
*/
|
|
194
|
-
function readReadme() {
|
|
195
|
-
const readmeFilenames = [
|
|
196
|
-
"README.md",
|
|
197
|
-
"readme.md",
|
|
198
|
-
"README",
|
|
199
|
-
"Readme.md",
|
|
200
|
-
"README.txt",
|
|
201
|
-
]
|
|
202
|
-
|
|
203
|
-
for (const filename of readmeFilenames) {
|
|
204
|
-
const readmePath = path.resolve(process.cwd(), filename)
|
|
205
|
-
|
|
206
|
-
if (!readmePath.startsWith(process.cwd())) {
|
|
207
|
-
continue
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (fs.existsSync(readmePath)) {
|
|
211
|
-
try {
|
|
212
|
-
const stats = fs.statSync(readmePath)
|
|
213
|
-
|
|
214
|
-
const MAX_README_SIZE = 1024 * 1024
|
|
215
|
-
if (stats.size > MAX_README_SIZE) {
|
|
216
|
-
log.warn(
|
|
217
|
-
`README file is too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Maximum size is 1MB. Skipping README.`,
|
|
218
|
-
)
|
|
219
|
-
return null
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const readmeBuffer = fs.readFileSync(readmePath)
|
|
223
|
-
|
|
224
|
-
const isBinary = readmeBuffer.some(
|
|
225
|
-
byte =>
|
|
226
|
-
byte === 0 ||
|
|
227
|
-
(byte < 32 && byte !== 9 && byte !== 10 && byte !== 13),
|
|
228
|
-
)
|
|
229
|
-
if (isBinary) {
|
|
230
|
-
log.warn("README appears to be binary. Skipping.")
|
|
231
|
-
return null
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
let readme = readmeBuffer.toString("utf8").trim()
|
|
235
|
-
|
|
236
|
-
if (readme.length > MAX_README_SIZE) {
|
|
237
|
-
readme = readme.substring(0, MAX_README_SIZE)
|
|
238
|
-
log.warn("README truncated to 1MB.")
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return readme
|
|
242
|
-
} catch (_err) {
|
|
243
|
-
// Continue to next filename
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return null
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Read lpm.config.json from the current directory.
|
|
253
|
-
*
|
|
254
|
-
* @returns {object|null}
|
|
255
|
-
*/
|
|
256
|
-
function readLpmConfig() {
|
|
257
|
-
const lpmConfigPath = path.resolve(process.cwd(), "lpm.config.json")
|
|
258
|
-
if (fs.existsSync(lpmConfigPath)) {
|
|
259
|
-
try {
|
|
260
|
-
const lpmConfigRaw = fs.readFileSync(lpmConfigPath, "utf-8")
|
|
261
|
-
return JSON.parse(lpmConfigRaw)
|
|
262
|
-
} catch (_err) {
|
|
263
|
-
log.warn("Could not parse lpm.config.json. Skipping.")
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return null
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Pack using npm pack (JS ecosystem).
|
|
271
|
-
*
|
|
272
|
-
* @returns {Promise<{ tarballPath: string, npmPackMeta: object }>}
|
|
273
|
-
*/
|
|
274
|
-
async function packWithNpm() {
|
|
275
|
-
const { stdout } = await execAsync("npm pack --json")
|
|
276
|
-
const packResult = JSON.parse(stdout)
|
|
277
|
-
const packInfo = packResult[0]
|
|
278
|
-
const tarballFilename = packInfo.filename
|
|
279
|
-
const tarballPath = path.resolve(process.cwd(), tarballFilename)
|
|
280
|
-
|
|
281
|
-
return {
|
|
282
|
-
tarballPath,
|
|
283
|
-
npmPackMeta: {
|
|
284
|
-
unpackedSize: packInfo.unpackedSize,
|
|
285
|
-
fileCount: packInfo.files?.length || 0,
|
|
286
|
-
files: packInfo.files || [],
|
|
287
|
-
},
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Pack using tar (non-JS ecosystems).
|
|
293
|
-
*
|
|
294
|
-
* @param {string} ecosystem - Ecosystem identifier
|
|
295
|
-
* @param {string} name - Package name
|
|
296
|
-
* @param {string} version - Package version
|
|
297
|
-
* @returns {Promise<{ tarballPath: string, npmPackMeta: object }>}
|
|
298
|
-
*/
|
|
299
|
-
async function packWithTar(ecosystem, name, version) {
|
|
300
|
-
const result = await createEcosystemTarball(ecosystem, name, version)
|
|
301
|
-
return {
|
|
302
|
-
tarballPath: result.tarballPath,
|
|
303
|
-
npmPackMeta: {
|
|
304
|
-
unpackedSize: result.unpackedSize,
|
|
305
|
-
fileCount: result.fileCount,
|
|
306
|
-
files: result.files,
|
|
307
|
-
},
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Scan local project directory for test files.
|
|
313
|
-
* This avoids requiring test files in the tarball to earn quality points.
|
|
314
|
-
* Uses a limited-depth scan, skipping known non-source directories.
|
|
315
|
-
*/
|
|
316
|
-
function scanProjectForTestFiles() {
|
|
317
|
-
const skipDirs = new Set([
|
|
318
|
-
"node_modules",
|
|
319
|
-
".git",
|
|
320
|
-
"dist",
|
|
321
|
-
"build",
|
|
322
|
-
".next",
|
|
323
|
-
".nuxt",
|
|
324
|
-
"coverage",
|
|
325
|
-
".lpm",
|
|
326
|
-
])
|
|
327
|
-
|
|
328
|
-
const result = []
|
|
329
|
-
|
|
330
|
-
function walk(dir, depth = 0) {
|
|
331
|
-
if (depth > 4) return
|
|
332
|
-
let entries
|
|
333
|
-
try {
|
|
334
|
-
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
335
|
-
} catch {
|
|
336
|
-
return
|
|
337
|
-
}
|
|
338
|
-
for (const entry of entries) {
|
|
339
|
-
if (entry.isDirectory()) {
|
|
340
|
-
if (!skipDirs.has(entry.name)) {
|
|
341
|
-
walk(path.join(dir, entry.name), depth + 1)
|
|
342
|
-
}
|
|
343
|
-
} else if (entry.isFile()) {
|
|
344
|
-
result.push({ path: path.join(dir, entry.name) })
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
walk(process.cwd())
|
|
350
|
-
return result
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export async function publish(options = {}) {
|
|
354
|
-
const checkOnly = !!options.check
|
|
355
|
-
const dryRun = !!options.dryRun
|
|
356
|
-
const minScore = options.minScore ? parseInt(options.minScore, 10) : null
|
|
357
|
-
const forceProvenance = !!options.provenance
|
|
358
|
-
|
|
359
|
-
printHeader()
|
|
360
|
-
|
|
361
|
-
// 1. Detect ecosystem
|
|
362
|
-
const { ecosystem, manifestFile } = detectEcosystem()
|
|
363
|
-
|
|
364
|
-
if (!ecosystem) {
|
|
365
|
-
log.error(
|
|
366
|
-
"No recognized project manifest found (package.json, Package.swift, Cargo.toml, pyproject.toml).",
|
|
367
|
-
)
|
|
368
|
-
process.exit(1)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (ecosystem !== "js") {
|
|
372
|
-
log.info(`Detected ${ecosystem} project (${manifestFile})`)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 2. Read or generate package.json
|
|
376
|
-
const packageJsonPath = path.resolve(process.cwd(), "package.json")
|
|
377
|
-
let pkg
|
|
378
|
-
let _isNewPackage = false
|
|
379
|
-
|
|
380
|
-
if (ecosystem !== "js" && !fs.existsSync(packageJsonPath)) {
|
|
381
|
-
// Non-JS project without package.json — run init flow
|
|
382
|
-
pkg = await initNonJsPackage(ecosystem)
|
|
383
|
-
_isNewPackage = true
|
|
384
|
-
} else if (!fs.existsSync(packageJsonPath)) {
|
|
385
|
-
log.error("No package.json found in current directory.")
|
|
386
|
-
process.exit(1)
|
|
387
|
-
} else {
|
|
388
|
-
try {
|
|
389
|
-
pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
|
|
390
|
-
} catch (err) {
|
|
391
|
-
log.error(`Invalid JSON in package.json: ${err.message}`)
|
|
392
|
-
process.exit(1)
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const { name, version } = pkg
|
|
397
|
-
|
|
398
|
-
// Parse package name to extract owner
|
|
399
|
-
const parsed = parsePackageName(name)
|
|
400
|
-
if (parsed.error) {
|
|
401
|
-
log.error(parsed.error)
|
|
402
|
-
log.info("LPM packages must use format: @lpm.dev/owner.package-name")
|
|
403
|
-
log.info(`Your current name: ${name}`)
|
|
404
|
-
|
|
405
|
-
// Suggest fix for legacy format
|
|
406
|
-
const oldMatch = name.match(/^@([^/]+)\/(.+)$/)
|
|
407
|
-
if (oldMatch) {
|
|
408
|
-
const suggested = `@lpm.dev/${oldMatch[1]}.${oldMatch[2]}`
|
|
409
|
-
log.info(`Suggested: ${suggested}`)
|
|
410
|
-
}
|
|
411
|
-
process.exit(1)
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const { owner, pkgName: packageName, isLegacy } = parsed
|
|
415
|
-
|
|
416
|
-
// Warn about legacy format
|
|
417
|
-
if (isLegacy) {
|
|
418
|
-
log.warn(`Legacy format detected: ${name}`)
|
|
419
|
-
log.warn(`Please migrate to: @lpm.dev/${owner}.${packageName}`)
|
|
420
|
-
console.log("")
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const spinner = createSpinner("Preparing to publish...").start()
|
|
424
|
-
|
|
425
|
-
// Track tarball path for cleanup in finally block
|
|
426
|
-
let tarballPath = null
|
|
427
|
-
// Hoist whoami for success message
|
|
428
|
-
let whoami = null
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
// 3. Read ecosystem-specific manifest data
|
|
432
|
-
let swiftManifest = null
|
|
433
|
-
let xcFramework = null
|
|
434
|
-
|
|
435
|
-
if (ecosystem === "swift") {
|
|
436
|
-
// Read Swift Package manifest
|
|
437
|
-
spinner.text = "Reading Package.swift..."
|
|
438
|
-
try {
|
|
439
|
-
const rawManifest = await readSwiftManifest()
|
|
440
|
-
swiftManifest = extractSwiftMetadata(rawManifest)
|
|
441
|
-
} catch (err) {
|
|
442
|
-
spinner.stop()
|
|
443
|
-
log.warn(`Could not read Package.swift: ${err.message}`)
|
|
444
|
-
log.info("Publishing without Swift manifest data.")
|
|
445
|
-
spinner.start()
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Detect XCFramework
|
|
449
|
-
xcFramework = detectXCFramework()
|
|
450
|
-
if (xcFramework.found) {
|
|
451
|
-
if (!xcFramework.hasInfoPlist) {
|
|
452
|
-
spinner.stop()
|
|
453
|
-
log.warn(
|
|
454
|
-
`XCFramework "${xcFramework.name}" found but missing Info.plist. Treating as source package.`,
|
|
455
|
-
)
|
|
456
|
-
spinner.start()
|
|
457
|
-
xcFramework = { found: false }
|
|
458
|
-
} else {
|
|
459
|
-
spinner.stop()
|
|
460
|
-
log.info(`XCFramework detected: ${xcFramework.name}`)
|
|
461
|
-
spinner.start()
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// 4. Create tarball
|
|
467
|
-
spinner.text = "Packing tarball..."
|
|
468
|
-
let npmPackMeta
|
|
469
|
-
|
|
470
|
-
if (ecosystem === "js") {
|
|
471
|
-
const packResult = await packWithNpm()
|
|
472
|
-
tarballPath = packResult.tarballPath
|
|
473
|
-
npmPackMeta = packResult.npmPackMeta
|
|
474
|
-
} else {
|
|
475
|
-
const packResult = await packWithTar(ecosystem, name, version)
|
|
476
|
-
tarballPath = packResult.tarballPath
|
|
477
|
-
npmPackMeta = packResult.npmPackMeta
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// 4b. Check if local skills exist but are missing from tarball
|
|
481
|
-
const localSkillsDir = path.join(process.cwd(), ".lpm", "skills")
|
|
482
|
-
if (fs.existsSync(localSkillsDir)) {
|
|
483
|
-
const fileList = npmPackMeta.files || []
|
|
484
|
-
const hasSkillsInTarball = fileList.some(f => {
|
|
485
|
-
const filePath = f.path || f
|
|
486
|
-
return (
|
|
487
|
-
filePath.includes(".lpm/skills/") ||
|
|
488
|
-
filePath.includes(".lpm\\skills\\")
|
|
489
|
-
)
|
|
490
|
-
})
|
|
491
|
-
if (!hasSkillsInTarball) {
|
|
492
|
-
spinner.stop()
|
|
493
|
-
log.warn(
|
|
494
|
-
"Found .lpm/skills/ directory but no skill files in the tarball.",
|
|
495
|
-
)
|
|
496
|
-
log.info(
|
|
497
|
-
'If using "files" in package.json, add ".lpm" to include skills.',
|
|
498
|
-
)
|
|
499
|
-
console.log("")
|
|
500
|
-
spinner.start()
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// 5. Read README
|
|
505
|
-
spinner.text = "Reading README..."
|
|
506
|
-
const readme = readReadme()
|
|
507
|
-
|
|
508
|
-
// 6. Read lpm.config.json if present
|
|
509
|
-
const lpmConfig = readLpmConfig()
|
|
510
|
-
|
|
511
|
-
// 7. Run quality checks and display report
|
|
512
|
-
spinner.text = "Running quality checks..."
|
|
513
|
-
const projectFiles = scanProjectForTestFiles()
|
|
514
|
-
const qualityResult = runQualityChecks({
|
|
515
|
-
packageJson: pkg,
|
|
516
|
-
readme,
|
|
517
|
-
lpmConfig,
|
|
518
|
-
files: npmPackMeta.files || [],
|
|
519
|
-
projectFiles,
|
|
520
|
-
unpackedSize: npmPackMeta.unpackedSize,
|
|
521
|
-
ecosystem,
|
|
522
|
-
swiftManifest,
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
// 7b. Check for stale skills (compare local with published)
|
|
526
|
-
spinner.text = "Checking skills..."
|
|
527
|
-
const prePublishWarnings = []
|
|
528
|
-
try {
|
|
529
|
-
const localSkillsDir = path.join(process.cwd(), ".lpm", "skills")
|
|
530
|
-
if (fs.existsSync(localSkillsDir)) {
|
|
531
|
-
const skillFiles = fs
|
|
532
|
-
.readdirSync(localSkillsDir)
|
|
533
|
-
.filter(f => f.endsWith(".md"))
|
|
534
|
-
.sort()
|
|
535
|
-
|
|
536
|
-
if (skillFiles.length > 0) {
|
|
537
|
-
// Read local skills content
|
|
538
|
-
const localSkills = skillFiles.map(f => ({
|
|
539
|
-
name: f.replace(/\.md$/, ""),
|
|
540
|
-
rawContent: fs.readFileSync(path.join(localSkillsDir, f), "utf8"),
|
|
541
|
-
}))
|
|
542
|
-
|
|
543
|
-
// Fetch published skills for comparison
|
|
544
|
-
const cleanedName = name.startsWith("@lpm.dev/")
|
|
545
|
-
? name.replace("@lpm.dev/", "")
|
|
546
|
-
: `${owner}.${packageName}`
|
|
547
|
-
const skillsResponse = await request(
|
|
548
|
-
`/skills?name=${encodeURIComponent(cleanedName)}`,
|
|
549
|
-
{ method: "GET" },
|
|
550
|
-
).catch(() => null)
|
|
551
|
-
|
|
552
|
-
if (skillsResponse?.ok) {
|
|
553
|
-
const published = await skillsResponse.json()
|
|
554
|
-
if (published.available && published.skills?.length > 0) {
|
|
555
|
-
const pubSorted = [...published.skills]
|
|
556
|
-
.map(s => ({ name: s.name, rawContent: s.rawContent }))
|
|
557
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
558
|
-
|
|
559
|
-
const isIdentical =
|
|
560
|
-
localSkills.length === pubSorted.length &&
|
|
561
|
-
localSkills.every(
|
|
562
|
-
(local, i) =>
|
|
563
|
-
local.name === pubSorted[i].name &&
|
|
564
|
-
local.rawContent === pubSorted[i].rawContent,
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
if (isIdentical) {
|
|
568
|
-
prePublishWarnings.push(
|
|
569
|
-
`Agent Skills haven't changed since v${published.version} - consider reviewing them for accuracy`,
|
|
570
|
-
)
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
} catch {
|
|
577
|
-
// Non-critical — skip skills staleness check
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
spinner.stop()
|
|
581
|
-
displayQualityReport({ ...qualityResult, warnings: prePublishWarnings })
|
|
582
|
-
|
|
583
|
-
// --dry-run: print summary and exit without uploading
|
|
584
|
-
if (dryRun) {
|
|
585
|
-
const unpackedSize = npmPackMeta.unpackedSize || 0
|
|
586
|
-
const fileCount =
|
|
587
|
-
npmPackMeta.fileCount || (npmPackMeta.files?.length ?? 0)
|
|
588
|
-
const sizeStr =
|
|
589
|
-
unpackedSize >= 1024 * 1024
|
|
590
|
-
? `${(unpackedSize / 1024 / 1024).toFixed(1)} MB unpacked`
|
|
591
|
-
: `${(unpackedSize / 1024).toFixed(1)} kB unpacked`
|
|
592
|
-
|
|
593
|
-
const githubOidc = isGitHubActionsWithOidc()
|
|
594
|
-
const gitlabOidc = isGitLabCiWithOidc()
|
|
595
|
-
const oidcStr = githubOidc
|
|
596
|
-
? "GitHub Actions detected ✓"
|
|
597
|
-
: gitlabOidc
|
|
598
|
-
? "GitLab CI detected ✓"
|
|
599
|
-
: "Not running in CI"
|
|
600
|
-
const authStr =
|
|
601
|
-
githubOidc || gitlabOidc
|
|
602
|
-
? "Token will be exchanged at publish time"
|
|
603
|
-
: "Run lpm login or configure Trusted Publishers"
|
|
604
|
-
|
|
605
|
-
console.log("")
|
|
606
|
-
console.log(` Package ${name}`)
|
|
607
|
-
console.log(` Version ${version}`)
|
|
608
|
-
console.log(` Files ${fileCount} files`)
|
|
609
|
-
console.log(` Size ${sizeStr}`)
|
|
610
|
-
console.log("")
|
|
611
|
-
console.log(` OIDC ${oidcStr}`)
|
|
612
|
-
console.log(` Auth ${authStr}`)
|
|
613
|
-
console.log("")
|
|
614
|
-
log.info("Dry run complete — run without --dry-run to publish.")
|
|
615
|
-
process.exit(0)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// --check mode: display report and exit
|
|
619
|
-
if (checkOnly) {
|
|
620
|
-
if (minScore && qualityResult.score < minScore) {
|
|
621
|
-
log.error(
|
|
622
|
-
`Quality score ${qualityResult.score} is below minimum ${minScore}.`,
|
|
623
|
-
)
|
|
624
|
-
process.exit(1)
|
|
625
|
-
}
|
|
626
|
-
process.exit(0)
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// --min-score gate: block publish if score is too low
|
|
630
|
-
if (minScore && qualityResult.score < minScore) {
|
|
631
|
-
log.error(
|
|
632
|
-
`Quality score ${qualityResult.score} is below minimum ${minScore}. Publish blocked.`,
|
|
633
|
-
)
|
|
634
|
-
log.info('Run "lpm publish --check" to see improvement suggestions.')
|
|
635
|
-
process.exit(1)
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// 8. Confirmation prompt (after quality report so user can decide)
|
|
639
|
-
const shouldPublish = await p.confirm({
|
|
640
|
-
message: `Publish ${name}@${version}?`,
|
|
641
|
-
initialValue: true,
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
if (p.isCancel(shouldPublish) || !shouldPublish) {
|
|
645
|
-
p.cancel("Publish cancelled.")
|
|
646
|
-
process.exit(0)
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// 8.5. OIDC auto-detection: exchange a GitHub Actions or GitLab CI OIDC
|
|
650
|
-
// token for a short-lived LPM publish token. Runs before auth verification
|
|
651
|
-
// so that verifyTokenScope() picks up the OIDC token via getToken().
|
|
652
|
-
const githubOidcAvailable = isGitHubActionsWithOidc()
|
|
653
|
-
const gitlabOidcAvailable = isGitLabCiWithOidc()
|
|
654
|
-
const oidcAvailable = githubOidcAvailable || gitlabOidcAvailable
|
|
655
|
-
|
|
656
|
-
if (forceProvenance && !oidcAvailable) {
|
|
657
|
-
p.cancel(
|
|
658
|
-
"--provenance requires OIDC to be available. " +
|
|
659
|
-
"For GitHub Actions, ensure `id-token: write` permission. " +
|
|
660
|
-
"For GitLab CI, configure id_tokens in your job.",
|
|
661
|
-
)
|
|
662
|
-
process.exit(1)
|
|
663
|
-
}
|
|
664
|
-
if (oidcAvailable) {
|
|
665
|
-
spinner.start()
|
|
666
|
-
if (githubOidcAvailable) {
|
|
667
|
-
spinner.text = "Requesting OIDC token from GitHub Actions..."
|
|
668
|
-
} else {
|
|
669
|
-
spinner.text = "Exchanging GitLab CI OIDC token..."
|
|
670
|
-
}
|
|
671
|
-
try {
|
|
672
|
-
const oidcToken = githubOidcAvailable
|
|
673
|
-
? await exchangeOidcToken(name)
|
|
674
|
-
: await exchangeGitLabOidcToken(name)
|
|
675
|
-
setTransientToken(oidcToken)
|
|
676
|
-
spinner.stop()
|
|
677
|
-
const provider = githubOidcAvailable ? "GitHub Actions" : "GitLab CI"
|
|
678
|
-
log.success(
|
|
679
|
-
`OIDC authentication successful (${provider} — no secrets needed)`,
|
|
680
|
-
)
|
|
681
|
-
} catch (err) {
|
|
682
|
-
spinner.stop()
|
|
683
|
-
if (forceProvenance) {
|
|
684
|
-
// When --provenance is explicit, a failure is fatal
|
|
685
|
-
throw new Error(`OIDC exchange failed: ${err.message}`)
|
|
686
|
-
}
|
|
687
|
-
// Auto-detection: non-fatal, fall back to configured credentials
|
|
688
|
-
log.warn(`OIDC exchange failed: ${err.message}`)
|
|
689
|
-
log.warn("Falling back to stored credentials...")
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// 9. Verify authentication and owner permissions
|
|
694
|
-
spinner.start()
|
|
695
|
-
spinner.text = "Verifying authentication..."
|
|
696
|
-
const scopeResult = await verifyTokenScope("publish")
|
|
697
|
-
|
|
698
|
-
if (!scopeResult.valid) {
|
|
699
|
-
throw new Error(scopeResult.error)
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
spinner.text = "Checking owner permissions..."
|
|
703
|
-
|
|
704
|
-
const whoamiResponse = await request("/-/whoami")
|
|
705
|
-
if (whoamiResponse.ok) {
|
|
706
|
-
whoami = await whoamiResponse.json()
|
|
707
|
-
|
|
708
|
-
const availableOwners = []
|
|
709
|
-
if (whoami.profile_username) {
|
|
710
|
-
availableOwners.push(whoami.profile_username)
|
|
711
|
-
}
|
|
712
|
-
whoami.organizations?.forEach(org => {
|
|
713
|
-
availableOwners.push(org.slug)
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
if (!availableOwners.includes(owner)) {
|
|
717
|
-
spinner.stop()
|
|
718
|
-
const registryUrl = getRegistryUrl()
|
|
719
|
-
|
|
720
|
-
log.error(
|
|
721
|
-
`You don't have permission to publish under "@lpm.dev/${owner}".`,
|
|
722
|
-
)
|
|
723
|
-
console.log("")
|
|
724
|
-
|
|
725
|
-
if (!whoami.profile_username) {
|
|
726
|
-
log.warn(WARNING_MESSAGES.usernameNotSet)
|
|
727
|
-
log.warn(` Set it at: ${registryUrl}/dashboard/settings`)
|
|
728
|
-
console.log("")
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (whoami.organizations?.length > 0) {
|
|
732
|
-
log.info("Your available owners:")
|
|
733
|
-
if (whoami.profile_username) {
|
|
734
|
-
log.info(` @lpm.dev/${whoami.profile_username}.* (personal)`)
|
|
735
|
-
}
|
|
736
|
-
for (const org of whoami.organizations) {
|
|
737
|
-
log.info(` @lpm.dev/${org.slug}.* (organization)`)
|
|
738
|
-
}
|
|
739
|
-
} else {
|
|
740
|
-
log.warn(WARNING_MESSAGES.noOrganizations)
|
|
741
|
-
log.warn(WARNING_MESSAGES.createOrgHint(registryUrl))
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
console.log("")
|
|
745
|
-
log.info(WARNING_MESSAGES.ownerFixHint)
|
|
746
|
-
process.exit(1)
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// 9.5. Pre-upload 2FA check — prompt for OTP BEFORE uploading the tarball
|
|
751
|
-
// This avoids uploading the entire tarball twice for large packages.
|
|
752
|
-
let otpCode = null
|
|
753
|
-
if (!oidcAvailable && whoami) {
|
|
754
|
-
const userHas2fa = whoami.mfa_enabled
|
|
755
|
-
const publishingOrg = whoami.organizations?.find(
|
|
756
|
-
org => org.slug === owner,
|
|
757
|
-
)
|
|
758
|
-
const orgRequires2fa = publishingOrg?.require_2fa
|
|
759
|
-
|
|
760
|
-
if (orgRequires2fa && !userHas2fa) {
|
|
761
|
-
spinner.stop()
|
|
762
|
-
log.error(
|
|
763
|
-
"This organization requires two-factor authentication. Enable 2FA in your account settings.",
|
|
764
|
-
)
|
|
765
|
-
process.exit(1)
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (userHas2fa || orgRequires2fa) {
|
|
769
|
-
spinner.stop()
|
|
770
|
-
otpCode = await p.text({
|
|
771
|
-
message: "Enter 2FA code",
|
|
772
|
-
placeholder: "123456",
|
|
773
|
-
validate: value => {
|
|
774
|
-
if (!/^\d{6}$/.test(value))
|
|
775
|
-
return "Enter a 6-digit code from your authenticator app"
|
|
776
|
-
},
|
|
777
|
-
})
|
|
778
|
-
|
|
779
|
-
if (p.isCancel(otpCode)) {
|
|
780
|
-
p.cancel("Publish cancelled.")
|
|
781
|
-
process.exit(0)
|
|
782
|
-
}
|
|
783
|
-
spinner.start()
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// 10. Read tarball and generate integrity hashes
|
|
788
|
-
spinner.text = "Reading tarball..."
|
|
789
|
-
const tarballData = await readFileAsync(tarballPath)
|
|
790
|
-
const tarballBase64 = tarballData.toString("base64")
|
|
791
|
-
const shasum = createHash("sha1").update(tarballData).digest("hex")
|
|
792
|
-
const integrity = generateIntegrity(tarballData, "sha512")
|
|
793
|
-
|
|
794
|
-
// 11. Build version metadata
|
|
795
|
-
const versionData = {
|
|
796
|
-
...pkg,
|
|
797
|
-
_id: `${name}@${version}`,
|
|
798
|
-
name: name,
|
|
799
|
-
version: version,
|
|
800
|
-
readme: readme,
|
|
801
|
-
dist: {
|
|
802
|
-
shasum: shasum,
|
|
803
|
-
integrity: integrity,
|
|
804
|
-
tarball: `${getRegistryUrl()}/api/registry/${name}/-/${name}-${version}.tgz`,
|
|
805
|
-
},
|
|
806
|
-
_npmPackMeta: npmPackMeta,
|
|
807
|
-
...(lpmConfig && { _lpmConfig: lpmConfig }),
|
|
808
|
-
_qualityChecks: qualityResult.checks,
|
|
809
|
-
_qualityMeta: qualityResult.meta,
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Add ecosystem-specific metadata
|
|
813
|
-
if (ecosystem !== "js") {
|
|
814
|
-
versionData._ecosystem = ecosystem
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if (swiftManifest) {
|
|
818
|
-
versionData._swiftManifest = swiftManifest
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (xcFramework?.found) {
|
|
822
|
-
versionData._packageType = "xcframework"
|
|
823
|
-
versionData._xcframeworkMeta = {
|
|
824
|
-
name: xcFramework.name,
|
|
825
|
-
slices: xcFramework.slices,
|
|
826
|
-
formatVersion: xcFramework.formatVersion,
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// 12. Upload to registry
|
|
831
|
-
spinner.text = `Uploading ${name}@${version}...`
|
|
832
|
-
const payload = {
|
|
833
|
-
_id: name,
|
|
834
|
-
name: name,
|
|
835
|
-
description: pkg.description,
|
|
836
|
-
"dist-tags": {
|
|
837
|
-
latest: version,
|
|
838
|
-
},
|
|
839
|
-
versions: {
|
|
840
|
-
[version]: versionData,
|
|
841
|
-
},
|
|
842
|
-
_attachments: {
|
|
843
|
-
[`${name}-${version}.tgz`]: {
|
|
844
|
-
content_type: "application/octet-stream",
|
|
845
|
-
data: tarballBase64,
|
|
846
|
-
length: tarballData.length,
|
|
847
|
-
},
|
|
848
|
-
},
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// Include ecosystem in top-level payload for API to store in packageSettings
|
|
852
|
-
if (ecosystem !== "js") {
|
|
853
|
-
payload._ecosystem = ecosystem
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const publishHeaders = {
|
|
857
|
-
"Content-Type": "application/json",
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Include OTP header if 2FA code was collected pre-upload
|
|
861
|
-
if (otpCode) {
|
|
862
|
-
publishHeaders["x-otp"] = otpCode
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const response = await request(`/${encodeURIComponent(name)}`, {
|
|
866
|
-
method: "PUT",
|
|
867
|
-
body: JSON.stringify(payload),
|
|
868
|
-
headers: publishHeaders,
|
|
869
|
-
onRetry: (attempt, max) => {
|
|
870
|
-
spinner.text = `Uploading to registry (retry ${attempt}/${max})...`
|
|
871
|
-
},
|
|
872
|
-
})
|
|
873
|
-
|
|
874
|
-
if (!response.ok) {
|
|
875
|
-
const errorText = await response.text()
|
|
876
|
-
throw new Error(`Publish failed: ${response.status} ${errorText}`)
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// Parse response for warnings (e.g., skills staleness)
|
|
880
|
-
let publishResult = {}
|
|
881
|
-
try {
|
|
882
|
-
publishResult = await response.json()
|
|
883
|
-
} catch {
|
|
884
|
-
// Response may not be JSON - that's fine
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Success message with dashboard link
|
|
888
|
-
const registryUrl = getRegistryUrl()
|
|
889
|
-
const isOrgOwner = whoami?.organizations?.some(org => org.slug === owner)
|
|
890
|
-
|
|
891
|
-
if (isOrgOwner) {
|
|
892
|
-
spinner.succeed(
|
|
893
|
-
SUCCESS_MESSAGES.publishOrg(registryUrl, owner, packageName, version),
|
|
894
|
-
)
|
|
895
|
-
} else {
|
|
896
|
-
spinner.succeed(
|
|
897
|
-
SUCCESS_MESSAGES.publishPersonal(
|
|
898
|
-
registryUrl,
|
|
899
|
-
owner,
|
|
900
|
-
packageName,
|
|
901
|
-
version,
|
|
902
|
-
),
|
|
903
|
-
)
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Server may still return warnings for edge cases not caught client-side
|
|
907
|
-
if (publishResult.warnings?.length > 0) {
|
|
908
|
-
const newWarnings = publishResult.warnings.filter(
|
|
909
|
-
w => !prePublishWarnings.includes(w),
|
|
910
|
-
)
|
|
911
|
-
if (newWarnings.length > 0) {
|
|
912
|
-
console.log("")
|
|
913
|
-
for (const warning of newWarnings) {
|
|
914
|
-
log.warn(warning)
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
} catch (error) {
|
|
919
|
-
spinner.fail(`Publish error: ${error.message}`)
|
|
920
|
-
|
|
921
|
-
// Show upgrade link for personal account limit errors
|
|
922
|
-
const registryUrl = getRegistryUrl()
|
|
923
|
-
const isLimitError =
|
|
924
|
-
error.message.includes("limit exceeded") ||
|
|
925
|
-
error.message.includes("Upgrade to Pro")
|
|
926
|
-
|
|
927
|
-
if (isLimitError) {
|
|
928
|
-
const isOrgOwner = whoami?.organizations?.some(org => org.slug === owner)
|
|
929
|
-
if (!isOrgOwner) {
|
|
930
|
-
console.log("")
|
|
931
|
-
log.info(`Upgrade plan: ${registryUrl}/dashboard/settings/billing`)
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
process.exit(1)
|
|
936
|
-
} finally {
|
|
937
|
-
// Clear OIDC transient token so it doesn't leak into subsequent operations
|
|
938
|
-
clearTransientToken()
|
|
939
|
-
|
|
940
|
-
// Cleanup tarball (even on error)
|
|
941
|
-
if (tarballPath && fs.existsSync(tarballPath)) {
|
|
942
|
-
try {
|
|
943
|
-
fs.unlinkSync(tarballPath)
|
|
944
|
-
} catch {
|
|
945
|
-
// Ignore cleanup errors
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|