@kikimoradev/kcloud 0.16.0 → 0.18.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/dist/main.js +1591 -450
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -386,6 +386,12 @@ const dispatcherOf = (spec) => {
|
|
|
386
386
|
}
|
|
387
387
|
};
|
|
388
388
|
};
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region ../../packages/cli-kit/src/list.ts
|
|
391
|
+
const listOf = (items) => {
|
|
392
|
+
const last = items.at(-1);
|
|
393
|
+
return items.length < 2 || last === void 0 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${last}`;
|
|
394
|
+
};
|
|
389
395
|
const permissionsOutsideOwner = 63;
|
|
390
396
|
const isMissing = (error) => error instanceof Error && error.code === "ENOENT";
|
|
391
397
|
const ensurePrivateDirectory = async (path) => {
|
|
@@ -509,243 +515,6 @@ const spec = {
|
|
|
509
515
|
const defineCommand = commandFactoryOf(spec);
|
|
510
516
|
const defineGroup = (definition) => defineGroup$1(definition);
|
|
511
517
|
const runCli = dispatcherOf(spec);
|
|
512
|
-
const agentEventLevelSchema = z.enum([
|
|
513
|
-
"info",
|
|
514
|
-
"warn",
|
|
515
|
-
"error"
|
|
516
|
-
]);
|
|
517
|
-
const agentRoles = [
|
|
518
|
-
"monitor",
|
|
519
|
-
"executor",
|
|
520
|
-
"summarizer"
|
|
521
|
-
];
|
|
522
|
-
const agentRoleSchema = z.enum(agentRoles);
|
|
523
|
-
const agentLogStreams = ["stdout", "stderr"];
|
|
524
|
-
z.enum(agentLogStreams);
|
|
525
|
-
const platformStream = "platform";
|
|
526
|
-
const agentEventStreams = [...agentLogStreams, platformStream];
|
|
527
|
-
const agentEventStreamSchema = z.enum(agentEventStreams);
|
|
528
|
-
const authKindSchema = z.enum([
|
|
529
|
-
"apiKey",
|
|
530
|
-
"oauth",
|
|
531
|
-
"claude.ai",
|
|
532
|
-
"unknown"
|
|
533
|
-
]);
|
|
534
|
-
const agentControlStateSchema = z.enum([
|
|
535
|
-
"running",
|
|
536
|
-
"pausing",
|
|
537
|
-
"paused"
|
|
538
|
-
]);
|
|
539
|
-
z.enum([
|
|
540
|
-
"session",
|
|
541
|
-
"diagnostic",
|
|
542
|
-
"task"
|
|
543
|
-
]);
|
|
544
|
-
const maxCostUsd = 1e3;
|
|
545
|
-
const countSchema = z.int().min(0).max(2147483647);
|
|
546
|
-
const costUsdSchema = z.number().min(0).max(maxCostUsd);
|
|
547
|
-
const instantSchema$2 = z.iso.datetime();
|
|
548
|
-
const workerEnvelope = (event) => ({
|
|
549
|
-
ts: instantSchema$2,
|
|
550
|
-
level: agentEventLevelSchema,
|
|
551
|
-
event: z.literal(event)
|
|
552
|
-
});
|
|
553
|
-
const roleEnvelope = (event) => ({
|
|
554
|
-
...workerEnvelope(event),
|
|
555
|
-
agent: agentRoleSchema.optional()
|
|
556
|
-
});
|
|
557
|
-
const agentEventSchema = z.discriminatedUnion("event", [
|
|
558
|
-
z.looseObject({
|
|
559
|
-
...workerEnvelope("worker.started"),
|
|
560
|
-
version: z.string(),
|
|
561
|
-
claudeVersion: z.string().optional(),
|
|
562
|
-
nodeVersion: z.string(),
|
|
563
|
-
authKind: authKindSchema,
|
|
564
|
-
pid: countSchema,
|
|
565
|
-
projectDir: z.string(),
|
|
566
|
-
paused: z.boolean().optional()
|
|
567
|
-
}),
|
|
568
|
-
z.looseObject({
|
|
569
|
-
...workerEnvelope("worker.stopped"),
|
|
570
|
-
signal: z.string().optional(),
|
|
571
|
-
drained: z.boolean().optional(),
|
|
572
|
-
forced: z.boolean().optional()
|
|
573
|
-
}),
|
|
574
|
-
z.looseObject({
|
|
575
|
-
...workerEnvelope("worker.draining"),
|
|
576
|
-
reason: z.string(),
|
|
577
|
-
timeoutMs: countSchema.optional()
|
|
578
|
-
}),
|
|
579
|
-
z.looseObject({
|
|
580
|
-
...workerEnvelope("update.available"),
|
|
581
|
-
from: z.string(),
|
|
582
|
-
to: z.string(),
|
|
583
|
-
installError: z.string().optional()
|
|
584
|
-
}),
|
|
585
|
-
z.looseObject({
|
|
586
|
-
...workerEnvelope("update.installed"),
|
|
587
|
-
from: z.string(),
|
|
588
|
-
to: z.string()
|
|
589
|
-
}),
|
|
590
|
-
z.looseObject({
|
|
591
|
-
...roleEnvelope("control.changed"),
|
|
592
|
-
state: agentControlStateSchema
|
|
593
|
-
}),
|
|
594
|
-
z.looseObject({
|
|
595
|
-
...roleEnvelope("cycle.started"),
|
|
596
|
-
cycle: countSchema
|
|
597
|
-
}),
|
|
598
|
-
z.looseObject({
|
|
599
|
-
...roleEnvelope("cycle.finished"),
|
|
600
|
-
cycle: countSchema,
|
|
601
|
-
ok: z.boolean(),
|
|
602
|
-
durationMs: countSchema,
|
|
603
|
-
costUsd: costUsdSchema.optional(),
|
|
604
|
-
addedTasks: countSchema,
|
|
605
|
-
skippedDuplicates: countSchema,
|
|
606
|
-
sessionId: z.string().optional(),
|
|
607
|
-
error: z.string().optional()
|
|
608
|
-
}),
|
|
609
|
-
z.looseObject({
|
|
610
|
-
...roleEnvelope("monitor.offHours"),
|
|
611
|
-
resumeAt: instantSchema$2
|
|
612
|
-
}),
|
|
613
|
-
z.looseObject({
|
|
614
|
-
...roleEnvelope("monitor.limitWait"),
|
|
615
|
-
resumeAt: instantSchema$2
|
|
616
|
-
}),
|
|
617
|
-
z.looseObject({
|
|
618
|
-
...roleEnvelope("monitor.sleeping"),
|
|
619
|
-
nextCycleAt: instantSchema$2
|
|
620
|
-
}),
|
|
621
|
-
z.looseObject({
|
|
622
|
-
...roleEnvelope("monitor.authBlocked"),
|
|
623
|
-
reason: z.string()
|
|
624
|
-
}),
|
|
625
|
-
z.looseObject({
|
|
626
|
-
...roleEnvelope("monitor.admissionWait"),
|
|
627
|
-
ticket: z.string(),
|
|
628
|
-
since: instantSchema$2,
|
|
629
|
-
seq: countSchema
|
|
630
|
-
}),
|
|
631
|
-
z.looseObject({
|
|
632
|
-
...roleEnvelope("executor.admissionWait"),
|
|
633
|
-
ticket: z.string(),
|
|
634
|
-
since: instantSchema$2,
|
|
635
|
-
seq: countSchema,
|
|
636
|
-
taskId: z.string()
|
|
637
|
-
}),
|
|
638
|
-
z.looseObject({
|
|
639
|
-
...roleEnvelope("admission.granted"),
|
|
640
|
-
ticket: z.string(),
|
|
641
|
-
seq: countSchema
|
|
642
|
-
}),
|
|
643
|
-
z.looseObject({
|
|
644
|
-
...roleEnvelope("admission.released"),
|
|
645
|
-
ticket: z.string(),
|
|
646
|
-
seq: countSchema
|
|
647
|
-
}),
|
|
648
|
-
z.looseObject({
|
|
649
|
-
...roleEnvelope("admission.cancelled"),
|
|
650
|
-
ticket: z.string(),
|
|
651
|
-
seq: countSchema
|
|
652
|
-
}),
|
|
653
|
-
z.looseObject({
|
|
654
|
-
...roleEnvelope("task.started"),
|
|
655
|
-
taskId: z.string(),
|
|
656
|
-
title: z.string()
|
|
657
|
-
}),
|
|
658
|
-
z.looseObject({
|
|
659
|
-
...roleEnvelope("task.finished"),
|
|
660
|
-
taskId: z.string(),
|
|
661
|
-
title: z.string(),
|
|
662
|
-
ok: z.boolean(),
|
|
663
|
-
durationMs: countSchema,
|
|
664
|
-
costUsd: costUsdSchema.optional(),
|
|
665
|
-
numTurns: countSchema.optional(),
|
|
666
|
-
willRetry: z.boolean().optional(),
|
|
667
|
-
attempt: countSchema.optional(),
|
|
668
|
-
maxAttempts: countSchema.optional(),
|
|
669
|
-
sessionId: z.string().optional(),
|
|
670
|
-
error: z.string().optional()
|
|
671
|
-
}),
|
|
672
|
-
z.looseObject({
|
|
673
|
-
...roleEnvelope("task.retryScheduled"),
|
|
674
|
-
taskId: z.string(),
|
|
675
|
-
resumeAt: instantSchema$2
|
|
676
|
-
}),
|
|
677
|
-
z.looseObject(roleEnvelope("executor.waiting")),
|
|
678
|
-
z.looseObject({
|
|
679
|
-
...roleEnvelope("executor.limitWait"),
|
|
680
|
-
resumeAt: instantSchema$2
|
|
681
|
-
}),
|
|
682
|
-
z.looseObject({
|
|
683
|
-
...roleEnvelope("executor.authBlocked"),
|
|
684
|
-
reason: z.string()
|
|
685
|
-
}),
|
|
686
|
-
z.looseObject({
|
|
687
|
-
...roleEnvelope("summary.started"),
|
|
688
|
-
taskId: z.string()
|
|
689
|
-
}),
|
|
690
|
-
z.looseObject({
|
|
691
|
-
...roleEnvelope("summary.finished"),
|
|
692
|
-
taskId: z.string(),
|
|
693
|
-
ok: z.boolean(),
|
|
694
|
-
durationMs: countSchema,
|
|
695
|
-
costUsd: costUsdSchema.optional(),
|
|
696
|
-
sessionId: z.string().optional(),
|
|
697
|
-
error: z.string().optional()
|
|
698
|
-
}),
|
|
699
|
-
z.looseObject({
|
|
700
|
-
...roleEnvelope("session.init"),
|
|
701
|
-
model: z.string(),
|
|
702
|
-
sessionId: z.string(),
|
|
703
|
-
taskId: z.string().optional(),
|
|
704
|
-
cycle: countSchema.optional()
|
|
705
|
-
}),
|
|
706
|
-
z.looseObject({
|
|
707
|
-
...roleEnvelope("session.stderr"),
|
|
708
|
-
line: z.string()
|
|
709
|
-
}),
|
|
710
|
-
z.looseObject({
|
|
711
|
-
...roleEnvelope("session.procError"),
|
|
712
|
-
message: z.string()
|
|
713
|
-
}),
|
|
714
|
-
z.looseObject({
|
|
715
|
-
...roleEnvelope("session.killed"),
|
|
716
|
-
reason: z.string()
|
|
717
|
-
}),
|
|
718
|
-
z.looseObject({
|
|
719
|
-
...roleEnvelope("session.text"),
|
|
720
|
-
text: z.string()
|
|
721
|
-
}),
|
|
722
|
-
z.looseObject({
|
|
723
|
-
...roleEnvelope("session.tool"),
|
|
724
|
-
tool: z.string(),
|
|
725
|
-
detail: z.string()
|
|
726
|
-
}),
|
|
727
|
-
z.looseObject({
|
|
728
|
-
...roleEnvelope("session.toolError"),
|
|
729
|
-
output: z.string()
|
|
730
|
-
})
|
|
731
|
-
]);
|
|
732
|
-
z.looseObject({
|
|
733
|
-
ts: instantSchema$2,
|
|
734
|
-
level: agentEventLevelSchema,
|
|
735
|
-
agent: agentRoleSchema.optional(),
|
|
736
|
-
event: z.string().min(1).max(64)
|
|
737
|
-
});
|
|
738
|
-
agentEventSchema.options.map((option) => option.shape.event.value);
|
|
739
|
-
({
|
|
740
|
-
rawLog: "log.raw",
|
|
741
|
-
rejectedLog: "log.rejected",
|
|
742
|
-
throttledLog: "log.throttled",
|
|
743
|
-
stateChanged: "platform.stateChanged",
|
|
744
|
-
crashLooping: "platform.crashLooping",
|
|
745
|
-
provisioned: "platform.provisioned",
|
|
746
|
-
sandboxStarted: "platform.sandboxStarted",
|
|
747
|
-
sandboxStopped: "platform.sandboxStopped"
|
|
748
|
-
}).rawLog;
|
|
749
518
|
//#endregion
|
|
750
519
|
//#region ../../packages/contracts/src/patterns.ts
|
|
751
520
|
const slugPattern = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
@@ -1021,6 +790,23 @@ const errorCodes = Object.keys({
|
|
|
1021
790
|
"node.in_use": "conflict",
|
|
1022
791
|
"node.host_unknown": "rule_violation",
|
|
1023
792
|
"node.split_exceeds_host": "rule_violation",
|
|
793
|
+
"node.package_unavailable": "not_found",
|
|
794
|
+
"agent.backup_not_found": "not_found",
|
|
795
|
+
"agent.backup_unavailable": "rule_violation",
|
|
796
|
+
"agent.backup_without_export": "rule_violation",
|
|
797
|
+
"agent.backup_in_progress": "conflict",
|
|
798
|
+
"node.machine_missing": "rule_violation",
|
|
799
|
+
"node.operation_running": "conflict",
|
|
800
|
+
"node.operation_not_failed": "conflict",
|
|
801
|
+
"node.size_unavailable": "rule_violation",
|
|
802
|
+
"node.size_not_larger": "rule_violation",
|
|
803
|
+
"node.region_unavailable": "rule_violation",
|
|
804
|
+
"provider.not_found": "not_found",
|
|
805
|
+
"provider.name_taken": "conflict",
|
|
806
|
+
"provider.in_use": "conflict",
|
|
807
|
+
"provider.token_unauthorized": "rule_violation",
|
|
808
|
+
"provider.token_scopes_missing": "rule_violation",
|
|
809
|
+
"provider.unavailable": "rule_violation",
|
|
1024
810
|
"project.last_admin": "conflict",
|
|
1025
811
|
"agent.node_unassigned": "rule_violation",
|
|
1026
812
|
"stream.too_many": "rate_limited",
|
|
@@ -1057,6 +843,243 @@ const errorBodySchema = z.object({
|
|
|
1057
843
|
message: z.string()
|
|
1058
844
|
})).optional()
|
|
1059
845
|
}).meta({ id: "ErrorBody" });
|
|
846
|
+
const agentEventLevelSchema = z.enum([
|
|
847
|
+
"info",
|
|
848
|
+
"warn",
|
|
849
|
+
"error"
|
|
850
|
+
]);
|
|
851
|
+
const agentRoles = [
|
|
852
|
+
"monitor",
|
|
853
|
+
"executor",
|
|
854
|
+
"summarizer"
|
|
855
|
+
];
|
|
856
|
+
const agentRoleSchema = z.enum(agentRoles);
|
|
857
|
+
const agentLogStreams = ["stdout", "stderr"];
|
|
858
|
+
z.enum(agentLogStreams);
|
|
859
|
+
const platformStream = "platform";
|
|
860
|
+
const agentEventStreams = [...agentLogStreams, platformStream];
|
|
861
|
+
const agentEventStreamSchema = z.enum(agentEventStreams);
|
|
862
|
+
const authKindSchema = z.enum([
|
|
863
|
+
"apiKey",
|
|
864
|
+
"oauth",
|
|
865
|
+
"claude.ai",
|
|
866
|
+
"unknown"
|
|
867
|
+
]);
|
|
868
|
+
const agentControlStateSchema = z.enum([
|
|
869
|
+
"running",
|
|
870
|
+
"pausing",
|
|
871
|
+
"paused"
|
|
872
|
+
]);
|
|
873
|
+
z.enum([
|
|
874
|
+
"session",
|
|
875
|
+
"diagnostic",
|
|
876
|
+
"task"
|
|
877
|
+
]);
|
|
878
|
+
const maxCostUsd = 1e3;
|
|
879
|
+
const countSchema = z.int().min(0).max(2147483647);
|
|
880
|
+
const costUsdSchema = z.number().min(0).max(maxCostUsd);
|
|
881
|
+
const instantSchema$2 = z.iso.datetime();
|
|
882
|
+
const workerEnvelope = (event) => ({
|
|
883
|
+
ts: instantSchema$2,
|
|
884
|
+
level: agentEventLevelSchema,
|
|
885
|
+
event: z.literal(event)
|
|
886
|
+
});
|
|
887
|
+
const roleEnvelope = (event) => ({
|
|
888
|
+
...workerEnvelope(event),
|
|
889
|
+
agent: agentRoleSchema.optional()
|
|
890
|
+
});
|
|
891
|
+
const agentEventSchema = z.discriminatedUnion("event", [
|
|
892
|
+
z.looseObject({
|
|
893
|
+
...workerEnvelope("worker.started"),
|
|
894
|
+
version: z.string(),
|
|
895
|
+
claudeVersion: z.string().optional(),
|
|
896
|
+
nodeVersion: z.string(),
|
|
897
|
+
authKind: authKindSchema,
|
|
898
|
+
pid: countSchema,
|
|
899
|
+
projectDir: z.string(),
|
|
900
|
+
paused: z.boolean().optional()
|
|
901
|
+
}),
|
|
902
|
+
z.looseObject({
|
|
903
|
+
...workerEnvelope("worker.stopped"),
|
|
904
|
+
signal: z.string().optional(),
|
|
905
|
+
drained: z.boolean().optional(),
|
|
906
|
+
forced: z.boolean().optional()
|
|
907
|
+
}),
|
|
908
|
+
z.looseObject({
|
|
909
|
+
...workerEnvelope("worker.draining"),
|
|
910
|
+
reason: z.string(),
|
|
911
|
+
timeoutMs: countSchema.optional()
|
|
912
|
+
}),
|
|
913
|
+
z.looseObject({
|
|
914
|
+
...workerEnvelope("update.available"),
|
|
915
|
+
from: z.string(),
|
|
916
|
+
to: z.string(),
|
|
917
|
+
installError: z.string().optional()
|
|
918
|
+
}),
|
|
919
|
+
z.looseObject({
|
|
920
|
+
...workerEnvelope("update.installed"),
|
|
921
|
+
from: z.string(),
|
|
922
|
+
to: z.string()
|
|
923
|
+
}),
|
|
924
|
+
z.looseObject({
|
|
925
|
+
...roleEnvelope("control.changed"),
|
|
926
|
+
state: agentControlStateSchema
|
|
927
|
+
}),
|
|
928
|
+
z.looseObject({
|
|
929
|
+
...roleEnvelope("cycle.started"),
|
|
930
|
+
cycle: countSchema
|
|
931
|
+
}),
|
|
932
|
+
z.looseObject({
|
|
933
|
+
...roleEnvelope("cycle.finished"),
|
|
934
|
+
cycle: countSchema,
|
|
935
|
+
ok: z.boolean(),
|
|
936
|
+
durationMs: countSchema,
|
|
937
|
+
costUsd: costUsdSchema.optional(),
|
|
938
|
+
addedTasks: countSchema,
|
|
939
|
+
skippedDuplicates: countSchema,
|
|
940
|
+
sessionId: z.string().optional(),
|
|
941
|
+
error: z.string().optional()
|
|
942
|
+
}),
|
|
943
|
+
z.looseObject({
|
|
944
|
+
...roleEnvelope("monitor.offHours"),
|
|
945
|
+
resumeAt: instantSchema$2
|
|
946
|
+
}),
|
|
947
|
+
z.looseObject({
|
|
948
|
+
...roleEnvelope("monitor.limitWait"),
|
|
949
|
+
resumeAt: instantSchema$2
|
|
950
|
+
}),
|
|
951
|
+
z.looseObject({
|
|
952
|
+
...roleEnvelope("monitor.sleeping"),
|
|
953
|
+
nextCycleAt: instantSchema$2
|
|
954
|
+
}),
|
|
955
|
+
z.looseObject({
|
|
956
|
+
...roleEnvelope("monitor.authBlocked"),
|
|
957
|
+
reason: z.string()
|
|
958
|
+
}),
|
|
959
|
+
z.looseObject({
|
|
960
|
+
...roleEnvelope("monitor.admissionWait"),
|
|
961
|
+
ticket: z.string(),
|
|
962
|
+
since: instantSchema$2,
|
|
963
|
+
seq: countSchema
|
|
964
|
+
}),
|
|
965
|
+
z.looseObject({
|
|
966
|
+
...roleEnvelope("executor.admissionWait"),
|
|
967
|
+
ticket: z.string(),
|
|
968
|
+
since: instantSchema$2,
|
|
969
|
+
seq: countSchema,
|
|
970
|
+
taskId: z.string()
|
|
971
|
+
}),
|
|
972
|
+
z.looseObject({
|
|
973
|
+
...roleEnvelope("admission.granted"),
|
|
974
|
+
ticket: z.string(),
|
|
975
|
+
seq: countSchema
|
|
976
|
+
}),
|
|
977
|
+
z.looseObject({
|
|
978
|
+
...roleEnvelope("admission.released"),
|
|
979
|
+
ticket: z.string(),
|
|
980
|
+
seq: countSchema
|
|
981
|
+
}),
|
|
982
|
+
z.looseObject({
|
|
983
|
+
...roleEnvelope("admission.cancelled"),
|
|
984
|
+
ticket: z.string(),
|
|
985
|
+
seq: countSchema
|
|
986
|
+
}),
|
|
987
|
+
z.looseObject({
|
|
988
|
+
...roleEnvelope("task.started"),
|
|
989
|
+
taskId: z.string(),
|
|
990
|
+
title: z.string()
|
|
991
|
+
}),
|
|
992
|
+
z.looseObject({
|
|
993
|
+
...roleEnvelope("task.finished"),
|
|
994
|
+
taskId: z.string(),
|
|
995
|
+
title: z.string(),
|
|
996
|
+
ok: z.boolean(),
|
|
997
|
+
durationMs: countSchema,
|
|
998
|
+
costUsd: costUsdSchema.optional(),
|
|
999
|
+
numTurns: countSchema.optional(),
|
|
1000
|
+
willRetry: z.boolean().optional(),
|
|
1001
|
+
attempt: countSchema.optional(),
|
|
1002
|
+
maxAttempts: countSchema.optional(),
|
|
1003
|
+
sessionId: z.string().optional(),
|
|
1004
|
+
error: z.string().optional()
|
|
1005
|
+
}),
|
|
1006
|
+
z.looseObject({
|
|
1007
|
+
...roleEnvelope("task.retryScheduled"),
|
|
1008
|
+
taskId: z.string(),
|
|
1009
|
+
resumeAt: instantSchema$2
|
|
1010
|
+
}),
|
|
1011
|
+
z.looseObject(roleEnvelope("executor.waiting")),
|
|
1012
|
+
z.looseObject({
|
|
1013
|
+
...roleEnvelope("executor.limitWait"),
|
|
1014
|
+
resumeAt: instantSchema$2
|
|
1015
|
+
}),
|
|
1016
|
+
z.looseObject({
|
|
1017
|
+
...roleEnvelope("executor.authBlocked"),
|
|
1018
|
+
reason: z.string()
|
|
1019
|
+
}),
|
|
1020
|
+
z.looseObject({
|
|
1021
|
+
...roleEnvelope("summary.started"),
|
|
1022
|
+
taskId: z.string()
|
|
1023
|
+
}),
|
|
1024
|
+
z.looseObject({
|
|
1025
|
+
...roleEnvelope("summary.finished"),
|
|
1026
|
+
taskId: z.string(),
|
|
1027
|
+
ok: z.boolean(),
|
|
1028
|
+
durationMs: countSchema,
|
|
1029
|
+
costUsd: costUsdSchema.optional(),
|
|
1030
|
+
sessionId: z.string().optional(),
|
|
1031
|
+
error: z.string().optional()
|
|
1032
|
+
}),
|
|
1033
|
+
z.looseObject({
|
|
1034
|
+
...roleEnvelope("session.init"),
|
|
1035
|
+
model: z.string(),
|
|
1036
|
+
sessionId: z.string(),
|
|
1037
|
+
taskId: z.string().optional(),
|
|
1038
|
+
cycle: countSchema.optional()
|
|
1039
|
+
}),
|
|
1040
|
+
z.looseObject({
|
|
1041
|
+
...roleEnvelope("session.stderr"),
|
|
1042
|
+
line: z.string()
|
|
1043
|
+
}),
|
|
1044
|
+
z.looseObject({
|
|
1045
|
+
...roleEnvelope("session.procError"),
|
|
1046
|
+
message: z.string()
|
|
1047
|
+
}),
|
|
1048
|
+
z.looseObject({
|
|
1049
|
+
...roleEnvelope("session.killed"),
|
|
1050
|
+
reason: z.string()
|
|
1051
|
+
}),
|
|
1052
|
+
z.looseObject({
|
|
1053
|
+
...roleEnvelope("session.text"),
|
|
1054
|
+
text: z.string()
|
|
1055
|
+
}),
|
|
1056
|
+
z.looseObject({
|
|
1057
|
+
...roleEnvelope("session.tool"),
|
|
1058
|
+
tool: z.string(),
|
|
1059
|
+
detail: z.string()
|
|
1060
|
+
}),
|
|
1061
|
+
z.looseObject({
|
|
1062
|
+
...roleEnvelope("session.toolError"),
|
|
1063
|
+
output: z.string()
|
|
1064
|
+
})
|
|
1065
|
+
]);
|
|
1066
|
+
z.looseObject({
|
|
1067
|
+
ts: instantSchema$2,
|
|
1068
|
+
level: agentEventLevelSchema,
|
|
1069
|
+
agent: agentRoleSchema.optional(),
|
|
1070
|
+
event: z.string().min(1).max(64)
|
|
1071
|
+
});
|
|
1072
|
+
agentEventSchema.options.map((option) => option.shape.event.value);
|
|
1073
|
+
({
|
|
1074
|
+
rawLog: "log.raw",
|
|
1075
|
+
rejectedLog: "log.rejected",
|
|
1076
|
+
throttledLog: "log.throttled",
|
|
1077
|
+
stateChanged: "platform.stateChanged",
|
|
1078
|
+
crashLooping: "platform.crashLooping",
|
|
1079
|
+
provisioned: "platform.provisioned",
|
|
1080
|
+
sandboxStarted: "platform.sandboxStarted",
|
|
1081
|
+
sandboxStopped: "platform.sandboxStopped"
|
|
1082
|
+
}).rawLog;
|
|
1060
1083
|
const agentStateSchema = z.enum([
|
|
1061
1084
|
"created",
|
|
1062
1085
|
"provisioning",
|
|
@@ -1429,6 +1452,83 @@ pageSchema(z.object({
|
|
|
1429
1452
|
finishedAt: z.iso.datetime().nullable()
|
|
1430
1453
|
}).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
|
|
1431
1454
|
costQuerySchema.extend({ cursor: cursorSchema.optional() });
|
|
1455
|
+
const providerKindSchema = z.enum(["digitalocean"]);
|
|
1456
|
+
const providerAccountNameSchema = z.string().trim().min(1).max(64);
|
|
1457
|
+
const providerTokenSchema = z.string().trim().min(16).max(512).regex(/^[\x21-\x7e]+$/u);
|
|
1458
|
+
const providerRegionSlugSchema = z.string().trim().regex(/^[a-z]{3}[0-9]$/u);
|
|
1459
|
+
const providerSizeSlugSchema = z.string().trim().regex(/^[a-z0-9][a-z0-9-]{0,63}$/u);
|
|
1460
|
+
const providerTokenScopes = [
|
|
1461
|
+
"regions:read",
|
|
1462
|
+
"sizes:read",
|
|
1463
|
+
"image:read",
|
|
1464
|
+
"vpc:read",
|
|
1465
|
+
"actions:read",
|
|
1466
|
+
"droplet:create",
|
|
1467
|
+
"droplet:read",
|
|
1468
|
+
"droplet:update",
|
|
1469
|
+
"droplet:delete",
|
|
1470
|
+
"block_storage:create",
|
|
1471
|
+
"block_storage:read",
|
|
1472
|
+
"block_storage:delete",
|
|
1473
|
+
"block_storage_action:create",
|
|
1474
|
+
"firewall:create",
|
|
1475
|
+
"firewall:read",
|
|
1476
|
+
"firewall:delete",
|
|
1477
|
+
"tag:create",
|
|
1478
|
+
"tag:read",
|
|
1479
|
+
"tag:delete"
|
|
1480
|
+
];
|
|
1481
|
+
const providerTokenScopeSchema = z.enum(providerTokenScopes).meta({ id: "ProviderTokenScope" });
|
|
1482
|
+
const providerVerificationSchema = z.object({
|
|
1483
|
+
checkedAt: z.iso.datetime(),
|
|
1484
|
+
missingScopes: z.array(providerTokenScopeSchema),
|
|
1485
|
+
failure: z.string().nullable()
|
|
1486
|
+
}).meta({ id: "ProviderVerification" });
|
|
1487
|
+
const providerAccountSchema = z.object({
|
|
1488
|
+
id: idSchema,
|
|
1489
|
+
kind: providerKindSchema,
|
|
1490
|
+
name: z.string(),
|
|
1491
|
+
defaultRegion: z.string(),
|
|
1492
|
+
revision: z.int().nonnegative(),
|
|
1493
|
+
verification: providerVerificationSchema,
|
|
1494
|
+
nodeCount: z.int().nonnegative(),
|
|
1495
|
+
createdAt: z.iso.datetime(),
|
|
1496
|
+
updatedAt: z.iso.datetime()
|
|
1497
|
+
}).meta({ id: "ProviderAccount" });
|
|
1498
|
+
z.object({ items: z.array(providerAccountSchema) }).meta({ id: "ProviderAccountList" });
|
|
1499
|
+
const createProviderAccountBodySchema = z.object({
|
|
1500
|
+
kind: providerKindSchema,
|
|
1501
|
+
name: providerAccountNameSchema,
|
|
1502
|
+
token: providerTokenSchema,
|
|
1503
|
+
defaultRegion: providerRegionSlugSchema
|
|
1504
|
+
}).meta({ id: "CreateProviderAccountBody" });
|
|
1505
|
+
z.object({
|
|
1506
|
+
revision: z.int().nonnegative(),
|
|
1507
|
+
name: providerAccountNameSchema.optional(),
|
|
1508
|
+
token: providerTokenSchema.optional(),
|
|
1509
|
+
defaultRegion: providerRegionSlugSchema.optional()
|
|
1510
|
+
}).meta({ id: "UpdateProviderAccountBody" });
|
|
1511
|
+
const providerRegionSchema = z.object({
|
|
1512
|
+
slug: z.string(),
|
|
1513
|
+
name: z.string(),
|
|
1514
|
+
available: z.boolean()
|
|
1515
|
+
}).meta({ id: "ProviderRegion" });
|
|
1516
|
+
const providerSizeSchema = z.object({
|
|
1517
|
+
slug: z.string(),
|
|
1518
|
+
vcpus: z.int().positive(),
|
|
1519
|
+
memoryMb: z.int().positive(),
|
|
1520
|
+
diskGb: z.int().positive(),
|
|
1521
|
+
priceMonthly: z.number().nonnegative(),
|
|
1522
|
+
regions: z.array(z.string())
|
|
1523
|
+
}).meta({ id: "ProviderSize" });
|
|
1524
|
+
z.object({
|
|
1525
|
+
regions: z.array(providerRegionSchema),
|
|
1526
|
+
sizes: z.array(providerSizeSchema)
|
|
1527
|
+
}).meta({ id: "ProviderCatalog" });
|
|
1528
|
+
z.object({
|
|
1529
|
+
organizationId: idSchema,
|
|
1530
|
+
accountId: idSchema
|
|
1531
|
+
});
|
|
1432
1532
|
z.enum(["monitor", "executor"]);
|
|
1433
1533
|
const sessionSlotCountSchema = z.int().min(1).max(64);
|
|
1434
1534
|
const sessionQueueEntrySchema = z.object({
|
|
@@ -1471,7 +1571,8 @@ const nodeStateSchema = z.enum([
|
|
|
1471
1571
|
"online",
|
|
1472
1572
|
"offline"
|
|
1473
1573
|
]);
|
|
1474
|
-
const
|
|
1574
|
+
const nodeProviders = ["self_hosted", "digitalocean"];
|
|
1575
|
+
const nodeProviderSchema = z.enum(nodeProviders);
|
|
1475
1576
|
const nodeNameSchema = z.string().trim().regex(slugPattern);
|
|
1476
1577
|
const nodeHostSchema = z.object({
|
|
1477
1578
|
cpus: z.number().nonnegative(),
|
|
@@ -1546,6 +1647,52 @@ const nodePoolsSchema = z.object({
|
|
|
1546
1647
|
monitor: nodePoolSchema,
|
|
1547
1648
|
executor: nodePoolSchema
|
|
1548
1649
|
}).meta({ id: "NodePools" });
|
|
1650
|
+
const nodeVolumeGbSchema = z.int().min(10).max(4096);
|
|
1651
|
+
const nodeMachineSchema = z.object({
|
|
1652
|
+
account: z.object({
|
|
1653
|
+
id: idSchema,
|
|
1654
|
+
name: z.string().nullable()
|
|
1655
|
+
}),
|
|
1656
|
+
region: z.string(),
|
|
1657
|
+
size: z.string(),
|
|
1658
|
+
volumeGb: z.int().positive(),
|
|
1659
|
+
dropletId: z.int().positive().nullable(),
|
|
1660
|
+
publicIpv4: z.string().nullable()
|
|
1661
|
+
}).meta({ id: "NodeMachine" });
|
|
1662
|
+
const nodeOperationKindSchema = z.enum([
|
|
1663
|
+
"create",
|
|
1664
|
+
"resize",
|
|
1665
|
+
"delete"
|
|
1666
|
+
]);
|
|
1667
|
+
const nodeOperationStateSchema = z.enum(["running", "failed"]);
|
|
1668
|
+
const nodeOperationStepSchema = z.enum([
|
|
1669
|
+
"tag",
|
|
1670
|
+
"firewall",
|
|
1671
|
+
"volume",
|
|
1672
|
+
"droplet",
|
|
1673
|
+
"boot",
|
|
1674
|
+
"installing",
|
|
1675
|
+
"power_off",
|
|
1676
|
+
"resize_droplet",
|
|
1677
|
+
"resize_volume",
|
|
1678
|
+
"power_on",
|
|
1679
|
+
"delete_droplet",
|
|
1680
|
+
"delete_volume",
|
|
1681
|
+
"delete_firewall",
|
|
1682
|
+
"delete_tag"
|
|
1683
|
+
]);
|
|
1684
|
+
const nodeOperationSchema = z.object({
|
|
1685
|
+
kind: nodeOperationKindSchema,
|
|
1686
|
+
state: nodeOperationStateSchema,
|
|
1687
|
+
step: nodeOperationStepSchema.nullable(),
|
|
1688
|
+
error: z.string().nullable(),
|
|
1689
|
+
target: z.object({
|
|
1690
|
+
size: z.string(),
|
|
1691
|
+
volumeGb: z.int().positive()
|
|
1692
|
+
}).nullable(),
|
|
1693
|
+
startedAt: z.iso.datetime(),
|
|
1694
|
+
updatedAt: z.iso.datetime()
|
|
1695
|
+
}).meta({ id: "NodeOperation" });
|
|
1549
1696
|
const nodeSchema = z.object({
|
|
1550
1697
|
id: idSchema,
|
|
1551
1698
|
name: nodeNameSchema,
|
|
@@ -1561,9 +1708,34 @@ const nodeSchema = z.object({
|
|
|
1561
1708
|
agentCount: z.int().nonnegative(),
|
|
1562
1709
|
connectedAt: z.iso.datetime().nullable(),
|
|
1563
1710
|
lastSeenAt: z.iso.datetime().nullable(),
|
|
1711
|
+
offlineSince: z.iso.datetime().nullable(),
|
|
1564
1712
|
registrationExpiresAt: z.iso.datetime().nullable(),
|
|
1713
|
+
machine: nodeMachineSchema.nullable(),
|
|
1714
|
+
operation: nodeOperationSchema.nullable(),
|
|
1565
1715
|
createdAt: z.iso.datetime()
|
|
1566
1716
|
}).meta({ id: "Node" });
|
|
1717
|
+
const nodeUpdateSchema = z.object({
|
|
1718
|
+
state: z.enum([
|
|
1719
|
+
"current",
|
|
1720
|
+
"waiting",
|
|
1721
|
+
"updating",
|
|
1722
|
+
"failed",
|
|
1723
|
+
"unmanaged"
|
|
1724
|
+
]),
|
|
1725
|
+
target: z.string().nullable(),
|
|
1726
|
+
sessions: z.int().nonnegative(),
|
|
1727
|
+
since: z.iso.datetime().nullable(),
|
|
1728
|
+
last: z.object({
|
|
1729
|
+
outcome: z.enum([
|
|
1730
|
+
"updated",
|
|
1731
|
+
"rolledBack",
|
|
1732
|
+
"failed"
|
|
1733
|
+
]),
|
|
1734
|
+
version: z.string().nullable(),
|
|
1735
|
+
reason: z.string().nullable(),
|
|
1736
|
+
at: z.iso.datetime()
|
|
1737
|
+
}).nullable()
|
|
1738
|
+
}).meta({ id: "NodeUpdate" });
|
|
1567
1739
|
const nodeAgentSchema = z.object({
|
|
1568
1740
|
id: idSchema,
|
|
1569
1741
|
name: z.string(),
|
|
@@ -1609,7 +1781,8 @@ const nodeRuntimeSchema = z.object({
|
|
|
1609
1781
|
}).meta({ id: "NodeRuntime" });
|
|
1610
1782
|
nodeSchema.extend({
|
|
1611
1783
|
agents: z.array(nodeAgentSchema),
|
|
1612
|
-
runtime: nodeRuntimeSchema.nullable()
|
|
1784
|
+
runtime: nodeRuntimeSchema.nullable(),
|
|
1785
|
+
update: nodeUpdateSchema.nullable()
|
|
1613
1786
|
}).meta({ id: "NodeDetail" });
|
|
1614
1787
|
z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
|
|
1615
1788
|
z.object({ nodeId: idSchema });
|
|
@@ -1621,15 +1794,34 @@ z.object({
|
|
|
1621
1794
|
revision: z.int().nonnegative(),
|
|
1622
1795
|
split: nodeSplitSchema.nullable()
|
|
1623
1796
|
}).meta({ id: "UpdateNodeSplitBody" });
|
|
1624
|
-
const
|
|
1797
|
+
const nodeMachineRequestSchema = z.object({
|
|
1798
|
+
accountId: idSchema,
|
|
1799
|
+
region: providerRegionSlugSchema,
|
|
1800
|
+
size: providerSizeSlugSchema,
|
|
1801
|
+
volumeGb: nodeVolumeGbSchema
|
|
1802
|
+
}).meta({ id: "NodeMachineRequest" });
|
|
1803
|
+
const createNodeBodySchema = z.object({
|
|
1804
|
+
name: nodeNameSchema,
|
|
1805
|
+
machine: nodeMachineRequestSchema.optional()
|
|
1806
|
+
}).meta({ id: "CreateNodeBody" });
|
|
1807
|
+
const resizeNodeBodySchema = z.object({
|
|
1808
|
+
revision: z.int().nonnegative(),
|
|
1809
|
+
size: providerSizeSlugSchema,
|
|
1810
|
+
volumeGb: nodeVolumeGbSchema
|
|
1811
|
+
}).meta({ id: "ResizeNodeBody" });
|
|
1812
|
+
z.object({ revision: z.int().nonnegative() }).meta({ id: "RetryNodeOperationBody" });
|
|
1625
1813
|
const nodeRegistrationSchema = z.object({
|
|
1626
1814
|
token: z.string(),
|
|
1627
1815
|
expiresAt: z.iso.datetime()
|
|
1628
1816
|
}).meta({ id: "NodeRegistration" });
|
|
1817
|
+
z.object({
|
|
1818
|
+
node: nodeSchema,
|
|
1819
|
+
registration: nodeRegistrationSchema.nullable()
|
|
1820
|
+
}).meta({ id: "CreatedNode" });
|
|
1629
1821
|
z.object({
|
|
1630
1822
|
node: nodeSchema,
|
|
1631
1823
|
registration: nodeRegistrationSchema
|
|
1632
|
-
}).meta({ id: "
|
|
1824
|
+
}).meta({ id: "RenewedNodeRegistration" });
|
|
1633
1825
|
z.object({
|
|
1634
1826
|
token: z.string().min(1).max(256),
|
|
1635
1827
|
version: z.string().min(1).max(128),
|
|
@@ -1637,6 +1829,16 @@ z.object({
|
|
|
1637
1829
|
host: nodeHostSchema.nullable(),
|
|
1638
1830
|
capabilities: nodeCapabilitiesSchema
|
|
1639
1831
|
}).meta({ id: "RegisterNodeBody" });
|
|
1832
|
+
z.object({ token: z.string().min(1).max(256) }).meta({ id: "NodeInstallationBody" });
|
|
1833
|
+
z.object({
|
|
1834
|
+
platformVersion: z.string(),
|
|
1835
|
+
image: z.string().nullable(),
|
|
1836
|
+
registry: z.object({
|
|
1837
|
+
server: z.string(),
|
|
1838
|
+
username: z.string(),
|
|
1839
|
+
password: z.string()
|
|
1840
|
+
}).nullable()
|
|
1841
|
+
}).meta({ id: "NodeInstallation" });
|
|
1640
1842
|
z.object({
|
|
1641
1843
|
nodeId: idSchema,
|
|
1642
1844
|
credential: z.string()
|
|
@@ -1825,6 +2027,8 @@ const auditActionSchema = z.enum([
|
|
|
1825
2027
|
"agent.updated",
|
|
1826
2028
|
"agent.deleted",
|
|
1827
2029
|
"agent.action",
|
|
2030
|
+
"agent.backup_requested",
|
|
2031
|
+
"agent.restored",
|
|
1828
2032
|
"platform.settings_updated",
|
|
1829
2033
|
"runtime.sessions_updated",
|
|
1830
2034
|
"platform.upgrade_requested",
|
|
@@ -1844,6 +2048,18 @@ const auditActionSchema = z.enum([
|
|
|
1844
2048
|
"node.registration_renewed",
|
|
1845
2049
|
"node.deleted",
|
|
1846
2050
|
"node.split_changed",
|
|
2051
|
+
"node.resize_requested",
|
|
2052
|
+
"node.removal_requested",
|
|
2053
|
+
"node.operation_retried",
|
|
2054
|
+
"node.operation_failed",
|
|
2055
|
+
"node.machine_created",
|
|
2056
|
+
"node.machine_resized",
|
|
2057
|
+
"node.machine_removed",
|
|
2058
|
+
"provider_account.created",
|
|
2059
|
+
"provider_account.updated",
|
|
2060
|
+
"provider_account.token_replaced",
|
|
2061
|
+
"provider_account.verified",
|
|
2062
|
+
"provider_account.deleted",
|
|
1847
2063
|
"master_key.reencrypted",
|
|
1848
2064
|
"dead_letter.retried",
|
|
1849
2065
|
"dead_letter.discarded"
|
|
@@ -1864,7 +2080,8 @@ const auditTargetTypeSchema = z.enum([
|
|
|
1864
2080
|
"host_operation",
|
|
1865
2081
|
"volume",
|
|
1866
2082
|
"queue",
|
|
1867
|
-
"node"
|
|
2083
|
+
"node",
|
|
2084
|
+
"provider_account"
|
|
1868
2085
|
]);
|
|
1869
2086
|
const auditEntrySchema = z.object({
|
|
1870
2087
|
id: idSchema,
|
|
@@ -2464,6 +2681,43 @@ z.object({
|
|
|
2464
2681
|
z.object({ code: errorCodeSchema })
|
|
2465
2682
|
])
|
|
2466
2683
|
}).meta({ id: "AgentEventFrame" });
|
|
2684
|
+
//#endregion
|
|
2685
|
+
//#region ../../packages/contracts/src/agent-backups.ts
|
|
2686
|
+
const agentBackupTriggers = ["nightly", "manual"];
|
|
2687
|
+
const agentBackupSchema = z.object({
|
|
2688
|
+
id: idSchema,
|
|
2689
|
+
agentId: idSchema,
|
|
2690
|
+
nodeId: idSchema,
|
|
2691
|
+
nodeName: z.string().nullable(),
|
|
2692
|
+
trigger: z.enum(agentBackupTriggers),
|
|
2693
|
+
kikimoraVersion: z.string(),
|
|
2694
|
+
entries: z.int().nonnegative(),
|
|
2695
|
+
sizeBytes: z.int().nonnegative(),
|
|
2696
|
+
createdAt: z.iso.datetime(),
|
|
2697
|
+
expiresAt: z.iso.datetime()
|
|
2698
|
+
}).meta({ id: "AgentBackup" });
|
|
2699
|
+
z.object({
|
|
2700
|
+
items: z.array(agentBackupSchema),
|
|
2701
|
+
retentionDays: z.int().positive()
|
|
2702
|
+
}).meta({ id: "AgentBackupList" });
|
|
2703
|
+
z.object({ state: z.literal("requested") }).meta({ id: "AgentBackupRequested" });
|
|
2704
|
+
z.object({
|
|
2705
|
+
id: idSchema,
|
|
2706
|
+
backupId: idSchema
|
|
2707
|
+
});
|
|
2708
|
+
z.object({
|
|
2709
|
+
name: agentNameSchema,
|
|
2710
|
+
slug: slugSchema.optional(),
|
|
2711
|
+
nodeId: idSchema
|
|
2712
|
+
}).meta({ id: "RestoreAgentBody" });
|
|
2713
|
+
z.object({ agentId: idSchema }).meta({ id: "RestoredAgent" });
|
|
2714
|
+
z.object({
|
|
2715
|
+
agentId: idSchema,
|
|
2716
|
+
trigger: z.enum(agentBackupTriggers),
|
|
2717
|
+
kikimoraVersion: z.string().min(1).max(64),
|
|
2718
|
+
entries: z.coerce.number().int().nonnegative()
|
|
2719
|
+
});
|
|
2720
|
+
z.object({ backupId: idSchema });
|
|
2467
2721
|
[
|
|
2468
2722
|
"application/vnd.oci.image.index.v1+json",
|
|
2469
2723
|
"application/vnd.oci.image.manifest.v1+json",
|
|
@@ -2795,7 +3049,8 @@ const masterKeyVersionSchema = z.object({
|
|
|
2795
3049
|
secrets: z.object({
|
|
2796
3050
|
credentials: secretCountSchema,
|
|
2797
3051
|
toolSecrets: secretCountSchema,
|
|
2798
|
-
platformSecrets: secretCountSchema
|
|
3052
|
+
platformSecrets: secretCountSchema,
|
|
3053
|
+
providerAccounts: secretCountSchema.default(0)
|
|
2799
3054
|
}),
|
|
2800
3055
|
backups: secretCountSchema,
|
|
2801
3056
|
newestBackupExpiresAt: instantSchema$1.nullable()
|
|
@@ -3644,6 +3899,254 @@ const createWhoamiCommand = (adaptersOf) => defineCommand({
|
|
|
3644
3899
|
}
|
|
3645
3900
|
});
|
|
3646
3901
|
//#endregion
|
|
3902
|
+
//#region src/render/body.ts
|
|
3903
|
+
const isTerminal$1 = (stream) => stream.isTTY === true;
|
|
3904
|
+
const pipedText$1 = async (stream) => {
|
|
3905
|
+
const chunks = [];
|
|
3906
|
+
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
3907
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3908
|
+
};
|
|
3909
|
+
const fileOptions = { file: { type: "string" } };
|
|
3910
|
+
const fileUsage = (what) => [["--file <path>", `read ${what} from a JSON file, or from stdin with -`]];
|
|
3911
|
+
const documentOf = async (run, file) => {
|
|
3912
|
+
const text = file === "-" ? isTerminal$1(run.context.io.stdin) ? "" : await pipedText$1(run.context.io.stdin) : await readFileOrRefuse(resolve(run.context.workingDirectory, file));
|
|
3913
|
+
if (text.trim() === "") throw new UsageError(file === "-" ? "Nothing is piped on stdin." : `${file} is empty.`);
|
|
3914
|
+
try {
|
|
3915
|
+
return JSON.parse(text);
|
|
3916
|
+
} catch {
|
|
3917
|
+
throw new CliError(`${file === "-" ? "stdin" : file} is not JSON.`);
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
const readFileOrRefuse = async (path) => {
|
|
3921
|
+
try {
|
|
3922
|
+
return await readFile(path, "utf8");
|
|
3923
|
+
} catch {
|
|
3924
|
+
throw new CliError(`${path} could not be read.`);
|
|
3925
|
+
}
|
|
3926
|
+
};
|
|
3927
|
+
const validated = (schema, document, what) => {
|
|
3928
|
+
const parsed = schema.safeParse(document);
|
|
3929
|
+
if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
|
|
3930
|
+
};
|
|
3931
|
+
//#endregion
|
|
3932
|
+
//#region src/commands/providers/shared.ts
|
|
3933
|
+
const providerHints = {
|
|
3934
|
+
...organizationHints,
|
|
3935
|
+
"auth.forbidden": "Changing a provider account takes an admin of the organization.",
|
|
3936
|
+
"provider.not_found": "kcloud providers list shows the provider accounts.",
|
|
3937
|
+
"provider.name_taken": "Another provider account of the organization has that name.",
|
|
3938
|
+
"provider.in_use": "Delete the nodes created with it first; kcloud nodes list shows the provider of each node.",
|
|
3939
|
+
"provider.token_unauthorized": "DigitalOcean refused the token. Create a new one under API, Tokens in its control panel.",
|
|
3940
|
+
"concurrency.conflict": "Someone changed the account meanwhile; run the command again."
|
|
3941
|
+
};
|
|
3942
|
+
const reasonOf$1 = (error) => {
|
|
3943
|
+
const reason = error.details.reason;
|
|
3944
|
+
return typeof reason === "string" && reason !== "" ? `: ${reason}` : "";
|
|
3945
|
+
};
|
|
3946
|
+
const missingScopesOf = (error) => {
|
|
3947
|
+
const scopes = error.details.missingScopes;
|
|
3948
|
+
return typeof scopes === "string" && scopes !== "" ? scopes : "some scopes";
|
|
3949
|
+
};
|
|
3950
|
+
const apiErrorOf$1 = (error) => {
|
|
3951
|
+
if (error instanceof ApiError) return error;
|
|
3952
|
+
return error instanceof Error ? apiErrorOf$1(error.cause) : null;
|
|
3953
|
+
};
|
|
3954
|
+
const explained = (error) => {
|
|
3955
|
+
const failure = apiErrorOf$1(error);
|
|
3956
|
+
switch (failure?.code) {
|
|
3957
|
+
case "provider.token_scopes_missing": return new CliError(failure.message, `The token lacks ${missingScopesOf(failure)}. kcloud providers add --help lists every scope it needs.`, { cause: failure });
|
|
3958
|
+
case "provider.unavailable": return new CliError(failure.message, `DigitalOcean did not answer as expected${reasonOf$1(failure)}. Try again in a moment.`, { cause: failure });
|
|
3959
|
+
default: return error;
|
|
3960
|
+
}
|
|
3961
|
+
};
|
|
3962
|
+
const providerDataOf = (action, answer, hints = providerHints) => {
|
|
3963
|
+
try {
|
|
3964
|
+
return dataOf(action, answer, hints);
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
throw explained(error);
|
|
3967
|
+
}
|
|
3968
|
+
};
|
|
3969
|
+
const providerDoneWith = (action, answer, hints = providerHints) => {
|
|
3970
|
+
try {
|
|
3971
|
+
doneWith(action, answer, hints);
|
|
3972
|
+
} catch (error) {
|
|
3973
|
+
throw explained(error);
|
|
3974
|
+
}
|
|
3975
|
+
};
|
|
3976
|
+
const providerAccountsOf = async (api, organizationId) => providerDataOf("list the provider accounts", await api.GET("/organizations/{organizationId}/provider-accounts", { params: { path: { organizationId } } })).items;
|
|
3977
|
+
const providerAccountOf = async (api, organizationId, accountId) => providerDataOf("describe the provider account", await api.GET("/organizations/{organizationId}/provider-accounts/{accountId}", { params: { path: {
|
|
3978
|
+
organizationId,
|
|
3979
|
+
accountId
|
|
3980
|
+
} } }));
|
|
3981
|
+
const resolveProviderAccount = async (scope, reference) => {
|
|
3982
|
+
const organization = await scope.organization();
|
|
3983
|
+
if (isId(reference)) return providerAccountOf(scope.api, organization.id, reference);
|
|
3984
|
+
const accounts = await providerAccountsOf(scope.api, organization.id);
|
|
3985
|
+
const found = accounts.find((account) => account.name === reference);
|
|
3986
|
+
if (found === void 0) throw new CliError(`There is no provider account ${reference} in ${organization.slug}.`, accounts.length === 0 ? "kcloud providers add <name> --region <slug> adds one." : `kcloud providers list shows ${accounts.map(({ name }) => name).join(", ")}.`);
|
|
3987
|
+
return found;
|
|
3988
|
+
};
|
|
3989
|
+
const verifiedAccount = async (api, organizationId, accountId) => providerDataOf("verify the provider account", await api.POST("/organizations/{organizationId}/provider-accounts/{accountId}/verification", { params: { path: {
|
|
3990
|
+
organizationId,
|
|
3991
|
+
accountId
|
|
3992
|
+
} } }));
|
|
3993
|
+
const providerCatalogOf = async (api, organizationId, accountId) => providerDataOf("read the regions and sizes of the provider", await api.GET("/organizations/{organizationId}/provider-accounts/{accountId}/catalog", { params: { path: {
|
|
3994
|
+
organizationId,
|
|
3995
|
+
accountId
|
|
3996
|
+
} } }));
|
|
3997
|
+
const catalogInRegion = (catalog, region) => {
|
|
3998
|
+
if (!catalog.regions.some(({ slug }) => slug === region)) throw new CliError(`The account offers no region ${region} for a node.`, `Its regions are ${catalog.regions.map(({ slug }) => slug).join(", ")}.`);
|
|
3999
|
+
return {
|
|
4000
|
+
region,
|
|
4001
|
+
regions: catalog.regions,
|
|
4002
|
+
sizes: catalog.sizes.filter(({ regions }) => regions.includes(region))
|
|
4003
|
+
};
|
|
4004
|
+
};
|
|
4005
|
+
const deleteProviderAccount = async (api, organizationId, accountId) => {
|
|
4006
|
+
providerDoneWith("delete the provider account", await api.DELETE("/organizations/{organizationId}/provider-accounts/{accountId}", { params: { path: {
|
|
4007
|
+
organizationId,
|
|
4008
|
+
accountId
|
|
4009
|
+
} } }));
|
|
4010
|
+
};
|
|
4011
|
+
const verificationText = ({ failure, missingScopes }) => {
|
|
4012
|
+
if (failure === "unauthorized") return "token refused";
|
|
4013
|
+
if (failure !== null) return `not checked: ${failure}`;
|
|
4014
|
+
return missingScopes.length === 0 ? "ok" : `missing ${missingScopes.length} of ${providerTokenScopes.length} scopes`;
|
|
4015
|
+
};
|
|
4016
|
+
const scopeLineWidth = 64;
|
|
4017
|
+
const scopeLines = () => providerTokenScopes.reduce((lines, scope) => {
|
|
4018
|
+
const last = lines.at(-1);
|
|
4019
|
+
if (last !== void 0 && last.length + scope.length + 2 <= scopeLineWidth) {
|
|
4020
|
+
lines[lines.length - 1] = `${last}, ${scope}`;
|
|
4021
|
+
return lines;
|
|
4022
|
+
}
|
|
4023
|
+
return [...lines, scope];
|
|
4024
|
+
}, []);
|
|
4025
|
+
//#endregion
|
|
4026
|
+
//#region src/commands/nodes/shared.ts
|
|
4027
|
+
const nodeHints = {
|
|
4028
|
+
...organizationHints,
|
|
4029
|
+
"node.not_found": "kcloud nodes list shows the nodes of the organization.",
|
|
4030
|
+
"node.name_taken": "Another node of the organization has that name.",
|
|
4031
|
+
"node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
|
|
4032
|
+
"node.registration_used": "The node has joined and holds its own credential; it needs no token.",
|
|
4033
|
+
"node.machine_missing": "The node runs on a host of its own, which the platform does not resize.",
|
|
4034
|
+
"node.operation_running": "The platform is still working on the node; kcloud nodes show <node> shows the step.",
|
|
4035
|
+
"node.operation_not_failed": "Only a failed operation can be retried; kcloud nodes show <node> shows the operation.",
|
|
4036
|
+
"node.region_unavailable": "kcloud providers catalog <account> lists the regions a node can go to.",
|
|
4037
|
+
"node.size_unavailable": "kcloud providers catalog <account> --region <slug> lists the sizes of a region.",
|
|
4038
|
+
"node.size_not_larger": "A node only grows: name a size with more CPUs or memory, or a larger volume.",
|
|
4039
|
+
"concurrency.conflict": "Someone changed the node meanwhile; run the command again."
|
|
4040
|
+
};
|
|
4041
|
+
const machineHints = {
|
|
4042
|
+
...providerHints,
|
|
4043
|
+
...nodeHints,
|
|
4044
|
+
"auth.forbidden": "Creating, resizing and deleting nodes take an admin of the organization."
|
|
4045
|
+
};
|
|
4046
|
+
const splitHints = {
|
|
4047
|
+
...nodeHints,
|
|
4048
|
+
"auth.forbidden": "Splitting a node takes an admin of its organization.",
|
|
4049
|
+
"node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
|
|
4050
|
+
"node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host."
|
|
4051
|
+
};
|
|
4052
|
+
const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
|
|
4053
|
+
const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
|
|
4054
|
+
const nodeIn = (nodes, reference, organization) => {
|
|
4055
|
+
const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
|
|
4056
|
+
if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf(nodes)}.`);
|
|
4057
|
+
return found;
|
|
4058
|
+
};
|
|
4059
|
+
const resolveNode = async (scope, reference) => {
|
|
4060
|
+
const organization = await scope.organization();
|
|
4061
|
+
if (isId(reference)) return {
|
|
4062
|
+
organizationId: organization.id,
|
|
4063
|
+
nodeId: reference
|
|
4064
|
+
};
|
|
4065
|
+
const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
|
|
4066
|
+
return {
|
|
4067
|
+
organizationId: organization.id,
|
|
4068
|
+
nodeId: node.id
|
|
4069
|
+
};
|
|
4070
|
+
};
|
|
4071
|
+
const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
|
|
4072
|
+
organizationId,
|
|
4073
|
+
nodeId
|
|
4074
|
+
} } }), nodeHints);
|
|
4075
|
+
const createdNode = async (api, organizationId, body) => {
|
|
4076
|
+
validated(createNodeBodySchema, body, "a node");
|
|
4077
|
+
return providerDataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
|
|
4078
|
+
params: { path: { organizationId } },
|
|
4079
|
+
body
|
|
4080
|
+
}), machineHints);
|
|
4081
|
+
};
|
|
4082
|
+
const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
|
|
4083
|
+
organizationId,
|
|
4084
|
+
nodeId
|
|
4085
|
+
} } }), nodeHints);
|
|
4086
|
+
const deleteNode = async (api, { organizationId, nodeId }) => {
|
|
4087
|
+
const answer = await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
|
|
4088
|
+
organizationId,
|
|
4089
|
+
nodeId
|
|
4090
|
+
} } });
|
|
4091
|
+
providerDoneWith("delete the node", answer, machineHints);
|
|
4092
|
+
return answer.response.status === 202 ? answer.data ?? null : null;
|
|
4093
|
+
};
|
|
4094
|
+
const resizeTargetOf = (node, { size, volumeGb }) => {
|
|
4095
|
+
const { machine } = node;
|
|
4096
|
+
if (machine === null) throw new CliError(`${node.name} runs on a host of its own, so the platform cannot resize it.`, "Only a node created with --provider digitalocean can be resized.");
|
|
4097
|
+
const target = {
|
|
4098
|
+
size: size ?? machine.size,
|
|
4099
|
+
volumeGb: volumeGb ?? machine.volumeGb
|
|
4100
|
+
};
|
|
4101
|
+
if (target.volumeGb < machine.volumeGb) throw new CliError(`The volume of ${node.name} holds ${machine.volumeGb} GB, and a volume cannot shrink.`);
|
|
4102
|
+
if (target.size === machine.size && target.volumeGb === machine.volumeGb) throw new CliError(`${node.name} is ${machine.size} with a volume of ${machine.volumeGb} GB already.`, "Name a larger size, a larger volume or both.");
|
|
4103
|
+
return target;
|
|
4104
|
+
};
|
|
4105
|
+
const resizedNode = async (api, { organizationId, nodeId }, node, change) => {
|
|
4106
|
+
const body = {
|
|
4107
|
+
revision: node.revision,
|
|
4108
|
+
...resizeTargetOf(node, change)
|
|
4109
|
+
};
|
|
4110
|
+
validated(resizeNodeBodySchema, body, "a size of a node");
|
|
4111
|
+
return providerDataOf("resize the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/resize", {
|
|
4112
|
+
params: { path: {
|
|
4113
|
+
organizationId,
|
|
4114
|
+
nodeId
|
|
4115
|
+
} },
|
|
4116
|
+
body
|
|
4117
|
+
}), machineHints);
|
|
4118
|
+
};
|
|
4119
|
+
const retriedOperation = async (api, { organizationId, nodeId }, revision) => providerDataOf("retry the operation of the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/operation/retry", {
|
|
4120
|
+
params: { path: {
|
|
4121
|
+
organizationId,
|
|
4122
|
+
nodeId
|
|
4123
|
+
} },
|
|
4124
|
+
body: { revision }
|
|
4125
|
+
}), machineHints);
|
|
4126
|
+
const splitNode = async (api, { organizationId, nodeId }, revision, split) => dataOf(split === null ? "clear the split of the node" : "split the node", await api.PUT("/organizations/{organizationId}/nodes/{nodeId}/split", {
|
|
4127
|
+
params: { path: {
|
|
4128
|
+
organizationId,
|
|
4129
|
+
nodeId
|
|
4130
|
+
} },
|
|
4131
|
+
body: {
|
|
4132
|
+
revision,
|
|
4133
|
+
split
|
|
4134
|
+
}
|
|
4135
|
+
}), splitHints);
|
|
4136
|
+
const nodeUsage = [["", "a node is named by its name or its id"]];
|
|
4137
|
+
//#endregion
|
|
4138
|
+
//#region src/commands/projects/shared.ts
|
|
4139
|
+
const defaultProjectColor = "#7c86ff";
|
|
4140
|
+
const projectHint = {
|
|
4141
|
+
"project.not_found": "kcloud projects list shows the projects you are a member of.",
|
|
4142
|
+
"project.slug_taken": "Another project of the organization has that slug.",
|
|
4143
|
+
"project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
|
|
4144
|
+
"project.not_empty": "Delete its agents, credentials, repositories and tools first.",
|
|
4145
|
+
"project.member_not_found": "kcloud projects members list <project> shows them.",
|
|
4146
|
+
"project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
|
|
4147
|
+
};
|
|
4148
|
+
const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
|
|
4149
|
+
//#endregion
|
|
3647
4150
|
//#region src/commands/run.ts
|
|
3648
4151
|
const runOf = async (adaptersOf, { settings, context }) => {
|
|
3649
4152
|
const adapters = adaptersOf(context);
|
|
@@ -3682,6 +4185,161 @@ const agentFrom = async (run, reference, project) => {
|
|
|
3682
4185
|
return resolveAgentId(run, reference, project);
|
|
3683
4186
|
};
|
|
3684
4187
|
//#endregion
|
|
4188
|
+
//#region src/commands/agents/backups.ts
|
|
4189
|
+
const backupsListHint = "kcloud agents backups list <agent> shows the backups of the agent and their ids.";
|
|
4190
|
+
const backupHints = {
|
|
4191
|
+
...agentHints,
|
|
4192
|
+
"agent.backup_unavailable": "Backups need the Spaces of the platform and a node that can make them. An administrator of the platform configures the Spaces.",
|
|
4193
|
+
"agent.backup_without_export": "The Kikimora release of the agent cannot export its memory. Move it to 0.12.0 or newer with kcloud agents set, then kcloud agents update.",
|
|
4194
|
+
"agent.backup_in_progress": "A backup of the agent is running already. kcloud agents backups list <agent> shows it once the node has sent it.",
|
|
4195
|
+
"node.offline": "The node of the agent is not connected, so it cannot make a backup. kcloud nodes show <node> tells when it was last seen."
|
|
4196
|
+
};
|
|
4197
|
+
const restoreHints = {
|
|
4198
|
+
...agentHints,
|
|
4199
|
+
"node.not_found": "kcloud nodes list shows the nodes of the organization.",
|
|
4200
|
+
"node.offline": "The node is not connected. kcloud nodes show <node> tells when it was last seen.",
|
|
4201
|
+
"agent.backup_not_found": `The backup does not exist or has expired. ${backupsListHint}`,
|
|
4202
|
+
"agent.slug_taken": "Another agent of the project has that slug. Pass another with --slug.",
|
|
4203
|
+
"agent.invalid_slug": "A slug holds lowercase letters, digits and hyphens."
|
|
4204
|
+
};
|
|
4205
|
+
const backupsOf = async (api, agentId) => dataOf("list the backups of the agent", await api.GET("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
|
|
4206
|
+
const requestedBackup = async (api, agentId) => dataOf("back the agent up", await api.POST("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
|
|
4207
|
+
const nodeIdFor = async (api, agentId, node) => {
|
|
4208
|
+
if (isId(node)) return node;
|
|
4209
|
+
const agent = dataOf("describe the agent", await api.GET("/agents/{id}", { params: { path: { id: agentId } } }), agentHints);
|
|
4210
|
+
const project = await projectOf(api, agent.projectId);
|
|
4211
|
+
const nodes = await nodesOf(api, project.organizationId);
|
|
4212
|
+
return nodeIn(nodes, node, `the organization of ${project.slug}`).id;
|
|
4213
|
+
};
|
|
4214
|
+
const restoredAgent = async (api, agentId, { backupId, name, node, slug }) => {
|
|
4215
|
+
const nodeId = await nodeIdFor(api, agentId, node);
|
|
4216
|
+
return dataOf("restore the agent from the backup", await api.POST("/agents/{id}/backups/{backupId}/restore", {
|
|
4217
|
+
params: { path: {
|
|
4218
|
+
id: agentId,
|
|
4219
|
+
backupId
|
|
4220
|
+
} },
|
|
4221
|
+
body: {
|
|
4222
|
+
name,
|
|
4223
|
+
nodeId,
|
|
4224
|
+
...slug === void 0 ? {} : { slug }
|
|
4225
|
+
}
|
|
4226
|
+
}), restoreHints);
|
|
4227
|
+
};
|
|
4228
|
+
const keptFor = (days) => `A backup is kept for ${String(days)} ${days === 1 ? "day" : "days"}.`;
|
|
4229
|
+
const backupsTable = ({ items, retentionDays }) => items.length === 0 ? `The agent has no backup yet. ${keptFor(retentionDays)}` : [renderTable([
|
|
4230
|
+
{ header: "CREATED" },
|
|
4231
|
+
{ header: "TRIGGER" },
|
|
4232
|
+
{ header: "NODE" },
|
|
4233
|
+
{ header: "KIKIMORA" },
|
|
4234
|
+
{
|
|
4235
|
+
header: "ENTRIES",
|
|
4236
|
+
align: "right"
|
|
4237
|
+
},
|
|
4238
|
+
{
|
|
4239
|
+
header: "SIZE",
|
|
4240
|
+
align: "right"
|
|
4241
|
+
},
|
|
4242
|
+
{ header: "EXPIRES" },
|
|
4243
|
+
{ header: "ID" }
|
|
4244
|
+
], items.map((backup) => [
|
|
4245
|
+
backup.createdAt,
|
|
4246
|
+
backup.trigger,
|
|
4247
|
+
backup.nodeName ?? backup.nodeId,
|
|
4248
|
+
backup.kikimoraVersion,
|
|
4249
|
+
String(backup.entries),
|
|
4250
|
+
bytesOf(backup.sizeBytes),
|
|
4251
|
+
backup.expiresAt,
|
|
4252
|
+
backup.id
|
|
4253
|
+
])), keptFor(retentionDays)].join("\n");
|
|
4254
|
+
const restoreRequestOf = ({ backup, name, node, slug }) => {
|
|
4255
|
+
if (backup === void 0 || name === void 0 || node === void 0 || node.trim() === "") {
|
|
4256
|
+
const missing = [
|
|
4257
|
+
...backup === void 0 ? ["--backup <id>"] : [],
|
|
4258
|
+
...name === void 0 ? ["--name <name>"] : [],
|
|
4259
|
+
...node === void 0 || node.trim() === "" ? ["--node <node>"] : []
|
|
4260
|
+
];
|
|
4261
|
+
throw new UsageError(`agents restore needs ${listOf(missing)}.`, backupsListHint);
|
|
4262
|
+
}
|
|
4263
|
+
if (!isId(backup)) throw new UsageError(`--backup ${backup} is not the id of a backup.`, backupsListHint);
|
|
4264
|
+
const named = agentNameSchema.safeParse(name);
|
|
4265
|
+
if (!named.success) throw new UsageError(`--name needs from 1 to ${String(80)} characters.`);
|
|
4266
|
+
if (slug !== void 0 && !slugSchema.safeParse(slug).success) throw new UsageError(`--slug ${slug} is not a slug.`, "A slug holds lowercase letters, digits and hyphens, at most 40 of them.");
|
|
4267
|
+
return {
|
|
4268
|
+
backupId: backup,
|
|
4269
|
+
name: named.data,
|
|
4270
|
+
node: node.trim(),
|
|
4271
|
+
slug
|
|
4272
|
+
};
|
|
4273
|
+
};
|
|
4274
|
+
const backupsCommand = (adaptersOf) => defineGroup({
|
|
4275
|
+
name: "backups",
|
|
4276
|
+
summary: "The backups of the memory of an agent.",
|
|
4277
|
+
usage: "agents backups <command>",
|
|
4278
|
+
subcommands: [defineCommand({
|
|
4279
|
+
name: "list",
|
|
4280
|
+
summary: "List the backups of an agent, the newest first.",
|
|
4281
|
+
usage: "agents backups list <agent> [--project <name>]",
|
|
4282
|
+
optionsUsage: agentUsage,
|
|
4283
|
+
options: agentOption,
|
|
4284
|
+
positionals: {
|
|
4285
|
+
min: 1,
|
|
4286
|
+
max: 1
|
|
4287
|
+
},
|
|
4288
|
+
run: async (invocation) => {
|
|
4289
|
+
const run = await runOf(adaptersOf, invocation);
|
|
4290
|
+
const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
|
|
4291
|
+
return printed(run, await backupsOf(run.api, agentId), backupsTable);
|
|
4292
|
+
}
|
|
4293
|
+
}), defineCommand({
|
|
4294
|
+
name: "create",
|
|
4295
|
+
summary: "Ask the node of an agent for a backup now.",
|
|
4296
|
+
usage: "agents backups create <agent> [--project <name>]",
|
|
4297
|
+
optionsUsage: agentUsage,
|
|
4298
|
+
options: agentOption,
|
|
4299
|
+
positionals: {
|
|
4300
|
+
min: 1,
|
|
4301
|
+
max: 1
|
|
4302
|
+
},
|
|
4303
|
+
run: async (invocation) => {
|
|
4304
|
+
const run = await runOf(adaptersOf, invocation);
|
|
4305
|
+
const reference = invocation.positionals[0] ?? "";
|
|
4306
|
+
const agentId = await agentFrom(run, reference, invocation.values.project);
|
|
4307
|
+
return printed(run, await requestedBackup(run.api, agentId), () => `Backup of ${reference} requested. kcloud agents backups list ${reference} shows it once the node has sent it.`);
|
|
4308
|
+
}
|
|
4309
|
+
})]
|
|
4310
|
+
});
|
|
4311
|
+
const restoreCommand = (adaptersOf) => defineCommand({
|
|
4312
|
+
name: "restore",
|
|
4313
|
+
summary: "Create a new agent from a backup of an agent.",
|
|
4314
|
+
usage: "agents restore <agent> --backup <id> --name <name> --node <node> [--slug <slug>] [--project <name>]",
|
|
4315
|
+
optionsUsage: [
|
|
4316
|
+
...agentUsage,
|
|
4317
|
+
["--backup <id>", "the backup, by its id from kcloud agents backups list"],
|
|
4318
|
+
["--name <name>", "the name of the new agent"],
|
|
4319
|
+
["--node <node>", "the node the new agent runs on, by name or id"],
|
|
4320
|
+
["--slug <slug>", "the slug of the new agent (default: one made from its name)"],
|
|
4321
|
+
["", "the new agent joins the project of the agent with its configuration and the memory of the backup"]
|
|
4322
|
+
],
|
|
4323
|
+
options: {
|
|
4324
|
+
...agentOption,
|
|
4325
|
+
backup: { type: "string" },
|
|
4326
|
+
name: { type: "string" },
|
|
4327
|
+
node: { type: "string" },
|
|
4328
|
+
slug: { type: "string" }
|
|
4329
|
+
},
|
|
4330
|
+
positionals: {
|
|
4331
|
+
min: 1,
|
|
4332
|
+
max: 1
|
|
4333
|
+
},
|
|
4334
|
+
run: async (invocation) => {
|
|
4335
|
+
const request = restoreRequestOf(invocation.values);
|
|
4336
|
+
const run = await runOf(adaptersOf, invocation);
|
|
4337
|
+
const reference = invocation.positionals[0] ?? "";
|
|
4338
|
+
const agentId = await agentFrom(run, reference, invocation.values.project);
|
|
4339
|
+
return printed(run, await restoredAgent(run.api, agentId, request), (restored) => [`Restored ${request.name} as ${restored.agentId} from backup ${request.backupId} of ${reference}.`, `kcloud agents start ${restored.agentId} starts it once it is provisioned.`].join("\n"));
|
|
4340
|
+
}
|
|
4341
|
+
});
|
|
4342
|
+
//#endregion
|
|
3685
4343
|
//#region src/commands/agents/check.ts
|
|
3686
4344
|
const checkHints = {
|
|
3687
4345
|
...agentHints,
|
|
@@ -3709,36 +4367,6 @@ const checkCommand = (adaptersOf) => defineCommand({
|
|
|
3709
4367
|
}
|
|
3710
4368
|
});
|
|
3711
4369
|
//#endregion
|
|
3712
|
-
//#region src/render/body.ts
|
|
3713
|
-
const isTerminal$1 = (stream) => stream.isTTY === true;
|
|
3714
|
-
const pipedText$1 = async (stream) => {
|
|
3715
|
-
const chunks = [];
|
|
3716
|
-
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
3717
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3718
|
-
};
|
|
3719
|
-
const fileOptions = { file: { type: "string" } };
|
|
3720
|
-
const fileUsage = (what) => [["--file <path>", `read ${what} from a JSON file, or from stdin with -`]];
|
|
3721
|
-
const documentOf = async (run, file) => {
|
|
3722
|
-
const text = file === "-" ? isTerminal$1(run.context.io.stdin) ? "" : await pipedText$1(run.context.io.stdin) : await readFileOrRefuse(resolve(run.context.workingDirectory, file));
|
|
3723
|
-
if (text.trim() === "") throw new UsageError(file === "-" ? "Nothing is piped on stdin." : `${file} is empty.`);
|
|
3724
|
-
try {
|
|
3725
|
-
return JSON.parse(text);
|
|
3726
|
-
} catch {
|
|
3727
|
-
throw new CliError(`${file === "-" ? "stdin" : file} is not JSON.`);
|
|
3728
|
-
}
|
|
3729
|
-
};
|
|
3730
|
-
const readFileOrRefuse = async (path) => {
|
|
3731
|
-
try {
|
|
3732
|
-
return await readFile(path, "utf8");
|
|
3733
|
-
} catch {
|
|
3734
|
-
throw new CliError(`${path} could not be read.`);
|
|
3735
|
-
}
|
|
3736
|
-
};
|
|
3737
|
-
const validated = (schema, document, what) => {
|
|
3738
|
-
const parsed = schema.safeParse(document);
|
|
3739
|
-
if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
|
|
3740
|
-
};
|
|
3741
|
-
//#endregion
|
|
3742
4370
|
//#region src/render/confirm.ts
|
|
3743
4371
|
const confirmed = async ({ prompt, settings, question }) => {
|
|
3744
4372
|
if (settings.yes) return;
|
|
@@ -4042,86 +4670,6 @@ const detailsOf = (agent) => [
|
|
|
4042
4670
|
]))
|
|
4043
4671
|
].join("\n");
|
|
4044
4672
|
//#endregion
|
|
4045
|
-
//#region src/commands/nodes/shared.ts
|
|
4046
|
-
const nodeHints = {
|
|
4047
|
-
...organizationHints,
|
|
4048
|
-
"node.not_found": "kcloud nodes list shows the nodes of the organization.",
|
|
4049
|
-
"node.name_taken": "Another node of the organization has that name.",
|
|
4050
|
-
"node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
|
|
4051
|
-
"node.registration_used": "The node has joined and holds its own credential; it needs no token."
|
|
4052
|
-
};
|
|
4053
|
-
const splitHints = {
|
|
4054
|
-
...nodeHints,
|
|
4055
|
-
"auth.forbidden": "Splitting a node takes an admin of its organization.",
|
|
4056
|
-
"node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
|
|
4057
|
-
"node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host.",
|
|
4058
|
-
"concurrency.conflict": "Someone changed the node meanwhile; run the command again."
|
|
4059
|
-
};
|
|
4060
|
-
const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
|
|
4061
|
-
const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
|
|
4062
|
-
const nodeIn = (nodes, reference, organization) => {
|
|
4063
|
-
const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
|
|
4064
|
-
if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf(nodes)}.`);
|
|
4065
|
-
return found;
|
|
4066
|
-
};
|
|
4067
|
-
const resolveNode = async (scope, reference) => {
|
|
4068
|
-
const organization = await scope.organization();
|
|
4069
|
-
if (isId(reference)) return {
|
|
4070
|
-
organizationId: organization.id,
|
|
4071
|
-
nodeId: reference
|
|
4072
|
-
};
|
|
4073
|
-
const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
|
|
4074
|
-
return {
|
|
4075
|
-
organizationId: organization.id,
|
|
4076
|
-
nodeId: node.id
|
|
4077
|
-
};
|
|
4078
|
-
};
|
|
4079
|
-
const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
|
|
4080
|
-
organizationId,
|
|
4081
|
-
nodeId
|
|
4082
|
-
} } }), nodeHints);
|
|
4083
|
-
const createdNode = async (api, organizationId, name) => {
|
|
4084
|
-
const body = { name };
|
|
4085
|
-
validated(createNodeBodySchema, body, "a node");
|
|
4086
|
-
return dataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
|
|
4087
|
-
params: { path: { organizationId } },
|
|
4088
|
-
body
|
|
4089
|
-
}), nodeHints);
|
|
4090
|
-
};
|
|
4091
|
-
const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
|
|
4092
|
-
organizationId,
|
|
4093
|
-
nodeId
|
|
4094
|
-
} } }), nodeHints);
|
|
4095
|
-
const deleteNode = async (api, { organizationId, nodeId }) => {
|
|
4096
|
-
doneWith("delete the node", await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
|
|
4097
|
-
organizationId,
|
|
4098
|
-
nodeId
|
|
4099
|
-
} } }), nodeHints);
|
|
4100
|
-
};
|
|
4101
|
-
const splitNode = async (api, { organizationId, nodeId }, revision, split) => dataOf(split === null ? "clear the split of the node" : "split the node", await api.PUT("/organizations/{organizationId}/nodes/{nodeId}/split", {
|
|
4102
|
-
params: { path: {
|
|
4103
|
-
organizationId,
|
|
4104
|
-
nodeId
|
|
4105
|
-
} },
|
|
4106
|
-
body: {
|
|
4107
|
-
revision,
|
|
4108
|
-
split
|
|
4109
|
-
}
|
|
4110
|
-
}), splitHints);
|
|
4111
|
-
const nodeUsage = [["", "a node is named by its name or its id"]];
|
|
4112
|
-
//#endregion
|
|
4113
|
-
//#region src/commands/projects/shared.ts
|
|
4114
|
-
const defaultProjectColor = "#7c86ff";
|
|
4115
|
-
const projectHint = {
|
|
4116
|
-
"project.not_found": "kcloud projects list shows the projects you are a member of.",
|
|
4117
|
-
"project.slug_taken": "Another project of the organization has that slug.",
|
|
4118
|
-
"project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
|
|
4119
|
-
"project.not_empty": "Delete its agents, credentials, repositories and tools first.",
|
|
4120
|
-
"project.member_not_found": "kcloud projects members list <project> shows them.",
|
|
4121
|
-
"project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
|
|
4122
|
-
};
|
|
4123
|
-
const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
|
|
4124
|
-
//#endregion
|
|
4125
4673
|
//#region src/commands/agents/placement.ts
|
|
4126
4674
|
const placedNodeId = async (run, { projectId, given, ask }) => {
|
|
4127
4675
|
const project = await projectOf(run.api, projectId);
|
|
@@ -5156,6 +5704,8 @@ const createAgentsCommand = (adaptersOf) => defineGroup({
|
|
|
5156
5704
|
...lifecycleCommands(adaptersOf),
|
|
5157
5705
|
checkCommand(adaptersOf),
|
|
5158
5706
|
tasksCommand(adaptersOf),
|
|
5707
|
+
backupsCommand(adaptersOf),
|
|
5708
|
+
restoreCommand(adaptersOf),
|
|
5159
5709
|
...observabilityCommands(adaptersOf),
|
|
5160
5710
|
...streamCommands(adaptersOf)
|
|
5161
5711
|
]
|
|
@@ -5788,75 +6338,6 @@ const agentTools = [
|
|
|
5788
6338
|
})
|
|
5789
6339
|
];
|
|
5790
6340
|
//#endregion
|
|
5791
|
-
//#region src/mcp/tools/credentials.ts
|
|
5792
|
-
const credentialField$1 = z.string().trim().min(1).describe("The credential, by its label or its id.");
|
|
5793
|
-
const credentialTools = [
|
|
5794
|
-
defineTool({
|
|
5795
|
-
name: "credentials_list",
|
|
5796
|
-
title: "List credentials",
|
|
5797
|
-
description: "List the Claude and GitHub credentials of a project, with their fingerprints and verification, never their secrets.",
|
|
5798
|
-
operations: ["listCredentials"],
|
|
5799
|
-
safety: "read",
|
|
5800
|
-
input: z.object({ project: projectField }),
|
|
5801
|
-
run: async ({ project }, run) => credentialsOf(run.api, await projectIdOf(run, project))
|
|
5802
|
-
}),
|
|
5803
|
-
defineTool({
|
|
5804
|
-
name: "credential_update",
|
|
5805
|
-
title: "Rename or verify a credential",
|
|
5806
|
-
description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer.",
|
|
5807
|
-
operations: ["renameCredential", "verifyCredential"],
|
|
5808
|
-
safety: "write",
|
|
5809
|
-
input: z.object({
|
|
5810
|
-
project: projectField,
|
|
5811
|
-
credential: credentialField$1,
|
|
5812
|
-
action: z.enum(["rename", "verify"]),
|
|
5813
|
-
label: z.string().trim().min(1).optional().describe("The new label, for rename.")
|
|
5814
|
-
}),
|
|
5815
|
-
run: async ({ project, credential, action, label }, run) => {
|
|
5816
|
-
const projectId = await projectIdOf(run, project);
|
|
5817
|
-
const credentialId = await resolveCredentialId(run.api, projectId, credential);
|
|
5818
|
-
if (action === "verify") {
|
|
5819
|
-
doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
|
|
5820
|
-
id: projectId,
|
|
5821
|
-
credentialId
|
|
5822
|
-
} } }), credentialHints);
|
|
5823
|
-
return {
|
|
5824
|
-
id: credentialId,
|
|
5825
|
-
verifying: true
|
|
5826
|
-
};
|
|
5827
|
-
}
|
|
5828
|
-
if (label === void 0) throw new UsageError("rename needs a label.");
|
|
5829
|
-
return dataOf("rename the credential", await run.api.PATCH("/projects/{id}/credentials/{credentialId}", {
|
|
5830
|
-
params: { path: {
|
|
5831
|
-
id: projectId,
|
|
5832
|
-
credentialId
|
|
5833
|
-
} },
|
|
5834
|
-
body: { label }
|
|
5835
|
-
}), credentialHints);
|
|
5836
|
-
}
|
|
5837
|
-
}),
|
|
5838
|
-
defineTool({
|
|
5839
|
-
name: "credential_delete",
|
|
5840
|
-
title: "Delete a credential",
|
|
5841
|
-
description: "Delete a credential of a project that no agent and no repository uses.",
|
|
5842
|
-
operations: ["deleteCredential"],
|
|
5843
|
-
safety: "destructive",
|
|
5844
|
-
input: z.object({
|
|
5845
|
-
project: projectField,
|
|
5846
|
-
credential: credentialField$1
|
|
5847
|
-
}),
|
|
5848
|
-
run: async ({ project, credential }, run) => {
|
|
5849
|
-
const projectId = await projectIdOf(run, project);
|
|
5850
|
-
const credentialId = await resolveCredentialId(run.api, projectId, credential);
|
|
5851
|
-
doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
|
|
5852
|
-
id: projectId,
|
|
5853
|
-
credentialId
|
|
5854
|
-
} } }), credentialHints);
|
|
5855
|
-
return { deleted: credentialId };
|
|
5856
|
-
}
|
|
5857
|
-
})
|
|
5858
|
-
];
|
|
5859
|
-
//#endregion
|
|
5860
6341
|
//#region src/commands/nodes/split.ts
|
|
5861
6342
|
const splitFields = [
|
|
5862
6343
|
"reserve.cpus",
|
|
@@ -6175,7 +6656,7 @@ const nodeTools = [
|
|
|
6175
6656
|
defineTool({
|
|
6176
6657
|
name: "nodes_list",
|
|
6177
6658
|
title: "List nodes",
|
|
6178
|
-
description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their state, engine, version, capabilities, split and
|
|
6659
|
+
description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their provider, state, engine, version, capabilities, split, agent count, the DigitalOcean machine of a node created there, and the operation the platform runs on it.",
|
|
6179
6660
|
operations: ["listNodes"],
|
|
6180
6661
|
safety: "read",
|
|
6181
6662
|
input: z.object({}),
|
|
@@ -6184,7 +6665,7 @@ const nodeTools = [
|
|
|
6184
6665
|
defineTool({
|
|
6185
6666
|
name: "node_show",
|
|
6186
6667
|
title: "Show a node",
|
|
6187
|
-
description: "Show one node with its host, its capabilities, its split, the agents placed on it with their pool limits, and the runtime and pools it reported last.",
|
|
6668
|
+
description: "Show one node with its provider, its DigitalOcean machine and the step and error of its operation, its host, its capabilities, its split, the agents placed on it with their pool limits, and the runtime and pools it reported last.",
|
|
6188
6669
|
operations: ["getNode"],
|
|
6189
6670
|
safety: "read",
|
|
6190
6671
|
input: z.object({ node: nodeField }),
|
|
@@ -6228,16 +6709,161 @@ const nodeTools = [
|
|
|
6228
6709
|
}
|
|
6229
6710
|
}),
|
|
6230
6711
|
defineTool({
|
|
6231
|
-
name: "
|
|
6232
|
-
title: "
|
|
6233
|
-
description: "
|
|
6234
|
-
operations: ["
|
|
6712
|
+
name: "node_resize",
|
|
6713
|
+
title: "Resize a node on DigitalOcean",
|
|
6714
|
+
description: "Give a node created on DigitalOcean a larger droplet size, a larger volume for its agents, or both; a field left out keeps its value, and a node never shrinks. A new size powers the droplet off, so its agents stop until it runs again. Returns the node with the operation that started; node_show follows its steps. Takes an admin of the organization.",
|
|
6715
|
+
operations: ["getNode", "resizeNode"],
|
|
6716
|
+
safety: "destructive",
|
|
6717
|
+
input: z.object({
|
|
6718
|
+
node: nodeField,
|
|
6719
|
+
size: providerSizeSlugSchema.optional().describe("The new droplet size, from provider_catalog."),
|
|
6720
|
+
volumeGb: nodeVolumeGbSchema.optional().describe("The new size of the volume in GB.")
|
|
6721
|
+
}),
|
|
6722
|
+
run: async ({ node, size, volumeGb }, run) => {
|
|
6723
|
+
if (size === void 0 && volumeGb === void 0) throw new CliError("Name a size, a volume or both.");
|
|
6724
|
+
const reference = await resolveNode(run, node);
|
|
6725
|
+
const detail = await nodeDetailOf(run.api, reference);
|
|
6726
|
+
return resizedNode(run.api, reference, detail, {
|
|
6727
|
+
size,
|
|
6728
|
+
volumeGb
|
|
6729
|
+
});
|
|
6730
|
+
}
|
|
6731
|
+
}),
|
|
6732
|
+
defineTool({
|
|
6733
|
+
name: "node_retry",
|
|
6734
|
+
title: "Retry the operation of a node",
|
|
6735
|
+
description: "Run the failed create, resize or delete operation of a node on DigitalOcean again from the step that failed. Returns the node; node_show follows its steps. Takes an admin of the organization.",
|
|
6736
|
+
operations: ["getNode", "retryNodeOperation"],
|
|
6737
|
+
safety: "write",
|
|
6738
|
+
input: z.object({ node: nodeField }),
|
|
6739
|
+
run: async ({ node }, run) => {
|
|
6740
|
+
const reference = await resolveNode(run, node);
|
|
6741
|
+
const detail = await nodeDetailOf(run.api, reference);
|
|
6742
|
+
return retriedOperation(run.api, reference, detail.revision);
|
|
6743
|
+
}
|
|
6744
|
+
}),
|
|
6745
|
+
defineTool({
|
|
6746
|
+
name: "node_delete",
|
|
6747
|
+
title: "Delete a node",
|
|
6748
|
+
description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it. A self-hosted node is deleted at once and returns deleted; for a node on DigitalOcean the platform destroys its droplet and its volume first and returns the node as removing.",
|
|
6749
|
+
operations: ["deleteNode"],
|
|
6750
|
+
safety: "destructive",
|
|
6751
|
+
input: z.object({ node: nodeField }),
|
|
6752
|
+
run: async ({ node }, run) => {
|
|
6753
|
+
const reference = await resolveNode(run, node);
|
|
6754
|
+
const removing = await deleteNode(run.api, reference);
|
|
6755
|
+
return removing === null ? { deleted: reference.nodeId } : { removing };
|
|
6756
|
+
}
|
|
6757
|
+
})
|
|
6758
|
+
];
|
|
6759
|
+
//#endregion
|
|
6760
|
+
//#region src/mcp/tools/backups.ts
|
|
6761
|
+
const agentBackupTools = [
|
|
6762
|
+
defineTool({
|
|
6763
|
+
name: "agent_backups_list",
|
|
6764
|
+
title: "List the backups of an agent",
|
|
6765
|
+
description: "List the backups of the memory of an agent, the newest first, with their trigger, node, Kikimora release, entries, size and expiry, and the days a backup is kept.",
|
|
6766
|
+
operations: ["listAgentBackups"],
|
|
6767
|
+
safety: "read",
|
|
6768
|
+
input: z.object(agentFields),
|
|
6769
|
+
run: async (reference, run) => backupsOf(run.api, await agentIdOf(run, reference))
|
|
6770
|
+
}),
|
|
6771
|
+
defineTool({
|
|
6772
|
+
name: "agent_backup_request",
|
|
6773
|
+
title: "Back an agent up now",
|
|
6774
|
+
description: "Ask the node of an agent for a backup of its memory now. state requested means the node makes it, and agent_backups_list shows it once the node has sent it.",
|
|
6775
|
+
operations: ["requestAgentBackup"],
|
|
6776
|
+
safety: "write",
|
|
6777
|
+
input: z.object(agentFields),
|
|
6778
|
+
run: async (reference, run) => requestedBackup(run.api, await agentIdOf(run, reference))
|
|
6779
|
+
}),
|
|
6780
|
+
defineTool({
|
|
6781
|
+
name: "agent_restore",
|
|
6782
|
+
title: "Restore an agent from a backup",
|
|
6783
|
+
description: "Create a new agent in the project of an agent, with its configuration and the memory of one of its backups, on the node named. Returns the id of the new agent, which stays stopped until agent_action starts it.",
|
|
6784
|
+
operations: ["restoreAgentFromBackup"],
|
|
6785
|
+
safety: "write",
|
|
6786
|
+
input: z.object({
|
|
6787
|
+
...agentFields,
|
|
6788
|
+
backup: idSchema.describe("The id of the backup, from agent_backups_list."),
|
|
6789
|
+
name: agentNameSchema.describe("The name of the new agent."),
|
|
6790
|
+
slug: slugSchema.optional().describe("The slug of the new agent, instead of one made from its name."),
|
|
6791
|
+
node: nodeField
|
|
6792
|
+
}),
|
|
6793
|
+
run: async ({ backup, name, slug, node, ...reference }, run) => restoredAgent(run.api, await agentIdOf(run, reference), {
|
|
6794
|
+
backupId: backup,
|
|
6795
|
+
name,
|
|
6796
|
+
node,
|
|
6797
|
+
slug
|
|
6798
|
+
})
|
|
6799
|
+
})
|
|
6800
|
+
];
|
|
6801
|
+
//#endregion
|
|
6802
|
+
//#region src/mcp/tools/credentials.ts
|
|
6803
|
+
const credentialField$1 = z.string().trim().min(1).describe("The credential, by its label or its id.");
|
|
6804
|
+
const credentialTools = [
|
|
6805
|
+
defineTool({
|
|
6806
|
+
name: "credentials_list",
|
|
6807
|
+
title: "List credentials",
|
|
6808
|
+
description: "List the Claude and GitHub credentials of a project, with their fingerprints and verification, never their secrets.",
|
|
6809
|
+
operations: ["listCredentials"],
|
|
6810
|
+
safety: "read",
|
|
6811
|
+
input: z.object({ project: projectField }),
|
|
6812
|
+
run: async ({ project }, run) => credentialsOf(run.api, await projectIdOf(run, project))
|
|
6813
|
+
}),
|
|
6814
|
+
defineTool({
|
|
6815
|
+
name: "credential_update",
|
|
6816
|
+
title: "Rename or verify a credential",
|
|
6817
|
+
description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer.",
|
|
6818
|
+
operations: ["renameCredential", "verifyCredential"],
|
|
6819
|
+
safety: "write",
|
|
6820
|
+
input: z.object({
|
|
6821
|
+
project: projectField,
|
|
6822
|
+
credential: credentialField$1,
|
|
6823
|
+
action: z.enum(["rename", "verify"]),
|
|
6824
|
+
label: z.string().trim().min(1).optional().describe("The new label, for rename.")
|
|
6825
|
+
}),
|
|
6826
|
+
run: async ({ project, credential, action, label }, run) => {
|
|
6827
|
+
const projectId = await projectIdOf(run, project);
|
|
6828
|
+
const credentialId = await resolveCredentialId(run.api, projectId, credential);
|
|
6829
|
+
if (action === "verify") {
|
|
6830
|
+
doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
|
|
6831
|
+
id: projectId,
|
|
6832
|
+
credentialId
|
|
6833
|
+
} } }), credentialHints);
|
|
6834
|
+
return {
|
|
6835
|
+
id: credentialId,
|
|
6836
|
+
verifying: true
|
|
6837
|
+
};
|
|
6838
|
+
}
|
|
6839
|
+
if (label === void 0) throw new UsageError("rename needs a label.");
|
|
6840
|
+
return dataOf("rename the credential", await run.api.PATCH("/projects/{id}/credentials/{credentialId}", {
|
|
6841
|
+
params: { path: {
|
|
6842
|
+
id: projectId,
|
|
6843
|
+
credentialId
|
|
6844
|
+
} },
|
|
6845
|
+
body: { label }
|
|
6846
|
+
}), credentialHints);
|
|
6847
|
+
}
|
|
6848
|
+
}),
|
|
6849
|
+
defineTool({
|
|
6850
|
+
name: "credential_delete",
|
|
6851
|
+
title: "Delete a credential",
|
|
6852
|
+
description: "Delete a credential of a project that no agent and no repository uses.",
|
|
6853
|
+
operations: ["deleteCredential"],
|
|
6235
6854
|
safety: "destructive",
|
|
6236
|
-
input: z.object({
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6855
|
+
input: z.object({
|
|
6856
|
+
project: projectField,
|
|
6857
|
+
credential: credentialField$1
|
|
6858
|
+
}),
|
|
6859
|
+
run: async ({ project, credential }, run) => {
|
|
6860
|
+
const projectId = await projectIdOf(run, project);
|
|
6861
|
+
const credentialId = await resolveCredentialId(run.api, projectId, credential);
|
|
6862
|
+
doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
|
|
6863
|
+
id: projectId,
|
|
6864
|
+
credentialId
|
|
6865
|
+
} } }), credentialHints);
|
|
6866
|
+
return { deleted: credentialId };
|
|
6241
6867
|
}
|
|
6242
6868
|
})
|
|
6243
6869
|
];
|
|
@@ -7854,6 +8480,71 @@ const projectTools = [
|
|
|
7854
8480
|
})
|
|
7855
8481
|
];
|
|
7856
8482
|
//#endregion
|
|
8483
|
+
//#region src/mcp/tools/providers.ts
|
|
8484
|
+
const accountField = z.string().trim().min(1).describe("The provider account, by its name or its id, from providers_list.");
|
|
8485
|
+
const providerTools = [
|
|
8486
|
+
defineTool({
|
|
8487
|
+
name: "providers_list",
|
|
8488
|
+
title: "List provider accounts",
|
|
8489
|
+
description: "List the DigitalOcean accounts of the organization this server acts in, with their default region, the number of nodes created with each, and the last check of their token, never the token itself.",
|
|
8490
|
+
operations: ["listProviderAccounts"],
|
|
8491
|
+
safety: "read",
|
|
8492
|
+
input: z.object({}),
|
|
8493
|
+
run: async (_, run) => providerAccountsOf(run.api, (await run.organization()).id)
|
|
8494
|
+
}),
|
|
8495
|
+
defineTool({
|
|
8496
|
+
name: "provider_show",
|
|
8497
|
+
title: "Show a provider account",
|
|
8498
|
+
description: "Show one provider account with its default region, its node count and the last check of its token: when it ran, the scopes it lacked and why it failed.",
|
|
8499
|
+
operations: ["getProviderAccount"],
|
|
8500
|
+
safety: "read",
|
|
8501
|
+
input: z.object({ account: accountField }),
|
|
8502
|
+
run: async ({ account }, run) => resolveProviderAccount(run, account)
|
|
8503
|
+
}),
|
|
8504
|
+
defineTool({
|
|
8505
|
+
name: "provider_catalog",
|
|
8506
|
+
title: "List regions and sizes",
|
|
8507
|
+
description: "List the regions of a provider account that offer volumes, and the sizes with at least 2 GB of memory a node can have in one region, with CPUs, memory in MB, disk in GB and the monthly price in USD. The region defaults to the default region of the account.",
|
|
8508
|
+
operations: ["getProviderCatalog"],
|
|
8509
|
+
safety: "read",
|
|
8510
|
+
input: z.object({
|
|
8511
|
+
account: accountField,
|
|
8512
|
+
region: providerRegionSlugSchema.optional().describe("The region whose sizes to list, as in fra1.")
|
|
8513
|
+
}),
|
|
8514
|
+
run: async ({ account, region }, run) => {
|
|
8515
|
+
const found = await resolveProviderAccount(run, account);
|
|
8516
|
+
const catalog = await providerCatalogOf(run.api, (await run.organization()).id, found.id);
|
|
8517
|
+
return catalogInRegion(catalog, region ?? found.defaultRegion);
|
|
8518
|
+
}
|
|
8519
|
+
}),
|
|
8520
|
+
defineTool({
|
|
8521
|
+
name: "provider_verify",
|
|
8522
|
+
title: "Check the token of a provider account",
|
|
8523
|
+
description: "Check the token of a provider account against DigitalOcean again and return the account with the result: missingScopes lists the scopes it lacks, and failure is unauthorized when DigitalOcean refused it. Takes an admin of the organization.",
|
|
8524
|
+
operations: ["verifyProviderAccount"],
|
|
8525
|
+
safety: "write",
|
|
8526
|
+
idempotent: true,
|
|
8527
|
+
input: z.object({ account: accountField }),
|
|
8528
|
+
run: async ({ account }, run) => {
|
|
8529
|
+
const found = await resolveProviderAccount(run, account);
|
|
8530
|
+
return verifiedAccount(run.api, (await run.organization()).id, found.id);
|
|
8531
|
+
}
|
|
8532
|
+
}),
|
|
8533
|
+
defineTool({
|
|
8534
|
+
name: "provider_delete",
|
|
8535
|
+
title: "Delete a provider account",
|
|
8536
|
+
description: "Delete a provider account and erase its token. The instance refuses while nodes created with it exist. Takes an admin of the organization.",
|
|
8537
|
+
operations: ["deleteProviderAccount"],
|
|
8538
|
+
safety: "destructive",
|
|
8539
|
+
input: z.object({ account: accountField }),
|
|
8540
|
+
run: async ({ account }, run) => {
|
|
8541
|
+
const found = await resolveProviderAccount(run, account);
|
|
8542
|
+
await deleteProviderAccount(run.api, (await run.organization()).id, found.id);
|
|
8543
|
+
return { deleted: found.id };
|
|
8544
|
+
}
|
|
8545
|
+
})
|
|
8546
|
+
];
|
|
8547
|
+
//#endregion
|
|
7857
8548
|
//#region src/mcp/tools/releases.ts
|
|
7858
8549
|
const releaseTools = [defineTool({
|
|
7859
8550
|
name: "releases_list",
|
|
@@ -8261,8 +8952,10 @@ const kcloudTools = [
|
|
|
8261
8952
|
...credentialTools,
|
|
8262
8953
|
...repositoryTools,
|
|
8263
8954
|
...projectToolTools,
|
|
8955
|
+
...providerTools,
|
|
8264
8956
|
...nodeTools,
|
|
8265
8957
|
...agentTools,
|
|
8958
|
+
...agentBackupTools,
|
|
8266
8959
|
...taskTools,
|
|
8267
8960
|
...observabilityTools,
|
|
8268
8961
|
...notificationTools,
|
|
@@ -8277,7 +8970,7 @@ const instructions = [
|
|
|
8277
8970
|
"These tools act on one Kikimora Cloud instance, as the account kcloud is signed in with, in one organization of it. whoami names all three.",
|
|
8278
8971
|
"Name a project by its slug, its name or its id. Name an agent as <project>/<agent>, as its slug or name together with project, or by its id.",
|
|
8279
8972
|
"A refusal quotes the error code of the instance. Its hint may name a kcloud command, such as kcloud agents list; the tool for the same resource and verb, agents_list, does that work here.",
|
|
8280
|
-
"Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
|
|
8973
|
+
"Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud providers add, kcloud providers update, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
|
|
8281
8974
|
].join("\n");
|
|
8282
8975
|
const createKcloudServer = ({ version, open }) => {
|
|
8283
8976
|
const server = new McpServer({
|
|
@@ -8341,6 +9034,7 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
|
|
|
8341
9034
|
});
|
|
8342
9035
|
//#endregion
|
|
8343
9036
|
//#region src/commands/nodes/index.ts
|
|
9037
|
+
const defaultVolumeGb = 40;
|
|
8344
9038
|
const gibibytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
8345
9039
|
const hostOf = (node) => node.host === null ? "-" : `${node.host.cpus} CPU, ${gibibytes(node.host.memoryBytes)} of memory`;
|
|
8346
9040
|
const capabilitiesOf = (node) => {
|
|
@@ -8350,6 +9044,25 @@ const capabilitiesOf = (node) => {
|
|
|
8350
9044
|
};
|
|
8351
9045
|
const registrationOf = (node) => node.registrationExpiresAt === null ? "-" : `a token waits until ${node.registrationExpiresAt}`;
|
|
8352
9046
|
const storageOf = ({ state, freeBytes, totalBytes, path }) => `${state}, ${gibibytes(freeBytes)} free of ${gibibytes(totalBytes)} at ${path}`;
|
|
9047
|
+
const shapeText = ({ size, volumeGb }) => `${size} with a volume of ${volumeGb} GB`;
|
|
9048
|
+
const machineText = (machine) => `${machine.size} in ${machine.region} with a volume of ${machine.volumeGb} GB`;
|
|
9049
|
+
const dropletText = ({ dropletId, publicIpv4 }) => dropletId === null ? "not created yet" : `${dropletId} at ${publicIpv4 ?? "no address yet"}`;
|
|
9050
|
+
const operationText = ({ kind, state, step }) => `${kind} ${state}${step === null ? "" : ` at ${step}`}`;
|
|
9051
|
+
const machineRowsOf = (machine) => machine === null ? [] : [
|
|
9052
|
+
["machine", machineText(machine)],
|
|
9053
|
+
["account", machine.account.name ?? machine.account.id],
|
|
9054
|
+
["droplet", dropletText(machine)]
|
|
9055
|
+
];
|
|
9056
|
+
const operationRowsOf = (operation) => {
|
|
9057
|
+
if (operation === null) return [];
|
|
9058
|
+
const { target, error } = operation;
|
|
9059
|
+
return [
|
|
9060
|
+
["operation", operationText(operation)],
|
|
9061
|
+
...target === null ? [] : [["target", shapeText(target)]],
|
|
9062
|
+
["started", operation.startedAt],
|
|
9063
|
+
...error === null ? [] : [["error", error]]
|
|
9064
|
+
];
|
|
9065
|
+
};
|
|
8353
9066
|
const runtimeRowsOf = (runtime) => [
|
|
8354
9067
|
["reported", runtime.reportedAt],
|
|
8355
9068
|
["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
|
|
@@ -8359,11 +9072,29 @@ const runtimeRowsOf = (runtime) => [
|
|
|
8359
9072
|
["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
|
|
8360
9073
|
["log readers", String(runtime.ingest.readers)]
|
|
8361
9074
|
];
|
|
9075
|
+
const updateOf = ({ update }) => {
|
|
9076
|
+
if (update === null) return "not reported";
|
|
9077
|
+
switch (update.state) {
|
|
9078
|
+
case "current": return "current";
|
|
9079
|
+
case "waiting": return `waits for ${String(update.sessions)} ${update.sessions === 1 ? "session" : "sessions"} to end before ${update.target ?? "-"}`;
|
|
9080
|
+
case "updating": return `updating to ${update.target ?? "-"}`;
|
|
9081
|
+
case "failed": return `failed to reach ${update.target ?? "-"}, runs its previous version`;
|
|
9082
|
+
case "unmanaged": return "updated with its platform";
|
|
9083
|
+
}
|
|
9084
|
+
};
|
|
9085
|
+
const lastUpdateOf = ({ update }) => {
|
|
9086
|
+
const last = update?.last ?? null;
|
|
9087
|
+
if (last === null) return null;
|
|
9088
|
+
const outcome = `${last.outcome} ${last.version ?? "-"} at ${last.at}`;
|
|
9089
|
+
return last.reason === null ? outcome : `${outcome}: ${last.reason}`;
|
|
9090
|
+
};
|
|
8362
9091
|
const nodeDetailsText = (node) => [
|
|
8363
9092
|
renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
8364
9093
|
["name", node.name],
|
|
8365
9094
|
["state", node.state],
|
|
8366
9095
|
["provider", node.provider],
|
|
9096
|
+
...machineRowsOf(node.machine),
|
|
9097
|
+
...operationRowsOf(node.operation),
|
|
8367
9098
|
["engine", node.engine ?? "-"],
|
|
8368
9099
|
["version", node.version ?? "-"],
|
|
8369
9100
|
["host", hostOf(node)],
|
|
@@ -8375,6 +9106,9 @@ const nodeDetailsText = (node) => [
|
|
|
8375
9106
|
["registration", registrationOf(node)],
|
|
8376
9107
|
["connected", node.connectedAt ?? "never"],
|
|
8377
9108
|
["last seen", node.lastSeenAt ?? "never"],
|
|
9109
|
+
...node.offlineSince === null ? [] : [["offline since", node.offlineSince]],
|
|
9110
|
+
["update", updateOf(node)],
|
|
9111
|
+
...lastUpdateOf(node) === null ? [] : [["last update", lastUpdateOf(node) ?? ""]],
|
|
8378
9112
|
["created", node.createdAt],
|
|
8379
9113
|
["id", node.id]
|
|
8380
9114
|
]),
|
|
@@ -8399,11 +9133,13 @@ const nodeDetailsText = (node) => [
|
|
|
8399
9133
|
poolsText(node.runtime.pools)
|
|
8400
9134
|
]
|
|
8401
9135
|
].join("\n");
|
|
8402
|
-
const
|
|
8403
|
-
"
|
|
8404
|
-
`
|
|
8405
|
-
|
|
8406
|
-
|
|
9136
|
+
const installCommandsOf = (origin) => [
|
|
9137
|
+
"Install the node with it, putting the token in place of <token>. On a Linux host:",
|
|
9138
|
+
` curl -fsSLo install-node.sh ${origin}/install-node.sh`,
|
|
9139
|
+
` printf '%s\\n' '<token>' | sudo sh install-node.sh --platform-url ${origin} --registration-token -`,
|
|
9140
|
+
"On Windows, in PowerShell as administrator:",
|
|
9141
|
+
` Invoke-WebRequest -UseBasicParsing -OutFile install-node.ps1 ${origin}/install-node.ps1`,
|
|
9142
|
+
` powershell -NoProfile -ExecutionPolicy Bypass -File .\\install-node.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
|
|
8407
9143
|
].join("\n");
|
|
8408
9144
|
const printedRegistration = (run, created) => {
|
|
8409
9145
|
if (run.settings.json) {
|
|
@@ -8411,9 +9147,56 @@ const printedRegistration = (run, created) => {
|
|
|
8411
9147
|
return exitCodes.ok;
|
|
8412
9148
|
}
|
|
8413
9149
|
run.context.terminal.line(created.registration.token);
|
|
8414
|
-
run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`,
|
|
9150
|
+
run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`, installCommandsOf(run.origin)].join("\n"));
|
|
8415
9151
|
return exitCodes.ok;
|
|
8416
9152
|
};
|
|
9153
|
+
const printedCreation = (run, created) => {
|
|
9154
|
+
const { node, registration } = created;
|
|
9155
|
+
if (registration !== null) return printedRegistration(run, {
|
|
9156
|
+
node,
|
|
9157
|
+
registration
|
|
9158
|
+
});
|
|
9159
|
+
return printed(run, created, () => [`Creating ${node.name} on DigitalOcean${node.machine === null ? "" : `, ${machineText(node.machine)}`}.`, `It joins the platform on its own; kcloud nodes show ${node.name} follows the steps.`].join("\n"));
|
|
9160
|
+
};
|
|
9161
|
+
const createOptions = {
|
|
9162
|
+
provider: { type: "string" },
|
|
9163
|
+
account: { type: "string" },
|
|
9164
|
+
region: { type: "string" },
|
|
9165
|
+
size: { type: "string" },
|
|
9166
|
+
"volume-gb": { type: "string" }
|
|
9167
|
+
};
|
|
9168
|
+
const volumeGbOf = (given) => {
|
|
9169
|
+
if (given === void 0) return void 0;
|
|
9170
|
+
if (!/^[0-9]+$/u.test(given)) throw new UsageError(`--volume-gb ${given} is not a whole number of gigabytes.`);
|
|
9171
|
+
return Number(given);
|
|
9172
|
+
};
|
|
9173
|
+
const machineFlags = [
|
|
9174
|
+
"account",
|
|
9175
|
+
"region",
|
|
9176
|
+
"size",
|
|
9177
|
+
"volume-gb"
|
|
9178
|
+
];
|
|
9179
|
+
const nodeBodyOf = async (run, name, values) => {
|
|
9180
|
+
const provider = nodeProviders.find((candidate) => candidate === (values.provider ?? "self_hosted"));
|
|
9181
|
+
if (provider === void 0) throw new UsageError(`--provider ${values.provider ?? ""} is not a provider of a node.`, `The providers are ${nodeProviders.join(", ")}.`);
|
|
9182
|
+
if (provider === "self_hosted") {
|
|
9183
|
+
const given = machineFlags.filter((flag) => values[flag] !== void 0);
|
|
9184
|
+
if (given.length > 0) throw new UsageError(`${given.map((flag) => `--${flag}`).join(", ")} need --provider digitalocean.`);
|
|
9185
|
+
return { name };
|
|
9186
|
+
}
|
|
9187
|
+
if (values.account === void 0 || values.size === void 0) throw new UsageError("A node on DigitalOcean needs --account <account> and --size <slug>.", "kcloud providers catalog <account> lists the sizes.");
|
|
9188
|
+
const volumeGb = volumeGbOf(values["volume-gb"]) ?? defaultVolumeGb;
|
|
9189
|
+
const account = await resolveProviderAccount(run, values.account);
|
|
9190
|
+
return {
|
|
9191
|
+
name,
|
|
9192
|
+
machine: {
|
|
9193
|
+
accountId: account.id,
|
|
9194
|
+
region: values.region ?? account.defaultRegion,
|
|
9195
|
+
size: values.size,
|
|
9196
|
+
volumeGb
|
|
9197
|
+
}
|
|
9198
|
+
};
|
|
9199
|
+
};
|
|
8417
9200
|
const createNodesCommand = (adaptersOf) => defineGroup({
|
|
8418
9201
|
name: "nodes",
|
|
8419
9202
|
summary: "The nodes of the organization, the hosts its agents run on.",
|
|
@@ -8434,7 +9217,9 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8434
9217
|
const items = await nodesOf(run.api, organization.id);
|
|
8435
9218
|
return printed(run, items, (nodes) => nodes.length === 0 ? `${organization.slug} has no node yet; kcloud nodes create <name> creates one.` : renderTable([
|
|
8436
9219
|
{ header: "NAME" },
|
|
9220
|
+
{ header: "PROVIDER" },
|
|
8437
9221
|
{ header: "STATE" },
|
|
9222
|
+
{ header: "OPERATION" },
|
|
8438
9223
|
{ header: "ENGINE" },
|
|
8439
9224
|
{ header: "VERSION" },
|
|
8440
9225
|
{
|
|
@@ -8445,7 +9230,9 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8445
9230
|
{ header: "ID" }
|
|
8446
9231
|
], nodes.map((node) => [
|
|
8447
9232
|
node.name,
|
|
9233
|
+
node.provider,
|
|
8448
9234
|
node.state,
|
|
9235
|
+
node.operation === null ? "-" : operationText(node.operation),
|
|
8449
9236
|
node.engine ?? "-",
|
|
8450
9237
|
node.version ?? "-",
|
|
8451
9238
|
String(node.agentCount),
|
|
@@ -8456,7 +9243,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8456
9243
|
}),
|
|
8457
9244
|
defineCommand({
|
|
8458
9245
|
name: "show",
|
|
8459
|
-
summary: "Show a node with its host, its split,
|
|
9246
|
+
summary: "Show a node with its machine, its host, its split, its agents, its runtime and its pools.",
|
|
8460
9247
|
usage: "nodes show <node>",
|
|
8461
9248
|
optionsUsage: nodeUsage,
|
|
8462
9249
|
options: {},
|
|
@@ -8473,10 +9260,18 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8473
9260
|
createSplitCommand(adaptersOf),
|
|
8474
9261
|
defineCommand({
|
|
8475
9262
|
name: "create",
|
|
8476
|
-
summary: "Create a node
|
|
8477
|
-
usage: "nodes create <name>",
|
|
8478
|
-
optionsUsage: [
|
|
8479
|
-
|
|
9263
|
+
summary: "Create a node on a host of your own, printing its one-time registration token, or on DigitalOcean.",
|
|
9264
|
+
usage: "nodes create <name> [--provider digitalocean --account <account> --size <slug>]",
|
|
9265
|
+
optionsUsage: [
|
|
9266
|
+
["", "the name holds lowercase letters, digits and hyphens"],
|
|
9267
|
+
["--provider <provider>", `one of ${nodeProviders.join(", ")} (default: self_hosted)`],
|
|
9268
|
+
["", "self_hosted: the token goes to stdout alone, how to install to stderr"],
|
|
9269
|
+
["--account <account>", "the DigitalOcean account, by name or id"],
|
|
9270
|
+
["--size <slug>", "the droplet size, as kcloud providers catalog lists them"],
|
|
9271
|
+
["--region <slug>", "the region (default: the region of the account)"],
|
|
9272
|
+
["--volume-gb <n>", `the volume for the agents (default: ${defaultVolumeGb})`]
|
|
9273
|
+
],
|
|
9274
|
+
options: createOptions,
|
|
8480
9275
|
positionals: {
|
|
8481
9276
|
min: 1,
|
|
8482
9277
|
max: 1
|
|
@@ -8484,7 +9279,65 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8484
9279
|
run: async (invocation) => {
|
|
8485
9280
|
const run = await runOf(adaptersOf, invocation);
|
|
8486
9281
|
const organization = await run.organization();
|
|
8487
|
-
|
|
9282
|
+
const body = await nodeBodyOf(run, invocation.positionals[0] ?? "", invocation.values);
|
|
9283
|
+
return printedCreation(run, await createdNode(run.api, organization.id, body));
|
|
9284
|
+
}
|
|
9285
|
+
}),
|
|
9286
|
+
defineCommand({
|
|
9287
|
+
name: "resize",
|
|
9288
|
+
summary: "Give a node on DigitalOcean a larger size, a larger volume or both.",
|
|
9289
|
+
usage: "nodes resize <node> [--size <slug>] [--volume-gb <n>]",
|
|
9290
|
+
optionsUsage: [
|
|
9291
|
+
...nodeUsage,
|
|
9292
|
+
["--size <slug>", "the new droplet size, with more CPUs or memory"],
|
|
9293
|
+
["--volume-gb <n>", "the new size of the volume for the agents"],
|
|
9294
|
+
["", "an option left out keeps its value; a node never shrinks"]
|
|
9295
|
+
],
|
|
9296
|
+
options: {
|
|
9297
|
+
size: { type: "string" },
|
|
9298
|
+
"volume-gb": { type: "string" }
|
|
9299
|
+
},
|
|
9300
|
+
positionals: {
|
|
9301
|
+
min: 1,
|
|
9302
|
+
max: 1
|
|
9303
|
+
},
|
|
9304
|
+
run: async (invocation) => {
|
|
9305
|
+
const { values } = invocation;
|
|
9306
|
+
const change = {
|
|
9307
|
+
size: values.size,
|
|
9308
|
+
volumeGb: volumeGbOf(values["volume-gb"])
|
|
9309
|
+
};
|
|
9310
|
+
if (change.size === void 0 && change.volumeGb === void 0) throw new UsageError("nodes resize needs --size <slug>, --volume-gb <n> or both.");
|
|
9311
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9312
|
+
const given = invocation.positionals[0] ?? "";
|
|
9313
|
+
const reference = await resolveNode(run, given);
|
|
9314
|
+
const node = await nodeDetailOf(run.api, reference);
|
|
9315
|
+
const target = resizeTargetOf(node, change);
|
|
9316
|
+
await confirmed({
|
|
9317
|
+
prompt: run.prompt,
|
|
9318
|
+
settings: run.settings,
|
|
9319
|
+
question: `Resize ${given} to ${shapeText(target)}? ${target.size === node.machine?.size ? "The volume cannot shrink afterwards." : "The droplet is off during the resize, so its agents stop until it runs again, and it cannot shrink afterwards."}`
|
|
9320
|
+
});
|
|
9321
|
+
const resized = await resizedNode(run.api, reference, node, change);
|
|
9322
|
+
return printed(run, resized, (detail) => `Resizing ${detail.name} to ${shapeText(target)}; kcloud nodes show ${detail.name} follows the steps.`);
|
|
9323
|
+
}
|
|
9324
|
+
}),
|
|
9325
|
+
defineCommand({
|
|
9326
|
+
name: "retry",
|
|
9327
|
+
summary: "Retry the failed operation of a node on DigitalOcean from the step that failed.",
|
|
9328
|
+
usage: "nodes retry <node>",
|
|
9329
|
+
optionsUsage: nodeUsage,
|
|
9330
|
+
options: {},
|
|
9331
|
+
positionals: {
|
|
9332
|
+
min: 1,
|
|
9333
|
+
max: 1
|
|
9334
|
+
},
|
|
9335
|
+
run: async (invocation) => {
|
|
9336
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9337
|
+
const reference = await resolveNode(run, invocation.positionals[0] ?? "");
|
|
9338
|
+
const node = await nodeDetailOf(run.api, reference);
|
|
9339
|
+
const retried = await retriedOperation(run.api, reference, node.revision);
|
|
9340
|
+
return printed(run, retried, (detail) => detail.operation === null ? `${detail.name} has no operation left to run.` : `Retrying ${operationText(detail.operation)} of ${detail.name}; kcloud nodes show ${detail.name} follows the steps.`);
|
|
8488
9341
|
}
|
|
8489
9342
|
}),
|
|
8490
9343
|
defineCommand({
|
|
@@ -8505,7 +9358,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8505
9358
|
}),
|
|
8506
9359
|
defineCommand({
|
|
8507
9360
|
name: "delete",
|
|
8508
|
-
summary: "Delete a node that runs no agent.",
|
|
9361
|
+
summary: "Delete a node that runs no agent, with its droplet on DigitalOcean.",
|
|
8509
9362
|
usage: "nodes delete <node>",
|
|
8510
9363
|
optionsUsage: nodeUsage,
|
|
8511
9364
|
options: {},
|
|
@@ -8517,13 +9370,300 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8517
9370
|
const run = await runOf(adaptersOf, invocation);
|
|
8518
9371
|
const given = invocation.positionals[0] ?? "";
|
|
8519
9372
|
const reference = await resolveNode(run, given);
|
|
9373
|
+
const node = await nodeDetailOf(run.api, reference);
|
|
9374
|
+
await confirmed({
|
|
9375
|
+
prompt: run.prompt,
|
|
9376
|
+
settings: run.settings,
|
|
9377
|
+
question: node.machine === null ? `Delete the node ${given}? Its host can no longer reach the platform.` : `Delete the node ${given}? The platform destroys its droplet and its volume with the data of its agents.`
|
|
9378
|
+
});
|
|
9379
|
+
const removing = await deleteNode(run.api, reference);
|
|
9380
|
+
if (removing === null) return said(run, `Deleted the node ${given}.`);
|
|
9381
|
+
return printed(run, removing, (removed) => `The removal of ${removed.name} started: the platform destroys its droplet, its volume and its firewall, then forgets the node. kcloud nodes list shows it until then.`);
|
|
9382
|
+
}
|
|
9383
|
+
})
|
|
9384
|
+
]
|
|
9385
|
+
});
|
|
9386
|
+
//#endregion
|
|
9387
|
+
//#region src/commands/providers/index.ts
|
|
9388
|
+
const accountUsage = [["", "an account is named by its name or its id"]];
|
|
9389
|
+
const regionOption = { region: { type: "string" } };
|
|
9390
|
+
const tokenOf = async (run, name, values) => {
|
|
9391
|
+
const token = await secretOf({
|
|
9392
|
+
run,
|
|
9393
|
+
file: values["secret-file"],
|
|
9394
|
+
fromStdin: values["secret-stdin"],
|
|
9395
|
+
question: `The DigitalOcean token of ${name}`,
|
|
9396
|
+
stdinHint: "Pipe the token in, or drop --secret-stdin to be asked for it."
|
|
9397
|
+
});
|
|
9398
|
+
validated(providerTokenSchema, token, "a DigitalOcean token");
|
|
9399
|
+
return token;
|
|
9400
|
+
};
|
|
9401
|
+
const gigabytes = (megabytes) => `${megabytes / 1024} GB`;
|
|
9402
|
+
const accountDetailsText = (account) => [renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
9403
|
+
["name", account.name],
|
|
9404
|
+
["kind", account.kind],
|
|
9405
|
+
["default region", account.defaultRegion],
|
|
9406
|
+
["nodes", String(account.nodeCount)],
|
|
9407
|
+
["token", verificationText(account.verification)],
|
|
9408
|
+
["checked", account.verification.checkedAt],
|
|
9409
|
+
["created", account.createdAt],
|
|
9410
|
+
["updated", account.updatedAt],
|
|
9411
|
+
["id", account.id]
|
|
9412
|
+
]), ...account.verification.missingScopes.length === 0 ? [] : ["", `The token lacks ${account.verification.missingScopes.join(", ")}.`]].join("\n");
|
|
9413
|
+
const verificationSentence = (account) => {
|
|
9414
|
+
const { failure, missingScopes } = account.verification;
|
|
9415
|
+
if (failure === "unauthorized") return `DigitalOcean refused the token of ${account.name}; kcloud providers update ${account.name} --token replaces it.`;
|
|
9416
|
+
if (failure !== null) return `The token of ${account.name} could not be checked: ${failure}.`;
|
|
9417
|
+
return missingScopes.length === 0 ? `The token of ${account.name} has every scope a node needs.` : `The token of ${account.name} lacks ${missingScopes.join(", ")}.`;
|
|
9418
|
+
};
|
|
9419
|
+
const catalogText = (catalog) => [
|
|
9420
|
+
renderTable([
|
|
9421
|
+
{ header: "REGION" },
|
|
9422
|
+
{ header: "NAME" },
|
|
9423
|
+
{ header: "AVAILABLE" }
|
|
9424
|
+
], catalog.regions.map((region) => [
|
|
9425
|
+
region.slug === catalog.region ? `${region.slug} *` : region.slug,
|
|
9426
|
+
region.name,
|
|
9427
|
+
region.available ? "yes" : "no"
|
|
9428
|
+
])),
|
|
9429
|
+
"",
|
|
9430
|
+
catalog.sizes.length === 0 ? `No size for a node is offered in ${catalog.region}.` : renderTable([
|
|
9431
|
+
{ header: `SIZE IN ${catalog.region.toUpperCase()}` },
|
|
9432
|
+
{
|
|
9433
|
+
header: "CPU",
|
|
9434
|
+
align: "right"
|
|
9435
|
+
},
|
|
9436
|
+
{
|
|
9437
|
+
header: "MEMORY",
|
|
9438
|
+
align: "right"
|
|
9439
|
+
},
|
|
9440
|
+
{
|
|
9441
|
+
header: "DISK",
|
|
9442
|
+
align: "right"
|
|
9443
|
+
},
|
|
9444
|
+
{
|
|
9445
|
+
header: "USD A MONTH",
|
|
9446
|
+
align: "right"
|
|
9447
|
+
}
|
|
9448
|
+
], catalog.sizes.map((size) => [
|
|
9449
|
+
size.slug,
|
|
9450
|
+
String(size.vcpus),
|
|
9451
|
+
gigabytes(size.memoryMb),
|
|
9452
|
+
`${size.diskGb} GB`,
|
|
9453
|
+
size.priceMonthly.toFixed(2)
|
|
9454
|
+
]))
|
|
9455
|
+
].join("\n");
|
|
9456
|
+
const createProvidersCommand = (adaptersOf) => defineGroup({
|
|
9457
|
+
name: "providers",
|
|
9458
|
+
summary: "The cloud accounts the platform creates the nodes of the organization in.",
|
|
9459
|
+
usage: "providers <command>",
|
|
9460
|
+
subcommands: [
|
|
9461
|
+
defineCommand({
|
|
9462
|
+
name: "list",
|
|
9463
|
+
summary: "List the provider accounts of the organization.",
|
|
9464
|
+
usage: "providers list",
|
|
9465
|
+
options: {},
|
|
9466
|
+
positionals: {
|
|
9467
|
+
min: 0,
|
|
9468
|
+
max: 0
|
|
9469
|
+
},
|
|
9470
|
+
run: async (invocation) => {
|
|
9471
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9472
|
+
const organization = await run.organization();
|
|
9473
|
+
const items = await providerAccountsOf(run.api, organization.id);
|
|
9474
|
+
return printed(run, items, (accounts) => accounts.length === 0 ? `${organization.slug} has no provider account yet; kcloud providers add <name> --region <slug> adds one.` : renderTable([
|
|
9475
|
+
{ header: "NAME" },
|
|
9476
|
+
{ header: "KIND" },
|
|
9477
|
+
{ header: "REGION" },
|
|
9478
|
+
{
|
|
9479
|
+
header: "NODES",
|
|
9480
|
+
align: "right"
|
|
9481
|
+
},
|
|
9482
|
+
{ header: "TOKEN" },
|
|
9483
|
+
{ header: "ID" }
|
|
9484
|
+
], accounts.map((account) => [
|
|
9485
|
+
account.name,
|
|
9486
|
+
account.kind,
|
|
9487
|
+
account.defaultRegion,
|
|
9488
|
+
String(account.nodeCount),
|
|
9489
|
+
verificationText(account.verification),
|
|
9490
|
+
account.id
|
|
9491
|
+
])));
|
|
9492
|
+
}
|
|
9493
|
+
}),
|
|
9494
|
+
defineCommand({
|
|
9495
|
+
name: "show",
|
|
9496
|
+
summary: "Show a provider account with the state of its token.",
|
|
9497
|
+
usage: "providers show <account>",
|
|
9498
|
+
optionsUsage: accountUsage,
|
|
9499
|
+
options: {},
|
|
9500
|
+
positionals: {
|
|
9501
|
+
min: 1,
|
|
9502
|
+
max: 1
|
|
9503
|
+
},
|
|
9504
|
+
run: async (invocation) => {
|
|
9505
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9506
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9507
|
+
return printed(run, account, accountDetailsText);
|
|
9508
|
+
}
|
|
9509
|
+
}),
|
|
9510
|
+
defineCommand({
|
|
9511
|
+
name: "add",
|
|
9512
|
+
summary: "Add a DigitalOcean account, asking for its token.",
|
|
9513
|
+
usage: "providers add <name> --region <slug> [--secret-file <path>]",
|
|
9514
|
+
optionsUsage: [
|
|
9515
|
+
["--region <slug>", "the region a new node goes to by default, as in fra1"],
|
|
9516
|
+
...secretUsage,
|
|
9517
|
+
["", "the instance checks the token before it stores it; the token needs"],
|
|
9518
|
+
...scopeLines().map((line) => ["", ` ${line}`])
|
|
9519
|
+
],
|
|
9520
|
+
options: {
|
|
9521
|
+
...regionOption,
|
|
9522
|
+
...secretOptions
|
|
9523
|
+
},
|
|
9524
|
+
positionals: {
|
|
9525
|
+
min: 1,
|
|
9526
|
+
max: 1
|
|
9527
|
+
},
|
|
9528
|
+
run: async (invocation) => {
|
|
9529
|
+
const { values } = invocation;
|
|
9530
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9531
|
+
const organization = await run.organization();
|
|
9532
|
+
const name = invocation.positionals[0] ?? "";
|
|
9533
|
+
if (values.region === void 0) throw new UsageError("providers add needs --region <slug>.", "Name the region a new node goes to by default, as in fra1.");
|
|
9534
|
+
const draft = {
|
|
9535
|
+
kind: "digitalocean",
|
|
9536
|
+
name,
|
|
9537
|
+
defaultRegion: values.region
|
|
9538
|
+
};
|
|
9539
|
+
validated(createProviderAccountBodySchema.omit({ token: true }), draft, "a provider account");
|
|
9540
|
+
const token = await tokenOf(run, name, values);
|
|
9541
|
+
const account = providerDataOf("add the provider account", await run.api.POST("/organizations/{organizationId}/provider-accounts", {
|
|
9542
|
+
params: { path: { organizationId: organization.id } },
|
|
9543
|
+
body: {
|
|
9544
|
+
...draft,
|
|
9545
|
+
token
|
|
9546
|
+
}
|
|
9547
|
+
}));
|
|
9548
|
+
return printed(run, account, (added) => `Added ${added.name} as ${added.id}; its token has every scope a node needs.`);
|
|
9549
|
+
}
|
|
9550
|
+
}),
|
|
9551
|
+
defineCommand({
|
|
9552
|
+
name: "update",
|
|
9553
|
+
summary: "Rename a provider account, change its default region or replace its token.",
|
|
9554
|
+
usage: "providers update <account> [--name <name>] [--region <slug>] [--token]",
|
|
9555
|
+
optionsUsage: [
|
|
9556
|
+
...accountUsage,
|
|
9557
|
+
["--name <name>", "the new name"],
|
|
9558
|
+
["--region <slug>", "the new default region of new nodes"],
|
|
9559
|
+
["--token", "replace the token, asking for the new one"],
|
|
9560
|
+
...secretUsage,
|
|
9561
|
+
["", `the new token needs the ${providerTokenScopes.length} scopes add lists`]
|
|
9562
|
+
],
|
|
9563
|
+
options: {
|
|
9564
|
+
name: { type: "string" },
|
|
9565
|
+
...regionOption,
|
|
9566
|
+
token: {
|
|
9567
|
+
type: "boolean",
|
|
9568
|
+
default: false
|
|
9569
|
+
},
|
|
9570
|
+
...secretOptions
|
|
9571
|
+
},
|
|
9572
|
+
positionals: {
|
|
9573
|
+
min: 1,
|
|
9574
|
+
max: 1
|
|
9575
|
+
},
|
|
9576
|
+
run: async (invocation) => {
|
|
9577
|
+
const { values } = invocation;
|
|
9578
|
+
const replacesToken = values.token || values["secret-stdin"] || values["secret-file"] !== void 0;
|
|
9579
|
+
if (values.name === void 0 && values.region === void 0 && !replacesToken) throw new UsageError("providers update needs --name, --region or --token.");
|
|
9580
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9581
|
+
const organization = await run.organization();
|
|
9582
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9583
|
+
const change = {
|
|
9584
|
+
revision: account.revision,
|
|
9585
|
+
...values.name === void 0 ? {} : { name: values.name },
|
|
9586
|
+
...values.region === void 0 ? {} : { defaultRegion: values.region }
|
|
9587
|
+
};
|
|
9588
|
+
validated(createProviderAccountBodySchema.pick({
|
|
9589
|
+
name: true,
|
|
9590
|
+
defaultRegion: true
|
|
9591
|
+
}).partial(), change, "a change of a provider account");
|
|
9592
|
+
const body = replacesToken ? {
|
|
9593
|
+
...change,
|
|
9594
|
+
token: await tokenOf(run, account.name, values)
|
|
9595
|
+
} : change;
|
|
9596
|
+
const updated = providerDataOf("update the provider account", await run.api.PATCH("/organizations/{organizationId}/provider-accounts/{accountId}", {
|
|
9597
|
+
params: { path: {
|
|
9598
|
+
organizationId: organization.id,
|
|
9599
|
+
accountId: account.id
|
|
9600
|
+
} },
|
|
9601
|
+
body
|
|
9602
|
+
}));
|
|
9603
|
+
return printed(run, updated, (changed) => `Updated ${changed.name}${replacesToken ? "; its new token has every scope a node needs" : ""}.`);
|
|
9604
|
+
}
|
|
9605
|
+
}),
|
|
9606
|
+
defineCommand({
|
|
9607
|
+
name: "verify",
|
|
9608
|
+
summary: "Check the token of a provider account against DigitalOcean again.",
|
|
9609
|
+
usage: "providers verify <account>",
|
|
9610
|
+
optionsUsage: accountUsage,
|
|
9611
|
+
options: {},
|
|
9612
|
+
positionals: {
|
|
9613
|
+
min: 1,
|
|
9614
|
+
max: 1
|
|
9615
|
+
},
|
|
9616
|
+
run: async (invocation) => {
|
|
9617
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9618
|
+
const organization = await run.organization();
|
|
9619
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9620
|
+
return printed(run, await verifiedAccount(run.api, organization.id, account.id), verificationSentence);
|
|
9621
|
+
}
|
|
9622
|
+
}),
|
|
9623
|
+
defineCommand({
|
|
9624
|
+
name: "catalog",
|
|
9625
|
+
summary: "List the regions of a provider account and the sizes a node can have in one.",
|
|
9626
|
+
usage: "providers catalog <account> [--region <slug>]",
|
|
9627
|
+
optionsUsage: [
|
|
9628
|
+
...accountUsage,
|
|
9629
|
+
["--region <slug>", "the region whose sizes to list (default: the account's)"],
|
|
9630
|
+
["", "only regions with volumes and sizes with at least 2 GB of memory"]
|
|
9631
|
+
],
|
|
9632
|
+
options: regionOption,
|
|
9633
|
+
positionals: {
|
|
9634
|
+
min: 1,
|
|
9635
|
+
max: 1
|
|
9636
|
+
},
|
|
9637
|
+
run: async (invocation) => {
|
|
9638
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9639
|
+
const organization = await run.organization();
|
|
9640
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9641
|
+
const catalog = await providerCatalogOf(run.api, organization.id, account.id);
|
|
9642
|
+
return printed(run, catalogInRegion(catalog, invocation.values.region ?? account.defaultRegion), catalogText);
|
|
9643
|
+
}
|
|
9644
|
+
}),
|
|
9645
|
+
defineCommand({
|
|
9646
|
+
name: "delete",
|
|
9647
|
+
summary: "Delete a provider account no node was created with.",
|
|
9648
|
+
usage: "providers delete <account>",
|
|
9649
|
+
optionsUsage: accountUsage,
|
|
9650
|
+
options: {},
|
|
9651
|
+
positionals: {
|
|
9652
|
+
min: 1,
|
|
9653
|
+
max: 1
|
|
9654
|
+
},
|
|
9655
|
+
run: async (invocation) => {
|
|
9656
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9657
|
+
const organization = await run.organization();
|
|
9658
|
+
const given = invocation.positionals[0] ?? "";
|
|
9659
|
+
const account = await resolveProviderAccount(run, given);
|
|
8520
9660
|
await confirmed({
|
|
8521
9661
|
prompt: run.prompt,
|
|
8522
9662
|
settings: run.settings,
|
|
8523
|
-
question: `Delete the
|
|
9663
|
+
question: `Delete the provider account ${given}? The platform erases its token.`
|
|
8524
9664
|
});
|
|
8525
|
-
await
|
|
8526
|
-
return said(run, `Deleted the
|
|
9665
|
+
await deleteProviderAccount(run.api, organization.id, account.id);
|
|
9666
|
+
return said(run, `Deleted the provider account ${given}.`);
|
|
8527
9667
|
}
|
|
8528
9668
|
})
|
|
8529
9669
|
]
|
|
@@ -8575,6 +9715,7 @@ const kcloudCommands = () => [
|
|
|
8575
9715
|
createCredentialsCommand(createAdapters),
|
|
8576
9716
|
createRepositoriesCommand(createAdapters),
|
|
8577
9717
|
createToolsCommand(createAdapters),
|
|
9718
|
+
createProvidersCommand(createAdapters),
|
|
8578
9719
|
createNodesCommand(createAdapters),
|
|
8579
9720
|
createAgentsCommand(createAdapters),
|
|
8580
9721
|
createNotificationsCommand(createAdapters),
|