airfoil 0.4.4 → 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/README.md +24 -0
- package/dist/index.mjs +283 -0
- package/package.json +18 -15
- package/src/commands/catalog.ts +48 -0
- package/src/commands/namespace.ts +75 -0
- package/src/commands/table.ts +111 -0
- package/src/index.ts +12 -6
- package/src/utils/iceberg.ts +58 -0
- package/src/utils/json.ts +48 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/options.ts +17 -31
- package/dist/index.js +0 -855
- package/src/commands/cluster/index.ts +0 -25
- package/src/commands/cluster/namespace/create.ts +0 -314
- package/src/commands/cluster/namespace/delete.ts +0 -54
- package/src/commands/cluster/namespace/get.ts +0 -46
- package/src/commands/cluster/namespace/list.ts +0 -53
- package/src/commands/cluster/table/create.ts +0 -279
- package/src/commands/cluster/table/delete.ts +0 -54
- package/src/commands/cluster/table/get.ts +0 -62
- package/src/commands/cluster/table/list.ts +0 -61
- package/src/commands/dev.ts +0 -136
- package/src/commands/sql.ts +0 -90
- package/src/utils/client.ts +0 -6
- package/src/utils/docker.ts +0 -137
- package/src/utils/wings.ts +0 -124
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.3";
|
|
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 { };
|