@wyxos/zephyr 0.2.21 → 0.2.22
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 +144 -144
- package/bin/zephyr.mjs +29 -29
- package/package.json +58 -58
- package/src/config/project.mjs +118 -118
- package/src/config/servers.mjs +57 -57
- package/src/dependency-scanner.mjs +412 -433
- package/src/deploy/local-repo.mjs +215 -215
- package/src/deploy/locks.mjs +171 -171
- package/src/deploy/preflight.mjs +117 -117
- package/src/deploy/remote-exec.mjs +99 -99
- package/src/deploy/snapshots.mjs +35 -35
- package/src/index.mjs +91 -91
- package/src/main.mjs +677 -652
- package/src/project/bootstrap.mjs +147 -147
- package/src/runtime/local-command.mjs +18 -18
- package/src/runtime/prompt.mjs +14 -14
- package/src/runtime/ssh-client.mjs +14 -14
- package/src/ssh/index.mjs +8 -8
- package/src/ssh/keys.mjs +146 -146
- package/src/ssh/ssh.mjs +134 -134
- package/src/utils/command.mjs +92 -92
- package/src/utils/config-flow.mjs +284 -284
- package/src/utils/git.mjs +91 -91
- package/src/utils/id.mjs +6 -6
- package/src/utils/log-file.mjs +76 -76
- package/src/utils/output.mjs +29 -29
- package/src/utils/paths.mjs +28 -28
- package/src/utils/php-version.mjs +137 -0
- package/src/utils/remote-path.mjs +23 -23
- package/src/utils/task-planner.mjs +99 -96
- package/src/version-checker.mjs +162 -162
|
@@ -1,433 +1,412 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
import chalk from 'chalk'
|
|
5
|
-
import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
|
|
6
|
-
|
|
7
|
-
function isLocalPathOutsideRepo(depPath, rootDir) {
|
|
8
|
-
if (!depPath || typeof depPath !== 'string') {
|
|
9
|
-
return false
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Remove file: prefix if present
|
|
13
|
-
let cleanPath = depPath
|
|
14
|
-
if (depPath.startsWith('file:')) {
|
|
15
|
-
cleanPath = depPath.slice(5)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Resolve the path relative to the root directory
|
|
19
|
-
const resolvedPath = path.resolve(rootDir, cleanPath)
|
|
20
|
-
const resolvedRoot = path.resolve(rootDir)
|
|
21
|
-
|
|
22
|
-
// Normalize paths to handle different separators
|
|
23
|
-
const normalizedResolved = path.normalize(resolvedPath)
|
|
24
|
-
const normalizedRoot = path.normalize(resolvedRoot)
|
|
25
|
-
|
|
26
|
-
// If paths are equal, it's not outside
|
|
27
|
-
if (normalizedResolved === normalizedRoot) {
|
|
28
|
-
return false
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Check if resolved path is outside the repository root
|
|
32
|
-
// Use path.relative to check if the path goes outside
|
|
33
|
-
const relative = path.relative(normalizedRoot, normalizedResolved)
|
|
34
|
-
|
|
35
|
-
// If relative path starts with .., it's outside the repo
|
|
36
|
-
// Also check if the resolved path doesn't start with the root + separator (for absolute paths)
|
|
37
|
-
return relative.startsWith('..') || !normalizedResolved.startsWith(normalizedRoot + path.sep)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function scanPackageJsonDependencies(rootDir) {
|
|
41
|
-
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
42
|
-
const localDeps = []
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const raw = await readFile(packageJsonPath, 'utf8')
|
|
46
|
-
const pkg = JSON.parse(raw)
|
|
47
|
-
|
|
48
|
-
const checkDeps = (deps, field) => {
|
|
49
|
-
if (!deps || typeof deps !== 'object') {
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
for (const [packageName, version] of Object.entries(deps)) {
|
|
54
|
-
if (typeof version === 'string' && version.startsWith('file:')) {
|
|
55
|
-
if (isLocalPathOutsideRepo(version, rootDir)) {
|
|
56
|
-
localDeps.push({
|
|
57
|
-
packageName,
|
|
58
|
-
path: version,
|
|
59
|
-
field
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
checkDeps(pkg.dependencies, 'dependencies')
|
|
67
|
-
checkDeps(pkg.devDependencies, 'devDependencies')
|
|
68
|
-
|
|
69
|
-
return localDeps
|
|
70
|
-
} catch (error) {
|
|
71
|
-
if (error.code === 'ENOENT') {
|
|
72
|
-
return []
|
|
73
|
-
}
|
|
74
|
-
throw error
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function scanComposerJsonDependencies(rootDir) {
|
|
79
|
-
const composerJsonPath = path.join(rootDir, 'composer.json')
|
|
80
|
-
const localDeps = []
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const raw = await readFile(composerJsonPath, 'utf8')
|
|
84
|
-
const composer = JSON.parse(raw)
|
|
85
|
-
|
|
86
|
-
// Check repositories field for local path repositories
|
|
87
|
-
if (composer.repositories && Array.isArray(composer.repositories)) {
|
|
88
|
-
for (const repo of composer.repositories) {
|
|
89
|
-
if (repo.type === 'path' && repo.url) {
|
|
90
|
-
if (isLocalPathOutsideRepo(repo.url, rootDir)) {
|
|
91
|
-
// Try to find which package uses this repository
|
|
92
|
-
// Check require and require-dev for packages that might use this repo
|
|
93
|
-
const repoPath = path.basename(repo.url.replace(/\/$/, ''))
|
|
94
|
-
const possiblePackages = []
|
|
95
|
-
|
|
96
|
-
const checkRequire = (requireObj, field) => {
|
|
97
|
-
if (!requireObj || typeof requireObj !== 'object') {
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
for (const [packageName] of Object.entries(requireObj)) {
|
|
101
|
-
// If package name matches the repo path or contains it, it's likely using this repo
|
|
102
|
-
if (packageName.includes(repoPath) || repoPath.includes(packageName.split('/').pop())) {
|
|
103
|
-
possiblePackages.push({ packageName, field })
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
checkRequire(composer.require, 'require')
|
|
109
|
-
checkRequire(composer['require-dev'], 'require-dev')
|
|
110
|
-
|
|
111
|
-
if (possiblePackages.length > 0) {
|
|
112
|
-
for (const { packageName, field } of possiblePackages) {
|
|
113
|
-
localDeps.push({
|
|
114
|
-
packageName,
|
|
115
|
-
path: repo.url,
|
|
116
|
-
field
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
// If we can't determine which package, still report the repository
|
|
121
|
-
localDeps.push({
|
|
122
|
-
packageName: repo.url,
|
|
123
|
-
path: repo.url,
|
|
124
|
-
field: 'repositories'
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return localDeps
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (error.code === 'ENOENT') {
|
|
135
|
-
return []
|
|
136
|
-
}
|
|
137
|
-
throw error
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function fetchLatestNpmVersion(packageName) {
|
|
142
|
-
try {
|
|
143
|
-
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`)
|
|
144
|
-
if (!response.ok) {
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
const data = await response.json()
|
|
148
|
-
return data.version || null
|
|
149
|
-
} catch (_error) {
|
|
150
|
-
return null
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function fetchLatestPackagistVersion(packageName) {
|
|
155
|
-
try {
|
|
156
|
-
// Packagist API v2 format
|
|
157
|
-
const response = await fetch(`https://repo.packagist.org/p2/${packageName}.json`)
|
|
158
|
-
if (!response.ok) {
|
|
159
|
-
return null
|
|
160
|
-
}
|
|
161
|
-
const data = await response.json()
|
|
162
|
-
if (data.packages && data.packages[packageName] && data.packages[packageName].length > 0) {
|
|
163
|
-
// Get the latest version (first in array is usually latest)
|
|
164
|
-
const latest = data.packages[packageName][0]
|
|
165
|
-
return latest.version || null
|
|
166
|
-
}
|
|
167
|
-
return null
|
|
168
|
-
} catch (_error) {
|
|
169
|
-
return null
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function updatePackageJsonDependency(rootDir, packageName, newVersion, field) {
|
|
174
|
-
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
175
|
-
const raw = await readFile(packageJsonPath, 'utf8')
|
|
176
|
-
const pkg = JSON.parse(raw)
|
|
177
|
-
|
|
178
|
-
if (!pkg[field]) {
|
|
179
|
-
pkg[field] = {}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
pkg[field][packageName] = `^${newVersion}`
|
|
183
|
-
|
|
184
|
-
const updatedContent = JSON.stringify(pkg, null, 2) + '\n'
|
|
185
|
-
await writeFile(packageJsonPath, updatedContent, 'utf8')
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function updateComposerJsonDependency(rootDir, packageName, newVersion, field) {
|
|
189
|
-
const composerJsonPath = path.join(rootDir, 'composer.json')
|
|
190
|
-
const raw = await readFile(composerJsonPath, 'utf8')
|
|
191
|
-
const composer = JSON.parse(raw)
|
|
192
|
-
|
|
193
|
-
if (field === 'repositories') {
|
|
194
|
-
// Remove the local repository entry
|
|
195
|
-
if (composer.repositories && Array.isArray(composer.repositories)) {
|
|
196
|
-
composer.repositories = composer.repositories.filter(
|
|
197
|
-
(repo) => !(repo.type === 'path' && repo.url === packageName)
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
// Update the dependency version in require or require-dev
|
|
201
|
-
// We need to find which field contains this package
|
|
202
|
-
if (composer.require && composer.require[packageName]) {
|
|
203
|
-
composer.require[packageName] = `^${newVersion}`
|
|
204
|
-
} else if (composer['require-dev'] && composer['require-dev'][packageName]) {
|
|
205
|
-
composer['require-dev'][packageName] = `^${newVersion}`
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
if (!composer[field]) {
|
|
209
|
-
composer[field] = {}
|
|
210
|
-
}
|
|
211
|
-
composer[field][packageName] = `^${newVersion}`
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const updatedContent = JSON.stringify(composer, null, 2) + '\n'
|
|
215
|
-
await writeFile(composerJsonPath, updatedContent, 'utf8')
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
|
|
219
|
-
if (capture) {
|
|
220
|
-
const { stdout, stderr } = await runCommandCaptureBase(command, args, { cwd })
|
|
221
|
-
return { stdout: stdout.trim(), stderr: stderr.trim() }
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
await runCommandBase(command, args, { cwd })
|
|
225
|
-
return undefined
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// Commit the changes if any files were updated
|
|
414
|
-
if (updatedFiles.size > 0) {
|
|
415
|
-
const committed = await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
|
|
416
|
-
if (!committed) {
|
|
417
|
-
throw new Error(
|
|
418
|
-
'Release cancelled: dependency updates were applied but were not committed. Commit/stash your changes and rerun.'
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export {
|
|
425
|
-
scanPackageJsonDependencies,
|
|
426
|
-
scanComposerJsonDependencies,
|
|
427
|
-
fetchLatestNpmVersion,
|
|
428
|
-
fetchLatestPackagistVersion,
|
|
429
|
-
updatePackageJsonDependency,
|
|
430
|
-
updateComposerJsonDependency,
|
|
431
|
-
isLocalPathOutsideRepo,
|
|
432
|
-
validateLocalDependencies
|
|
433
|
-
}
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
|
|
6
|
+
|
|
7
|
+
function isLocalPathOutsideRepo(depPath, rootDir) {
|
|
8
|
+
if (!depPath || typeof depPath !== 'string') {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Remove file: prefix if present
|
|
13
|
+
let cleanPath = depPath
|
|
14
|
+
if (depPath.startsWith('file:')) {
|
|
15
|
+
cleanPath = depPath.slice(5)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Resolve the path relative to the root directory
|
|
19
|
+
const resolvedPath = path.resolve(rootDir, cleanPath)
|
|
20
|
+
const resolvedRoot = path.resolve(rootDir)
|
|
21
|
+
|
|
22
|
+
// Normalize paths to handle different separators
|
|
23
|
+
const normalizedResolved = path.normalize(resolvedPath)
|
|
24
|
+
const normalizedRoot = path.normalize(resolvedRoot)
|
|
25
|
+
|
|
26
|
+
// If paths are equal, it's not outside
|
|
27
|
+
if (normalizedResolved === normalizedRoot) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if resolved path is outside the repository root
|
|
32
|
+
// Use path.relative to check if the path goes outside
|
|
33
|
+
const relative = path.relative(normalizedRoot, normalizedResolved)
|
|
34
|
+
|
|
35
|
+
// If relative path starts with .., it's outside the repo
|
|
36
|
+
// Also check if the resolved path doesn't start with the root + separator (for absolute paths)
|
|
37
|
+
return relative.startsWith('..') || !normalizedResolved.startsWith(normalizedRoot + path.sep)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function scanPackageJsonDependencies(rootDir) {
|
|
41
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
42
|
+
const localDeps = []
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(packageJsonPath, 'utf8')
|
|
46
|
+
const pkg = JSON.parse(raw)
|
|
47
|
+
|
|
48
|
+
const checkDeps = (deps, field) => {
|
|
49
|
+
if (!deps || typeof deps !== 'object') {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const [packageName, version] of Object.entries(deps)) {
|
|
54
|
+
if (typeof version === 'string' && version.startsWith('file:')) {
|
|
55
|
+
if (isLocalPathOutsideRepo(version, rootDir)) {
|
|
56
|
+
localDeps.push({
|
|
57
|
+
packageName,
|
|
58
|
+
path: version,
|
|
59
|
+
field
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
checkDeps(pkg.dependencies, 'dependencies')
|
|
67
|
+
checkDeps(pkg.devDependencies, 'devDependencies')
|
|
68
|
+
|
|
69
|
+
return localDeps
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.code === 'ENOENT') {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function scanComposerJsonDependencies(rootDir) {
|
|
79
|
+
const composerJsonPath = path.join(rootDir, 'composer.json')
|
|
80
|
+
const localDeps = []
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const raw = await readFile(composerJsonPath, 'utf8')
|
|
84
|
+
const composer = JSON.parse(raw)
|
|
85
|
+
|
|
86
|
+
// Check repositories field for local path repositories
|
|
87
|
+
if (composer.repositories && Array.isArray(composer.repositories)) {
|
|
88
|
+
for (const repo of composer.repositories) {
|
|
89
|
+
if (repo.type === 'path' && repo.url) {
|
|
90
|
+
if (isLocalPathOutsideRepo(repo.url, rootDir)) {
|
|
91
|
+
// Try to find which package uses this repository
|
|
92
|
+
// Check require and require-dev for packages that might use this repo
|
|
93
|
+
const repoPath = path.basename(repo.url.replace(/\/$/, ''))
|
|
94
|
+
const possiblePackages = []
|
|
95
|
+
|
|
96
|
+
const checkRequire = (requireObj, field) => {
|
|
97
|
+
if (!requireObj || typeof requireObj !== 'object') {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
for (const [packageName] of Object.entries(requireObj)) {
|
|
101
|
+
// If package name matches the repo path or contains it, it's likely using this repo
|
|
102
|
+
if (packageName.includes(repoPath) || repoPath.includes(packageName.split('/').pop())) {
|
|
103
|
+
possiblePackages.push({ packageName, field })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
checkRequire(composer.require, 'require')
|
|
109
|
+
checkRequire(composer['require-dev'], 'require-dev')
|
|
110
|
+
|
|
111
|
+
if (possiblePackages.length > 0) {
|
|
112
|
+
for (const { packageName, field } of possiblePackages) {
|
|
113
|
+
localDeps.push({
|
|
114
|
+
packageName,
|
|
115
|
+
path: repo.url,
|
|
116
|
+
field
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// If we can't determine which package, still report the repository
|
|
121
|
+
localDeps.push({
|
|
122
|
+
packageName: repo.url,
|
|
123
|
+
path: repo.url,
|
|
124
|
+
field: 'repositories'
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return localDeps
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.code === 'ENOENT') {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
throw error
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function fetchLatestNpmVersion(packageName) {
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`)
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
const data = await response.json()
|
|
148
|
+
return data.version || null
|
|
149
|
+
} catch (_error) {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function fetchLatestPackagistVersion(packageName) {
|
|
155
|
+
try {
|
|
156
|
+
// Packagist API v2 format
|
|
157
|
+
const response = await fetch(`https://repo.packagist.org/p2/${packageName}.json`)
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
const data = await response.json()
|
|
162
|
+
if (data.packages && data.packages[packageName] && data.packages[packageName].length > 0) {
|
|
163
|
+
// Get the latest version (first in array is usually latest)
|
|
164
|
+
const latest = data.packages[packageName][0]
|
|
165
|
+
return latest.version || null
|
|
166
|
+
}
|
|
167
|
+
return null
|
|
168
|
+
} catch (_error) {
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function updatePackageJsonDependency(rootDir, packageName, newVersion, field) {
|
|
174
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
175
|
+
const raw = await readFile(packageJsonPath, 'utf8')
|
|
176
|
+
const pkg = JSON.parse(raw)
|
|
177
|
+
|
|
178
|
+
if (!pkg[field]) {
|
|
179
|
+
pkg[field] = {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
pkg[field][packageName] = `^${newVersion}`
|
|
183
|
+
|
|
184
|
+
const updatedContent = JSON.stringify(pkg, null, 2) + '\n'
|
|
185
|
+
await writeFile(packageJsonPath, updatedContent, 'utf8')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function updateComposerJsonDependency(rootDir, packageName, newVersion, field) {
|
|
189
|
+
const composerJsonPath = path.join(rootDir, 'composer.json')
|
|
190
|
+
const raw = await readFile(composerJsonPath, 'utf8')
|
|
191
|
+
const composer = JSON.parse(raw)
|
|
192
|
+
|
|
193
|
+
if (field === 'repositories') {
|
|
194
|
+
// Remove the local repository entry
|
|
195
|
+
if (composer.repositories && Array.isArray(composer.repositories)) {
|
|
196
|
+
composer.repositories = composer.repositories.filter(
|
|
197
|
+
(repo) => !(repo.type === 'path' && repo.url === packageName)
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
// Update the dependency version in require or require-dev
|
|
201
|
+
// We need to find which field contains this package
|
|
202
|
+
if (composer.require && composer.require[packageName]) {
|
|
203
|
+
composer.require[packageName] = `^${newVersion}`
|
|
204
|
+
} else if (composer['require-dev'] && composer['require-dev'][packageName]) {
|
|
205
|
+
composer['require-dev'][packageName] = `^${newVersion}`
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
if (!composer[field]) {
|
|
209
|
+
composer[field] = {}
|
|
210
|
+
}
|
|
211
|
+
composer[field][packageName] = `^${newVersion}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const updatedContent = JSON.stringify(composer, null, 2) + '\n'
|
|
215
|
+
await writeFile(composerJsonPath, updatedContent, 'utf8')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
|
|
219
|
+
if (capture) {
|
|
220
|
+
const { stdout, stderr } = await runCommandCaptureBase(command, args, { cwd })
|
|
221
|
+
return { stdout: stdout.trim(), stderr: stderr.trim() }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await runCommandBase(command, args, { cwd })
|
|
225
|
+
return undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
|
|
230
|
+
try {
|
|
231
|
+
// Check if we're in a git repository
|
|
232
|
+
await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { capture: true, cwd: rootDir })
|
|
233
|
+
} catch {
|
|
234
|
+
// Not a git repository, skip commit
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const fileList = updatedFiles.map((f) => path.basename(f)).join(', ')
|
|
239
|
+
|
|
240
|
+
// Stage the updated files
|
|
241
|
+
for (const file of updatedFiles) {
|
|
242
|
+
try {
|
|
243
|
+
await runCommand('git', ['add', file], { cwd: rootDir })
|
|
244
|
+
} catch {
|
|
245
|
+
// File might not exist or not be tracked, continue
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Build commit message
|
|
250
|
+
const commitMessage = `chore: update local file dependencies to online versions (${fileList})`
|
|
251
|
+
|
|
252
|
+
if (logFn) {
|
|
253
|
+
logFn('Committing dependency updates...')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await runCommand('git', ['commit', '-m', commitMessage, '--', ...updatedFiles], { cwd: rootDir })
|
|
257
|
+
|
|
258
|
+
if (logFn) {
|
|
259
|
+
logFn('Dependency updates committed.')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
|
|
266
|
+
const packageDeps = await scanPackageJsonDependencies(rootDir)
|
|
267
|
+
const composerDeps = await scanComposerJsonDependencies(rootDir)
|
|
268
|
+
|
|
269
|
+
const allDeps = [...packageDeps, ...composerDeps]
|
|
270
|
+
|
|
271
|
+
if (allDeps.length === 0) {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fetch latest versions for all dependencies
|
|
276
|
+
const depsWithVersions = await Promise.all(
|
|
277
|
+
allDeps.map(async (dep) => {
|
|
278
|
+
let latestVersion = null
|
|
279
|
+
if (dep.field === 'dependencies' || dep.field === 'devDependencies') {
|
|
280
|
+
latestVersion = await fetchLatestNpmVersion(dep.packageName)
|
|
281
|
+
} else if (dep.field === 'require' || dep.field === 'require-dev') {
|
|
282
|
+
latestVersion = await fetchLatestPackagistVersion(dep.packageName)
|
|
283
|
+
} else if (dep.field === 'repositories') {
|
|
284
|
+
// For repositories, try to extract package name and fetch from Packagist
|
|
285
|
+
// The packageName might be the path, so we need to handle this differently
|
|
286
|
+
// For now, we'll show the path but can't fetch version
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
...dep,
|
|
291
|
+
latestVersion
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
// Build warning messages with colored output (danger color for package name and version)
|
|
297
|
+
const messages = depsWithVersions.map((dep) => {
|
|
298
|
+
const packageNameColored = chalk.red(dep.packageName)
|
|
299
|
+
const pathColored = chalk.dim(dep.path)
|
|
300
|
+
const versionInfo = dep.latestVersion
|
|
301
|
+
? ` Latest version available: ${chalk.red(dep.latestVersion)}.`
|
|
302
|
+
: ' Latest version could not be determined.'
|
|
303
|
+
return `Dependency ${packageNameColored} is pointing to a local path outside the repository: ${pathColored}.${versionInfo}`
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Build the prompt message with colored count (danger color)
|
|
307
|
+
const countColored = chalk.red(allDeps.length)
|
|
308
|
+
const countText = allDeps.length === 1 ? 'dependency' : 'dependencies'
|
|
309
|
+
const promptMessage = `Found ${countColored} local file ${countText} pointing outside the repository:\n\n${messages.join('\n\n')}\n\nUpdate to latest version?`
|
|
310
|
+
|
|
311
|
+
// Prompt user
|
|
312
|
+
const { shouldUpdate } = await promptFn([
|
|
313
|
+
{
|
|
314
|
+
type: 'confirm',
|
|
315
|
+
name: 'shouldUpdate',
|
|
316
|
+
message: promptMessage,
|
|
317
|
+
default: true
|
|
318
|
+
}
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
if (!shouldUpdate) {
|
|
322
|
+
throw new Error('Release cancelled: local file dependencies must be updated before release.')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If we cannot commit the update, do not proceed (otherwise release-node will fail later with a dirty tree).
|
|
326
|
+
// We allow users to opt-in to committing together with existing staged changes via prompt.
|
|
327
|
+
|
|
328
|
+
// Track which files were updated
|
|
329
|
+
const updatedFiles = new Set()
|
|
330
|
+
|
|
331
|
+
// Update dependencies
|
|
332
|
+
for (const dep of depsWithVersions) {
|
|
333
|
+
if (!dep.latestVersion) {
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (dep.field === 'dependencies' || dep.field === 'devDependencies') {
|
|
338
|
+
await updatePackageJsonDependency(rootDir, dep.packageName, dep.latestVersion, dep.field)
|
|
339
|
+
updatedFiles.add('package.json')
|
|
340
|
+
} else if (dep.field === 'require' || dep.field === 'require-dev') {
|
|
341
|
+
await updateComposerJsonDependency(rootDir, dep.packageName, dep.latestVersion, dep.field)
|
|
342
|
+
updatedFiles.add('composer.json')
|
|
343
|
+
} else if (dep.field === 'repositories') {
|
|
344
|
+
// For repositories, we need to remove the repository entry
|
|
345
|
+
// But we still need to update the dependency version
|
|
346
|
+
// This is more complex, so for now we'll just update if we can find the package
|
|
347
|
+
const composerJsonPath = path.join(rootDir, 'composer.json')
|
|
348
|
+
const raw = await readFile(composerJsonPath, 'utf8')
|
|
349
|
+
const composer = JSON.parse(raw)
|
|
350
|
+
|
|
351
|
+
// Try to find which package uses this repository
|
|
352
|
+
let packageToUpdate = null
|
|
353
|
+
let fieldToUpdate = null
|
|
354
|
+
|
|
355
|
+
if (composer.require) {
|
|
356
|
+
for (const [pkgName] of Object.entries(composer.require)) {
|
|
357
|
+
if (pkgName.includes(dep.packageName.split('/').pop()) || dep.packageName.includes(pkgName.split('/').pop())) {
|
|
358
|
+
packageToUpdate = pkgName
|
|
359
|
+
fieldToUpdate = 'require'
|
|
360
|
+
break
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!packageToUpdate && composer['require-dev']) {
|
|
366
|
+
for (const [pkgName] of Object.entries(composer['require-dev'])) {
|
|
367
|
+
if (pkgName.includes(dep.packageName.split('/').pop()) || dep.packageName.includes(pkgName.split('/').pop())) {
|
|
368
|
+
packageToUpdate = pkgName
|
|
369
|
+
fieldToUpdate = 'require-dev'
|
|
370
|
+
break
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (packageToUpdate && fieldToUpdate) {
|
|
376
|
+
await updateComposerJsonDependency(rootDir, packageToUpdate, dep.latestVersion, fieldToUpdate)
|
|
377
|
+
// Also remove the repository entry
|
|
378
|
+
const updatedRaw = await readFile(composerJsonPath, 'utf8')
|
|
379
|
+
const updatedComposer = JSON.parse(updatedRaw)
|
|
380
|
+
if (updatedComposer.repositories && Array.isArray(updatedComposer.repositories)) {
|
|
381
|
+
updatedComposer.repositories = updatedComposer.repositories.filter(
|
|
382
|
+
(repo) => !(repo.type === 'path' && repo.url === dep.path)
|
|
383
|
+
)
|
|
384
|
+
const updatedContent = JSON.stringify(updatedComposer, null, 2) + '\n'
|
|
385
|
+
await writeFile(composerJsonPath, updatedContent, 'utf8')
|
|
386
|
+
}
|
|
387
|
+
updatedFiles.add('composer.json')
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Commit the changes if any files were updated
|
|
393
|
+
if (updatedFiles.size > 0) {
|
|
394
|
+
const committed = await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
|
|
395
|
+
if (!committed) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
'Release cancelled: dependency updates were applied but were not committed. Commit/stash your changes and rerun.'
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export {
|
|
404
|
+
scanPackageJsonDependencies,
|
|
405
|
+
scanComposerJsonDependencies,
|
|
406
|
+
fetchLatestNpmVersion,
|
|
407
|
+
fetchLatestPackagistVersion,
|
|
408
|
+
updatePackageJsonDependency,
|
|
409
|
+
updateComposerJsonDependency,
|
|
410
|
+
isLocalPathOutsideRepo,
|
|
411
|
+
validateLocalDependencies
|
|
412
|
+
}
|