airfoil 0.4.5 → 0.4.6

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 DELETED
@@ -1,855 +0,0 @@
1
- #!/usr/bin/env node
2
- import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
- import { Config, Effect, FileSystem, Layer, Option, Path, Redacted, Stream, String as String$1 } from "effect";
4
- import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
5
- import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http";
6
- import * as p from "@clack/prompts";
7
- import { ClusterClient } from "@useairfoil/wings";
8
- import { printTable } from "console-table-printer";
9
- import * as fs from "node:fs";
10
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
11
- import { createHash } from "node:crypto";
12
- import { homedir } from "node:os";
13
- import { join } from "node:path";
14
- import { ArrowFlightSqlClient } from "@useairfoil/flight";
15
- import { Metadata } from "nice-grpc-common";
16
-
17
- //#region package.json
18
- var version$1 = "0.4.4";
19
-
20
- //#endregion
21
- //#region src/utils/client.ts
22
- const makeClusterClientLayer = (host, port) => ClusterClient.layer({ host: `${host}:${port}` });
23
-
24
- //#endregion
25
- //#region src/utils/options.ts
26
- /**
27
- * Common server connection options
28
- */
29
- const hostOption = Flag.string("host").pipe(Flag.withDescription("Server host"), Flag.withDefault("127.0.0.1"));
30
- const portOption = Flag.integer("port").pipe(Flag.withDescription("Server port"), Flag.withAlias("p"), Flag.withDefault(7777));
31
- /**
32
- * Common pagination options
33
- */
34
- const pageSizeOption = Flag.integer("page-size").pipe(Flag.withDescription("Number of items to return (max: 1000)"), Flag.withDefault(100));
35
- const pageTokenOption = Flag.optional(Flag.string("page-token").pipe(Flag.withDescription("Continuation token for pagination")));
36
- /**
37
- * Force flag for destructive operations
38
- */
39
- const forceOption = Flag.boolean("force").pipe(Flag.withDescription("Skip confirmation prompt"), Flag.withDefault(false));
40
-
41
- //#endregion
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: {
49
- _tag: "iceberg",
50
- iceberg: {}
51
- } };
52
- case "parquet": return { lakeConfig: {
53
- _tag: "parquet",
54
- parquet: {}
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.`);
61
- }
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
- }]);
69
- p.outro("✓ Done");
70
- }
71
- const createNamespaceAwsCommand = Command.make("aws", {
72
- namespaceId: namespaceIdOption,
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),
79
- host: hostOption,
80
- port: portOption
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
- });
87
- const s = p.spinner();
88
- s.start("Creating namespace...");
89
- const namespace = yield* ClusterClient.createNamespace({
90
- namespaceId,
91
- objectStore: { objectStoreConfig: {
92
- _tag: "aws",
93
- aws: {
94
- bucketName,
95
- prefix: Option.getOrUndefined(prefix),
96
- accessKeyId: Redacted.value(accessKeyId),
97
- secretAccessKey: Redacted.value(secretAccessKey),
98
- region: Option.getOrUndefined(region)
99
- }
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"))),
113
- host: hostOption,
114
- port: portOption
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
- });
121
- const s = p.spinner();
122
- s.start("Creating namespace...");
123
- const namespace = yield* ClusterClient.createNamespace({
124
- namespaceId,
125
- objectStore: { objectStoreConfig: {
126
- _tag: "azure",
127
- azure: {
128
- containerName,
129
- prefix: Option.getOrUndefined(prefix),
130
- storageAccountName,
131
- storageAccountKey: Redacted.value(storageAccountKey)
132
- }
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"))),
146
- host: hostOption,
147
- port: portOption
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
- });
154
- const s = p.spinner();
155
- s.start("Creating namespace...");
156
- const namespace = yield* ClusterClient.createNamespace({
157
- namespaceId,
158
- objectStore: { objectStoreConfig: {
159
- _tag: "google",
160
- google: {
161
- bucketName,
162
- prefix: Option.getOrUndefined(prefix),
163
- serviceAccount,
164
- serviceAccountKey: Redacted.value(serviceAccountKey)
165
- }
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")),
176
- prefix: prefixOption,
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),
182
- host: hostOption,
183
- port: portOption
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
- });
190
- const s = p.spinner();
191
- s.start("Creating namespace...");
192
- const namespace = yield* ClusterClient.createNamespace({
193
- namespaceId,
194
- objectStore: { objectStoreConfig: {
195
- _tag: "s3Compatible",
196
- s3Compatible: {
197
- bucketName,
198
- prefix: Option.getOrUndefined(prefix),
199
- accessKeyId: Redacted.value(accessKeyId),
200
- secretAccessKey: Redacted.value(secretAccessKey),
201
- endpoint,
202
- allowHttp,
203
- region: Option.getOrUndefined(region)
204
- }
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
216
- ]));
217
-
218
- //#endregion
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,
223
- force: forceOption,
224
- host: hostOption,
225
- port: portOption
226
- }, ({ name, force }) => Effect.gen(function* () {
227
- p.intro("🗑️ Delete Namespace");
228
- if (!force) {
229
- const confirm = yield* Effect.tryPromise({
230
- try: () => p.confirm({
231
- message: `Are you sure you want to delete namespace ${name}?`,
232
- initialValue: false
233
- }),
234
- catch: () => /* @__PURE__ */ new Error("Failed to confirm deletion")
235
- });
236
- if (p.isCancel(confirm) || !confirm) {
237
- p.cancel("Deletion cancelled");
238
- return;
239
- }
240
- }
241
- const s = p.spinner();
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");
245
- p.outro("✓ Done");
246
- })).pipe(Command.withDescription("Delete a namespace from the cluster"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
247
-
248
- //#endregion
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,
253
- host: hostOption,
254
- port: portOption
255
- }, ({ name }) => Effect.gen(function* () {
256
- const s = p.spinner();
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");
260
- yield* Effect.sync(() => {
261
- printTable([{
262
- name: namespace.name,
263
- object_store: namespace.objectStore?.objectStoreConfig._tag ?? "-",
264
- lake: namespace.lake?.lakeConfig._tag ?? "-"
265
- }]);
266
- p.outro("✓ Done");
267
- });
268
- })).pipe(Command.withDescription("Get details of a specific namespace"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
269
-
270
- //#endregion
271
- //#region src/commands/cluster/namespace/list.ts
272
- const listNamespacesCommand = Command.make("list-namespaces", {
273
- pageSize: pageSizeOption,
274
- pageToken: pageTokenOption,
275
- host: hostOption,
276
- port: portOption
277
- }, ({ pageSize, pageToken }) => Effect.gen(function* () {
278
- const s = p.spinner();
279
- s.start("Fetching namespaces...");
280
- const response = yield* ClusterClient.listNamespaces({
281
- pageSize,
282
- pageToken: Option.getOrUndefined(pageToken)
283
- }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to list namespaces"))));
284
- s.stop(`Found ${response.namespaces.length} namespace(s)`);
285
- yield* Effect.sync(() => {
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 ?? "-"
291
- })));
292
- if (response.nextPageToken) p.log.info(`Next page token: ${response.nextPageToken}`);
293
- p.outro("✓ Done");
294
- });
295
- })).pipe(Command.withDescription("List all namespaces in the cluster"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
296
-
297
- //#endregion
298
- //#region src/commands/cluster/table/create.ts
299
- const SUPPORTED_INLINE_TYPES = [
300
- "Int8",
301
- "Int16",
302
- "Int32",
303
- "Int64",
304
- "Uint8",
305
- "Uint16",
306
- "Uint32",
307
- "Uint64",
308
- "Bool",
309
- "Utf8",
310
- "Binary",
311
- "Null",
312
- "Float16",
313
- "Float32",
314
- "Float64",
315
- "DateDay",
316
- "DateMillisecond",
317
- "TimestampSecond",
318
- "TimestampMillisecond",
319
- "TimestampMicrosecond",
320
- "TimestampNanosecond",
321
- "DurationSecond",
322
- "DurationMillisecond",
323
- "DurationMicrosecond",
324
- "DurationNanosecond"
325
- ];
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);
329
- const fieldsOption = Flag.string("fields").pipe(Flag.withDescription("Field definitions in format \"name:Type\" or \"name:Type?\" for nullable"), Flag.atLeast(0));
330
- const schemaFileOption = Flag.string("schema-file").pipe(Flag.withDescription("Path to JSON file containing FieldConfig[] (for complex types)"), Flag.optional);
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));
335
- function parseFieldString(fieldStr, index) {
336
- const match = fieldStr.match(/^([^:]+):([^?]+)(\?)?$/);
337
- if (!match) throw new Error(`Invalid field format: "${fieldStr}". Expected format: "name:Type" or "name:Type?"`);
338
- const name = match[1];
339
- const dataType = match[2];
340
- const nullableMarker = match[3];
341
- if (!name || !dataType) throw new Error(`Invalid field format: "${fieldStr}". Expected format: "name:Type" or "name:Type?"`);
342
- const nullable = nullableMarker === "?";
343
- if (!SUPPORTED_INLINE_TYPES.includes(dataType)) throw new Error(`Unsupported type: "${dataType}". Supported types: ${SUPPORTED_INLINE_TYPES.join(", ")}. For complex types, use --schema-file.`);
344
- return {
345
- name: name.trim(),
346
- dataType,
347
- nullable,
348
- id: BigInt(index + 1)
349
- };
350
- }
351
- function parseFieldsFromArgs(fields) {
352
- return fields.map((field, index) => parseFieldString(field, index));
353
- }
354
- function loadFieldsFromFile(filePath) {
355
- if (!fs.existsSync(filePath)) throw new Error(`Schema file not found: "${filePath}"`);
356
- const content = fs.readFileSync(filePath, "utf-8");
357
- let parsed;
358
- try {
359
- parsed = JSON.parse(content);
360
- } catch {
361
- throw new Error(`Invalid JSON in schema file: "${filePath}"`);
362
- }
363
- if (!Array.isArray(parsed)) throw new Error(`Schema file must contain an array of fields. Got: ${typeof parsed}`);
364
- if (parsed.length === 0) throw new Error("Schema file contains an empty array. At least one field is required.");
365
- return parsed.map((field, index) => {
366
- const fieldId = field.id;
367
- return {
368
- ...field,
369
- id: typeof fieldId === "bigint" ? fieldId : BigInt(fieldId ?? index + 1)
370
- };
371
- });
372
- }
373
- const createTableCommand = Command.make("create-table", {
374
- parent: parentOption$1,
375
- tableId: tableIdOption,
376
- description: descriptionOption,
377
- fields: fieldsOption,
378
- schemaFile: schemaFileOption,
379
- keyField: keyFieldOption,
380
- versionField: versionFieldOption,
381
- partitionField: partitionFieldOption,
382
- freshnessSeconds: freshnessSecondsOption,
383
- host: hostOption,
384
- port: portOption
385
- }, ({ parent, tableId, description, fields, schemaFile, keyField, versionField, partitionField, freshnessSeconds }) => Effect.gen(function* () {
386
- p.intro("📋 Create Table");
387
- const schemaFilePath = Option.getOrUndefined(schemaFile);
388
- const hasFields = fields.length > 0;
389
- if (!hasFields && !schemaFilePath) return yield* Effect.fail(/* @__PURE__ */ new Error("Either --fields or --schema-file must be provided"));
390
- if (hasFields && schemaFilePath) return yield* Effect.fail(/* @__PURE__ */ new Error("Cannot use both --fields and --schema-file. Choose one."));
391
- const tableFields = yield* Effect.try({
392
- try: () => schemaFilePath ? loadFieldsFromFile(schemaFilePath) : parseFieldsFromArgs(fields),
393
- catch: (error) => error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to parse fields")
394
- });
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;
414
- const s = p.spinner();
415
- s.start("Creating table...");
416
- const table = yield* ClusterClient.createTable({
417
- parent,
418
- tableId,
419
- description: Option.getOrUndefined(description),
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");
427
- yield* Effect.sync(() => {
428
- printTable([{
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()
436
- }]);
437
- if (table.schema.fields.length > 0) {
438
- console.log("\nFields:");
439
- printTable(table.schema.fields.map((field) => ({
440
- name: field.name,
441
- id: field.id.toString(),
442
- type: field.arrowType?._tag ?? "unknown",
443
- nullable: field.nullable ? "yes" : "no"
444
- })));
445
- }
446
- p.outro("✓ Done");
447
- });
448
- })).pipe(Command.withDescription("Create a new table belonging to a namespace"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
449
-
450
- //#endregion
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", {
454
- name: nameOption$1,
455
- force: forceOption,
456
- host: hostOption,
457
- port: portOption
458
- }, ({ name, force }) => Effect.gen(function* () {
459
- p.intro("🗑️ Delete Table");
460
- if (!force) {
461
- const confirm = yield* Effect.tryPromise({
462
- try: () => p.confirm({
463
- message: `Are you sure you want to delete table ${name}?`,
464
- initialValue: false
465
- }),
466
- catch: () => /* @__PURE__ */ new Error("Failed to confirm deletion")
467
- });
468
- if (p.isCancel(confirm) || !confirm) {
469
- p.cancel("Deletion cancelled");
470
- return;
471
- }
472
- }
473
- const s = p.spinner();
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");
477
- p.outro("✓ Done");
478
- })).pipe(Command.withDescription("Delete a table from the cluster"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
479
-
480
- //#endregion
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", {
484
- name: nameOption,
485
- host: hostOption,
486
- port: portOption
487
- }, ({ name }) => Effect.gen(function* () {
488
- const s = p.spinner();
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");
492
- yield* Effect.sync(() => {
493
- printTable([{
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()
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
- }
510
- p.outro("✓ Done");
511
- });
512
- })).pipe(Command.withDescription("Get details of a specific table"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
513
-
514
- //#endregion
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", {
518
- parent: parentOption,
519
- pageSize: pageSizeOption,
520
- pageToken: pageTokenOption,
521
- host: hostOption,
522
- port: portOption
523
- }, ({ parent, pageSize, pageToken }) => Effect.gen(function* () {
524
- const s = p.spinner();
525
- s.start("Fetching tables...");
526
- const response = yield* ClusterClient.listTables({
527
- parent,
528
- pageSize,
529
- pageToken: Option.getOrUndefined(pageToken)
530
- }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to list tables"))));
531
- s.stop(`Found ${response.tables.length} table(s)`);
532
- yield* Effect.sync(() => {
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()
540
- })));
541
- if (response.nextPageToken) p.log.info(`Next page token: ${response.nextPageToken}`);
542
- p.outro("✓ Done");
543
- });
544
- })).pipe(Command.withDescription("List all tables belonging to a namespace"), Command.provide(({ host, port }) => makeClusterClientLayer(host, port)));
545
-
546
- //#endregion
547
- //#region src/commands/cluster/index.ts
548
- const clusterCommand = Command.make("cluster", {}, () => Effect.void).pipe(Command.withDescription("Manage Wings namespaces and tables"), Command.withSubcommands([
549
- createNamespaceCommand,
550
- listNamespacesCommand,
551
- getNamespaceCommand,
552
- deleteNamespaceCommand,
553
- createTableCommand,
554
- listTablesCommand,
555
- getTableCommand,
556
- deleteTableCommand
557
- ]));
558
-
559
- //#endregion
560
- //#region src/utils/docker.ts
561
- const checkDockerVersion = () => Effect.gen(function* () {
562
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
563
- const docker = ChildProcess.make("docker", ["--version"]);
564
- return yield* spawner.string(docker).pipe(Effect.map(String$1.trim));
565
- });
566
- const createDockerVolume = (volumeName) => Effect.gen(function* () {
567
- const exitCode = yield* (yield* ChildProcess.make("docker", [
568
- "volume",
569
- "create",
570
- volumeName
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}`));
608
- });
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)));
645
- const exitCode = yield* (yield* ChildProcess.make("docker", [
646
- "run",
647
- "-it",
648
- "--rm",
649
- "--name",
650
- "wings-dev",
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",
665
- "-p",
666
- "7777:7777",
667
- image,
668
- "dev",
669
- "--grpc.address=0.0.0.0:7777"
670
- ])).exitCode;
671
- if (exitCode !== 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Docker process exited with code ${exitCode}`));
672
- });
673
-
674
- //#endregion
675
- //#region src/utils/wings.ts
676
- const WINGS_DIR = join(homedir(), ".local", "share", "wings");
677
- const GITHUB_RELEASES_URL = "https://github.com/useairfoil/wings/releases";
678
- /**
679
- * Get the platform string for Wings binary naming
680
- * Examples: "aarch64-linux", "x86_64-linux", "aarch64-macos"
681
- */
682
- function getPlatformString() {
683
- const platform = process.platform;
684
- const arch = process.arch;
685
- return `${{
686
- arm64: "aarch64",
687
- x64: "x86_64"
688
- }[arch] || arch}-${{
689
- darwin: "macos",
690
- linux: "linux",
691
- win32: "windows"
692
- }[platform] || platform}`;
693
- }
694
- /**
695
- * Get the binary filename based on platform and stress variant
696
- * Examples:
697
- * - "wings-aarch64-macos"
698
- * - "wings-stress-aarch64-linux"
699
- */
700
- function getBinaryFilename(isStress) {
701
- const platformString = getPlatformString();
702
- return isStress ? `wings-stress-${platformString}` : `wings-${platformString}`;
703
- }
704
- const getWingsPath = (version$2, isStress = false) => Effect.gen(function* () {
705
- const binaryFilename = getBinaryFilename(isStress);
706
- return (yield* Path.Path).join(WINGS_DIR, `${binaryFilename}-${version$2}`);
707
- });
708
- const downloadWings = (version$2, targetPath, isStress = false) => Effect.gen(function* () {
709
- const fs$1 = yield* FileSystem.FileSystem;
710
- const client = yield* HttpClient.HttpClient;
711
- const filename = getBinaryFilename(isStress);
712
- const tempPath = `${targetPath}.download`;
713
- const url = `${GITHUB_RELEASES_URL}/${version$2 === "latest" ? "latest/download" : `download/${version$2}`}/${filename}`;
714
- yield* fs$1.makeDirectory(WINGS_DIR, { recursive: true });
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)));
727
- });
728
- /**
729
- * Download and verify the checksum of a Wings binary
730
- * The hash file format is: "<hash> <path>/wings"
731
- */
732
- const verifyChecksum = (version$2, filePath, isStress = false) => Effect.gen(function* () {
733
- const fs$1 = yield* FileSystem.FileSystem;
734
- const client = yield* HttpClient.HttpClient;
735
- const hashFilename = `${getBinaryFilename(isStress)}-hash.txt`;
736
- const url = `${GITHUB_RELEASES_URL}/${version$2 === "latest" ? "latest/download" : `download/${version$2}`}/${hashFilename}`;
737
- const response = yield* client.get(url);
738
- const expectedChecksum = (yield* HttpClientResponse.matchStatus({
739
- 200: (response$1) => response$1.text,
740
- orElse: (response$1) => {
741
- return Effect.fail(/* @__PURE__ */ new Error(`Failed to download hash file from ${url}: ${response$1.status}`));
742
- }
743
- })(response)).trim().split(/\s+/)[0];
744
- if (!expectedChecksum) return yield* Effect.fail(/* @__PURE__ */ new Error("Invalid hash file format"));
745
- const fileBuffer = yield* fs$1.readFile(filePath);
746
- const hash = createHash("sha256");
747
- hash.update(fileBuffer);
748
- if (hash.digest("hex") !== expectedChecksum) return yield* Effect.fail(/* @__PURE__ */ new Error("Wings binary checksum mismatch"));
749
- });
750
-
751
- //#endregion
752
- //#region src/commands/dev.ts
753
- const dockerOption = Flag.boolean("docker").pipe(Flag.withDescription("Run Wings using Docker (recommended)"), Flag.withDefault(false));
754
- const versionOption = Flag.string("version").pipe(Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.14 or 'latest')"), Flag.withDefault("latest"));
755
- const tagOption = Flag.string("tag").pipe(Flag.withDescription("Docker image tag (only with --docker)"), Flag.withDefault("latest"));
756
- const forcePullOption = Flag.boolean("force-pull").pipe(Flag.withDescription("Force pull Docker image even if it exists locally"), Flag.withDefault(false));
757
- const stressOption = Flag.boolean("stress").pipe(Flag.withDescription("Use Wings stress binary variant"), Flag.withDefault(false));
758
- const yesOption = Flag.boolean("yes").pipe(Flag.withAlias("y"), Flag.withDescription("Skip confirmation prompts"), Flag.withDefault(false));
759
- const devCommand = Command.make("dev", {
760
- docker: dockerOption,
761
- version: versionOption,
762
- tag: tagOption,
763
- forcePull: forcePullOption,
764
- stress: stressOption,
765
- yes: yesOption
766
- }, (options) => options.docker ? runWithDocker(options) : runWithBinary(options)).pipe(Command.withDescription("Download and run Wings dev server locally (Docker recommended for portability)"));
767
- const runWithBinary = (options) => Effect.gen(function* () {
768
- const fs$1 = yield* FileSystem.FileSystem;
769
- yield* Effect.logInfo("🪽 Airfoil Dev");
770
- const version$2 = options.version;
771
- const isStress = options.stress;
772
- const wingsPath = yield* getWingsPath(version$2, isStress);
773
- if (!(yield* fs$1.exists(wingsPath))) {
774
- yield* Effect.logWarning(`Wings${isStress ? " stress" : ""} binary not found for version ${version$2}`);
775
- if (!options.yes) {
776
- if (!(yield* Prompt.confirm({
777
- message: `Download Wings${isStress ? " stress" : ""} ${version$2} binary? (~100MB)`,
778
- initial: true
779
- }))) return yield* Effect.fail(/* @__PURE__ */ new Error("Download cancelled"));
780
- }
781
- yield* Effect.logInfo(`Downloading Wings${isStress ? " stress" : ""} ${version$2}...`);
782
- yield* downloadWings(version$2, wingsPath, isStress);
783
- } else yield* Effect.logInfo(`Using cached Wings${isStress ? " stress" : ""} binary (${version$2})`);
784
- yield* Effect.logInfo(`Starting Wings dev server...`);
785
- const exitCode = yield* (yield* ChildProcess.make(wingsPath, ["dev"], {
786
- stdin: "inherit",
787
- stdout: "inherit"
788
- })).exitCode;
789
- if (exitCode !== 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Process exited with code ${exitCode}`));
790
- });
791
- const runWithDocker = (options) => Effect.gen(function* () {
792
- yield* Effect.logInfo("🐳 Airfoil Dev (Docker)");
793
- let tag = options.tag;
794
- if (tag !== "latest" && !tag.includes("aarch64") && !tag.includes("x86_64")) {
795
- const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
796
- tag = `${tag.startsWith("v") ? tag.substring(1) : tag}-${arch}`;
797
- }
798
- const image = `docker.useairfoil.com/airfoil/wings:${tag}`;
799
- yield* Effect.logInfo(`Using Docker image: ${image}`);
800
- yield* Effect.logInfo("Ports: 7777 (gRPC)");
801
- yield* checkDockerVersion();
802
- if (options.forcePull) {
803
- yield* Effect.logInfo(`Pulling Docker image: ${image}`);
804
- yield* pullDockerImage(image);
805
- }
806
- yield* createDockerVolume("wings-data");
807
- yield* runDockerContainer(image, "wings-data");
808
- });
809
-
810
- //#endregion
811
- //#region src/commands/sql.ts
812
- const queryArg = Argument.string("query").pipe(Argument.withDescription("SQL query to execute"), Argument.optional);
813
- const fileOption = Flag.string("file").pipe(Flag.withAlias("f"), Flag.withDescription("Execute SQL from file"), Flag.optional);
814
- const namespaceOption = Flag.string("namespace").pipe(Flag.withAlias("n"), Flag.withDescription("Wings namespace"), Flag.withDefault("namespaces/default"));
815
- const jsonOption = Flag.boolean("json").pipe(Flag.withDescription("Output results as JSON"), Flag.withDefault(false));
816
- const sqlCommand = Command.make("sql", {
817
- query: queryArg,
818
- host: hostOption,
819
- port: portOption,
820
- file: fileOption,
821
- namespace: namespaceOption,
822
- json: jsonOption
823
- }, ({ query, host, port, file, namespace, json }) => Effect.gen(function* () {
824
- const fs$1 = yield* FileSystem.FileSystem;
825
- let sqlQuery = Option.getOrUndefined(query);
826
- const filePath = Option.getOrUndefined(file);
827
- if (filePath) sqlQuery = yield* fs$1.readFileString(filePath);
828
- if (!sqlQuery) return yield* Effect.fail(/* @__PURE__ */ new Error("No query provided. Use a query argument or --file option."));
829
- const hostPort = `${host}:${port}`;
830
- yield* Effect.logInfo(`Namespace: ${namespace}`);
831
- const client = yield* ArrowFlightSqlClient.make({
832
- host: hostPort,
833
- defaultCallOptions: { "*": { metadata: Metadata({ "x-wings-namespace": namespace }) } }
834
- });
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());
838
- if (json) yield* Effect.log(JSON.stringify(data, null, 2));
839
- else if (data.length === 0) yield* Effect.logWarning("No results returned");
840
- else printTable(data, { defaultColumnOptions: { alignment: "left" } });
841
- })).pipe(Command.withDescription("Execute SQL queries using Arrow Flight SQL"));
842
-
843
- //#endregion
844
- //#region src/index.ts
845
- const version = version$1;
846
- const program = Command.make("airfoil", {}, () => Effect.void).pipe(Command.withSubcommands([
847
- devCommand,
848
- sqlCommand,
849
- clusterCommand
850
- ]));
851
- const cli = Command.run(program, { version });
852
- NodeRuntime.runMain(cli.pipe(Effect.provide(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer)), Effect.scoped));
853
-
854
- //#endregion
855
- export { };