@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
package/src/main.mjs
CHANGED
|
@@ -1,652 +1,677 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import process from 'node:process'
|
|
5
|
-
import chalk from 'chalk'
|
|
6
|
-
import inquirer from 'inquirer'
|
|
7
|
-
import { NodeSSH } from 'node-ssh'
|
|
8
|
-
import { releaseNode } from './release-node.mjs'
|
|
9
|
-
import { releasePackagist } from './release-packagist.mjs'
|
|
10
|
-
import { validateLocalDependencies } from './dependency-scanner.mjs'
|
|
11
|
-
import { checkAndUpdateVersion } from './version-checker.mjs'
|
|
12
|
-
import { createChalkLogger, writeStderrLine, writeStdoutLine } from './utils/output.mjs'
|
|
13
|
-
import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
|
|
14
|
-
import { planLaravelDeploymentTasks } from './utils/task-planner.mjs'
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import * as
|
|
33
|
-
import * as
|
|
34
|
-
import * as
|
|
35
|
-
import * as
|
|
36
|
-
import
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
} else
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
await
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (ssh) {
|
|
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
|
-
async function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
} catch (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
appConfig =
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import inquirer from 'inquirer'
|
|
7
|
+
import { NodeSSH } from 'node-ssh'
|
|
8
|
+
import { releaseNode } from './release-node.mjs'
|
|
9
|
+
import { releasePackagist } from './release-packagist.mjs'
|
|
10
|
+
import { validateLocalDependencies } from './dependency-scanner.mjs'
|
|
11
|
+
import { checkAndUpdateVersion } from './version-checker.mjs'
|
|
12
|
+
import { createChalkLogger, writeStderrLine, writeStdoutLine } from './utils/output.mjs'
|
|
13
|
+
import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
|
|
14
|
+
import { planLaravelDeploymentTasks } from './utils/task-planner.mjs'
|
|
15
|
+
import { getPhpVersionRequirement, findPhpBinary } from './utils/php-version.mjs'
|
|
16
|
+
import {
|
|
17
|
+
PENDING_TASKS_FILE,
|
|
18
|
+
PROJECT_CONFIG_DIR
|
|
19
|
+
} from './utils/paths.mjs'
|
|
20
|
+
import { cleanupOldLogs, closeLogFile, getLogFilePath, writeToLogFile } from './utils/log-file.mjs'
|
|
21
|
+
import {
|
|
22
|
+
acquireRemoteLock,
|
|
23
|
+
compareLocksAndPrompt,
|
|
24
|
+
releaseLocalLock,
|
|
25
|
+
releaseRemoteLock
|
|
26
|
+
} from './deploy/locks.mjs'
|
|
27
|
+
import {
|
|
28
|
+
clearPendingTasksSnapshot,
|
|
29
|
+
loadPendingTasksSnapshot,
|
|
30
|
+
savePendingTasksSnapshot
|
|
31
|
+
} from './deploy/snapshots.mjs'
|
|
32
|
+
import * as bootstrap from './project/bootstrap.mjs'
|
|
33
|
+
import * as preflight from './deploy/preflight.mjs'
|
|
34
|
+
import * as sshKeys from './ssh/keys.mjs'
|
|
35
|
+
import * as localRepo from './deploy/local-repo.mjs'
|
|
36
|
+
import * as configFlow from './utils/config-flow.mjs'
|
|
37
|
+
import { createRemoteExecutor } from './deploy/remote-exec.mjs'
|
|
38
|
+
import { createRunPrompt } from './runtime/prompt.mjs'
|
|
39
|
+
import { createSshClientFactory } from './runtime/ssh-client.mjs'
|
|
40
|
+
import { createLocalCommandRunners } from './runtime/local-command.mjs'
|
|
41
|
+
import { generateId } from './utils/id.mjs'
|
|
42
|
+
import { loadServers, saveServers } from './config/servers.mjs'
|
|
43
|
+
import { loadProjectConfig, saveProjectConfig } from './config/project.mjs'
|
|
44
|
+
import { resolveRemotePath } from './utils/remote-path.mjs'
|
|
45
|
+
|
|
46
|
+
const RELEASE_SCRIPT_NAME = 'release'
|
|
47
|
+
const RELEASE_SCRIPT_COMMAND = 'npx @wyxos/zephyr@latest'
|
|
48
|
+
|
|
49
|
+
const { logProcessing, logSuccess, logWarning, logError } = createChalkLogger(chalk)
|
|
50
|
+
|
|
51
|
+
const runPrompt = createRunPrompt({ inquirer })
|
|
52
|
+
const createSshClient = createSshClientFactory({ NodeSSH })
|
|
53
|
+
const { runCommand, runCommandCapture } = createLocalCommandRunners({
|
|
54
|
+
runCommandBase,
|
|
55
|
+
runCommandCaptureBase
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Local repository state moved to src/deploy/local-repo.mjs
|
|
59
|
+
|
|
60
|
+
async function getGitStatus(rootDir) {
|
|
61
|
+
return await localRepo.getGitStatus(rootDir, { runCommandCapture })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function hasUncommittedChanges(rootDir) {
|
|
65
|
+
return await localRepo.hasUncommittedChanges(rootDir, { getGitStatus })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function ensureLocalRepositoryState(targetBranch, rootDir = process.cwd()) {
|
|
69
|
+
return await localRepo.ensureLocalRepositoryState(targetBranch, rootDir, {
|
|
70
|
+
runPrompt,
|
|
71
|
+
runCommand,
|
|
72
|
+
runCommandCapture,
|
|
73
|
+
logProcessing,
|
|
74
|
+
logSuccess,
|
|
75
|
+
logWarning
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function ensureProjectReleaseScript(rootDir) {
|
|
80
|
+
return await bootstrap.ensureProjectReleaseScript(rootDir, {
|
|
81
|
+
runPrompt,
|
|
82
|
+
runCommand,
|
|
83
|
+
logSuccess,
|
|
84
|
+
logWarning,
|
|
85
|
+
releaseScriptName: RELEASE_SCRIPT_NAME,
|
|
86
|
+
releaseScriptCommand: RELEASE_SCRIPT_COMMAND
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Locks and snapshots moved to src/deploy/*
|
|
91
|
+
|
|
92
|
+
async function ensureGitignoreEntry(rootDir) {
|
|
93
|
+
return await bootstrap.ensureGitignoreEntry(rootDir, {
|
|
94
|
+
projectConfigDir: PROJECT_CONFIG_DIR,
|
|
95
|
+
runCommand,
|
|
96
|
+
logSuccess,
|
|
97
|
+
logWarning
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Config storage/migrations moved to src/config/*
|
|
102
|
+
|
|
103
|
+
function defaultProjectPath(currentDir) {
|
|
104
|
+
return configFlow.defaultProjectPath(currentDir)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function listGitBranches(currentDir) {
|
|
108
|
+
return await configFlow.listGitBranches(currentDir, { runCommandCapture, logWarning })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function promptSshDetails(currentDir, existing = {}) {
|
|
112
|
+
return await sshKeys.promptSshDetails(currentDir, existing, { runPrompt })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function ensureSshDetails(config, currentDir) {
|
|
116
|
+
return await sshKeys.ensureSshDetails(config, currentDir, { runPrompt, logProcessing })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function resolveSshKeyPath(targetPath) {
|
|
120
|
+
return await sshKeys.resolveSshKeyPath(targetPath)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// resolveRemotePath moved to src/utils/remote-path.mjs
|
|
124
|
+
|
|
125
|
+
async function runLinting(rootDir) {
|
|
126
|
+
return await preflight.runLinting(rootDir, { runCommand, logProcessing, logSuccess })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function commitLintingChanges(rootDir) {
|
|
130
|
+
return await preflight.commitLintingChanges(rootDir, {
|
|
131
|
+
getGitStatus,
|
|
132
|
+
runCommand,
|
|
133
|
+
logProcessing,
|
|
134
|
+
logSuccess
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function runRemoteTasks(config, options = {}) {
|
|
139
|
+
const { snapshot = null, rootDir = process.cwd() } = options
|
|
140
|
+
|
|
141
|
+
await cleanupOldLogs(rootDir)
|
|
142
|
+
await ensureLocalRepositoryState(config.branch, rootDir)
|
|
143
|
+
|
|
144
|
+
// Detect PHP version requirement from local composer.json
|
|
145
|
+
let requiredPhpVersion = null
|
|
146
|
+
try {
|
|
147
|
+
requiredPhpVersion = await getPhpVersionRequirement(rootDir)
|
|
148
|
+
} catch {
|
|
149
|
+
// Ignore - composer.json might not exist or be unreadable
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const isLaravel = await preflight.isLocalLaravelProject(rootDir)
|
|
153
|
+
const hasHook = await preflight.hasPrePushHook(rootDir)
|
|
154
|
+
|
|
155
|
+
if (!hasHook) {
|
|
156
|
+
// Run linting before tests
|
|
157
|
+
const lintRan = await runLinting(rootDir)
|
|
158
|
+
if (lintRan) {
|
|
159
|
+
// Check if linting made changes and commit them
|
|
160
|
+
const hasChanges = await hasUncommittedChanges(rootDir)
|
|
161
|
+
if (hasChanges) {
|
|
162
|
+
await commitLintingChanges(rootDir)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Run tests for Laravel projects
|
|
167
|
+
if (isLaravel) {
|
|
168
|
+
logProcessing('Running Laravel tests locally...')
|
|
169
|
+
try {
|
|
170
|
+
await runCommand('php', ['artisan', 'test', '--compact'], { cwd: rootDir })
|
|
171
|
+
logSuccess('Local tests passed.')
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new Error(`Local tests failed. Fix test failures before deploying. ${error.message}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
logProcessing('Pre-push git hook detected. Skipping local linting and test execution.')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const ssh = createSshClient()
|
|
181
|
+
const sshUser = config.sshUser || os.userInfo().username
|
|
182
|
+
const privateKeyPath = await resolveSshKeyPath(config.sshKey)
|
|
183
|
+
const privateKey = await fs.readFile(privateKeyPath, 'utf8')
|
|
184
|
+
|
|
185
|
+
logProcessing(`\nConnecting to ${config.serverIp} as ${sshUser}...`)
|
|
186
|
+
|
|
187
|
+
let lockAcquired = false
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await ssh.connect({
|
|
191
|
+
host: config.serverIp,
|
|
192
|
+
username: sshUser,
|
|
193
|
+
privateKey
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const remoteHomeResult = await ssh.execCommand('printf "%s" "$HOME"')
|
|
197
|
+
const remoteHome = remoteHomeResult.stdout.trim() || `/home/${sshUser}`
|
|
198
|
+
const remoteCwd = resolveRemotePath(config.projectPath, remoteHome)
|
|
199
|
+
|
|
200
|
+
logProcessing(`Connection established. Acquiring deployment lock on server...`)
|
|
201
|
+
await acquireRemoteLock(ssh, remoteCwd, rootDir, { runPrompt, logWarning })
|
|
202
|
+
lockAcquired = true
|
|
203
|
+
logProcessing(`Lock acquired. Running deployment commands in ${remoteCwd}...`)
|
|
204
|
+
|
|
205
|
+
const executeRemote = createRemoteExecutor({
|
|
206
|
+
ssh,
|
|
207
|
+
rootDir,
|
|
208
|
+
remoteCwd,
|
|
209
|
+
writeToLogFile,
|
|
210
|
+
logProcessing,
|
|
211
|
+
logSuccess,
|
|
212
|
+
logError
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const laravelCheck = await ssh.execCommand(
|
|
216
|
+
'if [ -f artisan ] && [ -f composer.json ] && grep -q "laravel/framework" composer.json; then echo "yes"; else echo "no"; fi',
|
|
217
|
+
{ cwd: remoteCwd }
|
|
218
|
+
)
|
|
219
|
+
const isLaravel = laravelCheck.stdout.trim() === 'yes'
|
|
220
|
+
|
|
221
|
+
if (isLaravel) {
|
|
222
|
+
logSuccess('Laravel project detected.')
|
|
223
|
+
} else {
|
|
224
|
+
logWarning('Laravel project not detected; skipping Laravel-specific maintenance tasks.')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let changedFiles = []
|
|
228
|
+
|
|
229
|
+
if (snapshot && snapshot.changedFiles) {
|
|
230
|
+
changedFiles = snapshot.changedFiles
|
|
231
|
+
logProcessing('Resuming deployment with saved task snapshot.')
|
|
232
|
+
} else if (isLaravel) {
|
|
233
|
+
await executeRemote(`Fetch latest changes for ${config.branch}`, `git fetch origin ${config.branch}`)
|
|
234
|
+
|
|
235
|
+
const diffResult = await executeRemote(
|
|
236
|
+
'Inspect pending changes',
|
|
237
|
+
`git diff --name-only HEAD..origin/${config.branch}`,
|
|
238
|
+
{ printStdout: false }
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
changedFiles = diffResult.stdout
|
|
242
|
+
.split(/\r?\n/)
|
|
243
|
+
.map((line) => line.trim())
|
|
244
|
+
.filter(Boolean)
|
|
245
|
+
|
|
246
|
+
if (changedFiles.length > 0) {
|
|
247
|
+
const preview = changedFiles
|
|
248
|
+
.slice(0, 20)
|
|
249
|
+
.map((file) => ` - ${file}`)
|
|
250
|
+
.join('\n')
|
|
251
|
+
|
|
252
|
+
logProcessing(
|
|
253
|
+
`Detected ${changedFiles.length} changed file(s):\n${preview}${changedFiles.length > 20 ? '\n - ...' : ''
|
|
254
|
+
}`
|
|
255
|
+
)
|
|
256
|
+
} else {
|
|
257
|
+
logProcessing('No upstream file changes detected.')
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const hasPhpChanges = isLaravel && changedFiles.some((file) => file.endsWith('.php'))
|
|
262
|
+
|
|
263
|
+
let horizonConfigured = false
|
|
264
|
+
if (hasPhpChanges) {
|
|
265
|
+
const horizonCheck = await ssh.execCommand(
|
|
266
|
+
'if [ -f config/horizon.php ]; then echo "yes"; else echo "no"; fi',
|
|
267
|
+
{ cwd: remoteCwd }
|
|
268
|
+
)
|
|
269
|
+
horizonConfigured = horizonCheck.stdout.trim() === 'yes'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Find the appropriate PHP binary based on local composer.json requirement
|
|
273
|
+
let phpCommand = 'php'
|
|
274
|
+
if (requiredPhpVersion) {
|
|
275
|
+
try {
|
|
276
|
+
phpCommand = await findPhpBinary(ssh, remoteCwd, requiredPhpVersion)
|
|
277
|
+
|
|
278
|
+
if (phpCommand !== 'php') {
|
|
279
|
+
logProcessing(`Detected PHP requirement: ${requiredPhpVersion}, using ${phpCommand}`)
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// If we can't find the PHP binary, fall back to default 'php'
|
|
283
|
+
logWarning(`Could not find PHP binary for version ${requiredPhpVersion}: ${error.message}`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const steps = planLaravelDeploymentTasks({
|
|
288
|
+
branch: config.branch,
|
|
289
|
+
isLaravel,
|
|
290
|
+
changedFiles,
|
|
291
|
+
horizonConfigured,
|
|
292
|
+
phpCommand
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const usefulSteps = steps.length > 1
|
|
296
|
+
|
|
297
|
+
let pendingSnapshot
|
|
298
|
+
|
|
299
|
+
if (usefulSteps) {
|
|
300
|
+
pendingSnapshot = snapshot ?? {
|
|
301
|
+
serverName: config.serverName,
|
|
302
|
+
branch: config.branch,
|
|
303
|
+
projectPath: config.projectPath,
|
|
304
|
+
sshUser: config.sshUser,
|
|
305
|
+
createdAt: new Date().toISOString(),
|
|
306
|
+
changedFiles,
|
|
307
|
+
taskLabels: steps.map((step) => step.label)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await savePendingTasksSnapshot(rootDir, pendingSnapshot)
|
|
311
|
+
|
|
312
|
+
const payload = Buffer.from(JSON.stringify(pendingSnapshot)).toString('base64')
|
|
313
|
+
await executeRemote(
|
|
314
|
+
'Record pending deployment tasks',
|
|
315
|
+
`mkdir -p .zephyr && echo '${payload}' | base64 --decode > .zephyr/${PENDING_TASKS_FILE}`,
|
|
316
|
+
{ printStdout: false }
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (steps.length === 1) {
|
|
321
|
+
logProcessing('No additional maintenance tasks scheduled beyond git pull.')
|
|
322
|
+
} else {
|
|
323
|
+
const extraTasks = steps
|
|
324
|
+
.slice(1)
|
|
325
|
+
.map((step) => step.label)
|
|
326
|
+
.join(', ')
|
|
327
|
+
|
|
328
|
+
logProcessing(`Additional tasks scheduled: ${extraTasks}`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let completed = false
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
for (const step of steps) {
|
|
335
|
+
await executeRemote(step.label, step.command)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
completed = true
|
|
339
|
+
} finally {
|
|
340
|
+
if (usefulSteps && completed) {
|
|
341
|
+
await executeRemote(
|
|
342
|
+
'Clear pending deployment snapshot',
|
|
343
|
+
`rm -f .zephyr/${PENDING_TASKS_FILE}`,
|
|
344
|
+
{ printStdout: false, allowFailure: true }
|
|
345
|
+
)
|
|
346
|
+
await clearPendingTasksSnapshot(rootDir)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
logSuccess('\nDeployment commands completed successfully.')
|
|
351
|
+
|
|
352
|
+
const logPath = await getLogFilePath(rootDir)
|
|
353
|
+
logSuccess(`\nAll task output has been logged to: ${logPath}`)
|
|
354
|
+
} catch (error) {
|
|
355
|
+
const logPath = await getLogFilePath(rootDir).catch(() => null)
|
|
356
|
+
if (logPath) {
|
|
357
|
+
logError(`\nTask output has been logged to: ${logPath}`)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// If lock was acquired but deployment failed, check for stale locks
|
|
361
|
+
if (lockAcquired && ssh) {
|
|
362
|
+
try {
|
|
363
|
+
const remoteHomeResult = await ssh.execCommand('printf "%s" "$HOME"')
|
|
364
|
+
const remoteHome = remoteHomeResult.stdout.trim() || `/home/${sshUser}`
|
|
365
|
+
const remoteCwd = resolveRemotePath(config.projectPath, remoteHome)
|
|
366
|
+
await compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning })
|
|
367
|
+
} catch (_lockError) {
|
|
368
|
+
// Ignore lock comparison errors during error handling
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
throw new Error(`Deployment failed: ${error.message}`)
|
|
373
|
+
} finally {
|
|
374
|
+
if (lockAcquired && ssh) {
|
|
375
|
+
try {
|
|
376
|
+
const remoteHomeResult = await ssh.execCommand('printf "%s" "$HOME"')
|
|
377
|
+
const remoteHome = remoteHomeResult.stdout.trim() || `/home/${sshUser}`
|
|
378
|
+
const remoteCwd = resolveRemotePath(config.projectPath, remoteHome)
|
|
379
|
+
await releaseRemoteLock(ssh, remoteCwd, { logWarning })
|
|
380
|
+
await releaseLocalLock(rootDir, { logWarning })
|
|
381
|
+
} catch (error) {
|
|
382
|
+
logWarning(`Failed to release lock: ${error.message}`)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
await closeLogFile()
|
|
386
|
+
if (ssh) {
|
|
387
|
+
ssh.dispose()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function promptServerDetails(existingServers = []) {
|
|
393
|
+
return await configFlow.promptServerDetails(existingServers, { runPrompt, generateId })
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function selectServer(servers) {
|
|
397
|
+
return await configFlow.selectServer(servers, {
|
|
398
|
+
runPrompt,
|
|
399
|
+
logProcessing,
|
|
400
|
+
logSuccess,
|
|
401
|
+
saveServers,
|
|
402
|
+
promptServerDetails
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function promptAppDetails(currentDir, existing = {}) {
|
|
407
|
+
return await configFlow.promptAppDetails(currentDir, existing, {
|
|
408
|
+
runPrompt,
|
|
409
|
+
listGitBranches,
|
|
410
|
+
defaultProjectPath,
|
|
411
|
+
promptSshDetails
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function selectApp(projectConfig, server, currentDir) {
|
|
416
|
+
return await configFlow.selectApp(projectConfig, server, currentDir, {
|
|
417
|
+
runPrompt,
|
|
418
|
+
logWarning,
|
|
419
|
+
logProcessing,
|
|
420
|
+
logSuccess,
|
|
421
|
+
saveProjectConfig,
|
|
422
|
+
generateId,
|
|
423
|
+
promptAppDetails
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function selectPreset(projectConfig, servers) {
|
|
428
|
+
return await configFlow.selectPreset(projectConfig, servers, { runPrompt })
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function main(releaseType = null) {
|
|
432
|
+
// Best-effort update check (skip during tests or when explicitly disabled)
|
|
433
|
+
// If an update is accepted, the process will re-execute via npx @latest and we should exit early.
|
|
434
|
+
if (
|
|
435
|
+
process.env.ZEPHYR_SKIP_VERSION_CHECK !== '1' &&
|
|
436
|
+
process.env.NODE_ENV !== 'test' &&
|
|
437
|
+
process.env.VITEST !== 'true'
|
|
438
|
+
) {
|
|
439
|
+
try {
|
|
440
|
+
const args = process.argv.slice(2)
|
|
441
|
+
const reExecuted = await checkAndUpdateVersion(runPrompt, args)
|
|
442
|
+
if (reExecuted) {
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
} catch (_error) {
|
|
446
|
+
// Never block execution due to update check issues
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Handle node/vue package release
|
|
451
|
+
if (releaseType === 'node' || releaseType === 'vue') {
|
|
452
|
+
try {
|
|
453
|
+
await releaseNode()
|
|
454
|
+
return
|
|
455
|
+
} catch (error) {
|
|
456
|
+
logError('\nRelease failed:')
|
|
457
|
+
logError(error.message)
|
|
458
|
+
if (error.stack) {
|
|
459
|
+
writeStderrLine(error.stack)
|
|
460
|
+
}
|
|
461
|
+
process.exit(1)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Handle packagist/composer package release
|
|
466
|
+
if (releaseType === 'packagist') {
|
|
467
|
+
try {
|
|
468
|
+
await releasePackagist()
|
|
469
|
+
return
|
|
470
|
+
} catch (error) {
|
|
471
|
+
logError('\nRelease failed:')
|
|
472
|
+
logError(error.message)
|
|
473
|
+
if (error.stack) {
|
|
474
|
+
writeStderrLine(error.stack)
|
|
475
|
+
}
|
|
476
|
+
process.exit(1)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Default: Laravel deployment workflow
|
|
481
|
+
const rootDir = process.cwd()
|
|
482
|
+
|
|
483
|
+
await ensureGitignoreEntry(rootDir)
|
|
484
|
+
await ensureProjectReleaseScript(rootDir)
|
|
485
|
+
|
|
486
|
+
// Validate dependencies if package.json or composer.json exists
|
|
487
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
488
|
+
const composerJsonPath = path.join(rootDir, 'composer.json')
|
|
489
|
+
const hasPackageJson = await fs.access(packageJsonPath).then(() => true).catch(() => false)
|
|
490
|
+
const hasComposerJson = await fs.access(composerJsonPath).then(() => true).catch(() => false)
|
|
491
|
+
|
|
492
|
+
if (hasPackageJson || hasComposerJson) {
|
|
493
|
+
logProcessing('Validating dependencies...')
|
|
494
|
+
await validateLocalDependencies(rootDir, runPrompt, logSuccess)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Load servers first (they may be migrated)
|
|
498
|
+
const servers = await loadServers({ logSuccess, logWarning })
|
|
499
|
+
// Load project config with servers for migration
|
|
500
|
+
const projectConfig = await loadProjectConfig(rootDir, servers, { logSuccess, logWarning })
|
|
501
|
+
|
|
502
|
+
let server = null
|
|
503
|
+
let appConfig = null
|
|
504
|
+
let isCreatingNewPreset = false
|
|
505
|
+
|
|
506
|
+
const preset = await selectPreset(projectConfig, servers)
|
|
507
|
+
|
|
508
|
+
if (preset === 'create') {
|
|
509
|
+
// User explicitly chose to create a new preset
|
|
510
|
+
isCreatingNewPreset = true
|
|
511
|
+
server = await selectServer(servers)
|
|
512
|
+
appConfig = await selectApp(projectConfig, server, rootDir)
|
|
513
|
+
} else if (preset) {
|
|
514
|
+
// User selected an existing preset - look up by appId
|
|
515
|
+
if (preset.appId) {
|
|
516
|
+
appConfig = projectConfig.apps?.find((a) => a.id === preset.appId)
|
|
517
|
+
|
|
518
|
+
if (!appConfig) {
|
|
519
|
+
logWarning(`Preset references app configuration that no longer exists. Creating new configuration.`)
|
|
520
|
+
server = await selectServer(servers)
|
|
521
|
+
appConfig = await selectApp(projectConfig, server, rootDir)
|
|
522
|
+
} else {
|
|
523
|
+
server = servers.find((s) => s.id === appConfig.serverId || s.serverName === appConfig.serverName)
|
|
524
|
+
|
|
525
|
+
if (!server) {
|
|
526
|
+
logWarning(`Preset references server that no longer exists. Creating new configuration.`)
|
|
527
|
+
server = await selectServer(servers)
|
|
528
|
+
appConfig = await selectApp(projectConfig, server, rootDir)
|
|
529
|
+
} else if (preset.branch && appConfig.branch !== preset.branch) {
|
|
530
|
+
// Update branch if preset has a different branch
|
|
531
|
+
appConfig.branch = preset.branch
|
|
532
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
533
|
+
logSuccess(`Updated branch to ${preset.branch} from preset.`)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else if (preset.key) {
|
|
537
|
+
// Legacy preset format - migrate it
|
|
538
|
+
const keyParts = preset.key.split(':')
|
|
539
|
+
const serverName = keyParts[0]
|
|
540
|
+
const projectPath = keyParts[1]
|
|
541
|
+
const presetBranch = preset.branch || (keyParts.length === 3 ? keyParts[2] : null)
|
|
542
|
+
|
|
543
|
+
server = servers.find((s) => s.serverName === serverName)
|
|
544
|
+
|
|
545
|
+
if (!server) {
|
|
546
|
+
logWarning(`Preset references server "${serverName}" which no longer exists. Creating new configuration.`)
|
|
547
|
+
server = await selectServer(servers)
|
|
548
|
+
appConfig = await selectApp(projectConfig, server, rootDir)
|
|
549
|
+
} else {
|
|
550
|
+
appConfig = projectConfig.apps?.find(
|
|
551
|
+
(a) => (a.serverId === server.id || a.serverName === serverName) && a.projectPath === projectPath
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if (!appConfig) {
|
|
555
|
+
logWarning(`Preset references app configuration that no longer exists. Creating new configuration.`)
|
|
556
|
+
appConfig = await selectApp(projectConfig, server, rootDir)
|
|
557
|
+
} else {
|
|
558
|
+
// Migrate preset to use appId
|
|
559
|
+
preset.appId = appConfig.id
|
|
560
|
+
if (presetBranch && appConfig.branch !== presetBranch) {
|
|
561
|
+
appConfig.branch = presetBranch
|
|
562
|
+
}
|
|
563
|
+
preset.branch = appConfig.branch
|
|
564
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
logWarning(`Preset format is invalid. Creating new configuration.`)
|
|
569
|
+
server = await selectServer(servers)
|
|
570
|
+
appConfig = await selectApp(projectConfig, server, rootDir)
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
// No presets exist, go through normal flow
|
|
574
|
+
server = await selectServer(servers)
|
|
575
|
+
appConfig = await selectApp(projectConfig, server, rootDir)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const updated = await ensureSshDetails(appConfig, rootDir)
|
|
579
|
+
|
|
580
|
+
if (updated) {
|
|
581
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
582
|
+
logSuccess('Updated .zephyr/config.json with SSH details.')
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const deploymentConfig = {
|
|
586
|
+
serverName: server.serverName,
|
|
587
|
+
serverIp: server.serverIp,
|
|
588
|
+
projectPath: appConfig.projectPath,
|
|
589
|
+
branch: appConfig.branch,
|
|
590
|
+
sshUser: appConfig.sshUser,
|
|
591
|
+
sshKey: appConfig.sshKey
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
logProcessing('\nSelected deployment target:')
|
|
595
|
+
writeStdoutLine(JSON.stringify(deploymentConfig, null, 2))
|
|
596
|
+
|
|
597
|
+
if (isCreatingNewPreset || !preset) {
|
|
598
|
+
const { presetName } = await runPrompt([
|
|
599
|
+
{
|
|
600
|
+
type: 'input',
|
|
601
|
+
name: 'presetName',
|
|
602
|
+
message: 'Enter a name for this preset (leave blank to skip)',
|
|
603
|
+
default: isCreatingNewPreset ? '' : undefined
|
|
604
|
+
}
|
|
605
|
+
])
|
|
606
|
+
|
|
607
|
+
const trimmedName = presetName?.trim()
|
|
608
|
+
|
|
609
|
+
if (trimmedName && trimmedName.length > 0) {
|
|
610
|
+
const presets = projectConfig.presets ?? []
|
|
611
|
+
|
|
612
|
+
// Find app config to get its ID
|
|
613
|
+
const appId = appConfig.id
|
|
614
|
+
|
|
615
|
+
if (!appId) {
|
|
616
|
+
logWarning('Cannot save preset: app configuration missing ID.')
|
|
617
|
+
} else {
|
|
618
|
+
// Check if preset with this appId already exists
|
|
619
|
+
const existingIndex = presets.findIndex((p) => p.appId === appId)
|
|
620
|
+
if (existingIndex >= 0) {
|
|
621
|
+
presets[existingIndex].name = trimmedName
|
|
622
|
+
presets[existingIndex].branch = deploymentConfig.branch
|
|
623
|
+
} else {
|
|
624
|
+
presets.push({
|
|
625
|
+
name: trimmedName,
|
|
626
|
+
appId: appId,
|
|
627
|
+
branch: deploymentConfig.branch
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
projectConfig.presets = presets
|
|
632
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
633
|
+
logSuccess(`Saved preset "${trimmedName}" to .zephyr/config.json`)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const existingSnapshot = await loadPendingTasksSnapshot(rootDir)
|
|
639
|
+
let snapshotToUse = null
|
|
640
|
+
|
|
641
|
+
if (existingSnapshot) {
|
|
642
|
+
const matchesSelection =
|
|
643
|
+
existingSnapshot.serverName === deploymentConfig.serverName &&
|
|
644
|
+
existingSnapshot.branch === deploymentConfig.branch
|
|
645
|
+
|
|
646
|
+
const messageLines = [
|
|
647
|
+
'Pending deployment tasks were detected from a previous run.',
|
|
648
|
+
`Server: ${existingSnapshot.serverName}`,
|
|
649
|
+
`Branch: ${existingSnapshot.branch}`
|
|
650
|
+
]
|
|
651
|
+
|
|
652
|
+
if (existingSnapshot.taskLabels && existingSnapshot.taskLabels.length > 0) {
|
|
653
|
+
messageLines.push(`Tasks: ${existingSnapshot.taskLabels.join(', ')}`)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const { resumePendingTasks } = await runPrompt([
|
|
657
|
+
{
|
|
658
|
+
type: 'confirm',
|
|
659
|
+
name: 'resumePendingTasks',
|
|
660
|
+
message: `${messageLines.join(' | ')}. Resume using this plan?`,
|
|
661
|
+
default: matchesSelection
|
|
662
|
+
}
|
|
663
|
+
])
|
|
664
|
+
|
|
665
|
+
if (resumePendingTasks) {
|
|
666
|
+
snapshotToUse = existingSnapshot
|
|
667
|
+
logProcessing('Resuming deployment using saved task snapshot...')
|
|
668
|
+
} else {
|
|
669
|
+
await clearPendingTasksSnapshot(rootDir)
|
|
670
|
+
logWarning('Discarded pending deployment snapshot.')
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
await runRemoteTasks(deploymentConfig, { rootDir, snapshot: snapshotToUse })
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export { main, runRemoteTasks }
|