rubrkit 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +417 -404
- package/package.json +28 -28
- package/src/api.js +108 -101
- package/src/args.js +209 -175
- package/src/cli.js +97 -93
- package/src/config.js +186 -169
- package/src/formats.js +239 -222
- package/src/pull.js +682 -676
- package/src/sdk.js +451 -443
- package/src/testingCli.js +504 -431
package/src/pull.js
CHANGED
|
@@ -1,676 +1,682 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import fsp from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import readline from 'node:readline/promises';
|
|
5
|
-
|
|
6
|
-
import { RubrkitApiClient } from './api.js';
|
|
7
|
-
import { resolveAgentAdapter } from './adapters.js';
|
|
8
|
-
import { authError, RubrkitCliError } from './errors.js';
|
|
9
|
-
import {
|
|
10
|
-
findManifestEntry,
|
|
11
|
-
hashContent,
|
|
12
|
-
loadManifest,
|
|
13
|
-
removeManifestEntriesByDestination,
|
|
14
|
-
upsertManifestEntry,
|
|
15
|
-
writeManifest,
|
|
16
|
-
} from './manifest.js';
|
|
17
|
-
import { normalizeArtifactPath, relativeToRoot, resolveInsideRoot, slugifyPathSegment } from './pathSafety.js';
|
|
18
|
-
import { promptForPullSelections } from './prompts.js';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {{
|
|
22
|
-
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
23
|
-
* stdin?: NodeJS.ReadableStream,
|
|
24
|
-
* stdout?: NodeJS.WritableStream,
|
|
25
|
-
* stderr?: NodeJS.WritableStream,
|
|
26
|
-
* fetchImpl?: typeof fetch,
|
|
27
|
-
* fsImpl?: typeof fsp,
|
|
28
|
-
* fsSyncImpl?: Pick<typeof fs, 'existsSync'>,
|
|
29
|
-
* }} params
|
|
30
|
-
*/
|
|
31
|
-
export async function runPull({
|
|
32
|
-
config,
|
|
33
|
-
stdin = process.stdin,
|
|
34
|
-
stdout = process.stdout,
|
|
35
|
-
fetchImpl = globalThis.fetch,
|
|
36
|
-
fsImpl = fsp,
|
|
37
|
-
fsSyncImpl = fs,
|
|
38
|
-
}) {
|
|
39
|
-
if (!config.apiKey) {
|
|
40
|
-
throw authError('Missing Rubrkit API key. Set RUBRKIT_API_KEY or pass --api-key.');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const destinationRoot = path.resolve(config.destination);
|
|
44
|
-
const apiClient = new RubrkitApiClient({
|
|
45
|
-
apiUrl: config.apiUrl,
|
|
46
|
-
apiKey: config.apiKey,
|
|
47
|
-
fetchImpl,
|
|
48
|
-
});
|
|
49
|
-
const manifest = await loadManifest(destinationRoot, fsImpl);
|
|
50
|
-
const bundles = await apiClient.listArtifactBundles();
|
|
51
|
-
const selections = await resolveSelections({
|
|
52
|
-
config,
|
|
53
|
-
apiClient,
|
|
54
|
-
bundles,
|
|
55
|
-
manifest,
|
|
56
|
-
stdin,
|
|
57
|
-
stdout,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (selections.length === 0) {
|
|
61
|
-
stdout.write('No artifacts matched the pull request.\n');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const hydrated = await hydrateSelections({ apiClient, selections });
|
|
66
|
-
const plan = await createPullPlan({
|
|
67
|
-
destinationRoot,
|
|
68
|
-
items: hydrated,
|
|
69
|
-
manifest,
|
|
70
|
-
config,
|
|
71
|
-
fsImpl,
|
|
72
|
-
fsSyncImpl,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
printPlan({ plan, destinationRoot, dryRun: config.dryRun, stdout });
|
|
76
|
-
|
|
77
|
-
const conflicts = plan.actions.filter((action) => action.blocked);
|
|
78
|
-
|
|
79
|
-
if (conflicts.length > 0 && !config.dryRun) {
|
|
80
|
-
throw new RubrkitCliError(
|
|
81
|
-
`Pull stopped because ${conflicts.length} local change${conflicts.length === 1 ? '' : 's'} would be overwritten. Re-run with --force to overwrite.`,
|
|
82
|
-
{ code: 'local_changes_protected' },
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (config.dryRun) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await applyPlan({ plan, manifest, destinationRoot, fsImpl });
|
|
91
|
-
|
|
92
|
-
stdout.write(
|
|
93
|
-
`Rubrkit pull complete: ${plan.summary.writes} written, ${plan.summary.skips} skipped, ${plan.summary.prunes} pruned.\n`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @param {{
|
|
99
|
-
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
100
|
-
* apiClient: RubrkitApiClient,
|
|
101
|
-
* bundles: Array<Record<string, any>>,
|
|
102
|
-
* manifest: Awaited<ReturnType<typeof loadManifest>>,
|
|
103
|
-
* stdin: NodeJS.ReadableStream,
|
|
104
|
-
* stdout: NodeJS.WritableStream,
|
|
105
|
-
* }} params
|
|
106
|
-
*/
|
|
107
|
-
async function resolveSelections({ config, apiClient, bundles, manifest, stdin, stdout }) {
|
|
108
|
-
const fileCache = new Map();
|
|
109
|
-
const loadFiles = async (bundle) => {
|
|
110
|
-
if (!fileCache.has(bundle.id)) {
|
|
111
|
-
fileCache.set(bundle.id, await apiClient.listFiles(bundle.id));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return fileCache.get(bundle.id);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
if (config.updateOnly) {
|
|
118
|
-
return resolveUpdateOnlySelections({ config, bundles, manifest, loadFiles });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (config.
|
|
122
|
-
if (!config.yes) {
|
|
123
|
-
await confirmAll({ bundleCount: bundles.length, stdin, stdout });
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return selectAll({ bundles, loadFiles });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (config.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (config.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
destinationPath,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
530
|
-
}
|
|
531
|
-
|
|
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
|
-
stdout.write(
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import readline from 'node:readline/promises';
|
|
5
|
+
|
|
6
|
+
import { RubrkitApiClient } from './api.js';
|
|
7
|
+
import { resolveAgentAdapter } from './adapters.js';
|
|
8
|
+
import { authError, RubrkitCliError } from './errors.js';
|
|
9
|
+
import {
|
|
10
|
+
findManifestEntry,
|
|
11
|
+
hashContent,
|
|
12
|
+
loadManifest,
|
|
13
|
+
removeManifestEntriesByDestination,
|
|
14
|
+
upsertManifestEntry,
|
|
15
|
+
writeManifest,
|
|
16
|
+
} from './manifest.js';
|
|
17
|
+
import { normalizeArtifactPath, relativeToRoot, resolveInsideRoot, slugifyPathSegment } from './pathSafety.js';
|
|
18
|
+
import { promptForPullSelections } from './prompts.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{
|
|
22
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
23
|
+
* stdin?: NodeJS.ReadableStream,
|
|
24
|
+
* stdout?: NodeJS.WritableStream,
|
|
25
|
+
* stderr?: NodeJS.WritableStream,
|
|
26
|
+
* fetchImpl?: typeof fetch,
|
|
27
|
+
* fsImpl?: typeof fsp,
|
|
28
|
+
* fsSyncImpl?: Pick<typeof fs, 'existsSync'>,
|
|
29
|
+
* }} params
|
|
30
|
+
*/
|
|
31
|
+
export async function runPull({
|
|
32
|
+
config,
|
|
33
|
+
stdin = process.stdin,
|
|
34
|
+
stdout = process.stdout,
|
|
35
|
+
fetchImpl = globalThis.fetch,
|
|
36
|
+
fsImpl = fsp,
|
|
37
|
+
fsSyncImpl = fs,
|
|
38
|
+
}) {
|
|
39
|
+
if (!config.apiKey) {
|
|
40
|
+
throw authError('Missing Rubrkit API key. Set RUBRKIT_API_KEY or pass --api-key.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const destinationRoot = path.resolve(config.destination);
|
|
44
|
+
const apiClient = new RubrkitApiClient({
|
|
45
|
+
apiUrl: config.apiUrl,
|
|
46
|
+
apiKey: config.apiKey,
|
|
47
|
+
fetchImpl,
|
|
48
|
+
});
|
|
49
|
+
const manifest = await loadManifest(destinationRoot, fsImpl);
|
|
50
|
+
const bundles = await apiClient.listArtifactBundles({ labels: config.label ?? [] });
|
|
51
|
+
const selections = await resolveSelections({
|
|
52
|
+
config,
|
|
53
|
+
apiClient,
|
|
54
|
+
bundles,
|
|
55
|
+
manifest,
|
|
56
|
+
stdin,
|
|
57
|
+
stdout,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (selections.length === 0) {
|
|
61
|
+
stdout.write('No artifacts matched the pull request.\n');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hydrated = await hydrateSelections({ apiClient, selections });
|
|
66
|
+
const plan = await createPullPlan({
|
|
67
|
+
destinationRoot,
|
|
68
|
+
items: hydrated,
|
|
69
|
+
manifest,
|
|
70
|
+
config,
|
|
71
|
+
fsImpl,
|
|
72
|
+
fsSyncImpl,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
printPlan({ plan, destinationRoot, dryRun: config.dryRun, stdout });
|
|
76
|
+
|
|
77
|
+
const conflicts = plan.actions.filter((action) => action.blocked);
|
|
78
|
+
|
|
79
|
+
if (conflicts.length > 0 && !config.dryRun) {
|
|
80
|
+
throw new RubrkitCliError(
|
|
81
|
+
`Pull stopped because ${conflicts.length} local change${conflicts.length === 1 ? '' : 's'} would be overwritten. Re-run with --force to overwrite.`,
|
|
82
|
+
{ code: 'local_changes_protected' },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (config.dryRun) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await applyPlan({ plan, manifest, destinationRoot, fsImpl });
|
|
91
|
+
|
|
92
|
+
stdout.write(
|
|
93
|
+
`Rubrkit pull complete: ${plan.summary.writes} written, ${plan.summary.skips} skipped, ${plan.summary.prunes} pruned.\n`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {{
|
|
99
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
100
|
+
* apiClient: RubrkitApiClient,
|
|
101
|
+
* bundles: Array<Record<string, any>>,
|
|
102
|
+
* manifest: Awaited<ReturnType<typeof loadManifest>>,
|
|
103
|
+
* stdin: NodeJS.ReadableStream,
|
|
104
|
+
* stdout: NodeJS.WritableStream,
|
|
105
|
+
* }} params
|
|
106
|
+
*/
|
|
107
|
+
async function resolveSelections({ config, apiClient, bundles, manifest, stdin, stdout }) {
|
|
108
|
+
const fileCache = new Map();
|
|
109
|
+
const loadFiles = async (bundle) => {
|
|
110
|
+
if (!fileCache.has(bundle.id)) {
|
|
111
|
+
fileCache.set(bundle.id, await apiClient.listFiles(bundle.id));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return fileCache.get(bundle.id);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (config.updateOnly) {
|
|
118
|
+
return resolveUpdateOnlySelections({ config, bundles, manifest, loadFiles });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (Array.isArray(config.label) && config.label.length > 0) {
|
|
122
|
+
if (!config.yes) {
|
|
123
|
+
await confirmAll({ bundleCount: bundles.length, stdin, stdout });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return selectAll({ bundles, loadFiles });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (config.all) {
|
|
130
|
+
if (!config.yes) {
|
|
131
|
+
await confirmAll({ bundleCount: bundles.length, stdin, stdout });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return selectAll({ bundles, loadFiles });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (config.artifactBundle || config.artifact) {
|
|
138
|
+
return resolveFlagSelections({ config, bundles, loadFiles });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (config.selector) {
|
|
142
|
+
return resolveSelectorSelections({ selector: config.selector, yes: config.yes, bundles, loadFiles });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.yes) {
|
|
146
|
+
throw new RubrkitCliError('No selector was provided. Use "all", --artifact-bundle, or --artifact with --yes.', {
|
|
147
|
+
code: 'selector_required',
|
|
148
|
+
exitCode: 2,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return promptForPullSelections({ bundles, loadFiles, stdin, stdout });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {{ bundleCount: number, stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream }} params
|
|
157
|
+
*/
|
|
158
|
+
async function confirmAll({ bundleCount, stdin, stdout }) {
|
|
159
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const answer = await rl.question(`Pull all files from ${bundleCount} artifact bundle${bundleCount === 1 ? '' : 's'}? [y/N] `);
|
|
163
|
+
|
|
164
|
+
if (!['y', 'yes'].includes(answer.trim().toLowerCase())) {
|
|
165
|
+
throw new RubrkitCliError('Pull cancelled.', { code: 'pull_cancelled', exitCode: 2 });
|
|
166
|
+
}
|
|
167
|
+
} finally {
|
|
168
|
+
rl.close();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {{ bundles: Array<Record<string, any>>, loadFiles(bundle: Record<string, any>): Promise<Array<Record<string, any>>> }} params
|
|
174
|
+
*/
|
|
175
|
+
async function selectAll({ bundles, loadFiles }) {
|
|
176
|
+
const selections = [];
|
|
177
|
+
|
|
178
|
+
for (const bundle of bundles) {
|
|
179
|
+
const files = await loadFiles(bundle);
|
|
180
|
+
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
selections.push({ artifactBundle: bundle, file });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return selections;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {{
|
|
191
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
192
|
+
* bundles: Array<Record<string, any>>,
|
|
193
|
+
* loadFiles(bundle: Record<string, any>): Promise<Array<Record<string, any>>>,
|
|
194
|
+
* }} params
|
|
195
|
+
*/
|
|
196
|
+
async function resolveFlagSelections({ config, bundles, loadFiles }) {
|
|
197
|
+
const selectedBundles = config.artifactBundle ? findBundles(bundles, config.artifactBundle) : bundles;
|
|
198
|
+
|
|
199
|
+
if (selectedBundles.length === 0) {
|
|
200
|
+
throw new RubrkitCliError(`No artifact bundle matched "${config.artifactBundle}".`, { code: 'artifact_bundle_not_found' });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const selections = [];
|
|
204
|
+
|
|
205
|
+
for (const bundle of selectedBundles) {
|
|
206
|
+
const files = await loadFiles(bundle);
|
|
207
|
+
const selectedFiles = config.artifact ? findFiles(files, config.artifact) : files;
|
|
208
|
+
|
|
209
|
+
for (const file of selectedFiles) {
|
|
210
|
+
selections.push({ artifactBundle: bundle, file });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (config.artifact && selections.length === 0) {
|
|
215
|
+
throw new RubrkitCliError(`No artifact matched "${config.artifact}".`, { code: 'artifact_not_found' });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (config.yes && selectedBundles.length > 1 && config.artifactBundle) {
|
|
219
|
+
throw new RubrkitCliError(`Artifact bundle selector "${config.artifactBundle}" is ambiguous.`, {
|
|
220
|
+
code: 'ambiguous_selector',
|
|
221
|
+
exitCode: 2,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return selections;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @param {{
|
|
230
|
+
* selector: string,
|
|
231
|
+
* yes: boolean,
|
|
232
|
+
* bundles: Array<Record<string, any>>,
|
|
233
|
+
* loadFiles(bundle: Record<string, any>): Promise<Array<Record<string, any>>>,
|
|
234
|
+
* }} params
|
|
235
|
+
*/
|
|
236
|
+
async function resolveSelectorSelections({ selector, yes, bundles, loadFiles }) {
|
|
237
|
+
const bundleMatches = findBundles(bundles, selector);
|
|
238
|
+
const fileMatches = [];
|
|
239
|
+
|
|
240
|
+
for (const bundle of bundles) {
|
|
241
|
+
const files = await loadFiles(bundle);
|
|
242
|
+
|
|
243
|
+
for (const file of findFiles(files, selector)) {
|
|
244
|
+
fileMatches.push({ artifactBundle: bundle, file });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const candidateCount = bundleMatches.length + fileMatches.length;
|
|
249
|
+
|
|
250
|
+
if (candidateCount === 0) {
|
|
251
|
+
throw new RubrkitCliError(`No artifact bundle or artifact matched "${selector}".`, { code: 'selector_not_found' });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (candidateCount > 1 && yes) {
|
|
255
|
+
throw new RubrkitCliError(`Selector "${selector}" is ambiguous. Use --artifact-bundle or --artifact.`, {
|
|
256
|
+
code: 'ambiguous_selector',
|
|
257
|
+
exitCode: 2,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (bundleMatches.length === 1 && fileMatches.length === 0) {
|
|
262
|
+
const files = await loadFiles(bundleMatches[0]);
|
|
263
|
+
return files.map((file) => ({ artifactBundle: bundleMatches[0], file }));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (fileMatches.length === 1 && bundleMatches.length === 0) {
|
|
267
|
+
return fileMatches;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw new RubrkitCliError(`Selector "${selector}" is ambiguous. Use --artifact-bundle or --artifact.`, {
|
|
271
|
+
code: 'ambiguous_selector',
|
|
272
|
+
exitCode: 2,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {{
|
|
278
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
279
|
+
* bundles: Array<Record<string, any>>,
|
|
280
|
+
* manifest: Awaited<ReturnType<typeof loadManifest>>,
|
|
281
|
+
* loadFiles(bundle: Record<string, any>): Promise<Array<Record<string, any>>>,
|
|
282
|
+
* }} params
|
|
283
|
+
*/
|
|
284
|
+
async function resolveUpdateOnlySelections({ config, bundles, manifest, loadFiles }) {
|
|
285
|
+
const entries = manifest.entries.filter((entry) => matchesEntrySelectors(entry, config));
|
|
286
|
+
const selections = [];
|
|
287
|
+
|
|
288
|
+
for (const entry of entries) {
|
|
289
|
+
const bundle = bundles.find((candidate) => candidate.id === entry.artifactBundleId);
|
|
290
|
+
|
|
291
|
+
if (!bundle) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const files = await loadFiles(bundle);
|
|
296
|
+
const file = files.find((candidate) => candidate.id === entry.artifactId || candidate.path === entry.artifactPath);
|
|
297
|
+
|
|
298
|
+
if (file) {
|
|
299
|
+
selections.push({ artifactBundle: bundle, file });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return selections;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {Record<string, any>} entry
|
|
308
|
+
* @param {Awaited<ReturnType<import('./config.js').resolveConfig>>} config
|
|
309
|
+
*/
|
|
310
|
+
function matchesEntrySelectors(entry, config) {
|
|
311
|
+
const selector = config.selector && config.selector !== 'all' ? config.selector : null;
|
|
312
|
+
const bundleSelector = config.artifactBundle ?? selector;
|
|
313
|
+
const artifactSelector = config.artifact ?? (bundleSelector === selector ? null : selector);
|
|
314
|
+
const bundleMatches = !bundleSelector || [entry.artifactBundleId, entry.artifactBundleName].some((value) => matchesSelector(value, bundleSelector));
|
|
315
|
+
const artifactMatches =
|
|
316
|
+
!artifactSelector ||
|
|
317
|
+
[entry.artifactId, entry.artifactPath, path.posix.basename(String(entry.artifactPath ?? ''))].some((value) =>
|
|
318
|
+
matchesSelector(value, artifactSelector),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return bundleMatches && artifactMatches;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {Array<Record<string, any>>} bundles
|
|
326
|
+
* @param {string} selector
|
|
327
|
+
*/
|
|
328
|
+
function findBundles(bundles, selector) {
|
|
329
|
+
return bundles.filter((bundle) => {
|
|
330
|
+
const slug = slugifyPathSegment(bundle.name ?? bundle.id);
|
|
331
|
+
|
|
332
|
+
return [bundle.id, bundle.name, slug].some((value) => matchesSelector(value, selector));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {Array<Record<string, any>>} files
|
|
338
|
+
* @param {string} selector
|
|
339
|
+
*/
|
|
340
|
+
function findFiles(files, selector) {
|
|
341
|
+
return files.filter((file) => {
|
|
342
|
+
const normalizedPath = normalizeArtifactPath(file.path);
|
|
343
|
+
|
|
344
|
+
return [file.id, normalizedPath, path.posix.basename(normalizedPath)].some((value) => matchesSelector(value, selector));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {unknown} value
|
|
350
|
+
* @param {string} selector
|
|
351
|
+
*/
|
|
352
|
+
function matchesSelector(value, selector) {
|
|
353
|
+
if (typeof value !== 'string') {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return value === selector || value.toLowerCase() === selector.toLowerCase();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @param {{ apiClient: RubrkitApiClient, selections: Array<{ artifactBundle: Record<string, any>, file: Record<string, any> }> }} params
|
|
362
|
+
*/
|
|
363
|
+
async function hydrateSelections({ apiClient, selections }) {
|
|
364
|
+
const hydrated = [];
|
|
365
|
+
|
|
366
|
+
for (const selection of selections) {
|
|
367
|
+
const detail = await apiClient.getFile(selection.artifactBundle.id, selection.file.id);
|
|
368
|
+
|
|
369
|
+
hydrated.push({
|
|
370
|
+
artifactBundle: selection.artifactBundle,
|
|
371
|
+
file: detail.file ?? selection.file,
|
|
372
|
+
version: detail.version ?? {},
|
|
373
|
+
content: typeof detail.content === 'string' ? detail.content : '',
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return hydrated;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @param {{
|
|
382
|
+
* destinationRoot: string,
|
|
383
|
+
* items: Array<{ artifactBundle: Record<string, any>, file: Record<string, any>, version: Record<string, any>, content: string }>,
|
|
384
|
+
* manifest: Awaited<ReturnType<typeof loadManifest>>,
|
|
385
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
386
|
+
* fsImpl: typeof fsp,
|
|
387
|
+
* fsSyncImpl: Pick<typeof fs, 'existsSync'>,
|
|
388
|
+
* }} params
|
|
389
|
+
*/
|
|
390
|
+
async function createPullPlan({ destinationRoot, items, manifest, config, fsImpl, fsSyncImpl }) {
|
|
391
|
+
const adapter = resolveAgentAdapter({
|
|
392
|
+
root: destinationRoot,
|
|
393
|
+
requestedAgent: config.agent,
|
|
394
|
+
fsImpl: fsSyncImpl,
|
|
395
|
+
});
|
|
396
|
+
const actions = [];
|
|
397
|
+
const selectedDestinationPaths = new Set();
|
|
398
|
+
const selectedBundleIds = new Set(items.map((item) => item.artifactBundle.id));
|
|
399
|
+
|
|
400
|
+
for (const item of items) {
|
|
401
|
+
const placement = adapter.place({
|
|
402
|
+
artifactBundle: item.artifactBundle,
|
|
403
|
+
file: item.file,
|
|
404
|
+
exists(relativePath) {
|
|
405
|
+
return fsSyncImpl.existsSync(resolveInsideRoot(destinationRoot, relativePath));
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
const targetPath = resolveInsideRoot(destinationRoot, placement.destinationPath);
|
|
409
|
+
const destinationPath = relativeToRoot(destinationRoot, targetPath);
|
|
410
|
+
selectedDestinationPaths.add(destinationPath);
|
|
411
|
+
|
|
412
|
+
actions.push(
|
|
413
|
+
await createWriteAction({
|
|
414
|
+
adapterName: adapter.name,
|
|
415
|
+
destinationPath,
|
|
416
|
+
targetPath,
|
|
417
|
+
placementReason: placement.reason,
|
|
418
|
+
item,
|
|
419
|
+
manifest,
|
|
420
|
+
config,
|
|
421
|
+
fsImpl,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (config.prune) {
|
|
427
|
+
for (const entry of manifest.entries) {
|
|
428
|
+
const destinationPath = String(entry.destinationPath ?? '');
|
|
429
|
+
|
|
430
|
+
if (!selectedBundleIds.has(entry.artifactBundleId) || selectedDestinationPaths.has(destinationPath)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const targetPath = resolveInsideRoot(destinationRoot, destinationPath);
|
|
435
|
+
actions.push(await createPruneAction({ destinationRoot, destinationPath, targetPath, entry, config, fsImpl }));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const summary = {
|
|
440
|
+
writes: actions.filter((action) => ['write', 'overwrite', 'update'].includes(action.action)).length,
|
|
441
|
+
skips: actions.filter((action) => action.action === 'skip').length,
|
|
442
|
+
prunes: actions.filter((action) => action.action === 'prune').length,
|
|
443
|
+
conflicts: actions.filter((action) => action.blocked).length,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
return { adapterName: adapter.name, actions, summary };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @param {{
|
|
451
|
+
* adapterName: string,
|
|
452
|
+
* destinationPath: string,
|
|
453
|
+
* targetPath: string,
|
|
454
|
+
* placementReason: string,
|
|
455
|
+
* item: { artifactBundle: Record<string, any>, file: Record<string, any>, version: Record<string, any>, content: string },
|
|
456
|
+
* manifest: Awaited<ReturnType<typeof loadManifest>>,
|
|
457
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
458
|
+
* fsImpl: typeof fsp,
|
|
459
|
+
* }} params
|
|
460
|
+
*/
|
|
461
|
+
async function createWriteAction({ adapterName, destinationPath, targetPath, placementReason, item, manifest, config, fsImpl }) {
|
|
462
|
+
const existingContent = await readTextIfExists(targetPath, fsImpl);
|
|
463
|
+
const localHash = existingContent === null ? null : hashContent(existingContent);
|
|
464
|
+
const remoteHash = item.version.contentHash ?? item.file.contentHash ?? hashContent(item.content);
|
|
465
|
+
const manifestEntry = findManifestEntry(manifest, {
|
|
466
|
+
artifactBundleId: item.artifactBundle.id,
|
|
467
|
+
artifactId: item.file.id,
|
|
468
|
+
destinationPath,
|
|
469
|
+
});
|
|
470
|
+
const base = {
|
|
471
|
+
kind: 'write',
|
|
472
|
+
action: 'write',
|
|
473
|
+
adapterName,
|
|
474
|
+
destinationPath,
|
|
475
|
+
targetPath,
|
|
476
|
+
placementReason,
|
|
477
|
+
artifactBundle: item.artifactBundle,
|
|
478
|
+
file: item.file,
|
|
479
|
+
version: item.version,
|
|
480
|
+
content: item.content,
|
|
481
|
+
remoteHash,
|
|
482
|
+
localHash,
|
|
483
|
+
manifestEntry,
|
|
484
|
+
blocked: false,
|
|
485
|
+
reason: '',
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
if (manifestEntry) {
|
|
489
|
+
const expectedHash = String(manifestEntry.lastKnownLocalContentHash ?? '');
|
|
490
|
+
|
|
491
|
+
if (localHash && expectedHash && localHash !== expectedHash && !config.force) {
|
|
492
|
+
return {
|
|
493
|
+
...base,
|
|
494
|
+
action: 'conflict',
|
|
495
|
+
blocked: true,
|
|
496
|
+
reason: 'local changes differ from the last Rubrkit pull',
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (localHash === remoteHash && manifestEntry.contentHash === remoteHash) {
|
|
501
|
+
return {
|
|
502
|
+
...base,
|
|
503
|
+
action: 'skip',
|
|
504
|
+
reason: 'already up to date',
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
...base,
|
|
510
|
+
action: existingContent === null ? 'write' : 'update',
|
|
511
|
+
reason: existingContent === null ? 'tracked file is missing locally' : 'remote artifact changed',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (config.updateOnly) {
|
|
516
|
+
return {
|
|
517
|
+
...base,
|
|
518
|
+
action: 'skip',
|
|
519
|
+
reason: 'not tracked in the local manifest',
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (existingContent !== null && !config.force) {
|
|
524
|
+
return {
|
|
525
|
+
...base,
|
|
526
|
+
action: 'conflict',
|
|
527
|
+
blocked: true,
|
|
528
|
+
reason: 'destination exists and is not managed by Rubrkit',
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
...base,
|
|
534
|
+
action: existingContent === null ? 'write' : 'overwrite',
|
|
535
|
+
reason: existingContent === null ? 'new artifact' : 'forced overwrite of an unmanaged file',
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* @param {{
|
|
541
|
+
* destinationRoot: string,
|
|
542
|
+
* destinationPath: string,
|
|
543
|
+
* targetPath: string,
|
|
544
|
+
* entry: Record<string, any>,
|
|
545
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
546
|
+
* fsImpl: typeof fsp,
|
|
547
|
+
* }} params
|
|
548
|
+
*/
|
|
549
|
+
async function createPruneAction({ destinationRoot, destinationPath, targetPath, entry, config, fsImpl }) {
|
|
550
|
+
const existingContent = await readTextIfExists(targetPath, fsImpl);
|
|
551
|
+
const localHash = existingContent === null ? null : hashContent(existingContent);
|
|
552
|
+
const expectedHash = String(entry.lastKnownLocalContentHash ?? '');
|
|
553
|
+
|
|
554
|
+
if (existingContent !== null && expectedHash && localHash !== expectedHash && !config.force) {
|
|
555
|
+
return {
|
|
556
|
+
kind: 'prune',
|
|
557
|
+
action: 'conflict',
|
|
558
|
+
destinationPath,
|
|
559
|
+
targetPath,
|
|
560
|
+
entry,
|
|
561
|
+
localHash,
|
|
562
|
+
blocked: true,
|
|
563
|
+
reason: 'local changes differ from the last Rubrkit pull',
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
kind: 'prune',
|
|
569
|
+
action: 'prune',
|
|
570
|
+
destinationPath,
|
|
571
|
+
targetPath,
|
|
572
|
+
entry,
|
|
573
|
+
localHash,
|
|
574
|
+
blocked: false,
|
|
575
|
+
reason: existingContent === null ? 'tracked file is already missing' : 'remote artifact is no longer selected',
|
|
576
|
+
destinationRoot,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* @param {string} targetPath
|
|
582
|
+
* @param {typeof fsp} fsImpl
|
|
583
|
+
*/
|
|
584
|
+
async function readTextIfExists(targetPath, fsImpl) {
|
|
585
|
+
try {
|
|
586
|
+
return await fsImpl.readFile(targetPath, 'utf8');
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* @param {{ plan: Awaited<ReturnType<typeof createPullPlan>>, destinationRoot: string, dryRun: boolean, stdout: NodeJS.WritableStream }} params
|
|
598
|
+
*/
|
|
599
|
+
function printPlan({ plan, destinationRoot, dryRun, stdout }) {
|
|
600
|
+
stdout.write(`${dryRun ? 'Rubrkit pull dry run' : 'Rubrkit pull plan'}\n`);
|
|
601
|
+
stdout.write(`Destination: ${destinationRoot}\n`);
|
|
602
|
+
stdout.write(`Agent adapter: ${plan.adapterName}\n`);
|
|
603
|
+
|
|
604
|
+
for (const action of plan.actions) {
|
|
605
|
+
const label = action.blocked ? 'protected' : action.action;
|
|
606
|
+
let source;
|
|
607
|
+
|
|
608
|
+
if (action.kind === 'write') {
|
|
609
|
+
const writeAction = /** @type {Record<string, any>} */ (action);
|
|
610
|
+
source = `${writeAction.artifactBundle.name ?? writeAction.artifactBundle.id}/${writeAction.file.path}`;
|
|
611
|
+
} else {
|
|
612
|
+
const pruneAction = /** @type {Record<string, any>} */ (action);
|
|
613
|
+
source = String(pruneAction.entry.artifactPath ?? pruneAction.entry.destinationPath);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
stdout.write(`- ${label}: ${action.destinationPath} <- ${source}`);
|
|
617
|
+
|
|
618
|
+
if (action.reason) {
|
|
619
|
+
stdout.write(` (${action.reason})`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
stdout.write('\n');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
stdout.write(
|
|
626
|
+
`Summary: ${plan.summary.writes} write/update, ${plan.summary.skips} skipped, ${plan.summary.prunes} pruned, ${plan.summary.conflicts} protected.\n`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @param {{
|
|
632
|
+
* plan: Awaited<ReturnType<typeof createPullPlan>>,
|
|
633
|
+
* manifest: Awaited<ReturnType<typeof loadManifest>>,
|
|
634
|
+
* destinationRoot: string,
|
|
635
|
+
* fsImpl: typeof fsp,
|
|
636
|
+
* }} params
|
|
637
|
+
*/
|
|
638
|
+
async function applyPlan({ plan, manifest, destinationRoot, fsImpl }) {
|
|
639
|
+
let manifestChanged = false;
|
|
640
|
+
const prunedDestinations = new Set();
|
|
641
|
+
|
|
642
|
+
for (const action of plan.actions) {
|
|
643
|
+
if (action.blocked || action.action === 'skip') {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (action.kind === 'prune') {
|
|
648
|
+
const pruneAction = /** @type {Record<string, any>} */ (action);
|
|
649
|
+
await fsImpl.rm(pruneAction.targetPath, { force: true });
|
|
650
|
+
prunedDestinations.add(pruneAction.destinationPath);
|
|
651
|
+
manifestChanged = true;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const writeAction = /** @type {Record<string, any>} */ (action);
|
|
656
|
+
await fsImpl.mkdir(path.dirname(writeAction.targetPath), { recursive: true });
|
|
657
|
+
await fsImpl.writeFile(writeAction.targetPath, writeAction.content, 'utf8');
|
|
658
|
+
upsertManifestEntry(manifest, {
|
|
659
|
+
artifactBundleId: writeAction.artifactBundle.id,
|
|
660
|
+
artifactBundleName: writeAction.artifactBundle.name ?? null,
|
|
661
|
+
artifactId: writeAction.file.id,
|
|
662
|
+
artifactPath: normalizeArtifactPath(writeAction.file.path),
|
|
663
|
+
artifactType: writeAction.file.artifactType ?? null,
|
|
664
|
+
versionId: writeAction.version.id ?? null,
|
|
665
|
+
versionNumber: writeAction.version.versionNumber ?? writeAction.file.latestVersionNumber ?? null,
|
|
666
|
+
contentHash: writeAction.remoteHash,
|
|
667
|
+
destinationPath: writeAction.destinationPath,
|
|
668
|
+
agent: writeAction.adapterName,
|
|
669
|
+
lastPulledAt: new Date().toISOString(),
|
|
670
|
+
lastKnownLocalContentHash: hashContent(writeAction.content),
|
|
671
|
+
});
|
|
672
|
+
manifestChanged = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (prunedDestinations.size > 0) {
|
|
676
|
+
removeManifestEntriesByDestination(manifest, prunedDestinations);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (manifestChanged) {
|
|
680
|
+
await writeManifest(destinationRoot, manifest, fsImpl);
|
|
681
|
+
}
|
|
682
|
+
}
|