@tokenbuddy/tb-admin 1.0.15 → 1.0.28
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/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +323 -14
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +12 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +12 -8
- package/dist/src/client.js.map +1 -1
- package/dist/src/display-format.d.ts +39 -0
- package/dist/src/display-format.d.ts.map +1 -0
- package/dist/src/display-format.js +354 -0
- package/dist/src/display-format.js.map +1 -0
- package/dist/src/server-cmd.d.ts +3 -0
- package/dist/src/server-cmd.d.ts.map +1 -1
- package/dist/src/server-cmd.js +32 -9
- package/dist/src/server-cmd.js.map +1 -1
- package/dist/src/ui-actions.d.ts +2 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +123 -63
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-command.js +1 -1
- package/dist/src/ui-command.js.map +1 -1
- package/dist/src/ui-server.d.ts +0 -1
- package/dist/src/ui-server.d.ts.map +1 -1
- package/dist/src/ui-server.js +25 -9
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +7 -1
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +55 -24
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +371 -46
- package/dist/src/ui-static.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +367 -14
- package/src/client.ts +13 -8
- package/src/display-format.ts +398 -0
- package/src/server-cmd.ts +35 -9
- package/src/ui-actions.ts +129 -72
- package/src/ui-command.ts +1 -1
- package/src/ui-server.ts +24 -10
- package/src/ui-state.ts +64 -25
- package/src/ui-static.ts +374 -46
- package/tests/admin.test.ts +590 -41
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-static.js","sourceRoot":"","sources":["../../src/ui-static.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW;IACzB,OAAO
|
|
1
|
+
{"version":3,"file":"ui-static.js","sourceRoot":"","sources":["../../src/ui-static.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,qEAAqE;AACrE,qEAAqE;AACrE,0DAA0D;AAE1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,UAAU,WAAW;IACzB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA8RG,aAAa,EAAE;;QAEnB,CAAC;AACT,CAAC;AAED,SAAS,aAAa;IACpB,gFAAgF;IAChF,OAAO;EACP,mBAAmB,EAAE;EACrB,mBAAmB,EAAE;CACtB,CAAC;AACF,CAAC;AAED,SAAS,mBAAmB;IAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiKR,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "./bootstrap-registry.js";
|
|
11
11
|
import Table from "cli-table3";
|
|
12
12
|
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
13
15
|
import YAML from "js-yaml";
|
|
14
16
|
|
|
15
17
|
interface BootstrapConfigDocument {
|
|
@@ -33,6 +35,44 @@ interface BootstrapConfigDocument {
|
|
|
33
35
|
|
|
34
36
|
type SellerConfigDocument = Record<string, any>;
|
|
35
37
|
|
|
38
|
+
function currentModuleDir(): string {
|
|
39
|
+
if (typeof __dirname !== "undefined") {
|
|
40
|
+
return __dirname;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const stack = new Error().stack || "";
|
|
44
|
+
const fileUrlMatch = stack.match(/(file:\/\/\/[^)\n]+\/cli\.js):\d+:\d+/);
|
|
45
|
+
if (fileUrlMatch) {
|
|
46
|
+
return path.dirname(fileURLToPath(fileUrlMatch[1]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filePathMatch = stack.match(/(\/[^)\n]+\/cli\.(?:js|ts)):\d+:\d+/);
|
|
50
|
+
if (filePathMatch) {
|
|
51
|
+
return path.dirname(filePathMatch[1]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return process.cwd();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readAdminPackageVersion(): string {
|
|
58
|
+
let current = path.resolve(currentModuleDir(), "..");
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
while (!seen.has(current)) {
|
|
61
|
+
seen.add(current);
|
|
62
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
63
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
64
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
|
|
65
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
66
|
+
return packageJson.version;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const parent = path.dirname(current);
|
|
70
|
+
if (parent === current) break;
|
|
71
|
+
current = parent;
|
|
72
|
+
}
|
|
73
|
+
return "0.0.0";
|
|
74
|
+
}
|
|
75
|
+
|
|
36
76
|
function requireString(value: unknown, field: string): string {
|
|
37
77
|
const normalized = String(value || "").trim();
|
|
38
78
|
if (!normalized) {
|
|
@@ -143,11 +183,48 @@ function loadYamlOrJsonFile(filePath: string): any {
|
|
|
143
183
|
return YAML.load(fs.readFileSync(filePath, "utf8"));
|
|
144
184
|
}
|
|
145
185
|
|
|
186
|
+
function loadSellerRegistryEntryFile(filePath: string): any {
|
|
187
|
+
const parsed = loadYamlOrJsonFile(filePath);
|
|
188
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
189
|
+
throw new Error("seller registry entry document is required");
|
|
190
|
+
}
|
|
191
|
+
validateRegistryDocument({ version: 1, sellers: [parsed as any] });
|
|
192
|
+
return parsed;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loadSellerRegistryPatchFile(filePath: string): any {
|
|
196
|
+
const parsed = loadYamlOrJsonFile(filePath);
|
|
197
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
198
|
+
throw new Error("seller registry patch document is required");
|
|
199
|
+
}
|
|
200
|
+
if ("id" in parsed) {
|
|
201
|
+
throw new Error("seller registry patch must not include id");
|
|
202
|
+
}
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function withExpectedVersion(body: Record<string, any>, expectedVersion: unknown): Record<string, any> {
|
|
207
|
+
if (expectedVersion === undefined) {
|
|
208
|
+
return body;
|
|
209
|
+
}
|
|
210
|
+
const version = Number(expectedVersion);
|
|
211
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
212
|
+
throw new Error("expected version must be >= 1");
|
|
213
|
+
}
|
|
214
|
+
return { ...body, expectedVersion: version };
|
|
215
|
+
}
|
|
216
|
+
|
|
146
217
|
function requireUpstreamModels(data: any): any[] {
|
|
147
|
-
if (
|
|
148
|
-
|
|
218
|
+
if (Array.isArray(data?.models)) {
|
|
219
|
+
return data.models;
|
|
220
|
+
}
|
|
221
|
+
if (Array.isArray(data?.upstreams)) {
|
|
222
|
+
const firstWithModels = data.upstreams.find((entry: any) => Array.isArray(entry?.models));
|
|
223
|
+
if (firstWithModels) {
|
|
224
|
+
return firstWithModels.models;
|
|
225
|
+
}
|
|
149
226
|
}
|
|
150
|
-
|
|
227
|
+
throw new Error("operator upstream summary must contain models array");
|
|
151
228
|
}
|
|
152
229
|
|
|
153
230
|
/**
|
|
@@ -162,7 +239,7 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
162
239
|
program
|
|
163
240
|
.name("tb-admin")
|
|
164
241
|
.description("Remote admin CLI for TokenBuddy seller apps")
|
|
165
|
-
.version(
|
|
242
|
+
.version(readAdminPackageVersion())
|
|
166
243
|
.option("--url <url>", "Remote seller core API url")
|
|
167
244
|
.option("--token <token>", "Operator Bearer token")
|
|
168
245
|
.option("--profile <profile>", "Use custom profile instead of default")
|
|
@@ -614,14 +691,19 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
614
691
|
// 6. Bootstrap Registry Command
|
|
615
692
|
const bootstrap = program.command("bootstrap").description("Manage wallet bootstrap service");
|
|
616
693
|
const bootstrapSellers = bootstrap.command("sellers").description("Manage public seller registry");
|
|
694
|
+
const bootstrapDefaultSeller = bootstrap.command("default-seller").description("Manage bootstrap default seller");
|
|
695
|
+
const bootstrapRegistry = bootstrap.command("registry").description("Manage signed registry versions and publishing");
|
|
617
696
|
const bootstrapConfig = bootstrap.command("config").description("Manage wallet bootstrap YAML config");
|
|
618
697
|
|
|
619
698
|
bootstrapSellers
|
|
620
699
|
.command("get")
|
|
621
|
-
.description("Fetch public seller registry")
|
|
622
|
-
.
|
|
700
|
+
.description("Fetch public seller registry or one seller by id")
|
|
701
|
+
.argument("[id]", "Seller id")
|
|
702
|
+
.action(async (id?: string) => {
|
|
623
703
|
try {
|
|
624
|
-
const data =
|
|
704
|
+
const data = id
|
|
705
|
+
? await getClient().get(`/operator/registry/sellers/${encodeURIComponent(id)}`)
|
|
706
|
+
: await publicGet("/registry/sellers") as SellerRegistryDocument;
|
|
625
707
|
console.log(JSON.stringify(data, null, 2));
|
|
626
708
|
} catch (err: any) {
|
|
627
709
|
console.error("Error:", err.message);
|
|
@@ -629,16 +711,129 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
629
711
|
}
|
|
630
712
|
});
|
|
631
713
|
|
|
714
|
+
bootstrapSellers
|
|
715
|
+
.command("list")
|
|
716
|
+
.description("List seller entries from the operator registry")
|
|
717
|
+
.action(async () => {
|
|
718
|
+
try {
|
|
719
|
+
const data = await getClient().get("/operator/registry/sellers") as { sellers: any[] };
|
|
720
|
+
const table = new Table({ head: ["ID", "Status", "URL", "Models", "Protocols", "Payments"] });
|
|
721
|
+
for (const seller of data.sellers || []) {
|
|
722
|
+
table.push([
|
|
723
|
+
seller.id,
|
|
724
|
+
seller.status || "active",
|
|
725
|
+
seller.url,
|
|
726
|
+
String(seller.models?.length || seller.modelsCount || 0),
|
|
727
|
+
(seller.supportedProtocols || []).join(","),
|
|
728
|
+
(seller.paymentMethods || []).join(",")
|
|
729
|
+
]);
|
|
730
|
+
}
|
|
731
|
+
console.log(table.toString());
|
|
732
|
+
} catch (err: any) {
|
|
733
|
+
console.error("Error:", err.message);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
bootstrapSellers
|
|
739
|
+
.command("add")
|
|
740
|
+
.description("Add one seller entry without replacing the full registry")
|
|
741
|
+
.requiredOption("--file <path>", "Seller entry YAML/JSON file")
|
|
742
|
+
.requiredOption("--expect-version <n>", "Expected current registry version")
|
|
743
|
+
.option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
|
|
744
|
+
.action(async (options) => {
|
|
745
|
+
try {
|
|
746
|
+
const seller = loadSellerRegistryEntryFile(options.file);
|
|
747
|
+
const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
|
|
748
|
+
const response = await getClient().post(
|
|
749
|
+
"/operator/registry/sellers",
|
|
750
|
+
withExpectedVersion(seller, options.expectVersion),
|
|
751
|
+
headers
|
|
752
|
+
) as SellerRegistryDocument;
|
|
753
|
+
console.log(`Added seller ${seller.id}: version=${response.version} sellers=${response.sellers.length} publish=pending`);
|
|
754
|
+
} catch (err: any) {
|
|
755
|
+
console.error("Error:", err.message);
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
bootstrapSellers
|
|
761
|
+
.command("update <id>")
|
|
762
|
+
.description("Patch one seller entry without replacing the full registry")
|
|
763
|
+
.requiredOption("--file <path>", "Seller patch YAML/JSON file")
|
|
764
|
+
.requiredOption("--expect-version <n>", "Expected current registry version")
|
|
765
|
+
.option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
|
|
766
|
+
.action(async (id, options) => {
|
|
767
|
+
try {
|
|
768
|
+
const patch = loadSellerRegistryPatchFile(options.file);
|
|
769
|
+
const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
|
|
770
|
+
const response = await getClient().patch(
|
|
771
|
+
`/operator/registry/sellers/${encodeURIComponent(id)}`,
|
|
772
|
+
withExpectedVersion(patch, options.expectVersion),
|
|
773
|
+
headers
|
|
774
|
+
) as SellerRegistryDocument;
|
|
775
|
+
console.log(`Updated seller ${id}: version=${response.version} publish=pending`);
|
|
776
|
+
} catch (err: any) {
|
|
777
|
+
console.error("Error:", err.message);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
bootstrapSellers
|
|
783
|
+
.command("status <id> <status>")
|
|
784
|
+
.description("Set one seller registry status: active, draining, or offline")
|
|
785
|
+
.requiredOption("--expect-version <n>", "Expected current registry version")
|
|
786
|
+
.option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
|
|
787
|
+
.action(async (id, status, options) => {
|
|
788
|
+
try {
|
|
789
|
+
const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
|
|
790
|
+
const response = await getClient().put(
|
|
791
|
+
`/operator/registry/sellers/${encodeURIComponent(id)}/status`,
|
|
792
|
+
withExpectedVersion({ status }, options.expectVersion),
|
|
793
|
+
headers
|
|
794
|
+
) as SellerRegistryDocument;
|
|
795
|
+
console.log(`Set seller ${id} status=${status}: version=${response.version} publish=pending`);
|
|
796
|
+
} catch (err: any) {
|
|
797
|
+
console.error("Error:", err.message);
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
bootstrapSellers
|
|
803
|
+
.command("remove <id>")
|
|
804
|
+
.description("Remove one non-active seller entry")
|
|
805
|
+
.requiredOption("--expect-version <n>", "Expected current registry version")
|
|
806
|
+
.option("--force", "Allow forced removal when the service permits it")
|
|
807
|
+
.option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
|
|
808
|
+
.action(async (id, options) => {
|
|
809
|
+
try {
|
|
810
|
+
const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
|
|
811
|
+
const response = await getClient().delete(
|
|
812
|
+
`/operator/registry/sellers/${encodeURIComponent(id)}`,
|
|
813
|
+
withExpectedVersion({ force: Boolean(options.force) }, options.expectVersion),
|
|
814
|
+
headers
|
|
815
|
+
) as SellerRegistryDocument;
|
|
816
|
+
console.log(`Removed seller ${id}: version=${response.version} sellers=${response.sellers.length} publish=pending`);
|
|
817
|
+
} catch (err: any) {
|
|
818
|
+
console.error("Error:", err.message);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
|
|
632
823
|
bootstrapSellers
|
|
633
824
|
.command("put")
|
|
634
|
-
.description("
|
|
825
|
+
.description("Deprecated: dangerously replace public seller registry")
|
|
635
826
|
.requiredOption("--file <path>", "Seller registry JSON file")
|
|
827
|
+
.option("--force", "Acknowledge this full replacement is dangerous")
|
|
636
828
|
.action(async (options) => {
|
|
637
829
|
try {
|
|
830
|
+
if (!options.force) {
|
|
831
|
+
throw new Error("bootstrap sellers put is deprecated; use bootstrap registry import --dry-run first, then --force");
|
|
832
|
+
}
|
|
638
833
|
const document = loadRegistryFile(options.file);
|
|
639
834
|
const client = getClient();
|
|
640
835
|
const response = await client.put("/operator/registry/sellers", document) as SellerRegistryDocument;
|
|
641
|
-
console.log(`
|
|
836
|
+
console.log(`Dangerously replaced seller registry: version=${response.version} sellers=${response.sellers.length} publish=pending`);
|
|
642
837
|
} catch (err: any) {
|
|
643
838
|
console.error("Error:", err.message);
|
|
644
839
|
process.exit(1);
|
|
@@ -660,6 +855,159 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
660
855
|
}
|
|
661
856
|
});
|
|
662
857
|
|
|
858
|
+
bootstrapDefaultSeller
|
|
859
|
+
.command("set <id>")
|
|
860
|
+
.description("Set default seller")
|
|
861
|
+
.requiredOption("--expect-version <n>", "Expected current registry version")
|
|
862
|
+
.option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
|
|
863
|
+
.action(async (id, options) => {
|
|
864
|
+
try {
|
|
865
|
+
const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
|
|
866
|
+
const response = await getClient().put(
|
|
867
|
+
"/operator/registry/default-seller",
|
|
868
|
+
withExpectedVersion({ sellerId: id }, options.expectVersion),
|
|
869
|
+
headers
|
|
870
|
+
) as SellerRegistryDocument;
|
|
871
|
+
console.log(`Set default seller ${id}: version=${response.version} publish=pending`);
|
|
872
|
+
} catch (err: any) {
|
|
873
|
+
console.error("Error:", err.message);
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
bootstrapRegistry
|
|
879
|
+
.command("diff")
|
|
880
|
+
.description("Compare a local registry JSON file against the operator registry")
|
|
881
|
+
.requiredOption("--file <path>", "Seller registry JSON file")
|
|
882
|
+
.action(async (options) => {
|
|
883
|
+
try {
|
|
884
|
+
const local = loadRegistryFile(options.file);
|
|
885
|
+
const remote = await getClient().get("/operator/registry") as SellerRegistryDocument;
|
|
886
|
+
const localIds = new Set(local.sellers.map((seller) => seller.id));
|
|
887
|
+
const remoteIds = new Set(remote.sellers.map((seller) => seller.id));
|
|
888
|
+
const added = local.sellers.filter((seller) => !remoteIds.has(seller.id)).map((seller) => seller.id);
|
|
889
|
+
const removed = remote.sellers.filter((seller) => !localIds.has(seller.id)).map((seller) => seller.id);
|
|
890
|
+
const changed = local.sellers
|
|
891
|
+
.filter((seller) => remoteIds.has(seller.id))
|
|
892
|
+
.filter((seller) => JSON.stringify(seller) !== JSON.stringify(remote.sellers.find((entry) => entry.id === seller.id)))
|
|
893
|
+
.map((seller) => seller.id);
|
|
894
|
+
console.log(JSON.stringify({
|
|
895
|
+
remoteVersion: remote.version,
|
|
896
|
+
localVersion: local.version,
|
|
897
|
+
added,
|
|
898
|
+
removed,
|
|
899
|
+
changed,
|
|
900
|
+
defaultSellerChanged: remote.defaultSeller !== local.defaultSeller
|
|
901
|
+
}, null, 2));
|
|
902
|
+
} catch (err: any) {
|
|
903
|
+
console.error("Error:", err.message);
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
bootstrapRegistry
|
|
909
|
+
.command("import")
|
|
910
|
+
.description("Import a full registry JSON as a dangerous disaster-recovery operation")
|
|
911
|
+
.requiredOption("--file <path>", "Seller registry JSON file")
|
|
912
|
+
.option("--dry-run", "Only validate and show diff")
|
|
913
|
+
.option("--force", "Actually perform the full replacement")
|
|
914
|
+
.option("--expect-version <n>", "Expected current registry version before import")
|
|
915
|
+
.option("--force-delete-count <n>", "Maximum allowed delete count when forcing", "1")
|
|
916
|
+
.action(async (options) => {
|
|
917
|
+
try {
|
|
918
|
+
const local = loadRegistryFile(options.file);
|
|
919
|
+
const remote = await getClient().get("/operator/registry") as SellerRegistryDocument;
|
|
920
|
+
const localIds = new Set(local.sellers.map((seller) => seller.id));
|
|
921
|
+
const removed = remote.sellers.filter((seller) => !localIds.has(seller.id));
|
|
922
|
+
const forceDeleteCount = Number(options.forceDeleteCount);
|
|
923
|
+
if (!Number.isInteger(forceDeleteCount) || forceDeleteCount < 0) {
|
|
924
|
+
throw new Error("--force-delete-count must be >= 0");
|
|
925
|
+
}
|
|
926
|
+
console.log(`Import diff: remoteVersion=${remote.version} localVersion=${local.version} removes=${removed.length}`);
|
|
927
|
+
if (options.dryRun || !options.force) {
|
|
928
|
+
if (!options.dryRun) {
|
|
929
|
+
throw new Error("refusing full import without --force; run with --dry-run first");
|
|
930
|
+
}
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (removed.length > forceDeleteCount) {
|
|
934
|
+
throw new Error(`import would remove ${removed.length} sellers; pass --force-delete-count ${removed.length} to allow`);
|
|
935
|
+
}
|
|
936
|
+
const body = withExpectedVersion(local as any, options.expectVersion);
|
|
937
|
+
const response = await getClient().put("/operator/registry/sellers", body) as SellerRegistryDocument;
|
|
938
|
+
console.log(`Imported seller registry: version=${response.version} sellers=${response.sellers.length} publish=pending`);
|
|
939
|
+
} catch (err: any) {
|
|
940
|
+
console.error("Error:", err.message);
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
bootstrapRegistry
|
|
946
|
+
.command("publish")
|
|
947
|
+
.description("Publish the current signed registry to R2")
|
|
948
|
+
.option("--version <n>", "Registry version to publish")
|
|
949
|
+
.action(async (options) => {
|
|
950
|
+
try {
|
|
951
|
+
const version = options.version ? Number(options.version) : undefined;
|
|
952
|
+
if (version !== undefined && (!Number.isInteger(version) || version < 1)) {
|
|
953
|
+
throw new Error("--version must be a positive integer");
|
|
954
|
+
}
|
|
955
|
+
const body = version === undefined ? {} : { version };
|
|
956
|
+
const response = await getClient().post("/operator/registry/publish", body) as {
|
|
957
|
+
version: number;
|
|
958
|
+
registrySha256: string;
|
|
959
|
+
signingKeyId: string;
|
|
960
|
+
artifacts?: Array<{ key: string; url: string }>;
|
|
961
|
+
};
|
|
962
|
+
console.log(`Published registry version=${response.version} sha256=${response.registrySha256} signingKey=${response.signingKeyId}`);
|
|
963
|
+
for (const artifact of response.artifacts || []) {
|
|
964
|
+
console.log(` ${artifact.key} -> ${artifact.url}`);
|
|
965
|
+
}
|
|
966
|
+
} catch (err: any) {
|
|
967
|
+
console.error("Error:", err.message);
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
bootstrapRegistry
|
|
973
|
+
.command("versions")
|
|
974
|
+
.description("List registry versions")
|
|
975
|
+
.option("--limit <n>", "Maximum versions to list", "20")
|
|
976
|
+
.action(async (options) => {
|
|
977
|
+
try {
|
|
978
|
+
const limit = Number(options.limit);
|
|
979
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
980
|
+
throw new Error("--limit must be a positive integer");
|
|
981
|
+
}
|
|
982
|
+
const response = await getClient().get(`/operator/registry/versions?limit=${encodeURIComponent(String(limit))}`) as {
|
|
983
|
+
versions?: Array<{
|
|
984
|
+
version: number;
|
|
985
|
+
registrySha256: string;
|
|
986
|
+
signingKeyId?: string;
|
|
987
|
+
signed?: boolean;
|
|
988
|
+
sellerCount?: number;
|
|
989
|
+
defaultSeller?: string;
|
|
990
|
+
createdAt?: string;
|
|
991
|
+
publishedAt?: string;
|
|
992
|
+
}>;
|
|
993
|
+
};
|
|
994
|
+
for (const version of response.versions || []) {
|
|
995
|
+
console.log([
|
|
996
|
+
`version=${version.version}`,
|
|
997
|
+
`sellers=${version.sellerCount ?? "?"}`,
|
|
998
|
+
`default=${version.defaultSeller || "-"}`,
|
|
999
|
+
`signed=${version.signed ? "yes" : "no"}`,
|
|
1000
|
+
`published=${version.publishedAt || "pending"}`,
|
|
1001
|
+
`sha256=${version.registrySha256}`,
|
|
1002
|
+
`key=${version.signingKeyId || "-"}`
|
|
1003
|
+
].join(" "));
|
|
1004
|
+
}
|
|
1005
|
+
} catch (err: any) {
|
|
1006
|
+
console.error("Error:", err.message);
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
663
1011
|
bootstrapConfig
|
|
664
1012
|
.command("get")
|
|
665
1013
|
.description("Fetch wallet bootstrap runtime config")
|
|
@@ -765,14 +1113,19 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
765
1113
|
|
|
766
1114
|
// 8. Seller Command (Fly.io)
|
|
767
1115
|
const sellerCmd = program.command("seller").description("Deploy and manage seller containers on Fly.io");
|
|
768
|
-
|
|
1116
|
+
|
|
1117
|
+
function getFlyProvider(): FlyProvider {
|
|
1118
|
+
const opts = program.opts();
|
|
1119
|
+
const mgr = opts.config ? new ConfigManager(opts.config) : configManager;
|
|
1120
|
+
return new FlyProvider(mgr.getSellerProvider("fly"));
|
|
1121
|
+
}
|
|
769
1122
|
|
|
770
1123
|
sellerCmd
|
|
771
1124
|
.command("ls")
|
|
772
1125
|
.description("List all deployed apps on Fly.io")
|
|
773
1126
|
.action(() => {
|
|
774
1127
|
try {
|
|
775
|
-
const out =
|
|
1128
|
+
const out = getFlyProvider().listApps();
|
|
776
1129
|
console.log(out);
|
|
777
1130
|
} catch (err: any) {
|
|
778
1131
|
console.error("Error:", err.message);
|
|
@@ -796,7 +1149,7 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
796
1149
|
.option("--dry-run", "Dry run display without actual execution")
|
|
797
1150
|
.action((name, options) => {
|
|
798
1151
|
try {
|
|
799
|
-
const res =
|
|
1152
|
+
const res = getFlyProvider().createSeller({
|
|
800
1153
|
name,
|
|
801
1154
|
app: options.app,
|
|
802
1155
|
region: options.region,
|
|
@@ -824,7 +1177,7 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
824
1177
|
.option("--dry-run", "Dry run")
|
|
825
1178
|
.action((app, options) => {
|
|
826
1179
|
try {
|
|
827
|
-
const res =
|
|
1180
|
+
const res = getFlyProvider().deploySeller({
|
|
828
1181
|
app,
|
|
829
1182
|
image: options.image,
|
|
830
1183
|
dryRun: options.dryRun
|
|
@@ -845,7 +1198,7 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
845
1198
|
.action((name, options) => {
|
|
846
1199
|
try {
|
|
847
1200
|
const appName = options.app || name;
|
|
848
|
-
const res =
|
|
1201
|
+
const res = getFlyProvider().removeSeller(appName, options.dryRun);
|
|
849
1202
|
console.log(res);
|
|
850
1203
|
if (options.removeProfile && !options.dryRun) {
|
|
851
1204
|
const appBase = appName.includes("-") ? appName.replace(/^tb-seller-/, "") : appName;
|
package/src/client.ts
CHANGED
|
@@ -11,11 +11,12 @@ export class AdminClient {
|
|
|
11
11
|
this.token = token;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
private async request(path: string, method: string, body?: any): Promise<any> {
|
|
14
|
+
private async request(path: string, method: string, body?: any, extraHeaders: { [key: string]: string } = {}): Promise<any> {
|
|
15
15
|
const url = `${this.baseUrl}${path}`;
|
|
16
16
|
const headers: { [key: string]: string } = {
|
|
17
17
|
"Content-Type": "application/json",
|
|
18
|
-
"Authorization": `Bearer ${this.token}
|
|
18
|
+
"Authorization": `Bearer ${this.token}`,
|
|
19
|
+
...extraHeaders
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
const options: RequestInit = {
|
|
@@ -44,15 +45,19 @@ export class AdminClient {
|
|
|
44
45
|
return this.request(path, "GET");
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
public async put(path: string, body?: any): Promise<any> {
|
|
48
|
-
return this.request(path, "PUT", body);
|
|
48
|
+
public async put(path: string, body?: any, headers?: { [key: string]: string }): Promise<any> {
|
|
49
|
+
return this.request(path, "PUT", body, headers);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
public async post(path: string, body?: any): Promise<any> {
|
|
52
|
-
return this.request(path, "POST", body);
|
|
52
|
+
public async post(path: string, body?: any, headers?: { [key: string]: string }): Promise<any> {
|
|
53
|
+
return this.request(path, "POST", body, headers);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
public async
|
|
56
|
-
return this.request(path, "
|
|
56
|
+
public async patch(path: string, body?: any, headers?: { [key: string]: string }): Promise<any> {
|
|
57
|
+
return this.request(path, "PATCH", body, headers);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async delete(path: string, body?: any, headers?: { [key: string]: string }): Promise<any> {
|
|
61
|
+
return this.request(path, "DELETE", body, headers);
|
|
57
62
|
}
|
|
58
63
|
}
|