airfoil 0.2.0 → 0.4.0

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 (37) hide show
  1. package/dist/index.js +1181 -104238
  2. package/package.json +19 -24
  3. package/src/commands/cluster/data-lake/create-iceberg.ts +8 -10
  4. package/src/commands/cluster/data-lake/create-parquet.ts +8 -10
  5. package/src/commands/cluster/data-lake/create.ts +4 -10
  6. package/src/commands/cluster/data-lake/delete.ts +6 -10
  7. package/src/commands/cluster/data-lake/get.ts +7 -13
  8. package/src/commands/cluster/data-lake/list.ts +8 -18
  9. package/src/commands/cluster/index.ts +3 -6
  10. package/src/commands/cluster/namespace/create.ts +18 -28
  11. package/src/commands/cluster/namespace/delete.ts +6 -10
  12. package/src/commands/cluster/namespace/get.ts +6 -10
  13. package/src/commands/cluster/namespace/list.ts +7 -16
  14. package/src/commands/cluster/object-store/create-aws.ts +20 -24
  15. package/src/commands/cluster/object-store/create-azure.ts +17 -21
  16. package/src/commands/cluster/object-store/create-google.ts +19 -34
  17. package/src/commands/cluster/object-store/create-s3.ts +22 -34
  18. package/src/commands/cluster/object-store/create.ts +2 -1
  19. package/src/commands/cluster/object-store/delete.ts +6 -8
  20. package/src/commands/cluster/object-store/get.ts +6 -8
  21. package/src/commands/cluster/object-store/list.ts +8 -18
  22. package/src/commands/cluster/tenant/create.ts +6 -10
  23. package/src/commands/cluster/tenant/delete.ts +10 -16
  24. package/src/commands/cluster/tenant/get.ts +6 -10
  25. package/src/commands/cluster/tenant/list.ts +5 -12
  26. package/src/commands/cluster/topic/create.ts +43 -66
  27. package/src/commands/cluster/topic/delete.ts +9 -11
  28. package/src/commands/cluster/topic/get.ts +5 -5
  29. package/src/commands/cluster/topic/list.ts +7 -16
  30. package/src/commands/dev.ts +89 -192
  31. package/src/commands/sql.ts +38 -59
  32. package/src/index.ts +18 -9
  33. package/src/utils/docker.ts +43 -0
  34. package/src/utils/options.ts +16 -17
  35. package/src/utils/wings.ts +68 -64
  36. package/src/scripts/compile-all.ts +0 -21
  37. package/src/utils/effect.ts +0 -11
@@ -1,33 +1,31 @@
1
- import { readFileSync } from "node:fs";
2
- import * as p from "@clack/prompts";
3
- import { Args, Command, Options } from "@effect/cli";
4
1
  import { ArrowFlightSqlClient } from "@useairfoil/flight";
5
2
  import { printTable } from "console-table-printer";
6
- import { Effect, Option } from "effect";
3
+ import { Effect, FileSystem, Option } from "effect";
4
+ import { Argument, Command, Flag } from "effect/unstable/cli";
7
5
  import { Metadata } from "nice-grpc-common";
8
- import { handleCliError } from "../utils/effect.js";
9
- import { hostOption, portOption } from "../utils/options.js";
10
6
 
