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
@@ -1,16 +1,13 @@
1
- import type { FieldConfig } from "@useairfoil/wings";
2
-
3
1
  import * as p from "@clack/prompts";
4
- import { WingsClusterMetadata } from "@useairfoil/wings";
2
+ import { Arrow, ClusterClient } from "@useairfoil/wings";
5
3
  import { printTable } from "console-table-printer";
6
4
  import { Effect, Option } from "effect";
7
5
  import { Command, Flag } from "effect/unstable/cli";
8
6
  import * as fs from "node:fs";
9
7
 
10
- import { makeClusterMetadataLayer } from "../../../utils/client.js";
11
- import { hostOption, portOption } from "../../../utils/options.js";
8
+ import { makeClusterClientLayer } from "../../../utils/client";
9
+ import { hostOption, portOption } from "../../../utils/options";
12
10
 
13
- // Supported simple types (no config required)
14
11
  const SUPPORTED_INLINE_TYPES = [
15
12
  "Int8",
16
13
  "Int16",
@@ -40,17 +37,18 @@ const SUPPORTED_INLINE_TYPES = [
40
37
  ] as const;
41
38
 
42
39
  type SupportedInlineType = (typeof SUPPORTED_INLINE_TYPES)[number];
40
+ type FieldConfig = Arrow.FieldConfig;
43
41
 
44
42
  const parentOption = Flag.string("parent").pipe(
45
- Flag.withDescription("Parent namespace in format: tenants/{tenant}/namespaces/{namespace}"),
43
+ Flag.withDescription("Parent namespace in format: namespaces/{namespace}"),
46
44
  );
47
45
 
48
- const topicIdOption = Flag.string("topic-id").pipe(
49
- Flag.withDescription("Unique identifier for the topic"),
46
+ const tableIdOption = Flag.string("table-id").pipe(
47
+ Flag.withDescription("Unique identifier for the table"),
50
48
  );
51
49
 
52
50
  const descriptionOption = Flag.string("description").pipe(
53
- Flag.withDescription("Topic description"),
51
+ Flag.withDescription("Table description"),
54
52
  Flag.optional,
55
53
  );
56
54
 
@@ -64,29 +62,24 @@ const schemaFileOption = Flag.string("schema-file").pipe(
64
62
  Flag.optional,
65
63
  );
66
64
 
67
- const partitionKeyOption = Flag.string("partition-key").pipe(
68
- Flag.withDescription("Name of the field used for partitioning"),
69
- Flag.optional,
65
+ const keyFieldOption = Flag.string("key-field").pipe(
66
+ Flag.withDescription("Name of the field used as the primary key"),
70
67
  );
71
68
 
72
- const freshnessSecondsOption = Flag.integer("freshness-seconds").pipe(
73
- Flag.withDescription("How often to compact the topic (seconds)"),
74
- Flag.withDefault(60),
69
+ const versionFieldOption = Flag.string("version-field").pipe(
70
+ Flag.withDescription("Name of the field used as the version/timestamp"),
75
71
  );
76
72
 
77
- const ttlSecondsOption = Flag.integer("ttl-seconds").pipe(
78
- Flag.withDescription("How long to keep topic data (seconds)"),
73
+ const partitionFieldOption = Flag.string("partition-field").pipe(
74
+ Flag.withDescription("Name of the field used for partitioning"),
79
75
  Flag.optional,
80
76
  );
81
77
 
82
- const targetFileSizeBytesOption = Flag.integer("target-file-size-bytes").pipe(
83
- Flag.withDescription("Target file size for compaction (bytes)"),
84
- Flag.withDefault(1024 * 1024),
78
+ const freshnessSecondsOption = Flag.integer("freshness-seconds").pipe(
79
+ Flag.withDescription("Target freshness interval for compaction (seconds)"),
80
+ Flag.withDefault(60),
85
81
  );
86
82
 
87
- /**
88
- * Parse a field string in format "name:Type" or "name:Type?" (nullable)
89
- */
90
83
  function parseFieldString(fieldStr: string, index: number): FieldConfig {
91
84
  const match = fieldStr.match(/^([^:]+):([^?]+)(\?)?$/);
92
85
  if (!match) {
@@ -121,16 +114,10 @@ function parseFieldString(fieldStr: string, index: number): FieldConfig {
121
114
  };
122
115
  }
123
116
 
124
- /**
125
- * Parse multiple field strings into FieldConfig array
126
- */
127
117
  function parseFieldsFromArgs(fields: ReadonlyArray<string>): FieldConfig[] {
128
118
  return fields.map((field, index) => parseFieldString(field, index));
129
119
  }
130
120
 
131
- /**
132
- * Load fields from a JSON schema file with basic validation
133
- */
134
121
  function loadFieldsFromFile(filePath: string): FieldConfig[] {
135
122
  if (!fs.existsSync(filePath)) {
136
123
  throw new Error(`Schema file not found: "${filePath}"`);
@@ -162,36 +149,34 @@ function loadFieldsFromFile(filePath: string): FieldConfig[] {
162
149
  });
163
150
  }
164
151
 
165
- export const createTopicCommand = Command.make(
166
- "create-topic",
152
+ export const createTableCommand = Command.make(
153
+ "create-table",
167
154
  {
168
155
  parent: parentOption,
169
- topicId: topicIdOption,
156
+ tableId: tableIdOption,
170
157
  description: descriptionOption,
171
158
  fields: fieldsOption,
172
159
  schemaFile: schemaFileOption,
173
- partitionKey: partitionKeyOption,
160
+ keyField: keyFieldOption,
161
+ versionField: versionFieldOption,
162
+ partitionField: partitionFieldOption,
174
163
  freshnessSeconds: freshnessSecondsOption,
175
- ttlSeconds: ttlSecondsOption,
176
- targetFileSizeBytes: targetFileSizeBytesOption,
177
164
  host: hostOption,
178
165
  port: portOption,
179
166
  },
180
167
  ({
181
168
  parent,
182
- topicId,
169
+ tableId,
183
170
  description,
184
171
  fields,
185
172
  schemaFile,
186
- partitionKey,
173
+ keyField,
174
+ versionField,
175
+ partitionField,
187
176
  freshnessSeconds,
188
- ttlSeconds,
189
- targetFileSizeBytes,
190
- host,
191
- port,
192
177
  }) =>
193
178
  Effect.gen(function* () {
194
- p.intro("📋 Create Topic");
179
+ p.intro("📋 Create Table");
195
180
 
196
181
  const schemaFilePath = Option.getOrUndefined(schemaFile);
197
182
  const hasFields = fields.length > 0;
@@ -206,84 +191,79 @@ export const createTopicCommand = Command.make(
206
191
  );
207
192
  }
208
193
 
209
- const topicFields = yield* Effect.try({
194
+ const tableFields = yield* Effect.try({
210
195
  try: () =>
211
196
  schemaFilePath ? loadFieldsFromFile(schemaFilePath) : parseFieldsFromArgs(fields),
212
197
  catch: (error) => (error instanceof Error ? error : new Error("Failed to parse fields")),
213
198
  });
214
199
 
215
- if (topicFields.length === 0) {
200
+ if (tableFields.length === 0) {
216
201
  return yield* Effect.fail(new Error("At least one field is required"));
217
202
  }
218
203
 
219
- const partitionKeyName = Option.getOrUndefined(partitionKey);
220
- let partitionKeyId: bigint | undefined;
221
- if (partitionKeyName) {
222
- const index = topicFields.findIndex((field) => field.name === partitionKeyName);
223
- if (index === -1) {
224
- return yield* Effect.fail(
225
- new Error(
226
- `Partition key field "${partitionKeyName}" not found in fields. Available fields: ${topicFields.map((field) => field.name).join(", ")}`,
227
- ),
204
+ const resolveField = (fieldName: string, role: string): bigint => {
205
+ const field = tableFields.find((f) => f.name === fieldName);
206
+ if (!field) {
207
+ throw new Error(
208
+ `${role} field "${fieldName}" not found. Available fields: ${tableFields.map((f) => f.name).join(", ")}`,
228
209
  );
229
210
  }
230
- const partitionField = topicFields[index];
231
- if (!partitionField) {
232
- return yield* Effect.fail(
233
- new Error(
234
- `Partition key field "${partitionKeyName}" not found in fields. Available fields: ${topicFields.map((field) => field.name).join(", ")}`,
235
- ),
236
- );
237
- }
238
- partitionKeyId = partitionField.id;
239
- }
211
+ return field.id;
212
+ };
213
+
214
+ const keyFieldId = yield* Effect.try({
215
+ try: () => resolveField(keyField, "Key"),
216
+ catch: (e) => (e instanceof Error ? e : new Error(String(e))),
217
+ });
240
218
 
241
- const layer = makeClusterMetadataLayer(host, port);
219
+ const versionFieldId = yield* Effect.try({
220
+ try: () => resolveField(versionField, "Version"),
221
+ catch: (e) => (e instanceof Error ? e : new Error(String(e))),
222
+ });
223
+
224
+ const partitionFieldName = Option.getOrUndefined(partitionField);
225
+ const partitionFieldId = partitionFieldName
226
+ ? yield* Effect.try({
227
+ try: () => resolveField(partitionFieldName, "Partition"),
228
+ catch: (e) => (e instanceof Error ? e : new Error(String(e))),
229
+ })
230
+ : undefined;
242
231
 
243
232
  const s = p.spinner();
244
- s.start("Creating topic...");
233
+ s.start("Creating table...");
245
234
 
246
- const topic = yield* WingsClusterMetadata.createTopic({
235
+ const table = yield* ClusterClient.createTable({
247
236
  parent,
248
- topicId,
237
+ tableId,
249
238
  description: Option.getOrUndefined(description),
250
- fields: topicFields,
251
- partitionKey: partitionKeyId,
252
- compaction: {
253
- freshnessSeconds: BigInt(freshnessSeconds),
254
- ttlSeconds: Option.getOrUndefined(Option.map(ttlSeconds, (ttl) => BigInt(ttl))),
255
- targetFileSizeBytes: BigInt(targetFileSizeBytes),
256
- },
257
- }).pipe(
258
- Effect.provide(layer),
259
- Effect.tapError(() => Effect.sync(() => s.stop("Failed to create topic"))),
260
- );
261
-
262
- s.stop("Topic created successfully");
239
+ fields: tableFields,
240
+ keyFieldId,
241
+ versionFieldId,
242
+ partitionFieldId,
243
+ targetFreshnessSeconds: BigInt(freshnessSeconds),
244
+ }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create table"))));
245
+
246
+ s.stop("Table created successfully");
263
247
 
264
248
  yield* Effect.sync(() => {
265
249
  printTable([
266
250
  {
267
- name: topic.name,
268
- description: topic.description || "-",
269
- partition_key:
270
- topic.partitionKey !== undefined
271
- ? topicFields.find((field) => field.id === topic.partitionKey)?.name ||
272
- topic.partitionKey.toString()
273
- : "-",
274
- freshness_seconds: topic.compaction.freshnessSeconds.toString(),
275
- ttl_seconds: topic.compaction.ttlSeconds?.toString() || "-",
276
- target_file_size_bytes: topic.compaction.targetFileSizeBytes.toString(),
277
- fields_count: topic.schema.fields.length.toString(),
251
+ name: table.name,
252
+ description: table.description || "-",
253
+ key_field_id: table.keyFieldId.toString(),
254
+ version_field_id: table.versionFieldId.toString(),
255
+ partition_field_id: table.partitionFieldId?.toString() || "-",
256
+ freshness_seconds: table.targetFreshnessSeconds.toString(),
257
+ fields_count: table.schema.fields.length.toString(),
278
258
  },
279
259
  ]);
280
260
 
281
- if (topic.schema.fields.length > 0) {
261
+ if (table.schema.fields.length > 0) {
282
262
  console.log("\nFields:");
283
263
  printTable(
284
- topic.schema.fields.map((field, index) => ({
285
- index: index.toString(),
264
+ table.schema.fields.map((field) => ({
286
265
  name: field.name,
266
+ id: field.id.toString(),
287
267
  type: field.arrowType?._tag ?? "unknown",
288
268
  nullable: field.nullable ? "yes" : "no",
289
269
  })),
@@ -293,4 +273,7 @@ export const createTopicCommand = Command.make(
293
273
  p.outro("✓ Done");
294
274
  });
295
275
  }),
296
- ).pipe(Command.withDescription("Create a new topic belonging to a namespace"));
276
+ ).pipe(
277
+ Command.withDescription("Create a new table belonging to a namespace"),
278
+ Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
279
+ );
@@ -1,32 +1,32 @@
1
1
  import * as p from "@clack/prompts";
2
- import { WingsClusterMetadata } from "@useairfoil/wings";
2
+ import { ClusterClient } from "@useairfoil/wings";
3
3
  import { Effect } from "effect";
4
4
  import { Command, Flag } from "effect/unstable/cli";
5
5
 
6
- import { makeClusterMetadataLayer } from "../../../utils/client";
6
+ import { makeClusterClientLayer } from "../../../utils/client";
7
7
  import { forceOption, hostOption, portOption } from "../../../utils/options";
8
8
 
9
9
  const nameOption = Flag.string("name").pipe(
10
- Flag.withDescription("Tenant name in format: tenants/{tenant} (e.g., 'tenants/acme-corp')"),
10
+ Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"),
11
11
  );
12
12
 
13
- export const deleteTenantCommand = Command.make(
14
- "delete-tenant",
13
+ export const deleteTableCommand = Command.make(
14
+ "delete-table",
15
15
  {
16
16
  name: nameOption,
17
17
  force: forceOption,
18
18
  host: hostOption,
19
19
  port: portOption,
20
20
  },
21
- ({ name, force, host, port }) =>
21
+ ({ name, force }) =>
22
22
  Effect.gen(function* () {
23
- p.intro("🗑️ Delete Tenant");
23
+ p.intro("🗑️ Delete Table");
24
24
 
25
25
  if (!force) {
26
26
  const confirm = yield* Effect.tryPromise({
27
27
  try: () =>
28
28
  p.confirm({
29
- message: `Are you sure you want to delete tenant ${name}?`,
29
+ message: `Are you sure you want to delete table ${name}?`,
30
30
  initialValue: false,
31
31
  }),
32
32
  catch: () => new Error("Failed to confirm deletion"),
@@ -34,23 +34,21 @@ export const deleteTenantCommand = Command.make(
34
34
 
35
35
  if (p.isCancel(confirm) || !confirm) {
36
36
  p.cancel("Deletion cancelled");
37
- process.exit(0);
37
+ return;
38
38
  }
39
39
  }
40
40
 
41
- const layer = makeClusterMetadataLayer(host, port);
42
-
43
41
  const s = p.spinner();
44
- s.start("Deleting tenant...");
42
+ s.start("Deleting table...");
45
43
 
46
- yield* WingsClusterMetadata.deleteTenant({ name }).pipe(
47
- Effect.provide(layer),
48
- Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete tenant"))),
44
+ yield* ClusterClient.deleteTable({ name }).pipe(
45
+ Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete table"))),
49
46
  );
50
47
 
51
- s.stop("Tenant deleted successfully");
48
+ s.stop("Table deleted successfully");
52
49
  p.outro("✓ Done");
53
50
  }),
54
51
  ).pipe(
55
- Command.withDescription("Delete a tenant from the cluster (fails if tenant has any namespaces)"),
52
+ Command.withDescription("Delete a table from the cluster"),
53
+ Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
56
54
  );
@@ -0,0 +1,62 @@
1
+ import * as p from "@clack/prompts";
2
+ import { ClusterClient } from "@useairfoil/wings";
3
+ import { printTable } from "console-table-printer";
4
+ import { Effect } from "effect";
5
+ import { Command, Flag } from "effect/unstable/cli";
6
+
7
+ import { makeClusterClientLayer } from "../../../utils/client";
8
+ import { hostOption, portOption } from "../../../utils/options";
9
+
10
+ const nameOption = Flag.string("name").pipe(
11
+ Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"),
12
+ );
13
+
14
+ export const getTableCommand = Command.make(
15
+ "get-table",
16
+ {
17
+ name: nameOption,
18
+ host: hostOption,
19
+ port: portOption,
20
+ },
21
+ ({ name }) =>
22
+ Effect.gen(function* () {
23
+ const s = p.spinner();
24
+ s.start("Fetching table...");
25
+
26
+ const table = yield* ClusterClient.getTable({ name }).pipe(
27
+ Effect.tapError(() => Effect.sync(() => s.stop("Failed to get table"))),
28
+ );
29
+
30
+ s.stop("Table retrieved");
31
+
32
+ yield* Effect.sync(() => {
33
+ printTable([
34
+ {
35
+ name: table.name,
36
+ description: table.description || "-",
37
+ key_field_id: table.keyFieldId.toString(),
38
+ version_field_id: table.versionFieldId.toString(),
39
+ partition_field_id: table.partitionFieldId?.toString() || "-",
40
+ freshness_seconds: table.targetFreshnessSeconds.toString(),
41
+ },
42
+ ]);
43
+
44
+ if (table.schema.fields.length > 0) {
45
+ console.log("\nFields:");
46
+ printTable(
47
+ table.schema.fields.map((field) => ({
48
+ name: field.name,
49
+ id: field.id.toString(),
50
+ type: field.arrowType?._tag ?? "unknown",
51
+ nullable: field.nullable ? "yes" : "no",
52
+ })),
53
+ );
54
+ }
55
+
56
+ p.outro("✓ Done");
57
+ });
58
+ }),
59
+ ).pipe(
60
+ Command.withDescription("Get details of a specific table"),
61
+ Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
62
+ );
@@ -0,0 +1,61 @@
1
+ import * as p from "@clack/prompts";
2
+ import { ClusterClient } from "@useairfoil/wings";
3
+ import { printTable } from "console-table-printer";
4
+ import { Effect, Option } from "effect";
5
+ import { Command, Flag } from "effect/unstable/cli";
6
+
7
+ import { makeClusterClientLayer } from "../../../utils/client";
8
+ import { hostOption, pageSizeOption, pageTokenOption, portOption } from "../../../utils/options";
9
+
10
+ const parentOption = Flag.string("parent").pipe(
11
+ Flag.withDescription("Parent namespace in format: namespaces/{namespace}"),
12
+ );
13
+
14
+ export const listTablesCommand = Command.make(
15
+ "list-tables",
16
+ {
17
+ parent: parentOption,
18
+ pageSize: pageSizeOption,
19
+ pageToken: pageTokenOption,
20
+ host: hostOption,
21
+ port: portOption,
22
+ },
23
+ ({ parent, pageSize, pageToken }) =>
24
+ Effect.gen(function* () {
25
+ const s = p.spinner();
26
+ s.start("Fetching tables...");
27
+
28
+ const response = yield* ClusterClient.listTables({
29
+ parent,
30
+ pageSize,
31
+ pageToken: Option.getOrUndefined(pageToken),
32
+ }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to list tables"))));
33
+
34
+ s.stop(`Found ${response.tables.length} table(s)`);
35
+
36
+ yield* Effect.sync(() => {
37
+ if (response.tables.length === 0) {
38
+ p.log.warn("No tables found");
39
+ } else {
40
+ printTable(
41
+ response.tables.map((table) => ({
42
+ name: table.name,
43
+ description: table.description || "-",
44
+ key_field_id: table.keyFieldId.toString(),
45
+ partition_field_id: table.partitionFieldId?.toString() || "-",
46
+ freshness_seconds: table.targetFreshnessSeconds.toString(),
47
+ })),
48
+ );
49
+ }
50
+
51
+ if (response.nextPageToken) {
52
+ p.log.info(`Next page token: ${response.nextPageToken}`);
53
+ }
54
+
55
+ p.outro("✓ Done");
56
+ });
57
+ }),
58
+ ).pipe(
59
+ Command.withDescription("List all tables belonging to a namespace"),
60
+ Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
61
+ );
@@ -1,8 +1,13 @@
1
1
  import { Effect, FileSystem } from "effect";
2
2
  import { Command, Flag, Prompt } from "effect/unstable/cli";
3
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
4
-
5
- import { checkDockerVersion, createDockerVolume, runDockerContainer } from "../utils/docker";
3
+ import { ChildProcess } from "effect/unstable/process";
4
+
5
+ import {
6
+ checkDockerVersion,
7
+ createDockerVolume,
8
+ pullDockerImage,
9
+ runDockerContainer,
10
+ } from "../utils/docker";
6
11
  import { downloadWings, getWingsPath } from "../utils/wings";
7
12
 
8
13
  const dockerOption = Flag.boolean("docker").pipe(
@@ -11,7 +16,7 @@ const dockerOption = Flag.boolean("docker").pipe(
11
16
  );
12
17
 
13
18
  const versionOption = Flag.string("version").pipe(
14
- Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.11 or 'latest')"),
19
+ Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.14 or 'latest')"),
15
20
  Flag.withDefault("latest"),
16
21
  );
17
22
 
@@ -55,7 +60,6 @@ export const devCommand = Command.make(
55
60
 
56
61
  const runWithBinary = (options: { version: string; yes: boolean; stress: boolean }) =>
57
62
  Effect.gen(function* () {
58
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
59
63
  const fs = yield* FileSystem.FileSystem;
60
64
 
61
65
  yield* Effect.logInfo("🪽 Airfoil Dev");
@@ -90,17 +94,15 @@ const runWithBinary = (options: { version: string; yes: boolean; stress: boolean
90
94
 
91
95
  yield* Effect.logInfo(`Starting Wings dev server...`);
92
96
 
93
- const proc = ChildProcess.make(wingsPath, ["dev"], {
97
+ const handle = yield* ChildProcess.make(wingsPath, ["dev"], {
94
98
  stdin: "inherit",
95
99
  stdout: "inherit",
96
100
  });
97
101
 
98
- const handle = yield* spawner.spawn(proc);
99
-
100
102
  const exitCode = yield* handle.exitCode;
101
103
 
102
104
  if (exitCode !== 0) {
103
- yield* Effect.fail(new Error(`Process exited with code ${exitCode}`));
105
+ return yield* Effect.fail(new Error(`Process exited with code ${exitCode}`));
104
106
  }
105
107
  });
106
108
 
@@ -110,11 +112,8 @@ const runWithDocker = (options: { tag: string; forcePull: boolean }) =>
110
112
 
111
113
  let tag = options.tag;
112
114
 
113
- // If tag is not "latest" and doesn't include architecture, append it
114
- // Format: 0.1.0-alpha.10-aarch64 or 0.1.0-alpha.10-x86_64
115
115
  if (tag !== "latest" && !tag.includes("aarch64") && !tag.includes("x86_64")) {
116
116
  const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
117
- // Remove 'v' prefix if present for docker tags
118
117
  const cleanTag = tag.startsWith("v") ? tag.substring(1) : tag;
119
118
  tag = `${cleanTag}-${arch}`;
120
119
  }
@@ -122,10 +121,15 @@ const runWithDocker = (options: { tag: string; forcePull: boolean }) =>
122
121
  const image = `docker.useairfoil.com/airfoil/wings:${tag}`;
123
122
 
124
123
  yield* Effect.logInfo(`Using Docker image: ${image}`);
125
- yield* Effect.logInfo("Ports: 7777 (gRPC), 7780 (http)");
124
+ yield* Effect.logInfo("Ports: 7777 (gRPC)");
126
125
 
127
126
  yield* checkDockerVersion();
128
127
 
128
+ if (options.forcePull) {
129
+ yield* Effect.logInfo(`Pulling Docker image: ${image}`);
130
+ yield* pullDockerImage(image);
131
+ }
132
+
129
133
  yield* createDockerVolume("wings-data");
130
134
 
131
135
  yield* runDockerContainer(image, "wings-data");
@@ -1,6 +1,6 @@
1
1
  import { ArrowFlightSqlClient } from "@useairfoil/flight";
2
2
  import { printTable } from "console-table-printer";
3
- import { Effect, FileSystem, Option } from "effect";
3
+ import { Effect, FileSystem, Option, Stream } from "effect";
4
4
  import { Argument, Command, Flag } from "effect/unstable/cli";
5
5
  import { Metadata } from "nice-grpc-common";
6
6
 
@@ -20,7 +20,7 @@ const fileOption = Flag.string("file").pipe(
20
20
  const namespaceOption = Flag.string("namespace").pipe(
21
21
  Flag.withAlias("n"),
22
22
  Flag.withDescription("Wings namespace"),
23
- Flag.withDefault("tenants/default/namespaces/default"),
23
+ Flag.withDefault("namespaces/default"),
24
24
  );
25
25
 
26
26
  const jsonOption = Flag.boolean("json").pipe(
@@ -58,32 +58,22 @@ export const sqlCommand = Command.make(
58
58
 
59
59
  yield* Effect.logInfo(`Namespace: ${namespace}`);
60
60
 
61
- const client = new ArrowFlightSqlClient(
62
- {
63
- host: hostPort,
64
- },
65
- {
66
- defaultCallOptions: {
67
- "*": {
68
- metadata: Metadata({
69
- "x-wings-namespace": namespace,
70
- }),
71
- },
61
+ const client = yield* ArrowFlightSqlClient.make({
62
+ host: hostPort,
63
+ defaultCallOptions: {
64
+ "*": {
65
+ metadata: Metadata({
66
+ "x-wings-namespace": namespace,
67
+ }),
72
68
  },
73
69
  },
74
- );
75
-
76
- const flightInfo = yield* Effect.tryPromise({
77
- try: () => client.executeQuery({ query: sqlQuery }),
78
- catch: (error) => (error instanceof Error ? error : new Error("Query failed")),
79
70
  });
80
71
 
81
- const batches = yield* Effect.tryPromise({
82
- try: () => Array.fromAsync(client.executeFlightInfo(flightInfo)),
83
- catch: (error) => (error instanceof Error ? error : new Error("Query failed")),
84
- });
72
+ const flightInfo = yield* client.executeQuery({ query: sqlQuery });
73
+
74
+ const batches = yield* client.executeFlightInfo(flightInfo).pipe(Stream.runCollect);
85
75
 
86
- const data = batches.flatMap((batch) => batch.toArray());
76
+ const data = Array.from(batches).flatMap(({ batch }) => batch.toArray());
87
77
 
88
78
  if (json) {
89
79
  yield* Effect.log(JSON.stringify(data, null, 2));
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
- import { Effect } from "effect";
3
+ import { Effect, Layer } from "effect";
4
4
  import { Command } from "effect/unstable/cli";
5
5
  import { FetchHttpClient } from "effect/unstable/http";
6
6
 
@@ -19,9 +19,9 @@ const cli = Command.run(program, {
19
19
  version,
20
20
  });
21
21
 
22
- cli.pipe(
23
- Effect.provide(FetchHttpClient.layer),
24
- Effect.provide(NodeServices.layer),
25
- Effect.scoped,
26
- NodeRuntime.runMain,
22
+ NodeRuntime.runMain(
23
+ cli.pipe(
24
+ Effect.provide(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer)),
25
+ Effect.scoped,
26
+ ),
27
27
  );
@@ -1,6 +1,6 @@
1
- import { WingsClusterMetadata } from "@useairfoil/wings";
1
+ import { ClusterClient } from "@useairfoil/wings";
2
2
 
3
- export const makeClusterMetadataLayer = (host: string, port: number) =>
4
- WingsClusterMetadata.layer({
3
+ export const makeClusterClientLayer = (host: string, port: number) =>
4
+ ClusterClient.layer({
5
5
  host: `${host}:${port}`,
6
6
  });