airfoil 0.4.0 → 0.4.1
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/index.js +401 -739
- package/package.json +9 -6
- package/src/commands/cluster/index.ts +13 -37
- package/src/commands/cluster/namespace/create.ts +278 -61
- package/src/commands/cluster/namespace/delete.ts +9 -13
- package/src/commands/cluster/namespace/get.ts +12 -14
- package/src/commands/cluster/namespace/list.ts +16 -34
- package/src/commands/cluster/{topic → table}/create.ts +79 -96
- package/src/commands/cluster/{tenant → table}/delete.ts +15 -17
- package/src/commands/cluster/table/get.ts +62 -0
- package/src/commands/cluster/table/list.ts +61 -0
- package/src/commands/dev.ts +17 -13
- package/src/commands/sql.ts +13 -23
- package/src/index.ts +6 -6
- package/src/utils/client.ts +3 -3
- package/src/utils/docker.ts +101 -7
- package/src/utils/options.ts +1 -1
- package/src/utils/wings.ts +18 -14
- package/src/commands/cluster/data-lake/create-iceberg.ts +0 -59
- package/src/commands/cluster/data-lake/create-parquet.ts +0 -59
- package/src/commands/cluster/data-lake/create.ts +0 -10
- package/src/commands/cluster/data-lake/delete.ts +0 -54
- package/src/commands/cluster/data-lake/get.ts +0 -40
- package/src/commands/cluster/data-lake/list.ts +0 -62
- package/src/commands/cluster/object-store/create-aws.ts +0 -104
- package/src/commands/cluster/object-store/create-azure.ts +0 -96
- package/src/commands/cluster/object-store/create-google.ts +0 -87
- package/src/commands/cluster/object-store/create-s3.ts +0 -114
- package/src/commands/cluster/object-store/create.ts +0 -21
- package/src/commands/cluster/object-store/delete.ts +0 -56
- package/src/commands/cluster/object-store/get.ts +0 -49
- package/src/commands/cluster/object-store/list.ts +0 -62
- package/src/commands/cluster/tenant/create.ts +0 -44
- package/src/commands/cluster/tenant/get.ts +0 -40
- package/src/commands/cluster/tenant/list.ts +0 -53
- package/src/commands/cluster/topic/delete.ts +0 -65
- package/src/commands/cluster/topic/get.ts +0 -51
- package/src/commands/cluster/topic/list.ts +0 -61
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
3
|
-
import { Effect, FileSystem, Option, Path, String } from "effect";
|
|
3
|
+
import { Config, Effect, FileSystem, Layer, Option, Path, Redacted, Stream, String as String$1 } from "effect";
|
|
4
4
|
import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
|
|
5
5
|
import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http";
|
|
6
6
|
import * as p from "@clack/prompts";
|
|
7
|
-
import {
|
|
7
|
+
import { ClusterClient } from "@useairfoil/wings";
|
|
8
8
|
import { printTable } from "console-table-printer";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
@@ -15,18 +15,18 @@ import { ArrowFlightSqlClient } from "@useairfoil/flight";
|
|
|
15
15
|
import { Metadata } from "nice-grpc-common";
|
|
16
16
|
|
|
17
17
|
//#region package.json
|
|
18
|
-
var version$1 = "0.
|
|
18
|
+
var version$1 = "0.4.0";
|
|
19
19
|
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/utils/client.ts
|
|
22
|
-
const
|
|
22
|
+
const makeClusterClientLayer = (host, port) => ClusterClient.layer({ host: `${host}:${port}` });
|
|
23
23
|
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/utils/options.ts
|
|
26
26
|
/**
|
|
27
27
|
* Common server connection options
|
|
28
28
|
*/
|
|
29
|
-
const hostOption = Flag.string("host").pipe(Flag.withDescription("Server host"), Flag.withDefault("
|
|
29
|
+
const hostOption = Flag.string("host").pipe(Flag.withDescription("Server host"), Flag.withDefault("127.0.0.1"));
|
|
30
30
|
const portOption = Flag.integer("port").pipe(Flag.withDescription("Server port"), Flag.withAlias("p"), Flag.withDefault(7777));
|
|
31
31
|
/**
|
|
32
32
|
* Common pagination options
|
|
@@ -39,674 +39,263 @@ const pageTokenOption = Flag.optional(Flag.string("page-token").pipe(Flag.withDe
|
|
|
39
39
|
const forceOption = Flag.boolean("force").pipe(Flag.withDescription("Skip confirmation prompt"), Flag.withDefault(false));
|
|
40
40
|
|
|
41
41
|
//#endregion
|
|
42
|
-
//#region src/commands/cluster/
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
port: portOption
|
|
50
|
-
}, ({ parent, dataLakeId, host, port }) => Effect.gen(function* () {
|
|
51
|
-
p.intro("🏞️ Create Iceberg Data Lake");
|
|
52
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
53
|
-
const s = p.spinner();
|
|
54
|
-
s.start("Creating Iceberg data lake...");
|
|
55
|
-
const result = yield* WingsClusterMetadata.createDataLake({
|
|
56
|
-
parent,
|
|
57
|
-
dataLakeId,
|
|
58
|
-
dataLakeConfig: {
|
|
42
|
+
//#region src/commands/cluster/namespace/create.ts
|
|
43
|
+
const namespaceIdOption = Flag.string("namespace-id").pipe(Flag.withDescription("Unique identifier for the namespace (e.g., 'production')"));
|
|
44
|
+
const lakeOption = Flag.string("lake").pipe(Flag.withDescription("Lake format: iceberg, parquet, or delta"), Flag.withDefault("iceberg"));
|
|
45
|
+
const prefixOption = Flag.string("prefix").pipe(Flag.withDescription("Storage prefix (optional)"), Flag.optional);
|
|
46
|
+
function makeLakeConfig(lake) {
|
|
47
|
+
switch (lake) {
|
|
48
|
+
case "iceberg": return { lakeConfig: {
|
|
59
49
|
_tag: "iceberg",
|
|
60
50
|
iceberg: {}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
s.stop("Iceberg data lake created successfully");
|
|
64
|
-
yield* Effect.sync(() => {
|
|
65
|
-
printTable([{
|
|
66
|
-
name: result.name,
|
|
67
|
-
type: "Iceberg"
|
|
68
|
-
}]);
|
|
69
|
-
p.outro("✓ Done");
|
|
70
|
-
});
|
|
71
|
-
})).pipe(Command.withDescription("Create a new Iceberg data lake"));
|
|
72
|
-
|
|
73
|
-
//#endregion
|
|
74
|
-
//#region src/commands/cluster/data-lake/create-parquet.ts
|
|
75
|
-
const parentOption$10 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
|
|
76
|
-
const dataLakeIdOption = Flag.string("data-lake-id").pipe(Flag.withDescription("Unique identifier for the data lake"));
|
|
77
|
-
const createDataLakeParquetCommand = Command.make("parquet", {
|
|
78
|
-
parent: parentOption$10,
|
|
79
|
-
dataLakeId: dataLakeIdOption,
|
|
80
|
-
host: hostOption,
|
|
81
|
-
port: portOption
|
|
82
|
-
}, ({ parent, dataLakeId, host, port }) => Effect.gen(function* () {
|
|
83
|
-
p.intro("🏞️ Create Parquet Data Lake");
|
|
84
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
85
|
-
const s = p.spinner();
|
|
86
|
-
s.start("Creating Parquet data lake...");
|
|
87
|
-
const result = yield* WingsClusterMetadata.createDataLake({
|
|
88
|
-
parent,
|
|
89
|
-
dataLakeId,
|
|
90
|
-
dataLakeConfig: {
|
|
51
|
+
} };
|
|
52
|
+
case "parquet": return { lakeConfig: {
|
|
91
53
|
_tag: "parquet",
|
|
92
54
|
parquet: {}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
type: "Parquet"
|
|
100
|
-
}]);
|
|
101
|
-
p.outro("✓ Done");
|
|
102
|
-
});
|
|
103
|
-
})).pipe(Command.withDescription("Create a new Parquet data lake"));
|
|
104
|
-
|
|
105
|
-
//#endregion
|
|
106
|
-
//#region src/commands/cluster/data-lake/create.ts
|
|
107
|
-
const createDataLakeCommand = Command.make("create-data-lake", {}, () => Effect.void).pipe(Command.withDescription("Create a new data lake"), Command.withSubcommands([createDataLakeIcebergCommand, createDataLakeParquetCommand]));
|
|
108
|
-
|
|
109
|
-
//#endregion
|
|
110
|
-
//#region src/commands/cluster/data-lake/delete.ts
|
|
111
|
-
const nameOption$9 = Flag.string("name").pipe(Flag.withDescription("Data lake name in format: tenants/{tenant}/data-lakes/{data-lake}"));
|
|
112
|
-
const deleteDataLakeCommand = Command.make("delete-data-lake", {
|
|
113
|
-
name: nameOption$9,
|
|
114
|
-
force: forceOption,
|
|
115
|
-
host: hostOption,
|
|
116
|
-
port: portOption
|
|
117
|
-
}, ({ name, force, host, port }) => Effect.gen(function* () {
|
|
118
|
-
p.intro("🗑️ Delete Data Lake");
|
|
119
|
-
if (!force) {
|
|
120
|
-
const confirm = yield* Effect.tryPromise({
|
|
121
|
-
try: () => p.confirm({
|
|
122
|
-
message: `Are you sure you want to delete data lake ${name}?`,
|
|
123
|
-
initialValue: false
|
|
124
|
-
}),
|
|
125
|
-
catch: () => /* @__PURE__ */ new Error("Failed to confirm deletion")
|
|
126
|
-
});
|
|
127
|
-
if (p.isCancel(confirm) || !confirm) {
|
|
128
|
-
p.cancel("Deletion cancelled");
|
|
129
|
-
process.exit(0);
|
|
130
|
-
}
|
|
55
|
+
} };
|
|
56
|
+
case "delta": return { lakeConfig: {
|
|
57
|
+
_tag: "delta",
|
|
58
|
+
delta: {}
|
|
59
|
+
} };
|
|
60
|
+
default: throw new Error(`Invalid lake type: "${lake}". Must be iceberg, parquet, or delta.`);
|
|
131
61
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
62
|
+
}
|
|
63
|
+
function displayNamespace(namespace) {
|
|
64
|
+
printTable([{
|
|
65
|
+
name: namespace.name,
|
|
66
|
+
object_store: namespace.objectStore?.objectStoreConfig._tag ?? "-",
|
|
67
|
+
lake: namespace.lake?.lakeConfig._tag ?? "-"
|
|
68
|
+
}]);
|
|
137
69
|
p.outro("✓ Done");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
//#endregion
|
|
141
|
-
//#region src/commands/cluster/data-lake/get.ts
|
|
142
|
-
const nameOption$8 = Flag.string("name").pipe(Flag.withDescription("Data lake name in format: tenants/{tenant}/data-lakes/{data-lake}"));
|
|
143
|
-
const getDataLakeCommand = Command.make("get-data-lake", {
|
|
144
|
-
name: nameOption$8,
|
|
145
|
-
host: hostOption,
|
|
146
|
-
port: portOption
|
|
147
|
-
}, ({ name, host, port }) => Effect.gen(function* () {
|
|
148
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
149
|
-
const s = p.spinner();
|
|
150
|
-
s.start("Fetching data lake...");
|
|
151
|
-
const dataLake = yield* WingsClusterMetadata.getDataLake({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to get data lake"))));
|
|
152
|
-
s.stop("Data lake retrieved");
|
|
153
|
-
yield* Effect.sync(() => {
|
|
154
|
-
printTable([{
|
|
155
|
-
name: dataLake.name,
|
|
156
|
-
type: dataLake.dataLakeConfig._tag || "-"
|
|
157
|
-
}]);
|
|
158
|
-
p.outro("✓ Done");
|
|
159
|
-
});
|
|
160
|
-
})).pipe(Command.withDescription("Get details of a specific data lake"));
|
|
161
|
-
|
|
162
|
-
//#endregion
|
|
163
|
-
//#region src/commands/cluster/data-lake/list.ts
|
|
164
|
-
const parentOption$9 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
|
|
165
|
-
const listDataLakesCommand = Command.make("list-data-lakes", {
|
|
166
|
-
parent: parentOption$9,
|
|
167
|
-
pageSize: pageSizeOption,
|
|
168
|
-
pageToken: pageTokenOption,
|
|
169
|
-
host: hostOption,
|
|
170
|
-
port: portOption
|
|
171
|
-
}, ({ parent, pageSize, pageToken, host, port }) => Effect.gen(function* () {
|
|
172
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
173
|
-
const s = p.spinner();
|
|
174
|
-
s.start("Fetching data lakes...");
|
|
175
|
-
const response = yield* WingsClusterMetadata.listDataLakes({
|
|
176
|
-
parent,
|
|
177
|
-
pageSize,
|
|
178
|
-
pageToken: Option.getOrUndefined(pageToken)
|
|
179
|
-
}).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to list data lakes"))));
|
|
180
|
-
s.stop(`Found ${response.dataLakes.length} data lake(s)`);
|
|
181
|
-
yield* Effect.sync(() => {
|
|
182
|
-
if (response.dataLakes.length === 0) p.log.warn("No data lakes found");
|
|
183
|
-
else printTable(response.dataLakes.map((dataLake) => ({
|
|
184
|
-
name: dataLake.name,
|
|
185
|
-
type: dataLake.dataLakeConfig._tag || "-"
|
|
186
|
-
})));
|
|
187
|
-
if (response.nextPageToken) p.log.info(`Next page token: ${response.nextPageToken}`);
|
|
188
|
-
p.outro("✓ Done");
|
|
189
|
-
});
|
|
190
|
-
})).pipe(Command.withDescription("List all data lakes belonging to a tenant"));
|
|
191
|
-
|
|
192
|
-
//#endregion
|
|
193
|
-
//#region src/commands/cluster/namespace/create.ts
|
|
194
|
-
const parentOption$8 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant} (e.g., 'tenants/default')"));
|
|
195
|
-
const namespaceIdOption = Flag.string("namespace-id").pipe(Flag.withDescription("Unique identifier for the namespace (e.g., 'production')"));
|
|
196
|
-
const flushSizeBytesOption = Flag.integer("flush-size-bytes").pipe(Flag.withDescription("Size at which the current segment is flushed to object storage"), Flag.withDefault(0));
|
|
197
|
-
const flushIntervalMillisOption = Flag.integer("flush-interval-millis").pipe(Flag.withDescription("Maximum interval at which the current segment is flushed (milliseconds)"), Flag.withDefault(0));
|
|
198
|
-
const objectStoreOption = Flag.string("object-store").pipe(Flag.withDescription("Object store used by this namespace (format: tenants/{tenant}/object-stores/{object-store})"));
|
|
199
|
-
const dataLakeOption = Flag.string("data-lake").pipe(Flag.withDescription("Data lake used by this namespace (format: tenants/{tenant}/data-lakes/{data-lake})"));
|
|
200
|
-
const createNamespaceCommand = Command.make("create-namespace", {
|
|
201
|
-
parent: parentOption$8,
|
|
70
|
+
}
|
|
71
|
+
const createNamespaceAwsCommand = Command.make("aws", {
|
|
202
72
|
namespaceId: namespaceIdOption,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
73
|
+
lake: lakeOption,
|
|
74
|
+
bucketName: Flag.string("bucket-name").pipe(Flag.withDescription("AWS S3 bucket name")),
|
|
75
|
+
prefix: prefixOption,
|
|
76
|
+
accessKeyId: Flag.redacted("access-key-id").pipe(Flag.withDescription("AWS access key ID (or set AWS_ACCESS_KEY_ID env var)"), Flag.withFallbackConfig(Config.redacted("AWS_ACCESS_KEY_ID"))),
|
|
77
|
+
secretAccessKey: Flag.redacted("secret-access-key").pipe(Flag.withDescription("AWS secret access key (or set AWS_SECRET_ACCESS_KEY env var)"), Flag.withFallbackConfig(Config.redacted("AWS_SECRET_ACCESS_KEY"))),
|
|
78
|
+
region: Flag.string("region").pipe(Flag.withDescription("AWS region (e.g., us-east-1)"), Flag.optional),
|
|
207
79
|
host: hostOption,
|
|
208
80
|
port: portOption
|
|
209
|
-
}, ({
|
|
210
|
-
p.intro("📁 Create Namespace");
|
|
211
|
-
const
|
|
81
|
+
}, ({ namespaceId, lake, bucketName, prefix, accessKeyId, secretAccessKey, region }) => Effect.gen(function* () {
|
|
82
|
+
p.intro("📁 Create Namespace (AWS S3)");
|
|
83
|
+
const lakeConfig = yield* Effect.try({
|
|
84
|
+
try: () => makeLakeConfig(lake),
|
|
85
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
86
|
+
});
|
|
212
87
|
const s = p.spinner();
|
|
213
88
|
s.start("Creating namespace...");
|
|
214
|
-
const namespace = yield*
|
|
215
|
-
parent,
|
|
89
|
+
const namespace = yield* ClusterClient.createNamespace({
|
|
216
90
|
namespaceId,
|
|
217
|
-
|
|
218
|
-
flushIntervalMillis: BigInt(flushIntervalMillis),
|
|
219
|
-
objectStore,
|
|
220
|
-
dataLake
|
|
221
|
-
}).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create namespace"))));
|
|
222
|
-
s.stop("Namespace created successfully");
|
|
223
|
-
yield* Effect.sync(() => {
|
|
224
|
-
printTable([{
|
|
225
|
-
name: namespace.name,
|
|
226
|
-
flush_size_bytes: namespace.flushSizeBytes.toString(),
|
|
227
|
-
flush_interval_millis: namespace.flushIntervalMillis.toString(),
|
|
228
|
-
object_store: namespace.objectStore || "-",
|
|
229
|
-
data_lake: namespace.dataLake || "-"
|
|
230
|
-
}]);
|
|
231
|
-
p.outro("✓ Done");
|
|
232
|
-
});
|
|
233
|
-
})).pipe(Command.withDescription("Create a new namespace belonging to a tenant"));
|
|
234
|
-
|
|
235
|
-
//#endregion
|
|
236
|
-
//#region src/commands/cluster/namespace/delete.ts
|
|
237
|
-
const nameOption$7 = Flag.string("name").pipe(Flag.withDescription("Namespace name in format: tenants/{tenant}/namespaces/{namespace}"));
|
|
238
|
-
const deleteNamespaceCommand = Command.make("delete-namespace", {
|
|
239
|
-
name: nameOption$7,
|
|
240
|
-
force: forceOption,
|
|
241
|
-
host: hostOption,
|
|
242
|
-
port: portOption
|
|
243
|
-
}, ({ name, force, host, port }) => Effect.gen(function* () {
|
|
244
|
-
p.intro("🗑️ Delete Namespace");
|
|
245
|
-
if (!force) {
|
|
246
|
-
const confirm = yield* Effect.tryPromise({
|
|
247
|
-
try: () => p.confirm({
|
|
248
|
-
message: `Are you sure you want to delete namespace ${name}?`,
|
|
249
|
-
initialValue: false
|
|
250
|
-
}),
|
|
251
|
-
catch: () => /* @__PURE__ */ new Error("Failed to confirm deletion")
|
|
252
|
-
});
|
|
253
|
-
if (p.isCancel(confirm) || !confirm) {
|
|
254
|
-
p.cancel("Deletion cancelled");
|
|
255
|
-
process.exit(0);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
259
|
-
const s = p.spinner();
|
|
260
|
-
s.start("Deleting namespace...");
|
|
261
|
-
yield* WingsClusterMetadata.deleteNamespace({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete namespace"))));
|
|
262
|
-
s.stop("Namespace deleted successfully");
|
|
263
|
-
p.outro("✓ Done");
|
|
264
|
-
})).pipe(Command.withDescription("Delete a namespace from the cluster (fails if namespace has any topics)"));
|
|
265
|
-
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/commands/cluster/namespace/get.ts
|
|
268
|
-
const nameOption$6 = Flag.string("name").pipe(Flag.withDescription("Namespace name in format: tenants/{tenant}/namespaces/{namespace}"));
|
|
269
|
-
const getNamespaceCommand = Command.make("get-namespace", {
|
|
270
|
-
name: nameOption$6,
|
|
271
|
-
host: hostOption,
|
|
272
|
-
port: portOption
|
|
273
|
-
}, ({ name, host, port }) => Effect.gen(function* () {
|
|
274
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
275
|
-
const s = p.spinner();
|
|
276
|
-
s.start("Fetching namespace...");
|
|
277
|
-
const namespace = yield* WingsClusterMetadata.getNamespace({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to get namespace"))));
|
|
278
|
-
s.stop("Namespace retrieved");
|
|
279
|
-
yield* Effect.sync(() => {
|
|
280
|
-
printTable([{
|
|
281
|
-
name: namespace.name,
|
|
282
|
-
flush_size_bytes: namespace.flushSizeBytes.toString(),
|
|
283
|
-
flush_interval_millis: namespace.flushIntervalMillis.toString(),
|
|
284
|
-
object_store: namespace.objectStore || "-",
|
|
285
|
-
data_lake: namespace.dataLake || "-"
|
|
286
|
-
}]);
|
|
287
|
-
p.outro("✓ Done");
|
|
288
|
-
});
|
|
289
|
-
})).pipe(Command.withDescription("Get details of a specific namespace"));
|
|
290
|
-
|
|
291
|
-
//#endregion
|
|
292
|
-
//#region src/commands/cluster/namespace/list.ts
|
|
293
|
-
const parentOption$7 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant} (e.g., 'tenants/default')"));
|
|
294
|
-
const listNamespacesCommand = Command.make("list-namespaces", {
|
|
295
|
-
parent: parentOption$7,
|
|
296
|
-
pageSize: pageSizeOption,
|
|
297
|
-
pageToken: pageTokenOption,
|
|
298
|
-
host: hostOption,
|
|
299
|
-
port: portOption
|
|
300
|
-
}, ({ parent, pageSize, pageToken, host, port }) => Effect.gen(function* () {
|
|
301
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
302
|
-
const s = p.spinner();
|
|
303
|
-
s.start("Fetching namespaces...");
|
|
304
|
-
const response = yield* WingsClusterMetadata.listNamespaces({
|
|
305
|
-
parent,
|
|
306
|
-
pageSize,
|
|
307
|
-
pageToken: Option.getOrUndefined(pageToken)
|
|
308
|
-
}).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to list namespaces"))));
|
|
309
|
-
s.stop(`Found ${response.namespaces.length} namespace(s)`);
|
|
310
|
-
yield* Effect.sync(() => {
|
|
311
|
-
if (response.namespaces.length === 0) p.log.warn("No namespaces found");
|
|
312
|
-
else printTable(response.namespaces.map((namespace) => ({
|
|
313
|
-
name: namespace.name,
|
|
314
|
-
flush_size_bytes: namespace.flushSizeBytes.toString(),
|
|
315
|
-
flush_interval_millis: namespace.flushIntervalMillis.toString(),
|
|
316
|
-
object_store: namespace.objectStore || "-",
|
|
317
|
-
data_lake: namespace.dataLake || "-"
|
|
318
|
-
})));
|
|
319
|
-
if (response.nextPageToken) p.log.info(`Next page token: ${response.nextPageToken}`);
|
|
320
|
-
p.outro("✓ Done");
|
|
321
|
-
});
|
|
322
|
-
})).pipe(Command.withDescription("List all namespaces belonging to a tenant"));
|
|
323
|
-
|
|
324
|
-
//#endregion
|
|
325
|
-
//#region src/commands/cluster/object-store/create-aws.ts
|
|
326
|
-
const parentOption$6 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
|
|
327
|
-
const objectStoreIdOption$3 = Flag.string("object-store-id").pipe(Flag.withDescription("Unique identifier for the object store"));
|
|
328
|
-
const bucketNameOption$2 = Flag.string("bucket-name").pipe(Flag.withDescription("AWS S3 bucket name"));
|
|
329
|
-
const prefixOption$3 = Flag.string("prefix").pipe(Flag.withDescription("Bucket prefix (optional)"), Flag.optional);
|
|
330
|
-
const accessKeyIdOption$1 = Flag.string("access-key-id").pipe(Flag.withDescription("AWS_ACCESS_KEY_ID"));
|
|
331
|
-
const secretAccessKeyOption$1 = Flag.string("secret-access-key").pipe(Flag.withDescription("AWS_SECRET_ACCESS_KEY"));
|
|
332
|
-
const regionOption$1 = Flag.string("region").pipe(Flag.withDescription("AWS_DEFAULT_REGION (e.g., us-east-1)"), Flag.optional);
|
|
333
|
-
const createObjectStoreAwsCommand = Command.make("aws", {
|
|
334
|
-
parent: parentOption$6,
|
|
335
|
-
objectStoreId: objectStoreIdOption$3,
|
|
336
|
-
bucketName: bucketNameOption$2,
|
|
337
|
-
prefix: prefixOption$3,
|
|
338
|
-
accessKeyId: accessKeyIdOption$1,
|
|
339
|
-
secretAccessKey: secretAccessKeyOption$1,
|
|
340
|
-
region: regionOption$1,
|
|
341
|
-
host: hostOption,
|
|
342
|
-
port: portOption
|
|
343
|
-
}, ({ parent, objectStoreId, bucketName, prefix, accessKeyId, secretAccessKey, region, host, port }) => Effect.gen(function* () {
|
|
344
|
-
p.intro("🗄️ Create AWS S3 Object Store");
|
|
345
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
346
|
-
const s = p.spinner();
|
|
347
|
-
s.start("Creating AWS S3 object store...");
|
|
348
|
-
const result = yield* WingsClusterMetadata.createObjectStore({
|
|
349
|
-
parent,
|
|
350
|
-
objectStoreId,
|
|
351
|
-
objectStoreConfig: {
|
|
91
|
+
objectStore: { objectStoreConfig: {
|
|
352
92
|
_tag: "aws",
|
|
353
93
|
aws: {
|
|
354
94
|
bucketName,
|
|
355
95
|
prefix: Option.getOrUndefined(prefix),
|
|
356
|
-
accessKeyId,
|
|
357
|
-
secretAccessKey,
|
|
96
|
+
accessKeyId: Redacted.value(accessKeyId),
|
|
97
|
+
secretAccessKey: Redacted.value(secretAccessKey),
|
|
358
98
|
region: Option.getOrUndefined(region)
|
|
359
99
|
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
s.stop("
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
//#endregion
|
|
375
|
-
//#region src/commands/cluster/object-store/create-azure.ts
|
|
376
|
-
const parentOption$5 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
|
|
377
|
-
const objectStoreIdOption$2 = Flag.string("object-store-id").pipe(Flag.withDescription("Unique identifier for the object store"));
|
|
378
|
-
const containerNameOption = Flag.string("container-name").pipe(Flag.withDescription("Azure container name"));
|
|
379
|
-
const prefixOption$2 = Flag.string("prefix").pipe(Flag.withDescription("Container prefix (optional)"), Flag.optional);
|
|
380
|
-
const storageAccountNameOption = Flag.string("storage-account-name").pipe(Flag.withDescription("AZURE_STORAGE_ACCOUNT_NAME"));
|
|
381
|
-
const storageAccountKeyOption = Flag.string("storage-account-key").pipe(Flag.withDescription("AZURE_STORAGE_ACCOUNT_KEY"));
|
|
382
|
-
const createObjectStoreAzureCommand = Command.make("azure", {
|
|
383
|
-
parent: parentOption$5,
|
|
384
|
-
objectStoreId: objectStoreIdOption$2,
|
|
385
|
-
containerName: containerNameOption,
|
|
386
|
-
prefix: prefixOption$2,
|
|
387
|
-
storageAccountName: storageAccountNameOption,
|
|
388
|
-
storageAccountKey: storageAccountKeyOption,
|
|
100
|
+
} },
|
|
101
|
+
lake: lakeConfig
|
|
102
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create namespace"))));
|
|
103
|
+
s.stop("Namespace created successfully");
|
|
104
|
+
yield* Effect.sync(() => displayNamespace(namespace));
|
|
105
|
+
})).pipe(Command.withDescription("Create a namespace backed by AWS S3"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
106
|
+
const createNamespaceAzureCommand = Command.make("azure", {
|
|
107
|
+
namespaceId: namespaceIdOption,
|
|
108
|
+
lake: lakeOption,
|
|
109
|
+
containerName: Flag.string("container-name").pipe(Flag.withDescription("Azure Blob container name")),
|
|
110
|
+
prefix: prefixOption,
|
|
111
|
+
storageAccountName: Flag.string("storage-account-name").pipe(Flag.withDescription("Azure storage account name")),
|
|
112
|
+
storageAccountKey: Flag.redacted("storage-account-key").pipe(Flag.withDescription("Azure storage account key (or set AZURE_STORAGE_ACCOUNT_KEY env var)"), Flag.withFallbackConfig(Config.redacted("AZURE_STORAGE_ACCOUNT_KEY"))),
|
|
389
113
|
host: hostOption,
|
|
390
114
|
port: portOption
|
|
391
|
-
}, ({
|
|
392
|
-
p.intro("
|
|
393
|
-
const
|
|
115
|
+
}, ({ namespaceId, lake, containerName, prefix, storageAccountName, storageAccountKey }) => Effect.gen(function* () {
|
|
116
|
+
p.intro("📁 Create Namespace (Azure Blob)");
|
|
117
|
+
const lakeConfig = yield* Effect.try({
|
|
118
|
+
try: () => makeLakeConfig(lake),
|
|
119
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
120
|
+
});
|
|
394
121
|
const s = p.spinner();
|
|
395
|
-
s.start("Creating
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
objectStoreConfig: {
|
|
122
|
+
s.start("Creating namespace...");
|
|
123
|
+
const namespace = yield* ClusterClient.createNamespace({
|
|
124
|
+
namespaceId,
|
|
125
|
+
objectStore: { objectStoreConfig: {
|
|
400
126
|
_tag: "azure",
|
|
401
127
|
azure: {
|
|
402
128
|
containerName,
|
|
403
129
|
prefix: Option.getOrUndefined(prefix),
|
|
404
130
|
storageAccountName,
|
|
405
|
-
storageAccountKey
|
|
131
|
+
storageAccountKey: Redacted.value(storageAccountKey)
|
|
406
132
|
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
s.stop("
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
//#endregion
|
|
422
|
-
//#region src/commands/cluster/object-store/create-google.ts
|
|
423
|
-
const parentOption$4 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
|
|
424
|
-
const objectStoreIdOption$1 = Flag.string("object-store-id").pipe(Flag.withDescription("Unique identifier for the object store"));
|
|
425
|
-
const bucketNameOption$1 = Flag.string("bucket-name").pipe(Flag.withDescription("Google Cloud Storage bucket name"));
|
|
426
|
-
const prefixOption$1 = Flag.string("prefix").pipe(Flag.withDescription("Bucket prefix (optional)"), Flag.optional);
|
|
427
|
-
const serviceAccountOption = Flag.string("service-account").pipe(Flag.withDescription("GOOGLE_SERVICE_ACCOUNT"));
|
|
428
|
-
const serviceAccountKeyOption = Flag.string("service-account-key").pipe(Flag.withDescription("GOOGLE_SERVICE_ACCOUNT_KEY"));
|
|
429
|
-
const createObjectStoreGoogleCommand = Command.make("google", {
|
|
430
|
-
parent: parentOption$4,
|
|
431
|
-
objectStoreId: objectStoreIdOption$1,
|
|
432
|
-
bucketName: bucketNameOption$1,
|
|
433
|
-
prefix: prefixOption$1,
|
|
434
|
-
serviceAccount: serviceAccountOption,
|
|
435
|
-
serviceAccountKey: serviceAccountKeyOption,
|
|
133
|
+
} },
|
|
134
|
+
lake: lakeConfig
|
|
135
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create namespace"))));
|
|
136
|
+
s.stop("Namespace created successfully");
|
|
137
|
+
yield* Effect.sync(() => displayNamespace(namespace));
|
|
138
|
+
})).pipe(Command.withDescription("Create a namespace backed by Azure Blob Storage"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
139
|
+
const createNamespaceGoogleCommand = Command.make("google", {
|
|
140
|
+
namespaceId: namespaceIdOption,
|
|
141
|
+
lake: lakeOption,
|
|
142
|
+
bucketName: Flag.string("bucket-name").pipe(Flag.withDescription("GCS bucket name")),
|
|
143
|
+
prefix: prefixOption,
|
|
144
|
+
serviceAccount: Flag.string("service-account").pipe(Flag.withDescription("GCP service account email")),
|
|
145
|
+
serviceAccountKey: Flag.redacted("service-account-key").pipe(Flag.withDescription("GCP service account key JSON (or set GOOGLE_SERVICE_ACCOUNT_KEY env var)"), Flag.withFallbackConfig(Config.redacted("GOOGLE_SERVICE_ACCOUNT_KEY"))),
|
|
436
146
|
host: hostOption,
|
|
437
147
|
port: portOption
|
|
438
|
-
}, ({
|
|
439
|
-
p.intro("
|
|
440
|
-
const
|
|
148
|
+
}, ({ namespaceId, lake, bucketName, prefix, serviceAccount, serviceAccountKey }) => Effect.gen(function* () {
|
|
149
|
+
p.intro("📁 Create Namespace (Google Cloud Storage)");
|
|
150
|
+
const lakeConfig = yield* Effect.try({
|
|
151
|
+
try: () => makeLakeConfig(lake),
|
|
152
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
153
|
+
});
|
|
441
154
|
const s = p.spinner();
|
|
442
|
-
s.start("Creating
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
objectStoreConfig: {
|
|
155
|
+
s.start("Creating namespace...");
|
|
156
|
+
const namespace = yield* ClusterClient.createNamespace({
|
|
157
|
+
namespaceId,
|
|
158
|
+
objectStore: { objectStoreConfig: {
|
|
447
159
|
_tag: "google",
|
|
448
160
|
google: {
|
|
449
161
|
bucketName,
|
|
450
162
|
prefix: Option.getOrUndefined(prefix),
|
|
451
163
|
serviceAccount,
|
|
452
|
-
serviceAccountKey
|
|
164
|
+
serviceAccountKey: Redacted.value(serviceAccountKey)
|
|
453
165
|
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
s.stop("
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
p.outro("✓ Done");
|
|
465
|
-
});
|
|
466
|
-
})).pipe(Command.withDescription("Create a new Google Cloud Storage object store"));
|
|
467
|
-
|
|
468
|
-
//#endregion
|
|
469
|
-
//#region src/commands/cluster/object-store/create-s3.ts
|
|
470
|
-
const parentOption$3 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
|
|
471
|
-
const objectStoreIdOption = Flag.string("object-store-id").pipe(Flag.withDescription("Unique identifier for the object store"));
|
|
472
|
-
const bucketNameOption = Flag.string("bucket-name").pipe(Flag.withDescription("S3 bucket name"));
|
|
473
|
-
const prefixOption = Flag.string("prefix").pipe(Flag.withDescription("Bucket prefix (optional)"), Flag.optional);
|
|
474
|
-
const accessKeyIdOption = Flag.string("access-key-id").pipe(Flag.withDescription("AWS_ACCESS_KEY_ID"));
|
|
475
|
-
const secretAccessKeyOption = Flag.string("secret-access-key").pipe(Flag.withDescription("AWS_SECRET_ACCESS_KEY"));
|
|
476
|
-
const regionOption = Flag.string("region").pipe(Flag.withDescription("AWS_DEFAULT_REGION"), Flag.optional);
|
|
477
|
-
const endpointOption = Flag.string("endpoint").pipe(Flag.withDescription("AWS_ENDPOINT (e.g., https://nyc3.digitaloceanspaces.com)"));
|
|
478
|
-
const createObjectStoreS3Command = Command.make("s3", {
|
|
479
|
-
parent: parentOption$3,
|
|
480
|
-
objectStoreId: objectStoreIdOption,
|
|
481
|
-
bucketName: bucketNameOption,
|
|
166
|
+
} },
|
|
167
|
+
lake: lakeConfig
|
|
168
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create namespace"))));
|
|
169
|
+
s.stop("Namespace created successfully");
|
|
170
|
+
yield* Effect.sync(() => displayNamespace(namespace));
|
|
171
|
+
})).pipe(Command.withDescription("Create a namespace backed by Google Cloud Storage"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
172
|
+
const createNamespaceS3Command = Command.make("s3", {
|
|
173
|
+
namespaceId: namespaceIdOption,
|
|
174
|
+
lake: lakeOption,
|
|
175
|
+
bucketName: Flag.string("bucket-name").pipe(Flag.withDescription("S3-compatible bucket name")),
|
|
482
176
|
prefix: prefixOption,
|
|
483
|
-
accessKeyId:
|
|
484
|
-
secretAccessKey:
|
|
485
|
-
|
|
486
|
-
|
|
177
|
+
accessKeyId: Flag.redacted("access-key-id").pipe(Flag.withDescription("Access key ID (or set S3_ACCESS_KEY_ID env var)"), Flag.withFallbackConfig(Config.redacted("S3_ACCESS_KEY_ID"))),
|
|
178
|
+
secretAccessKey: Flag.redacted("secret-access-key").pipe(Flag.withDescription("Secret access key (or set S3_SECRET_ACCESS_KEY env var)"), Flag.withFallbackConfig(Config.redacted("S3_SECRET_ACCESS_KEY"))),
|
|
179
|
+
endpoint: Flag.string("endpoint").pipe(Flag.withDescription("S3-compatible endpoint URL (e.g., http://minio:9000)")),
|
|
180
|
+
allowHttp: Flag.boolean("allow-http").pipe(Flag.withDescription("Allow plain HTTP connections"), Flag.withDefault(false)),
|
|
181
|
+
region: Flag.string("region").pipe(Flag.withDescription("Region"), Flag.optional),
|
|
487
182
|
host: hostOption,
|
|
488
183
|
port: portOption
|
|
489
|
-
}, ({
|
|
490
|
-
p.intro("
|
|
491
|
-
const
|
|
184
|
+
}, ({ namespaceId, lake, bucketName, prefix, accessKeyId, secretAccessKey, endpoint, allowHttp, region }) => Effect.gen(function* () {
|
|
185
|
+
p.intro("📁 Create Namespace (S3-Compatible)");
|
|
186
|
+
const lakeConfig = yield* Effect.try({
|
|
187
|
+
try: () => makeLakeConfig(lake),
|
|
188
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
189
|
+
});
|
|
492
190
|
const s = p.spinner();
|
|
493
|
-
s.start("Creating
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
objectStoreConfig: {
|
|
191
|
+
s.start("Creating namespace...");
|
|
192
|
+
const namespace = yield* ClusterClient.createNamespace({
|
|
193
|
+
namespaceId,
|
|
194
|
+
objectStore: { objectStoreConfig: {
|
|
498
195
|
_tag: "s3Compatible",
|
|
499
196
|
s3Compatible: {
|
|
500
197
|
bucketName,
|
|
501
198
|
prefix: Option.getOrUndefined(prefix),
|
|
502
|
-
accessKeyId,
|
|
503
|
-
secretAccessKey,
|
|
504
|
-
region: Option.getOrUndefined(region),
|
|
199
|
+
accessKeyId: Redacted.value(accessKeyId),
|
|
200
|
+
secretAccessKey: Redacted.value(secretAccessKey),
|
|
505
201
|
endpoint,
|
|
506
|
-
allowHttp
|
|
202
|
+
allowHttp,
|
|
203
|
+
region: Option.getOrUndefined(region)
|
|
507
204
|
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
s.stop("
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
});
|
|
520
|
-
})).pipe(Command.withDescription("Create a new S3-compatible object store (MinIO, DigitalOcean, etc.)"));
|
|
521
|
-
|
|
522
|
-
//#endregion
|
|
523
|
-
//#region src/commands/cluster/object-store/create.ts
|
|
524
|
-
const createObjectStoreCommand = Command.make("create-object-store", {}, () => Effect.void).pipe(Command.withDescription("Create a new object store"), Command.withSubcommands([
|
|
525
|
-
createObjectStoreAwsCommand,
|
|
526
|
-
createObjectStoreAzureCommand,
|
|
527
|
-
createObjectStoreGoogleCommand,
|
|
528
|
-
createObjectStoreS3Command
|
|
205
|
+
} },
|
|
206
|
+
lake: lakeConfig
|
|
207
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create namespace"))));
|
|
208
|
+
s.stop("Namespace created successfully");
|
|
209
|
+
yield* Effect.sync(() => displayNamespace(namespace));
|
|
210
|
+
})).pipe(Command.withDescription("Create a namespace backed by an S3-compatible object store"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
211
|
+
const createNamespaceCommand = Command.make("create-namespace", {}, () => Effect.void).pipe(Command.withDescription("Create a new namespace (choose a storage provider as subcommand: aws, azure, google, s3)"), Command.withSubcommands([
|
|
212
|
+
createNamespaceAwsCommand,
|
|
213
|
+
createNamespaceAzureCommand,
|
|
214
|
+
createNamespaceGoogleCommand,
|
|
215
|
+
createNamespaceS3Command
|
|
529
216
|
]));
|
|
530
217
|
|
|
531
218
|
//#endregion
|
|
532
|
-
//#region src/commands/cluster/
|
|
533
|
-
const nameOption$
|
|
534
|
-
const
|
|
535
|
-
name: nameOption$
|
|
219
|
+
//#region src/commands/cluster/namespace/delete.ts
|
|
220
|
+
const nameOption$3 = Flag.string("name").pipe(Flag.withDescription("Namespace name in format: namespaces/{namespace}"));
|
|
221
|
+
const deleteNamespaceCommand = Command.make("delete-namespace", {
|
|
222
|
+
name: nameOption$3,
|
|
536
223
|
force: forceOption,
|
|
537
224
|
host: hostOption,
|
|
538
225
|
port: portOption
|
|
539
|
-
}, ({ name, force
|
|
540
|
-
p.intro("🗑️ Delete
|
|
226
|
+
}, ({ name, force }) => Effect.gen(function* () {
|
|
227
|
+
p.intro("🗑️ Delete Namespace");
|
|
541
228
|
if (!force) {
|
|
542
229
|
const confirm = yield* Effect.tryPromise({
|
|
543
230
|
try: () => p.confirm({
|
|
544
|
-
message: `Are you sure you want to delete
|
|
231
|
+
message: `Are you sure you want to delete namespace ${name}?`,
|
|
545
232
|
initialValue: false
|
|
546
233
|
}),
|
|
547
234
|
catch: () => /* @__PURE__ */ new Error("Failed to confirm deletion")
|
|
548
235
|
});
|
|
549
236
|
if (p.isCancel(confirm) || !confirm) {
|
|
550
237
|
p.cancel("Deletion cancelled");
|
|
551
|
-
|
|
238
|
+
return;
|
|
552
239
|
}
|
|
553
240
|
}
|
|
554
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
555
241
|
const s = p.spinner();
|
|
556
|
-
s.start("Deleting
|
|
557
|
-
yield*
|
|
558
|
-
s.stop("
|
|
242
|
+
s.start("Deleting namespace...");
|
|
243
|
+
yield* ClusterClient.deleteNamespace({ name }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete namespace"))));
|
|
244
|
+
s.stop("Namespace deleted successfully");
|
|
559
245
|
p.outro("✓ Done");
|
|
560
|
-
})).pipe(Command.withDescription("Delete
|
|
246
|
+
})).pipe(Command.withDescription("Delete a namespace from the cluster"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
561
247
|
|
|
562
248
|
//#endregion
|
|
563
|
-
//#region src/commands/cluster/
|
|
564
|
-
const nameOption$
|
|
565
|
-
const
|
|
566
|
-
name: nameOption$
|
|
249
|
+
//#region src/commands/cluster/namespace/get.ts
|
|
250
|
+
const nameOption$2 = Flag.string("name").pipe(Flag.withDescription("Namespace name in format: namespaces/{namespace}"));
|
|
251
|
+
const getNamespaceCommand = Command.make("get-namespace", {
|
|
252
|
+
name: nameOption$2,
|
|
567
253
|
host: hostOption,
|
|
568
254
|
port: portOption
|
|
569
|
-
}, ({ name
|
|
570
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
255
|
+
}, ({ name }) => Effect.gen(function* () {
|
|
571
256
|
const s = p.spinner();
|
|
572
|
-
s.start("Fetching
|
|
573
|
-
const
|
|
574
|
-
s.stop("
|
|
257
|
+
s.start("Fetching namespace...");
|
|
258
|
+
const namespace = yield* ClusterClient.getNamespace({ name }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to get namespace"))));
|
|
259
|
+
s.stop("Namespace retrieved");
|
|
575
260
|
yield* Effect.sync(() => {
|
|
576
261
|
printTable([{
|
|
577
|
-
name:
|
|
578
|
-
|
|
262
|
+
name: namespace.name,
|
|
263
|
+
object_store: namespace.objectStore?.objectStoreConfig._tag ?? "-",
|
|
264
|
+
lake: namespace.lake?.lakeConfig._tag ?? "-"
|
|
579
265
|
}]);
|
|
580
266
|
p.outro("✓ Done");
|
|
581
267
|
});
|
|
582
|
-
})).pipe(Command.withDescription("Get details of a specific
|
|
268
|
+
})).pipe(Command.withDescription("Get details of a specific namespace"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
583
269
|
|
|
584
270
|
//#endregion
|
|
585
|
-
//#region src/commands/cluster/
|
|
586
|
-
const
|
|
587
|
-
const listObjectStoresCommand = Command.make("list-object-stores", {
|
|
588
|
-
parent: parentOption$2,
|
|
271
|
+
//#region src/commands/cluster/namespace/list.ts
|
|
272
|
+
const listNamespacesCommand = Command.make("list-namespaces", {
|
|
589
273
|
pageSize: pageSizeOption,
|
|
590
274
|
pageToken: pageTokenOption,
|
|
591
275
|
host: hostOption,
|
|
592
276
|
port: portOption
|
|
593
|
-
}, ({
|
|
594
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
277
|
+
}, ({ pageSize, pageToken }) => Effect.gen(function* () {
|
|
595
278
|
const s = p.spinner();
|
|
596
|
-
s.start("Fetching
|
|
597
|
-
const response = yield*
|
|
598
|
-
parent,
|
|
279
|
+
s.start("Fetching namespaces...");
|
|
280
|
+
const response = yield* ClusterClient.listNamespaces({
|
|
599
281
|
pageSize,
|
|
600
282
|
pageToken: Option.getOrUndefined(pageToken)
|
|
601
|
-
}).pipe(Effect.
|
|
602
|
-
s.stop(`Found ${response.
|
|
283
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to list namespaces"))));
|
|
284
|
+
s.stop(`Found ${response.namespaces.length} namespace(s)`);
|
|
603
285
|
yield* Effect.sync(() => {
|
|
604
|
-
if (response.
|
|
605
|
-
else printTable(response.
|
|
606
|
-
name:
|
|
607
|
-
|
|
286
|
+
if (response.namespaces.length === 0) p.log.warn("No namespaces found");
|
|
287
|
+
else printTable(response.namespaces.map((namespace) => ({
|
|
288
|
+
name: namespace.name,
|
|
289
|
+
object_store: namespace.objectStore?.objectStoreConfig._tag ?? "-",
|
|
290
|
+
lake: namespace.lake?.lakeConfig._tag ?? "-"
|
|
608
291
|
})));
|
|
609
292
|
if (response.nextPageToken) p.log.info(`Next page token: ${response.nextPageToken}`);
|
|
610
293
|
p.outro("✓ Done");
|
|
611
294
|
});
|
|
612
|
-
})).pipe(Command.withDescription("List all
|
|
613
|
-
|
|
614
|
-
//#endregion
|
|
615
|
-
//#region src/commands/cluster/tenant/create.ts
|
|
616
|
-
const tenantIdOption = Flag.string("tenant-id").pipe(Flag.withDescription("Unique identifier for the tenant (e.g., 'acme-corp')"));
|
|
617
|
-
const createTenantCommand = Command.make("create-tenant", {
|
|
618
|
-
tenantId: tenantIdOption,
|
|
619
|
-
host: hostOption,
|
|
620
|
-
port: portOption
|
|
621
|
-
}, ({ tenantId, host, port }) => Effect.gen(function* () {
|
|
622
|
-
p.intro("🏢 Create Tenant");
|
|
623
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
624
|
-
const s = p.spinner();
|
|
625
|
-
s.start("Creating tenant...");
|
|
626
|
-
const tenant = yield* WingsClusterMetadata.createTenant({ tenantId }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create tenant"))));
|
|
627
|
-
s.stop("Tenant created successfully");
|
|
628
|
-
yield* Effect.sync(() => {
|
|
629
|
-
printTable([{ name: tenant.name }]);
|
|
630
|
-
p.outro("✓ Done");
|
|
631
|
-
});
|
|
632
|
-
})).pipe(Command.withDescription("Create a new tenant in the cluster"));
|
|
633
|
-
|
|
634
|
-
//#endregion
|
|
635
|
-
//#region src/commands/cluster/tenant/delete.ts
|
|
636
|
-
const nameOption$3 = Flag.string("name").pipe(Flag.withDescription("Tenant name in format: tenants/{tenant} (e.g., 'tenants/acme-corp')"));
|
|
637
|
-
const deleteTenantCommand = Command.make("delete-tenant", {
|
|
638
|
-
name: nameOption$3,
|
|
639
|
-
force: forceOption,
|
|
640
|
-
host: hostOption,
|
|
641
|
-
port: portOption
|
|
642
|
-
}, ({ name, force, host, port }) => Effect.gen(function* () {
|
|
643
|
-
p.intro("🗑️ Delete Tenant");
|
|
644
|
-
if (!force) {
|
|
645
|
-
const confirm = yield* Effect.tryPromise({
|
|
646
|
-
try: () => p.confirm({
|
|
647
|
-
message: `Are you sure you want to delete tenant ${name}?`,
|
|
648
|
-
initialValue: false
|
|
649
|
-
}),
|
|
650
|
-
catch: () => /* @__PURE__ */ new Error("Failed to confirm deletion")
|
|
651
|
-
});
|
|
652
|
-
if (p.isCancel(confirm) || !confirm) {
|
|
653
|
-
p.cancel("Deletion cancelled");
|
|
654
|
-
process.exit(0);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
658
|
-
const s = p.spinner();
|
|
659
|
-
s.start("Deleting tenant...");
|
|
660
|
-
yield* WingsClusterMetadata.deleteTenant({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete tenant"))));
|
|
661
|
-
s.stop("Tenant deleted successfully");
|
|
662
|
-
p.outro("✓ Done");
|
|
663
|
-
})).pipe(Command.withDescription("Delete a tenant from the cluster (fails if tenant has any namespaces)"));
|
|
664
|
-
|
|
665
|
-
//#endregion
|
|
666
|
-
//#region src/commands/cluster/tenant/get.ts
|
|
667
|
-
const nameOption$2 = Flag.string("name").pipe(Flag.withDescription("Tenant name in format: tenants/{tenant} (e.g., 'tenants/acme-corp')"));
|
|
668
|
-
const getTenantCommand = Command.make("get-tenant", {
|
|
669
|
-
name: nameOption$2,
|
|
670
|
-
host: hostOption,
|
|
671
|
-
port: portOption
|
|
672
|
-
}, ({ name, host, port }) => Effect.gen(function* () {
|
|
673
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
674
|
-
const s = p.spinner();
|
|
675
|
-
s.start("Fetching tenant...");
|
|
676
|
-
const tenant = yield* WingsClusterMetadata.getTenant({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to get tenant"))));
|
|
677
|
-
s.stop("Tenant retrieved");
|
|
678
|
-
yield* Effect.sync(() => {
|
|
679
|
-
printTable([{ name: tenant.name }]);
|
|
680
|
-
p.outro("✓ Done");
|
|
681
|
-
});
|
|
682
|
-
})).pipe(Command.withDescription("Get details of a specific tenant"));
|
|
683
|
-
|
|
684
|
-
//#endregion
|
|
685
|
-
//#region src/commands/cluster/tenant/list.ts
|
|
686
|
-
const listTenantsCommand = Command.make("list-tenants", {
|
|
687
|
-
pageSize: pageSizeOption,
|
|
688
|
-
pageToken: pageTokenOption,
|
|
689
|
-
host: hostOption,
|
|
690
|
-
port: portOption
|
|
691
|
-
}, ({ pageSize, pageToken, host, port }) => Effect.gen(function* () {
|
|
692
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
693
|
-
const s = p.spinner();
|
|
694
|
-
s.start("Fetching tenants...");
|
|
695
|
-
const response = yield* WingsClusterMetadata.listTenants({
|
|
696
|
-
pageSize,
|
|
697
|
-
pageToken: Option.getOrUndefined(pageToken)
|
|
698
|
-
}).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to list tenants"))));
|
|
699
|
-
s.stop(`Found ${response.tenants.length} tenant(s)`);
|
|
700
|
-
yield* Effect.sync(() => {
|
|
701
|
-
if (response.tenants.length === 0) p.log.warn("No tenants found");
|
|
702
|
-
else printTable(response.tenants.map((tenant) => ({ name: tenant.name })));
|
|
703
|
-
if (response.nextPageToken) p.log.info(`Next page token: ${response.nextPageToken}`);
|
|
704
|
-
p.outro("✓ Done");
|
|
705
|
-
});
|
|
706
|
-
})).pipe(Command.withDescription("List all tenants in the cluster"));
|
|
295
|
+
})).pipe(Command.withDescription("List all namespaces in the cluster"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
707
296
|
|
|
708
297
|
//#endregion
|
|
709
|
-
//#region src/commands/cluster/
|
|
298
|
+
//#region src/commands/cluster/table/create.ts
|
|
710
299
|
const SUPPORTED_INLINE_TYPES = [
|
|
711
300
|
"Int8",
|
|
712
301
|
"Int16",
|
|
@@ -734,18 +323,15 @@ const SUPPORTED_INLINE_TYPES = [
|
|
|
734
323
|
"DurationMicrosecond",
|
|
735
324
|
"DurationNanosecond"
|
|
736
325
|
];
|
|
737
|
-
const parentOption$1 = Flag.string("parent").pipe(Flag.withDescription("Parent namespace in format:
|
|
738
|
-
const
|
|
739
|
-
const descriptionOption = Flag.string("description").pipe(Flag.withDescription("
|
|
326
|
+
const parentOption$1 = Flag.string("parent").pipe(Flag.withDescription("Parent namespace in format: namespaces/{namespace}"));
|
|
327
|
+
const tableIdOption = Flag.string("table-id").pipe(Flag.withDescription("Unique identifier for the table"));
|
|
328
|
+
const descriptionOption = Flag.string("description").pipe(Flag.withDescription("Table description"), Flag.optional);
|
|
740
329
|
const fieldsOption = Flag.string("fields").pipe(Flag.withDescription("Field definitions in format \"name:Type\" or \"name:Type?\" for nullable"), Flag.atLeast(0));
|
|
741
330
|
const schemaFileOption = Flag.string("schema-file").pipe(Flag.withDescription("Path to JSON file containing FieldConfig[] (for complex types)"), Flag.optional);
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
/**
|
|
747
|
-
* Parse a field string in format "name:Type" or "name:Type?" (nullable)
|
|
748
|
-
*/
|
|
331
|
+
const keyFieldOption = Flag.string("key-field").pipe(Flag.withDescription("Name of the field used as the primary key"));
|
|
332
|
+
const versionFieldOption = Flag.string("version-field").pipe(Flag.withDescription("Name of the field used as the version/timestamp"));
|
|
333
|
+
const partitionFieldOption = Flag.string("partition-field").pipe(Flag.withDescription("Name of the field used for partitioning"), Flag.optional);
|
|
334
|
+
const freshnessSecondsOption = Flag.integer("freshness-seconds").pipe(Flag.withDescription("Target freshness interval for compaction (seconds)"), Flag.withDefault(60));
|
|
749
335
|
function parseFieldString(fieldStr, index) {
|
|
750
336
|
const match = fieldStr.match(/^([^:]+):([^?]+)(\?)?$/);
|
|
751
337
|
if (!match) throw new Error(`Invalid field format: "${fieldStr}". Expected format: "name:Type" or "name:Type?"`);
|
|
@@ -762,15 +348,9 @@ function parseFieldString(fieldStr, index) {
|
|
|
762
348
|
id: BigInt(index + 1)
|
|
763
349
|
};
|
|
764
350
|
}
|
|
765
|
-
/**
|
|
766
|
-
* Parse multiple field strings into FieldConfig array
|
|
767
|
-
*/
|
|
768
351
|
function parseFieldsFromArgs(fields) {
|
|
769
352
|
return fields.map((field, index) => parseFieldString(field, index));
|
|
770
353
|
}
|
|
771
|
-
/**
|
|
772
|
-
* Load fields from a JSON schema file with basic validation
|
|
773
|
-
*/
|
|
774
354
|
function loadFieldsFromFile(filePath) {
|
|
775
355
|
if (!fs.existsSync(filePath)) throw new Error(`Schema file not found: "${filePath}"`);
|
|
776
356
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -790,192 +370,190 @@ function loadFieldsFromFile(filePath) {
|
|
|
790
370
|
};
|
|
791
371
|
});
|
|
792
372
|
}
|
|
793
|
-
const
|
|
373
|
+
const createTableCommand = Command.make("create-table", {
|
|
794
374
|
parent: parentOption$1,
|
|
795
|
-
|
|
375
|
+
tableId: tableIdOption,
|
|
796
376
|
description: descriptionOption,
|
|
797
377
|
fields: fieldsOption,
|
|
798
378
|
schemaFile: schemaFileOption,
|
|
799
|
-
|
|
379
|
+
keyField: keyFieldOption,
|
|
380
|
+
versionField: versionFieldOption,
|
|
381
|
+
partitionField: partitionFieldOption,
|
|
800
382
|
freshnessSeconds: freshnessSecondsOption,
|
|
801
|
-
ttlSeconds: ttlSecondsOption,
|
|
802
|
-
targetFileSizeBytes: targetFileSizeBytesOption,
|
|
803
383
|
host: hostOption,
|
|
804
384
|
port: portOption
|
|
805
|
-
}, ({ parent,
|
|
806
|
-
p.intro("📋 Create
|
|
385
|
+
}, ({ parent, tableId, description, fields, schemaFile, keyField, versionField, partitionField, freshnessSeconds }) => Effect.gen(function* () {
|
|
386
|
+
p.intro("📋 Create Table");
|
|
807
387
|
const schemaFilePath = Option.getOrUndefined(schemaFile);
|
|
808
388
|
const hasFields = fields.length > 0;
|
|
809
389
|
if (!hasFields && !schemaFilePath) return yield* Effect.fail(/* @__PURE__ */ new Error("Either --fields or --schema-file must be provided"));
|
|
810
390
|
if (hasFields && schemaFilePath) return yield* Effect.fail(/* @__PURE__ */ new Error("Cannot use both --fields and --schema-file. Choose one."));
|
|
811
|
-
const
|
|
391
|
+
const tableFields = yield* Effect.try({
|
|
812
392
|
try: () => schemaFilePath ? loadFieldsFromFile(schemaFilePath) : parseFieldsFromArgs(fields),
|
|
813
393
|
catch: (error) => error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to parse fields")
|
|
814
394
|
});
|
|
815
|
-
if (
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
const
|
|
395
|
+
if (tableFields.length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("At least one field is required"));
|
|
396
|
+
const resolveField = (fieldName, role) => {
|
|
397
|
+
const field = tableFields.find((f) => f.name === fieldName);
|
|
398
|
+
if (!field) throw new Error(`${role} field "${fieldName}" not found. Available fields: ${tableFields.map((f) => f.name).join(", ")}`);
|
|
399
|
+
return field.id;
|
|
400
|
+
};
|
|
401
|
+
const keyFieldId = yield* Effect.try({
|
|
402
|
+
try: () => resolveField(keyField, "Key"),
|
|
403
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
404
|
+
});
|
|
405
|
+
const versionFieldId = yield* Effect.try({
|
|
406
|
+
try: () => resolveField(versionField, "Version"),
|
|
407
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
408
|
+
});
|
|
409
|
+
const partitionFieldName = Option.getOrUndefined(partitionField);
|
|
410
|
+
const partitionFieldId = partitionFieldName ? yield* Effect.try({
|
|
411
|
+
try: () => resolveField(partitionFieldName, "Partition"),
|
|
412
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
413
|
+
}) : void 0;
|
|
826
414
|
const s = p.spinner();
|
|
827
|
-
s.start("Creating
|
|
828
|
-
const
|
|
415
|
+
s.start("Creating table...");
|
|
416
|
+
const table = yield* ClusterClient.createTable({
|
|
829
417
|
parent,
|
|
830
|
-
|
|
418
|
+
tableId,
|
|
831
419
|
description: Option.getOrUndefined(description),
|
|
832
|
-
fields:
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create topic"))));
|
|
840
|
-
s.stop("Topic created successfully");
|
|
420
|
+
fields: tableFields,
|
|
421
|
+
keyFieldId,
|
|
422
|
+
versionFieldId,
|
|
423
|
+
partitionFieldId,
|
|
424
|
+
targetFreshnessSeconds: BigInt(freshnessSeconds)
|
|
425
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create table"))));
|
|
426
|
+
s.stop("Table created successfully");
|
|
841
427
|
yield* Effect.sync(() => {
|
|
842
428
|
printTable([{
|
|
843
|
-
name:
|
|
844
|
-
description:
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
fields_count:
|
|
429
|
+
name: table.name,
|
|
430
|
+
description: table.description || "-",
|
|
431
|
+
key_field_id: table.keyFieldId.toString(),
|
|
432
|
+
version_field_id: table.versionFieldId.toString(),
|
|
433
|
+
partition_field_id: table.partitionFieldId?.toString() || "-",
|
|
434
|
+
freshness_seconds: table.targetFreshnessSeconds.toString(),
|
|
435
|
+
fields_count: table.schema.fields.length.toString()
|
|
850
436
|
}]);
|
|
851
|
-
if (
|
|
437
|
+
if (table.schema.fields.length > 0) {
|
|
852
438
|
console.log("\nFields:");
|
|
853
|
-
printTable(
|
|
854
|
-
index: index.toString(),
|
|
439
|
+
printTable(table.schema.fields.map((field) => ({
|
|
855
440
|
name: field.name,
|
|
441
|
+
id: field.id.toString(),
|
|
856
442
|
type: field.arrowType?._tag ?? "unknown",
|
|
857
443
|
nullable: field.nullable ? "yes" : "no"
|
|
858
444
|
})));
|
|
859
445
|
}
|
|
860
446
|
p.outro("✓ Done");
|
|
861
447
|
});
|
|
862
|
-
})).pipe(Command.withDescription("Create a new
|
|
448
|
+
})).pipe(Command.withDescription("Create a new table belonging to a namespace"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
863
449
|
|
|
864
450
|
//#endregion
|
|
865
|
-
//#region src/commands/cluster/
|
|
866
|
-
const nameOption$1 = Flag.string("name").pipe(Flag.withDescription("
|
|
867
|
-
const
|
|
868
|
-
const deleteTopicCommand = Command.make("delete-topic", {
|
|
451
|
+
//#region src/commands/cluster/table/delete.ts
|
|
452
|
+
const nameOption$1 = Flag.string("name").pipe(Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"));
|
|
453
|
+
const deleteTableCommand = Command.make("delete-table", {
|
|
869
454
|
name: nameOption$1,
|
|
870
|
-
forceDelete: forceDeleteOption,
|
|
871
455
|
force: forceOption,
|
|
872
456
|
host: hostOption,
|
|
873
457
|
port: portOption
|
|
874
|
-
}, ({ name,
|
|
875
|
-
p.intro("🗑️ Delete
|
|
458
|
+
}, ({ name, force }) => Effect.gen(function* () {
|
|
459
|
+
p.intro("🗑️ Delete Table");
|
|
876
460
|
if (!force) {
|
|
877
461
|
const confirm = yield* Effect.tryPromise({
|
|
878
462
|
try: () => p.confirm({
|
|
879
|
-
message: `Are you sure you want to delete
|
|
463
|
+
message: `Are you sure you want to delete table ${name}?`,
|
|
880
464
|
initialValue: false
|
|
881
465
|
}),
|
|
882
466
|
catch: () => /* @__PURE__ */ new Error("Failed to confirm deletion")
|
|
883
467
|
});
|
|
884
468
|
if (p.isCancel(confirm) || !confirm) {
|
|
885
469
|
p.cancel("Deletion cancelled");
|
|
886
|
-
|
|
470
|
+
return;
|
|
887
471
|
}
|
|
888
472
|
}
|
|
889
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
890
473
|
const s = p.spinner();
|
|
891
|
-
s.start("Deleting
|
|
892
|
-
yield*
|
|
893
|
-
|
|
894
|
-
force: forceDelete
|
|
895
|
-
}).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete topic"))));
|
|
896
|
-
s.stop("Topic deleted successfully");
|
|
474
|
+
s.start("Deleting table...");
|
|
475
|
+
yield* ClusterClient.deleteTable({ name }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete table"))));
|
|
476
|
+
s.stop("Table deleted successfully");
|
|
897
477
|
p.outro("✓ Done");
|
|
898
|
-
})).pipe(Command.withDescription("Delete a
|
|
478
|
+
})).pipe(Command.withDescription("Delete a table from the cluster"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
899
479
|
|
|
900
480
|
//#endregion
|
|
901
|
-
//#region src/commands/cluster/
|
|
902
|
-
const nameOption = Flag.string("name").pipe(Flag.withDescription("
|
|
903
|
-
const
|
|
481
|
+
//#region src/commands/cluster/table/get.ts
|
|
482
|
+
const nameOption = Flag.string("name").pipe(Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"));
|
|
483
|
+
const getTableCommand = Command.make("get-table", {
|
|
904
484
|
name: nameOption,
|
|
905
485
|
host: hostOption,
|
|
906
486
|
port: portOption
|
|
907
|
-
}, ({ name
|
|
908
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
487
|
+
}, ({ name }) => Effect.gen(function* () {
|
|
909
488
|
const s = p.spinner();
|
|
910
|
-
s.start("Fetching
|
|
911
|
-
const
|
|
912
|
-
s.stop("
|
|
489
|
+
s.start("Fetching table...");
|
|
490
|
+
const table = yield* ClusterClient.getTable({ name }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to get table"))));
|
|
491
|
+
s.stop("Table retrieved");
|
|
913
492
|
yield* Effect.sync(() => {
|
|
914
493
|
printTable([{
|
|
915
|
-
name:
|
|
916
|
-
description:
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
494
|
+
name: table.name,
|
|
495
|
+
description: table.description || "-",
|
|
496
|
+
key_field_id: table.keyFieldId.toString(),
|
|
497
|
+
version_field_id: table.versionFieldId.toString(),
|
|
498
|
+
partition_field_id: table.partitionFieldId?.toString() || "-",
|
|
499
|
+
freshness_seconds: table.targetFreshnessSeconds.toString()
|
|
920
500
|
}]);
|
|
501
|
+
if (table.schema.fields.length > 0) {
|
|
502
|
+
console.log("\nFields:");
|
|
503
|
+
printTable(table.schema.fields.map((field) => ({
|
|
504
|
+
name: field.name,
|
|
505
|
+
id: field.id.toString(),
|
|
506
|
+
type: field.arrowType?._tag ?? "unknown",
|
|
507
|
+
nullable: field.nullable ? "yes" : "no"
|
|
508
|
+
})));
|
|
509
|
+
}
|
|
921
510
|
p.outro("✓ Done");
|
|
922
511
|
});
|
|
923
|
-
})).pipe(Command.withDescription("Get details of a specific
|
|
512
|
+
})).pipe(Command.withDescription("Get details of a specific table"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
924
513
|
|
|
925
514
|
//#endregion
|
|
926
|
-
//#region src/commands/cluster/
|
|
927
|
-
const parentOption = Flag.string("parent").pipe(Flag.withDescription("Parent namespace in format:
|
|
928
|
-
const
|
|
515
|
+
//#region src/commands/cluster/table/list.ts
|
|
516
|
+
const parentOption = Flag.string("parent").pipe(Flag.withDescription("Parent namespace in format: namespaces/{namespace}"));
|
|
517
|
+
const listTablesCommand = Command.make("list-tables", {
|
|
929
518
|
parent: parentOption,
|
|
930
519
|
pageSize: pageSizeOption,
|
|
931
520
|
pageToken: pageTokenOption,
|
|
932
521
|
host: hostOption,
|
|
933
522
|
port: portOption
|
|
934
|
-
}, ({ parent, pageSize, pageToken
|
|
935
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
523
|
+
}, ({ parent, pageSize, pageToken }) => Effect.gen(function* () {
|
|
936
524
|
const s = p.spinner();
|
|
937
|
-
s.start("Fetching
|
|
938
|
-
const response = yield*
|
|
525
|
+
s.start("Fetching tables...");
|
|
526
|
+
const response = yield* ClusterClient.listTables({
|
|
939
527
|
parent,
|
|
940
528
|
pageSize,
|
|
941
529
|
pageToken: Option.getOrUndefined(pageToken)
|
|
942
|
-
}).pipe(Effect.
|
|
943
|
-
s.stop(`Found ${response.
|
|
530
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to list tables"))));
|
|
531
|
+
s.stop(`Found ${response.tables.length} table(s)`);
|
|
944
532
|
yield* Effect.sync(() => {
|
|
945
|
-
if (response.
|
|
946
|
-
else printTable(response.
|
|
947
|
-
name:
|
|
948
|
-
description:
|
|
949
|
-
|
|
533
|
+
if (response.tables.length === 0) p.log.warn("No tables found");
|
|
534
|
+
else printTable(response.tables.map((table) => ({
|
|
535
|
+
name: table.name,
|
|
536
|
+
description: table.description || "-",
|
|
537
|
+
key_field_id: table.keyFieldId.toString(),
|
|
538
|
+
partition_field_id: table.partitionFieldId?.toString() || "-",
|
|
539
|
+
freshness_seconds: table.targetFreshnessSeconds.toString()
|
|
950
540
|
})));
|
|
951
541
|
if (response.nextPageToken) p.log.info(`Next page token: ${response.nextPageToken}`);
|
|
952
542
|
p.outro("✓ Done");
|
|
953
543
|
});
|
|
954
|
-
})).pipe(Command.withDescription("List all
|
|
544
|
+
})).pipe(Command.withDescription("List all tables belonging to a namespace"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
|
|
955
545
|
|
|
956
546
|
//#endregion
|
|
957
547
|
//#region src/commands/cluster/index.ts
|
|
958
|
-
const clusterCommand = Command.make("cluster", {}, () => Effect.void).pipe(Command.withDescription("
|
|
959
|
-
createTenantCommand,
|
|
960
|
-
listTenantsCommand,
|
|
961
|
-
getTenantCommand,
|
|
962
|
-
deleteTenantCommand,
|
|
548
|
+
const clusterCommand = Command.make("cluster", {}, () => Effect.void).pipe(Command.withDescription("Manage Wings namespaces and tables"), Command.withSubcommands([
|
|
963
549
|
createNamespaceCommand,
|
|
964
550
|
listNamespacesCommand,
|
|
965
551
|
getNamespaceCommand,
|
|
966
552
|
deleteNamespaceCommand,
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
createObjectStoreCommand,
|
|
972
|
-
listObjectStoresCommand,
|
|
973
|
-
getObjectStoreCommand,
|
|
974
|
-
deleteObjectStoreCommand,
|
|
975
|
-
createDataLakeCommand,
|
|
976
|
-
listDataLakesCommand,
|
|
977
|
-
getDataLakeCommand,
|
|
978
|
-
deleteDataLakeCommand
|
|
553
|
+
createTableCommand,
|
|
554
|
+
listTablesCommand,
|
|
555
|
+
getTableCommand,
|
|
556
|
+
deleteTableCommand
|
|
979
557
|
]));
|
|
980
558
|
|
|
981
559
|
//#endregion
|
|
@@ -983,34 +561,114 @@ const clusterCommand = Command.make("cluster", {}, () => Effect.void).pipe(Comma
|
|
|
983
561
|
const checkDockerVersion = () => Effect.gen(function* () {
|
|
984
562
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
985
563
|
const docker = ChildProcess.make("docker", ["--version"]);
|
|
986
|
-
return yield* spawner.string(docker).pipe(Effect.map(String.trim));
|
|
564
|
+
return yield* spawner.string(docker).pipe(Effect.map(String$1.trim));
|
|
987
565
|
});
|
|
988
566
|
const createDockerVolume = (volumeName) => Effect.gen(function* () {
|
|
989
|
-
yield* (yield* ChildProcess.make("docker", [
|
|
567
|
+
const exitCode = yield* (yield* ChildProcess.make("docker", [
|
|
990
568
|
"volume",
|
|
991
569
|
"create",
|
|
992
570
|
volumeName
|
|
993
571
|
])).exitCode;
|
|
572
|
+
if (exitCode !== 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Docker volume create failed with code ${exitCode}`));
|
|
573
|
+
});
|
|
574
|
+
const createDockerNetwork = (networkName) => Effect.gen(function* () {
|
|
575
|
+
const exitCode = yield* (yield* ChildProcess.make("docker", [
|
|
576
|
+
"network",
|
|
577
|
+
"create",
|
|
578
|
+
networkName
|
|
579
|
+
])).exitCode;
|
|
580
|
+
if (exitCode !== 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Docker network create failed with code ${exitCode}`));
|
|
581
|
+
});
|
|
582
|
+
const removeDockerContainer = (containerName) => Effect.gen(function* () {
|
|
583
|
+
yield* (yield* ChildProcess.make("docker", [
|
|
584
|
+
"rm",
|
|
585
|
+
"-f",
|
|
586
|
+
containerName
|
|
587
|
+
], {
|
|
588
|
+
stdout: "ignore",
|
|
589
|
+
stderr: "ignore"
|
|
590
|
+
})).exitCode;
|
|
591
|
+
});
|
|
592
|
+
const removeDockerNetwork = (networkName) => Effect.gen(function* () {
|
|
593
|
+
yield* (yield* ChildProcess.make("docker", [
|
|
594
|
+
"network",
|
|
595
|
+
"rm",
|
|
596
|
+
networkName
|
|
597
|
+
], {
|
|
598
|
+
stdout: "ignore",
|
|
599
|
+
stderr: "ignore"
|
|
600
|
+
})).exitCode;
|
|
601
|
+
});
|
|
602
|
+
const pullDockerImage = (image) => Effect.gen(function* () {
|
|
603
|
+
const exitCode = yield* (yield* ChildProcess.make("docker", ["pull", image], {
|
|
604
|
+
stdout: "inherit",
|
|
605
|
+
stderr: "inherit"
|
|
606
|
+
})).exitCode;
|
|
607
|
+
if (exitCode !== 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Docker pull failed with code ${exitCode}`));
|
|
994
608
|
});
|
|
995
609
|
const runDockerContainer = (image, volumeName) => Effect.gen(function* () {
|
|
610
|
+
const networkName = "airfoil-dev";
|
|
611
|
+
const seaweedContainerName = "airfoil-seaweedfs";
|
|
612
|
+
yield* removeDockerContainer(seaweedContainerName);
|
|
613
|
+
yield* removeDockerNetwork(networkName);
|
|
614
|
+
yield* createDockerNetwork(networkName);
|
|
615
|
+
yield* Effect.addFinalizer(() => removeDockerNetwork(networkName).pipe(Effect.catchCause(() => Effect.void)));
|
|
616
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
617
|
+
const seaweed = ChildProcess.make("docker", [
|
|
618
|
+
"run",
|
|
619
|
+
"-d",
|
|
620
|
+
"--rm",
|
|
621
|
+
"--name",
|
|
622
|
+
seaweedContainerName,
|
|
623
|
+
"--network",
|
|
624
|
+
networkName,
|
|
625
|
+
"--network-alias",
|
|
626
|
+
"seaweedfs",
|
|
627
|
+
"-e",
|
|
628
|
+
"AWS_ACCESS_KEY_ID=wingsdevaccesskey",
|
|
629
|
+
"-e",
|
|
630
|
+
"AWS_SECRET_ACCESS_KEY=wingsdevsecretkey",
|
|
631
|
+
"-e",
|
|
632
|
+
"S3_BUCKET=default-bucket",
|
|
633
|
+
"-v",
|
|
634
|
+
`${volumeName}:/data`,
|
|
635
|
+
"-p",
|
|
636
|
+
"8333:8333",
|
|
637
|
+
"chrislusf/seaweedfs:4.29",
|
|
638
|
+
"weed",
|
|
639
|
+
"mini",
|
|
640
|
+
"-s3",
|
|
641
|
+
"-dir=/data"
|
|
642
|
+
]);
|
|
643
|
+
yield* spawner.string(seaweed).pipe(Effect.map(String$1.trim));
|
|
644
|
+
yield* Effect.addFinalizer(() => removeDockerContainer(seaweedContainerName).pipe(Effect.catchCause(() => Effect.void)));
|
|
996
645
|
const exitCode = yield* (yield* ChildProcess.make("docker", [
|
|
997
646
|
"run",
|
|
998
647
|
"-it",
|
|
999
648
|
"--rm",
|
|
1000
649
|
"--name",
|
|
1001
650
|
"wings-dev",
|
|
1002
|
-
"
|
|
1003
|
-
|
|
651
|
+
"--network",
|
|
652
|
+
networkName,
|
|
653
|
+
"-e",
|
|
654
|
+
"AWS_ACCESS_KEY_ID=wingsdevaccesskey",
|
|
655
|
+
"-e",
|
|
656
|
+
"AWS_SECRET_ACCESS_KEY=wingsdevsecretkey",
|
|
657
|
+
"-e",
|
|
658
|
+
"AWS_ENDPOINT=http://seaweedfs:8333",
|
|
659
|
+
"-e",
|
|
660
|
+
"AWS_BUCKET_NAME=default-bucket",
|
|
661
|
+
"-e",
|
|
662
|
+
"AWS_DEFAULT_REGION=us-east-1",
|
|
663
|
+
"-e",
|
|
664
|
+
"AWS_ALLOW_HTTP=true",
|
|
1004
665
|
"-p",
|
|
1005
666
|
"7777:7777",
|
|
1006
|
-
"-p",
|
|
1007
|
-
"7780:7780",
|
|
1008
667
|
image,
|
|
1009
668
|
"dev",
|
|
1010
|
-
"--
|
|
1011
|
-
"--metadata.address=0.0.0.0:7777"
|
|
669
|
+
"--grpc.address=0.0.0.0:7777"
|
|
1012
670
|
])).exitCode;
|
|
1013
|
-
if (exitCode !== 0) yield* Effect.fail(/* @__PURE__ */ new Error(`Docker process exited with code ${exitCode}`));
|
|
671
|
+
if (exitCode !== 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Docker process exited with code ${exitCode}`));
|
|
1014
672
|
});
|
|
1015
673
|
|
|
1016
674
|
//#endregion
|
|
@@ -1051,18 +709,21 @@ const downloadWings = (version$2, targetPath, isStress = false) => Effect.gen(fu
|
|
|
1051
709
|
const fs$1 = yield* FileSystem.FileSystem;
|
|
1052
710
|
const client = yield* HttpClient.HttpClient;
|
|
1053
711
|
const filename = getBinaryFilename(isStress);
|
|
712
|
+
const tempPath = `${targetPath}.download`;
|
|
1054
713
|
const url = `${GITHUB_RELEASES_URL}/${version$2 === "latest" ? "latest/download" : `download/${version$2}`}/${filename}`;
|
|
1055
714
|
yield* fs$1.makeDirectory(WINGS_DIR, { recursive: true });
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
715
|
+
yield* fs$1.remove(tempPath).pipe(Effect.ignore);
|
|
716
|
+
yield* Effect.gen(function* () {
|
|
717
|
+
const response = yield* client.get(url);
|
|
718
|
+
const buffer = yield* HttpClientResponse.matchStatus({
|
|
719
|
+
200: (response$1) => response$1.arrayBuffer,
|
|
720
|
+
orElse: (response$1) => Effect.fail(/* @__PURE__ */ new Error(`Failed to download Wings from ${url}: ${response$1.status}`))
|
|
721
|
+
})(response);
|
|
722
|
+
yield* fs$1.writeFile(tempPath, new Uint8Array(buffer));
|
|
723
|
+
yield* verifyChecksum(version$2, tempPath, isStress);
|
|
724
|
+
yield* fs$1.chmod(tempPath, 493);
|
|
725
|
+
yield* fs$1.rename(tempPath, targetPath);
|
|
726
|
+
}).pipe(Effect.onError(() => fs$1.remove(tempPath).pipe(Effect.ignore)));
|
|
1066
727
|
});
|
|
1067
728
|
/**
|
|
1068
729
|
* Download and verify the checksum of a Wings binary
|
|
@@ -1090,7 +751,7 @@ const verifyChecksum = (version$2, filePath, isStress = false) => Effect.gen(fun
|
|
|
1090
751
|
//#endregion
|
|
1091
752
|
//#region src/commands/dev.ts
|
|
1092
753
|
const dockerOption = Flag.boolean("docker").pipe(Flag.withDescription("Run Wings using Docker (recommended)"), Flag.withDefault(false));
|
|
1093
|
-
const versionOption = Flag.string("version").pipe(Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.
|
|
754
|
+
const versionOption = Flag.string("version").pipe(Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.14 or 'latest')"), Flag.withDefault("latest"));
|
|
1094
755
|
const tagOption = Flag.string("tag").pipe(Flag.withDescription("Docker image tag (only with --docker)"), Flag.withDefault("latest"));
|
|
1095
756
|
const forcePullOption = Flag.boolean("force-pull").pipe(Flag.withDescription("Force pull Docker image even if it exists locally"), Flag.withDefault(false));
|
|
1096
757
|
const stressOption = Flag.boolean("stress").pipe(Flag.withDescription("Use Wings stress binary variant"), Flag.withDefault(false));
|
|
@@ -1104,7 +765,6 @@ const devCommand = Command.make("dev", {
|
|
|
1104
765
|
yes: yesOption
|
|
1105
766
|
}, (options) => options.docker ? runWithDocker(options) : runWithBinary(options)).pipe(Command.withDescription("Download and run Wings dev server locally (Docker recommended for portability)"));
|
|
1106
767
|
const runWithBinary = (options) => Effect.gen(function* () {
|
|
1107
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
1108
768
|
const fs$1 = yield* FileSystem.FileSystem;
|
|
1109
769
|
yield* Effect.logInfo("🪽 Airfoil Dev");
|
|
1110
770
|
const version$2 = options.version;
|
|
@@ -1122,12 +782,11 @@ const runWithBinary = (options) => Effect.gen(function* () {
|
|
|
1122
782
|
yield* downloadWings(version$2, wingsPath, isStress);
|
|
1123
783
|
} else yield* Effect.logInfo(`Using cached Wings${isStress ? " stress" : ""} binary (${version$2})`);
|
|
1124
784
|
yield* Effect.logInfo(`Starting Wings dev server...`);
|
|
1125
|
-
const
|
|
785
|
+
const exitCode = yield* (yield* ChildProcess.make(wingsPath, ["dev"], {
|
|
1126
786
|
stdin: "inherit",
|
|
1127
787
|
stdout: "inherit"
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
if (exitCode !== 0) yield* Effect.fail(/* @__PURE__ */ new Error(`Process exited with code ${exitCode}`));
|
|
788
|
+
})).exitCode;
|
|
789
|
+
if (exitCode !== 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Process exited with code ${exitCode}`));
|
|
1131
790
|
});
|
|
1132
791
|
const runWithDocker = (options) => Effect.gen(function* () {
|
|
1133
792
|
yield* Effect.logInfo("🐳 Airfoil Dev (Docker)");
|
|
@@ -1138,8 +797,12 @@ const runWithDocker = (options) => Effect.gen(function* () {
|
|
|
1138
797
|
}
|
|
1139
798
|
const image = `docker.useairfoil.com/airfoil/wings:${tag}`;
|
|
1140
799
|
yield* Effect.logInfo(`Using Docker image: ${image}`);
|
|
1141
|
-
yield* Effect.logInfo("Ports: 7777 (gRPC)
|
|
800
|
+
yield* Effect.logInfo("Ports: 7777 (gRPC)");
|
|
1142
801
|
yield* checkDockerVersion();
|
|
802
|
+
if (options.forcePull) {
|
|
803
|
+
yield* Effect.logInfo(`Pulling Docker image: ${image}`);
|
|
804
|
+
yield* pullDockerImage(image);
|
|
805
|
+
}
|
|
1143
806
|
yield* createDockerVolume("wings-data");
|
|
1144
807
|
yield* runDockerContainer(image, "wings-data");
|
|
1145
808
|
});
|
|
@@ -1148,7 +811,7 @@ const runWithDocker = (options) => Effect.gen(function* () {
|
|
|
1148
811
|
//#region src/commands/sql.ts
|
|
1149
812
|
const queryArg = Argument.string("query").pipe(Argument.withDescription("SQL query to execute"), Argument.optional);
|
|
1150
813
|
const fileOption = Flag.string("file").pipe(Flag.withAlias("f"), Flag.withDescription("Execute SQL from file"), Flag.optional);
|
|
1151
|
-
const namespaceOption = Flag.string("namespace").pipe(Flag.withAlias("n"), Flag.withDescription("Wings namespace"), Flag.withDefault("
|
|
814
|
+
const namespaceOption = Flag.string("namespace").pipe(Flag.withAlias("n"), Flag.withDescription("Wings namespace"), Flag.withDefault("namespaces/default"));
|
|
1152
815
|
const jsonOption = Flag.boolean("json").pipe(Flag.withDescription("Output results as JSON"), Flag.withDefault(false));
|
|
1153
816
|
const sqlCommand = Command.make("sql", {
|
|
1154
817
|
query: queryArg,
|
|
@@ -1165,15 +828,13 @@ const sqlCommand = Command.make("sql", {
|
|
|
1165
828
|
if (!sqlQuery) return yield* Effect.fail(/* @__PURE__ */ new Error("No query provided. Use a query argument or --file option."));
|
|
1166
829
|
const hostPort = `${host}:${port}`;
|
|
1167
830
|
yield* Effect.logInfo(`Namespace: ${namespace}`);
|
|
1168
|
-
const client =
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
catch: (error) => error instanceof Error ? error : /* @__PURE__ */ new Error("Query failed")
|
|
831
|
+
const client = yield* ArrowFlightSqlClient.make({
|
|
832
|
+
host: hostPort,
|
|
833
|
+
defaultCallOptions: { "*": { metadata: Metadata({ "x-wings-namespace": namespace }) } }
|
|
1172
834
|
});
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
})).flatMap((batch) => batch.toArray());
|
|
835
|
+
const flightInfo = yield* client.executeQuery({ query: sqlQuery });
|
|
836
|
+
const batches = yield* client.executeFlightInfo(flightInfo).pipe(Stream.runCollect);
|
|
837
|
+
const data = Array.from(batches).flatMap(({ batch }) => batch.toArray());
|
|
1177
838
|
if (json) yield* Effect.log(JSON.stringify(data, null, 2));
|
|
1178
839
|
else if (data.length === 0) yield* Effect.logWarning("No results returned");
|
|
1179
840
|
else printTable(data, { defaultColumnOptions: { alignment: "left" } });
|
|
@@ -1187,7 +848,8 @@ const program = Command.make("airfoil", {}, () => Effect.void).pipe(Command.with
|
|
|
1187
848
|
sqlCommand,
|
|
1188
849
|
clusterCommand
|
|
1189
850
|
]));
|
|
1190
|
-
Command.run(program, { version })
|
|
851
|
+
const cli = Command.run(program, { version });
|
|
852
|
+
NodeRuntime.runMain(cli.pipe(Effect.provide(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer)), Effect.scoped));
|
|
1191
853
|
|
|
1192
854
|
//#endregion
|
|
1193
855
|
export { };
|