11
- const queryArg = Args.text({ name: "query" }).pipe(
12
- Args.withDescription("SQL query to execute"),
13
- Args.optional,
7
+ import { hostOption, portOption } from "../utils/options";
8
+
9
+ const queryArg = Argument.string("query").pipe(
10
+ Argument.withDescription("SQL query to execute"),
11
+ Argument.optional,
14
12
  );
15
13
 
16
- const fileOption = Options.text("file").pipe(
17
- Options.withAlias("f"),
18
- Options.withDescription("Execute SQL from file"),
19
- Options.optional,
14
+ const fileOption = Flag.string("file").pipe(
15
+ Flag.withAlias("f"),
16
+ Flag.withDescription("Execute SQL from file"),
17
+ Flag.optional,
20
18
  );
21
19
 
22
- const namespaceOption = Options.text("namespace").pipe(
23
- Options.withAlias("n"),
24
- Options.withDescription("Wings namespace"),
25
- Options.withDefault("tenants/default/namespaces/default"),
20
+ const namespaceOption = Flag.string("namespace").pipe(
21
+ Flag.withAlias("n"),
22
+ Flag.withDescription("Wings namespace"),
23
+ Flag.withDefault("tenants/default/namespaces/default"),
26
24
  );
27
25
 
28
- const jsonOption = Options.boolean("json").pipe(
29
- Options.withDescription("Output results as JSON"),
30
- Options.withDefault(false),
26
+ const jsonOption = Flag.boolean("json").pipe(
27
+ Flag.withDescription("Output results as JSON"),
28
+ Flag.withDefault(false),
31
29
  );
32
30
 
33
31
  export const sqlCommand = Command.make(
@@ -42,32 +40,23 @@ export const sqlCommand = Command.make(
42
40
  },
43
41
  ({ query, host, port, file, namespace, json }) =>
44
42
  Effect.gen(function* () {
43
+ const fs = yield* FileSystem.FileSystem;
45
44
  let sqlQuery = Option.getOrUndefined(query);
46
45
  const filePath = Option.getOrUndefined(file);
47
46
 
48
47
  if (filePath) {
49
- sqlQuery = yield* Effect.try({
50
- try: () => readFileSync(filePath, "utf-8"),
51
- catch: (error) =>
52
- new Error(
53
- `Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`,
54
- ),
55
- });
48
+ sqlQuery = yield* fs.readFileString(filePath);
56
49
  }
57
50
 
58
51
  if (!sqlQuery) {
59
52
  return yield* Effect.fail(
60
- new Error(
61
- "No query provided. Use a query argument or --file option.",
62
- ),
53
+ new Error("No query provided. Use a query argument or --file option."),
63
54
  );
64
55
  }
65
56
 
66
57
  const hostPort = `${host}:${port}`;
67
58
 
68
- p.intro("🔍 Arrow Flight SQL");
69
- p.log.info(`Connecting to: ${hostPort}`);
70
- p.log.info(`Namespace: ${namespace}`);
59
+ yield* Effect.logInfo(`Namespace: ${namespace}`);
71
60
 
72
61
  const client = new ArrowFlightSqlClient(
73
62
  {
@@ -84,38 +73,28 @@ export const sqlCommand = Command.make(
84
73
  },
85
74
  );
86
75
 
87
- const s = p.spinner();
88
- s.start("Executing query...");
89
-
90
76
  const flightInfo = yield* Effect.tryPromise({
91
77
  try: () => client.executeQuery({ query: sqlQuery }),
92
- catch: (error) =>
93
- error instanceof Error ? error : new Error("Query failed"),
94
- }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Query failed"))));
78
+ catch: (error) => (error instanceof Error ? error : new Error("Query failed")),
79
+ });
95
80
 
96
81
  const batches = yield* Effect.tryPromise({
97
82
  try: () => Array.fromAsync(client.executeFlightInfo(flightInfo)),
98
- catch: (error) =>
99
- error instanceof Error ? error : new Error("Query failed"),
100
- }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Query failed"))));
83
+ catch: (error) => (error instanceof Error ? error : new Error("Query failed")),
84
+ });
101
85
 
102
86
  const data = batches.flatMap((batch) => batch.toArray());
103
87
 
104
- s.stop(`Query executed: ${sqlQuery}`);
105
-
106
- yield* Effect.sync(() => {
107
- if (json) {
108
- console.log(JSON.stringify(data, null, 2));
109
- } else if (data.length === 0) {
110
- p.log.warn("No results returned");
111
- } else {
112
- printTable(data, {
113
- defaultColumnOptions: {
114
- alignment: "left",
115
- },
116
- });
117
- }
118
- p.outro("✓ Done");
119
- });
120
- }).pipe(Effect.catchAll(handleCliError("Command failed"))),
88
+ if (json) {
89
+ yield* Effect.log(JSON.stringify(data, null, 2));
90
+ } else if (data.length === 0) {
91
+ yield* Effect.logWarning("No results returned");
92
+ } else {
93
+ printTable(data, {
94
+ defaultColumnOptions: {
95
+ alignment: "left",
96
+ },
97
+ });
98
+ }
99
+ }),
121
100
  ).pipe(Command.withDescription("Execute SQL queries using Arrow Flight SQL"));
package/src/index.ts CHANGED
@@ -1,18 +1,27 @@
1
- #!/usr/bin/env bun
2
- import { Command } from "@effect/cli";
3
- import { NodeContext, NodeRuntime } from "@effect/platform-node";
1
+ #!/usr/bin/env node
2
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
4
3
  import { Effect } from "effect";
5
- import { clusterCommand } from "./commands/cluster/index.js";
6
- import { devCommand } from "./commands/dev.js";
7
- import { sqlCommand } from "./commands/sql.js";
4
+ import { Command } from "effect/unstable/cli";
5
+ import { FetchHttpClient } from "effect/unstable/http";
6
+
7
+ import packageJson from "../package.json";
8
+ import { clusterCommand } from "./commands/cluster/index";
9
+ import { devCommand } from "./commands/dev";
10
+ import { sqlCommand } from "./commands/sql";
11
+
12
+ const version = packageJson.version;
8
13
 
9
14
  const program = Command.make("airfoil", {}, () => Effect.void).pipe(
10
15
  Command.withSubcommands([devCommand, sqlCommand, clusterCommand]),
11
16
  );
12
17
 
13
18
  const cli = Command.run(program, {
14
- name: "Airfoil CLI - Manage your Wings deployments",
15
- version: "0.1.0",
19
+ version,
16
20
  });
17
21
 
18
- cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
22
+ cli.pipe(
23
+ Effect.provide(FetchHttpClient.layer),
24
+ Effect.provide(NodeServices.layer),
25
+ Effect.scoped,
26
+ NodeRuntime.runMain,
27
+ );
@@ -0,0 +1,43 @@
1
+ import { Effect, String } from "effect";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3
+
4
+ export const checkDockerVersion = () =>
5
+ Effect.gen(function* () {
6
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
7
+ const docker = ChildProcess.make("docker", ["--version"]);
8
+
9
+ return yield* spawner.string(docker).pipe(Effect.map(String.trim));
10
+ });
11
+
12
+ export const createDockerVolume = (volumeName: string) =>
13
+ Effect.gen(function* () {
14
+ const docker = yield* ChildProcess.make("docker", ["volume", "create", volumeName]);
15
+ yield* docker.exitCode;
16
+ });
17
+
18
+ export const runDockerContainer = (image: string, volumeName: string) =>
19
+ Effect.gen(function* () {
20
+ const docker = yield* ChildProcess.make("docker", [
21
+ "run",
22
+ "-it",
23
+ "--rm",
24
+ "--name",
25
+ "wings-dev",
26
+ "-v",
27
+ `${volumeName}:/tmp`,
28
+ "-p",
29
+ "7777:7777",
30
+ "-p",
31
+ "7780:7780",
32
+ image,
33
+ "dev",
34
+ "--http.address=0.0.0.0:7780",
35
+ "--metadata.address=0.0.0.0:7777",
36
+ ]);
37
+
38
+ const exitCode = yield* docker.exitCode;
39
+
40
+ if (exitCode !== 0) {
41
+ yield* Effect.fail(new Error(`Docker process exited with code ${exitCode}`));
42
+ }
43
+ });
@@ -1,36 +1,35 @@
1
- import { Options } from "@effect/cli";
1
+ import { Flag } from "effect/unstable/cli";
2
2
 
3
3
  /**
4
4
  * Common server connection options
5
5
  */
6
- export const hostOption = Options.text("host").pipe(
7
- Options.withDescription("Server host"),
8
- Options.withDefault("localhost"),
6
+ export const hostOption = Flag.string("host").pipe(
7
+ Flag.withDescription("Server host"),
8
+ Flag.withDefault("localhost"),
9
9
  );
10
10
 
11
- export const portOption = Options.integer("port").pipe(
12
- Options.withDescription("Server port"),
13
- Options.withAlias("p"),
14
- Options.withDefault(7777),
11
+ export const portOption = Flag.integer("port").pipe(
12
+ Flag.withDescription("Server port"),
13
+ Flag.withAlias("p"),
14
+ Flag.withDefault(7777),
15
15
  );
16
16
 
17
17
  /**
18
18
  * Common pagination options
19
19
  */
20
- export const pageSizeOption = Options.integer("page-size").pipe(
21
- Options.withDescription("Number of items to return (max: 1000)"),
22
- Options.withDefault(100),
20
+ export const pageSizeOption = Flag.integer("page-size").pipe(
21
+ Flag.withDescription("Number of items to return (max: 1000)"),
22
+ Flag.withDefault(100),
23
23
  );
24
24
 
25
- export const pageTokenOption = Options.text("page-token").pipe(
26
- Options.withDescription("Continuation token for pagination"),
27
- Options.optional,
25
+ export const pageTokenOption = Flag.optional(
26
+ Flag.string("page-token").pipe(Flag.withDescription("Continuation token for pagination")),
28
27
  );
29
28
 
30
29
  /**
31
30
  * Force flag for destructive operations
32
31
  */
33
- export const forceOption = Options.boolean("force").pipe(
34
- Options.withDescription("Skip confirmation prompt"),
35
- Options.withDefault(false),
32
+ export const forceOption = Flag.boolean("force").pipe(
33
+ Flag.withDescription("Skip confirmation prompt"),
34
+ Flag.withDefault(false),
36
35
  );
@@ -1,8 +1,10 @@
1
+ import { Effect, FileSystem, Path } from "effect";
2
+ import { HttpClient, HttpClientResponse } from "effect/unstable/http";
1
3
  import { createHash } from "node:crypto";
2
4
  import { homedir } from "node:os";
3
5
  import { join } from "node:path";
4
6
 
5
- const WINGS_DIR = join(homedir(), ".airfoil", "wings");
7
+ const WINGS_DIR = join(homedir(), ".local", "share", "wings");
6
8
  const GITHUB_RELEASES_URL = "https://github.com/useairfoil/wings/releases";
7
9
 
8
10
  /**
@@ -38,79 +40,81 @@ function getPlatformString(): string {
38
40
  */
39
41
  function getBinaryFilename(isStress: boolean): string {
40
42
  const platformString = getPlatformString();
41
- return isStress
42
- ? `wings-stress-${platformString}`
43
- : `wings-${platformString}`;
43
+ return isStress ? `wings-stress-${platformString}` : `wings-${platformString}`;
44
44
  }
45
45
 
46
- export function getWingsPath(version: string, isStress = false): string {
47
- const binaryFilename = getBinaryFilename(isStress);
48
- return join(WINGS_DIR, `${binaryFilename}-${version}`);
49
- }
46
+ export const getWingsPath = (version: string, isStress = false) =>
47
+ Effect.gen(function* () {
48
+ const binaryFilename = getBinaryFilename(isStress);
49
+ const path = yield* Path.Path;
50
+ return path.join(WINGS_DIR, `${binaryFilename}-${version}`);
51
+ });
50
52
 
51
- export async function downloadWings(
52
- version: string,
53
- targetPath: string,
54
- isStress = false,
55
- ): Promise<void> {
56
- const filename = getBinaryFilename(isStress);
53
+ export const downloadWings = (version: string, targetPath: string, isStress = false) =>
54
+ Effect.gen(function* () {
55
+ const fs = yield* FileSystem.FileSystem;
56
+ const client = yield* HttpClient.HttpClient;
57
57
 
58
- const downloadPath =
59
- version === "latest" ? "latest/download" : `download/${version}`;
60
- const url = `${GITHUB_RELEASES_URL}/${downloadPath}/${filename}`;
58
+ const filename = getBinaryFilename(isStress);
61
59
 
62
- await Bun.$`mkdir -p ${WINGS_DIR}`;
60
+ const downloadPath = version === "latest" ? "latest/download" : `download/${version}`;
61
+ const url = `${GITHUB_RELEASES_URL}/${downloadPath}/${filename}`;
63
62
 
64
- const response = await fetch(url);
63
+ yield* fs.makeDirectory(WINGS_DIR, { recursive: true });
65
64
 
66
- if (!response.ok) {
67
- throw new Error(
68
- `Failed to download Wings from ${url}: ${response.status} ${response.statusText}`,
69
- );
70
- }
65
+ const response = yield* client.get(url);
66
+ const buffer = yield* HttpClientResponse.matchStatus({
67
+ 200: (response) => response.arrayBuffer,
68
+ orElse: (response) => {
69
+ throw new Error(`Failed to download Wings from ${url}: ${response.status}`);
70
+ },
71
+ })(response);
71
72
 
72
- const buffer = await response.arrayBuffer();
73
- await Bun.write(targetPath, buffer);
74
- }
73
+ yield* fs.writeFile(targetPath, new Uint8Array(buffer));
74
+
75
+ yield* verifyChecksum(version, targetPath, isStress);
76
+
77
+ yield* fs.chmod(targetPath, 0o755);
78
+ });
75
79
 
76
80
  /**
77
81
  * Download and verify the checksum of a Wings binary
78
82
  * The hash file format is: "<hash> <path>/wings"
79
83
  */
80
- export async function verifyChecksum(
81
- version: string,
82
- filePath: string,
83
- isStress = false,
84
- ): Promise<boolean> {
85
- const filename = getBinaryFilename(isStress);
86
- const hashFilename = `${filename}-hash.txt`;
87
-
88
- const downloadPath =
89
- version === "latest" ? "latest/download" : `download/${version}`;
90
- const hashUrl = `${GITHUB_RELEASES_URL}/${downloadPath}/${hashFilename}`;
91
-
92
- const hashResponse = await fetch(hashUrl);
93
-
94
- if (!hashResponse.ok) {
95
- throw new Error(
96
- `Failed to download hash file from ${hashUrl}: ${hashResponse.status} ${hashResponse.statusText}`,
97
- );
98
- }
99
-
100
- const hashContent = await hashResponse.text();
101
-
102
- const expectedChecksum = hashContent.trim().split(/\s+/)[0];
103
-
104
- if (!expectedChecksum) {
105
- throw new Error("Invalid hash file format");
106
- }
107
-
108
- const file = Bun.file(filePath);
109
- const buffer = await file.arrayBuffer();
110
-
111
- const hash = createHash("sha256");
112
- hash.update(new Uint8Array(buffer));
113
- const actualChecksum = hash.digest("hex");
114
-
115
- return actualChecksum === expectedChecksum;
116
- }
84
+ export const verifyChecksum = (version: string, filePath: string, isStress = false) =>
85
+ Effect.gen(function* () {
86
+ const fs = yield* FileSystem.FileSystem;
87
+ const client = yield* HttpClient.HttpClient;
88
+
89
+ const filename = getBinaryFilename(isStress);
90
+ const hashFilename = `${filename}-hash.txt`;
91
+
92
+ const downloadPath = version === "latest" ? "latest/download" : `download/${version}`;
93
+ const url = `${GITHUB_RELEASES_URL}/${downloadPath}/${hashFilename}`;
94
+
95
+ const response = yield* client.get(url);
96
+ const hashContent = yield* HttpClientResponse.matchStatus({
97
+ 200: (response) => response.text,
98
+ orElse: (response) => {
99
+ return Effect.fail(
100
+ new Error(`Failed to download hash file from ${url}: ${response.status}`),
101
+ );
102
+ },
103
+ })(response);
104
+
105
+ const expectedChecksum = hashContent.trim().split(/\s+/)[0];
106
+
107
+ if (!expectedChecksum) {
108
+ return yield* Effect.fail(new Error("Invalid hash file format"));
109
+ }
110
+
111
+ const fileBuffer = yield* fs.readFile(filePath);
112
+
113
+ const hash = createHash("sha256");
114
+ hash.update(fileBuffer);
115
+ const actualChecksum = hash.digest("hex");
116
+
117
+ if (actualChecksum !== expectedChecksum) {
118
+ return yield* Effect.fail(new Error("Wings binary checksum mismatch"));
119
+ }
120
+ });
@@ -1,21 +0,0 @@
1
- import { $ } from "bun";
2
-
3
- const targets = [
4
- { platform: "linux", arch: "x64" },
5
- { platform: "linux", arch: "arm64" },
6
- { platform: "darwin", arch: "x64" },
7
- { platform: "darwin", arch: "arm64" },
8
- { platform: "windows", arch: "x64" },
9
- ];
10
-
11
- for (const { platform, arch } of targets) {
12
- const outfile = `dist/airfoil-${platform}-${arch}${platform === "windows" ? ".exe" : ""}`;
13
-
14
- console.log(`Compiling for ${platform}-${arch}...`);
15
-
16
- await $`bun build src/index.ts --compile --target=bun-${platform}-${arch} --outfile ${outfile}`;
17
-
18
- console.log(`✓ Created ${outfile}`);
19
- }
20
-
21
- console.log("\n✓ All binaries compiled successfully!");
@@ -1,11 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import { Effect } from "effect";
3
-
4
- export const handleCliError = (fallbackMessage: string) => (error: unknown) =>
5
- Effect.sync(() => {
6
- p.cancel(error instanceof Error ? error.message : fallbackMessage);
7
- }).pipe(
8
- Effect.andThen(
9
- Effect.fail(error instanceof Error ? error : new Error(fallbackMessage)),
10
- ),
11
- );