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.
Files changed (38) hide show
  1. package/dist/index.js +401 -739
  2. package/package.json +9 -6
  3. package/src/commands/cluster/index.ts +13 -37
  4. package/src/commands/cluster/namespace/create.ts +278 -61
  5. package/src/commands/cluster/namespace/delete.ts +9 -13
  6. package/src/commands/cluster/namespace/get.ts +12 -14
  7. package/src/commands/cluster/namespace/list.ts +16 -34
  8. package/src/commands/cluster/{topic → table}/create.ts +79 -96
  9. package/src/commands/cluster/{tenant → table}/delete.ts +15 -17
  10. package/src/commands/cluster/table/get.ts +62 -0
  11. package/src/commands/cluster/table/list.ts +61 -0
  12. package/src/commands/dev.ts +17 -13
  13. package/src/commands/sql.ts +13 -23
  14. package/src/index.ts +6 -6
  15. package/src/utils/client.ts +3 -3
  16. package/src/utils/docker.ts +101 -7
  17. package/src/utils/options.ts +1 -1
  18. package/src/utils/wings.ts +18 -14
  19. package/src/commands/cluster/data-lake/create-iceberg.ts +0 -59
  20. package/src/commands/cluster/data-lake/create-parquet.ts +0 -59
  21. package/src/commands/cluster/data-lake/create.ts +0 -10
  22. package/src/commands/cluster/data-lake/delete.ts +0 -54
  23. package/src/commands/cluster/data-lake/get.ts +0 -40
  24. package/src/commands/cluster/data-lake/list.ts +0 -62
  25. package/src/commands/cluster/object-store/create-aws.ts +0 -104
  26. package/src/commands/cluster/object-store/create-azure.ts +0 -96
  27. package/src/commands/cluster/object-store/create-google.ts +0 -87
  28. package/src/commands/cluster/object-store/create-s3.ts +0 -114
  29. package/src/commands/cluster/object-store/create.ts +0 -21
  30. package/src/commands/cluster/object-store/delete.ts +0 -56
  31. package/src/commands/cluster/object-store/get.ts +0 -49
  32. package/src/commands/cluster/object-store/list.ts +0 -62
  33. package/src/commands/cluster/tenant/create.ts +0 -44
  34. package/src/commands/cluster/tenant/get.ts +0 -40
  35. package/src/commands/cluster/tenant/list.ts +0 -53
  36. package/src/commands/cluster/topic/delete.ts +0 -65
  37. package/src/commands/cluster/topic/get.ts +0 -51
  38. 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 { WingsClusterMetadata } from "@useairfoil/wings";
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.3.0";
18
+ var version$1 = "0.4.0";
19
19
 
20
20
  //#endregion
21
21
  //#region src/utils/client.ts
22
- const makeClusterMetadataLayer = (host, port) => WingsClusterMetadata.layer({ host: `${host}:${port}` });
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("localhost"));
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/data-lake/create-iceberg.ts
43
- const parentOption$11 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
44
- const dataLakeIdOption$1 = Flag.string("data-lake-id").pipe(Flag.withDescription("Unique identifier for the data lake"));
45
- const createDataLakeIcebergCommand = Command.make("iceberg", {
46
- parent: parentOption$11,
47
- dataLakeId: dataLakeIdOption$1,
48
- host: hostOption,
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
- }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create data lake"))));
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
- }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create data lake"))));
95
- s.stop("Parquet data lake created successfully");
96
- yield* Effect.sync(() => {
97
- printTable([{
98
- name: result.name,
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
- const layer = makeClusterMetadataLayer(host, port);
133
- const s = p.spinner();
134
- s.start("Deleting data lake...");
135
- yield* WingsClusterMetadata.deleteDataLake({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete data lake"))));
136
- s.stop("Data lake deleted successfully");
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
- })).pipe(Command.withDescription("Delete a data lake from the cluster"));
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
- flushSizeBytes: flushSizeBytesOption,
204
- flushIntervalMillis: flushIntervalMillisOption,
205
- objectStore: objectStoreOption,
206
- dataLake: dataLakeOption,
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
- }, ({ parent, namespaceId, flushSizeBytes, flushIntervalMillis, objectStore, dataLake, host, port }) => Effect.gen(function* () {
210
- p.intro("📁 Create Namespace");
211
- const layer = makeClusterMetadataLayer(host, port);
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* WingsClusterMetadata.createNamespace({
215
- parent,
89
+ const namespace = yield* ClusterClient.createNamespace({
216
90
  namespaceId,
217
- flushSizeBytes: BigInt(flushSizeBytes),
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
- }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create AWS object store"))));
362
- s.stop("AWS S3 object store created successfully");
363
- yield* Effect.sync(() => {
364
- printTable([{
365
- name: result.name,
366
- type: "AWS S3",
367
- bucket: bucketName,
368
- region: Option.getOrUndefined(region) || "-"
369
- }]);
370
- p.outro("✓ Done");
371
- });
372
- })).pipe(Command.withDescription("Create a new AWS S3 object store"));
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
- }, ({ parent, objectStoreId, containerName, prefix, storageAccountName, storageAccountKey, host, port }) => Effect.gen(function* () {
392
- p.intro("🗄️ Create Azure Blob Storage Object Store");
393
- const layer = makeClusterMetadataLayer(host, port);
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 Azure object store...");
396
- const result = yield* WingsClusterMetadata.createObjectStore({
397
- parent,
398
- objectStoreId,
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
- }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create Azure object store"))));
409
- s.stop("Azure object store created successfully");
410
- yield* Effect.sync(() => {
411
- printTable([{
412
- name: result.name,
413
- type: "Azure Blob",
414
- container: containerName,
415
- account: storageAccountName
416
- }]);
417
- p.outro("✓ Done");
418
- });
419
- })).pipe(Command.withDescription("Create a new Azure Blob Storage object store"));
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
- }, ({ parent, objectStoreId, bucketName, prefix, serviceAccount, serviceAccountKey, host, port }) => Effect.gen(function* () {
439
- p.intro("🗄️ Create Google Cloud Storage Object Store");
440
- const layer = makeClusterMetadataLayer(host, port);
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 Google Cloud Storage object store...");
443
- const result = yield* WingsClusterMetadata.createObjectStore({
444
- parent,
445
- objectStoreId,
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
- }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create Google object store"))));
456
- s.stop("Google Cloud Storage object store created successfully");
457
- yield* Effect.sync(() => {
458
- printTable([{
459
- name: result.name,
460
- type: "Google Cloud",
461
- bucket: bucketName,
462
- service_account: serviceAccount
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: accessKeyIdOption,
484
- secretAccessKey: secretAccessKeyOption,
485
- region: regionOption,
486
- endpoint: endpointOption,
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
- }, ({ parent, objectStoreId, bucketName, prefix, accessKeyId, secretAccessKey, region, endpoint, host, port }) => Effect.gen(function* () {
490
- p.intro("🗄️ Create S3-Compatible Object Store");
491
- const layer = makeClusterMetadataLayer(host, port);
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 S3-compatible object store...");
494
- const result = yield* WingsClusterMetadata.createObjectStore({
495
- parent,
496
- objectStoreId,
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: false
202
+ allowHttp,
203
+ region: Option.getOrUndefined(region)
507
204
  }
508
- }
509
- }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to create S3-compatible object store"))));
510
- s.stop("S3-compatible object store created successfully");
511
- yield* Effect.sync(() => {
512
- printTable([{
513
- name: result.name,
514
- type: "S3-Compatible",
515
- bucket: bucketName,
516
- endpoint
517
- }]);
518
- p.outro("✓ Done");
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/object-store/delete.ts
533
- const nameOption$5 = Flag.string("name").pipe(Flag.withDescription("Object store name in format: tenants/{tenant}/object-stores/{object-store}"));
534
- const deleteObjectStoreCommand = Command.make("delete-object-store", {
535
- name: nameOption$5,
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, host, port }) => Effect.gen(function* () {
540
- p.intro("🗑️ Delete Object Store");
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 object store ${name}?`,
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
- process.exit(0);
238
+ return;
552
239
  }
553
240
  }
554
- const layer = makeClusterMetadataLayer(host, port);
555
241
  const s = p.spinner();
556
- s.start("Deleting object store...");
557
- yield* WingsClusterMetadata.deleteObjectStore({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete object store"))));
558
- s.stop("Object store deleted successfully");
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 an object store from the cluster"));
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/object-store/get.ts
564
- const nameOption$4 = Flag.string("name").pipe(Flag.withDescription("Object store name in format: tenants/{tenant}/object-stores/{object-store}"));
565
- const getObjectStoreCommand = Command.make("get-object-store", {
566
- name: nameOption$4,
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, host, port }) => Effect.gen(function* () {
570
- const layer = makeClusterMetadataLayer(host, port);
255
+ }, ({ name }) => Effect.gen(function* () {
571
256
  const s = p.spinner();
572
- s.start("Fetching object store...");
573
- const objectStore = yield* WingsClusterMetadata.getObjectStore({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to get object store"))));
574
- s.stop("Object store retrieved");
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: objectStore.name,
578
- type: objectStore.objectStoreConfig._tag || "-"
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 object store"));
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/object-store/list.ts
586
- const parentOption$2 = Flag.string("parent").pipe(Flag.withDescription("Parent tenant in format: tenants/{tenant}"));
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
- }, ({ parent, pageSize, pageToken, host, port }) => Effect.gen(function* () {
594
- const layer = makeClusterMetadataLayer(host, port);
277
+ }, ({ pageSize, pageToken }) => Effect.gen(function* () {
595
278
  const s = p.spinner();
596
- s.start("Fetching object stores...");
597
- const response = yield* WingsClusterMetadata.listObjectStores({
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.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to list object stores"))));
602
- s.stop(`Found ${response.objectStores.length} object store(s)`);
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.objectStores.length === 0) p.log.warn("No object stores found");
605
- else printTable(response.objectStores.map((objectStore) => ({
606
- name: objectStore.name,
607
- type: objectStore.objectStoreConfig._tag || "-"
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 object stores belonging to a tenant"));
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/topic/create.ts
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: tenants/{tenant}/namespaces/{namespace}"));
738
- const topicIdOption = Flag.string("topic-id").pipe(Flag.withDescription("Unique identifier for the topic"));
739
- const descriptionOption = Flag.string("description").pipe(Flag.withDescription("Topic description"), Flag.optional);
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 partitionKeyOption = Flag.string("partition-key").pipe(Flag.withDescription("Name of the field used for partitioning"), Flag.optional);
743
- const freshnessSecondsOption = Flag.integer("freshness-seconds").pipe(Flag.withDescription("How often to compact the topic (seconds)"), Flag.withDefault(60));
744
- const ttlSecondsOption = Flag.integer("ttl-seconds").pipe(Flag.withDescription("How long to keep topic data (seconds)"), Flag.optional);
745
- const targetFileSizeBytesOption = Flag.integer("target-file-size-bytes").pipe(Flag.withDescription("Target file size for compaction (bytes)"), Flag.withDefault(1024 * 1024));
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 createTopicCommand = Command.make("create-topic", {
373
+ const createTableCommand = Command.make("create-table", {
794
374
  parent: parentOption$1,
795
- topicId: topicIdOption,
375
+ tableId: tableIdOption,
796
376
  description: descriptionOption,
797
377
  fields: fieldsOption,
798
378
  schemaFile: schemaFileOption,
799
- partitionKey: partitionKeyOption,
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, topicId, description, fields, schemaFile, partitionKey, freshnessSeconds, ttlSeconds, targetFileSizeBytes, host, port }) => Effect.gen(function* () {
806
- p.intro("📋 Create Topic");
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 topicFields = yield* Effect.try({
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 (topicFields.length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("At least one field is required"));
816
- const partitionKeyName = Option.getOrUndefined(partitionKey);
817
- let partitionKeyId;
818
- if (partitionKeyName) {
819
- const index = topicFields.findIndex((field) => field.name === partitionKeyName);
820
- if (index === -1) return yield* Effect.fail(/* @__PURE__ */ new Error(`Partition key field "${partitionKeyName}" not found in fields. Available fields: ${topicFields.map((field) => field.name).join(", ")}`));
821
- const partitionField = topicFields[index];
822
- if (!partitionField) return yield* Effect.fail(/* @__PURE__ */ new Error(`Partition key field "${partitionKeyName}" not found in fields. Available fields: ${topicFields.map((field) => field.name).join(", ")}`));
823
- partitionKeyId = partitionField.id;
824
- }
825
- const layer = makeClusterMetadataLayer(host, port);
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 topic...");
828
- const topic = yield* WingsClusterMetadata.createTopic({
415
+ s.start("Creating table...");
416
+ const table = yield* ClusterClient.createTable({
829
417
  parent,
830
- topicId,
418
+ tableId,
831
419
  description: Option.getOrUndefined(description),
832
- fields: topicFields,
833
- partitionKey: partitionKeyId,
834
- compaction: {
835
- freshnessSeconds: BigInt(freshnessSeconds),
836
- ttlSeconds: Option.getOrUndefined(Option.map(ttlSeconds, (ttl) => BigInt(ttl))),
837
- targetFileSizeBytes: BigInt(targetFileSizeBytes)
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: topic.name,
844
- description: topic.description || "-",
845
- partition_key: topic.partitionKey !== void 0 ? topicFields.find((field) => field.id === topic.partitionKey)?.name || topic.partitionKey.toString() : "-",
846
- freshness_seconds: topic.compaction.freshnessSeconds.toString(),
847
- ttl_seconds: topic.compaction.ttlSeconds?.toString() || "-",
848
- target_file_size_bytes: topic.compaction.targetFileSizeBytes.toString(),
849
- fields_count: topic.schema.fields.length.toString()
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 (topic.schema.fields.length > 0) {
437
+ if (table.schema.fields.length > 0) {
852
438
  console.log("\nFields:");
853
- printTable(topic.schema.fields.map((field, index) => ({
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 topic belonging to a namespace"));
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/topic/delete.ts
866
- const nameOption$1 = Flag.string("name").pipe(Flag.withDescription("Topic name in format: tenants/{tenant}/namespaces/{namespace}/topics/{topic}"));
867
- const forceDeleteOption = Flag.boolean("force-delete").pipe(Flag.withDescription("Also delete data associated with the topic"), Flag.withDefault(false));
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, forceDelete, force, host, port }) => Effect.gen(function* () {
875
- p.intro("🗑️ Delete Topic");
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 topic ${name}?${forceDelete ? " (including all data)" : ""}`,
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
- process.exit(0);
470
+ return;
887
471
  }
888
472
  }
889
- const layer = makeClusterMetadataLayer(host, port);
890
473
  const s = p.spinner();
891
- s.start("Deleting topic...");
892
- yield* WingsClusterMetadata.deleteTopic({
893
- name,
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 topic from the cluster"));
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/topic/get.ts
902
- const nameOption = Flag.string("name").pipe(Flag.withDescription("Topic name in format: tenants/{tenant}/namespaces/{namespace}/topics/{topic}"));
903
- const getTopicCommand = Command.make("get-topic", {
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, host, port }) => Effect.gen(function* () {
908
- const layer = makeClusterMetadataLayer(host, port);
487
+ }, ({ name }) => Effect.gen(function* () {
909
488
  const s = p.spinner();
910
- s.start("Fetching topic...");
911
- const topic = yield* WingsClusterMetadata.getTopic({ name }).pipe(Effect.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to get topic"))));
912
- s.stop("Topic retrieved");
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: topic.name,
916
- description: topic.description || "-",
917
- partition_key: topic.partitionKey?.toString() || "-",
918
- freshness_seconds: topic.compaction.freshnessSeconds.toString(),
919
- ttl_seconds: topic.compaction.ttlSeconds?.toString() || "-"
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 topic"));
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/topic/list.ts
927
- const parentOption = Flag.string("parent").pipe(Flag.withDescription("Parent namespace in format: tenants/{tenant}/namespaces/{namespace}"));
928
- const listTopicsCommand = Command.make("list-topics", {
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, host, port }) => Effect.gen(function* () {
935
- const layer = makeClusterMetadataLayer(host, port);
523
+ }, ({ parent, pageSize, pageToken }) => Effect.gen(function* () {
936
524
  const s = p.spinner();
937
- s.start("Fetching topics...");
938
- const response = yield* WingsClusterMetadata.listTopics({
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.provide(layer), Effect.tapError(() => Effect.sync(() => s.stop("Failed to list topics"))));
943
- s.stop(`Found ${response.topics.length} topic(s)`);
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.topics.length === 0) p.log.warn("No topics found");
946
- else printTable(response.topics.map((topic) => ({
947
- name: topic.name,
948
- description: topic.description || "-",
949
- partition_key: topic.partitionKey?.toString() || "-"
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 topics belonging to a namespace"));
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("Interact with the cluster metadata server"), Command.withSubcommands([
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
- createTopicCommand,
968
- listTopicsCommand,
969
- getTopicCommand,
970
- deleteTopicCommand,
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
- "-v",
1003
- `${volumeName}:/tmp`,
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
- "--http.address=0.0.0.0:7780",
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
- const response = yield* client.get(url);
1057
- const buffer = yield* HttpClientResponse.matchStatus({
1058
- 200: (response$1) => response$1.arrayBuffer,
1059
- orElse: (response$1) => {
1060
- throw new Error(`Failed to download Wings from ${url}: ${response$1.status}`);
1061
- }
1062
- })(response);
1063
- yield* fs$1.writeFile(targetPath, new Uint8Array(buffer));
1064
- yield* verifyChecksum(version$2, targetPath, isStress);
1065
- yield* fs$1.chmod(targetPath, 493);
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.11 or 'latest')"), Flag.withDefault("latest"));
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 proc = ChildProcess.make(wingsPath, ["dev"], {
785
+ const exitCode = yield* (yield* ChildProcess.make(wingsPath, ["dev"], {
1126
786
  stdin: "inherit",
1127
787
  stdout: "inherit"
1128
- });
1129
- const exitCode = yield* (yield* spawner.spawn(proc)).exitCode;
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), 7780 (http)");
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("tenants/default/namespaces/default"));
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 = new ArrowFlightSqlClient({ host: hostPort }, { defaultCallOptions: { "*": { metadata: Metadata({ "x-wings-namespace": namespace }) } } });
1169
- const flightInfo = yield* Effect.tryPromise({
1170
- try: () => client.executeQuery({ query: sqlQuery }),
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 data = (yield* Effect.tryPromise({
1174
- try: () => Array.fromAsync(client.executeFlightInfo(flightInfo)),
1175
- catch: (error) => error instanceof Error ? error : /* @__PURE__ */ new Error("Query failed")
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 }).pipe(Effect.provide(FetchHttpClient.layer), Effect.provide(NodeServices.layer), Effect.scoped, NodeRuntime.runMain);
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 { };