airfoil 0.3.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 +1182 -99651
  2. package/package.json +19 -23
  3. package/src/commands/cluster/data-lake/create-iceberg.ts +3 -5
  4. package/src/commands/cluster/data-lake/create-parquet.ts +3 -5
  5. package/src/commands/cluster/data-lake/create.ts +3 -9
  6. package/src/commands/cluster/data-lake/delete.ts +4 -8
  7. package/src/commands/cluster/data-lake/get.ts +5 -11
  8. package/src/commands/cluster/data-lake/list.ts +5 -15
  9. package/src/commands/cluster/index.ts +2 -5
  10. package/src/commands/cluster/namespace/create.ts +7 -17
  11. package/src/commands/cluster/namespace/delete.ts +4 -8
  12. package/src/commands/cluster/namespace/get.ts +4 -8
  13. package/src/commands/cluster/namespace/list.ts +5 -14
  14. package/src/commands/cluster/object-store/create-aws.ts +3 -5
  15. package/src/commands/cluster/object-store/create-azure.ts +3 -7
  16. package/src/commands/cluster/object-store/create-google.ts +5 -20
  17. package/src/commands/cluster/object-store/create-s3.ts +6 -18
  18. package/src/commands/cluster/object-store/create.ts +1 -0
  19. package/src/commands/cluster/object-store/delete.ts +3 -5
  20. package/src/commands/cluster/object-store/get.ts +3 -5
  21. package/src/commands/cluster/object-store/list.ts +5 -15
  22. package/src/commands/cluster/tenant/create.ts +3 -5
  23. package/src/commands/cluster/tenant/delete.ts +7 -13
  24. package/src/commands/cluster/tenant/get.ts +4 -8
  25. package/src/commands/cluster/tenant/list.ts +4 -11
  26. package/src/commands/cluster/topic/create.ts +19 -40
  27. package/src/commands/cluster/topic/delete.ts +3 -5
  28. package/src/commands/cluster/topic/get.ts +2 -2
  29. package/src/commands/cluster/topic/list.ts +5 -14
  30. package/src/commands/dev.ts +70 -173
  31. package/src/commands/sql.ts +23 -44
  32. package/src/index.ts +12 -5
  33. package/src/utils/docker.ts +43 -0
  34. package/src/utils/options.ts +1 -3
  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,8 +1,9 @@
1
- import * as p from "@clack/prompts";
2
- import { Effect } from "effect";
3
- import { Command, Flag } from "effect/unstable/cli";
4
- import { handleCliError } from "../utils/effect.js";
5
- import { downloadWings, getWingsPath, verifyChecksum } from "../utils/wings.js";
1
+ import { Effect, FileSystem } from "effect";
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";
6
+ import { downloadWings, getWingsPath } from "../utils/wings";
6
7
 
7
8
  const dockerOption = Flag.boolean("docker").pipe(
8
9
  Flag.withDescription("Run Wings using Docker (recommended)"),
@@ -10,9 +11,7 @@ const dockerOption = Flag.boolean("docker").pipe(
10
11
  );
11
12
 
12
13
  const versionOption = Flag.string("version").pipe(
13
- Flag.withDescription(
14
- "Specify Wings version (e.g., v0.1.0-alpha.11 or 'latest')",
15
- ),
14
+ Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.11 or 'latest')"),
16
15
  Flag.withDefault("latest"),
17
16
  );
18
17
 
@@ -47,189 +46,87 @@ export const devCommand = Command.make(
47
46
  stress: stressOption,
48
47
  yes: yesOption,
49
48
  },
50
- (options) =>
51
- Effect.tryPromise({
52
- try: () =>
53
- options.docker ? runWithDocker(options) : runWithBinary(options),
54
- catch: (error) =>
55
- error instanceof Error ? error : new Error("Operation failed"),
56
- }).pipe(Effect.catch(handleCliError("Operation failed"))),
49
+ (options) => (options.docker ? runWithDocker(options) : runWithBinary(options)),
57
50
  ).pipe(
58
51
  Command.withDescription(
59
52
  "Download and run Wings dev server locally (Docker recommended for portability)",
60
53
  ),
61
54
  );
62
55
 
63
- async function runWithBinary(options: {
64
- version: string;
65
- yes: boolean;
66
- stress: boolean;
67
- }) {
68
- p.intro("🪽 Airfoil Dev");
69
-
70
- const version = options.version;
71
- const isStress = options.stress;
72
- const wingsPath = getWingsPath(version, isStress);
73
- const fileExists = await Bun.file(wingsPath).exists();
74
-
75
- if (!fileExists) {
76
- p.log.warn(
77
- `Wings${isStress ? " stress" : ""} binary not found for version ${version}`,
78
- );
79
-
80
- if (!options.yes) {
81
- const confirm = await p.confirm({
82
- message: `Download Wings${isStress ? " stress" : ""} ${version} binary? (~100MB)`,
83
- initialValue: true,
84
- });
85
-
86
- if (p.isCancel(confirm) || !confirm) {
87
- p.cancel("Download cancelled");
88
- process.exit(0);
89
- }
90
- }
56
+ const runWithBinary = (options: { version: string; yes: boolean; stress: boolean }) =>
57
+ Effect.gen(function* () {
58
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
59
+ const fs = yield* FileSystem.FileSystem;
91
60
 
92
- const s = p.spinner();
93
- s.start(`Downloading Wings${isStress ? " stress" : ""} ${version}...`);
61
+ yield* Effect.logInfo("🪽 Airfoil Dev");
94
62
 
95
- try {
96
- await downloadWings(version, wingsPath, isStress);
97
- s.stop(`Downloaded Wings${isStress ? " stress" : ""} ${version}`);
98
- } catch (error) {
99
- s.stop("Download failed");
100
- throw error;
101
- }
63
+ const version = options.version;
64
+ const isStress = options.stress;
65
+ const wingsPath = yield* getWingsPath(version, isStress);
66
+ const exists = yield* fs.exists(wingsPath);
102
67
 
103
- const s2 = p.spinner();
104
- s2.start("Verifying checksum...");
68
+ if (!exists) {
69
+ yield* Effect.logWarning(
70
+ `Wings${isStress ? " stress" : ""} binary not found for version ${version}`,
71
+ );
105
72
 
106
- try {
107
- const isValid = await verifyChecksum(version, wingsPath, isStress);
73
+ if (!options.yes) {
74
+ const confirm = yield* Prompt.confirm({
75
+ message: `Download Wings${isStress ? " stress" : ""} ${version} binary? (~100MB)`,
76
+ initial: true,
77
+ });
108
78
 
109
- if (!isValid) {
110
- s2.stop("Checksum verification failed");
111
- throw new Error(
112
- "Binary checksum does not match expected value. This could indicate a corrupted download or security issue.",
113
- );
79
+ if (!confirm) {
80
+ return yield* Effect.fail(new Error("Download cancelled"));
81
+ }
114
82
  }
115
83
 
116
- s2.stop("Checksum verified āœ“");
117
- } catch (error) {
118
- s2.stop("Checksum verification failed");
119
- throw error;
84
+ yield* Effect.logInfo(`Downloading Wings${isStress ? " stress" : ""} ${version}...`);
85
+
86
+ yield* downloadWings(version, wingsPath, isStress);
87
+ } else {
88
+ yield* Effect.logInfo(`Using cached Wings${isStress ? " stress" : ""} binary (${version})`);
120
89
  }
121
90
 
122
- await Bun.$`chmod +x ${wingsPath}`;
123
- } else {
124
- p.log.success(
125
- `Using cached Wings${isStress ? " stress" : ""} binary (${version})`,
126
- );
127
- }
91
+ yield* Effect.logInfo(`Starting Wings dev server...`);
92
+
93
+ const proc = ChildProcess.make(wingsPath, ["dev"], {
94
+ stdin: "inherit",
95
+ stdout: "inherit",
96
+ });
128
97
 
129
- p.log.info("Starting Wings dev server...");
98
+ const handle = yield* spawner.spawn(proc);
130
99
 
131
- const proc = Bun.spawn([wingsPath, "dev"], {
132
- stdout: "inherit",
133
- stderr: "inherit",
134
- stdin: "inherit",
135
- });
100
+ const exitCode = yield* handle.exitCode;
136
101
 
137
- await proc.exited;
138
- }
139
-
140
- async function runWithDocker(options: { tag: string; forcePull: boolean }) {
141
- p.intro("🐳 Airfoil Dev (Docker)");
142
-
143
- let tag = options.tag;
144
-
145
- // If tag is not "latest" and doesn't include architecture, append it
146
- // Format: 0.1.0-alpha.10-aarch64 or 0.1.0-alpha.10-x86_64
147
- if (tag !== "latest" && !tag.includes("aarch64") && !tag.includes("x86_64")) {
148
- const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
149
- // Remove 'v' prefix if present for docker tags
150
- const cleanTag = tag.startsWith("v") ? tag.substring(1) : tag;
151
- tag = `${cleanTag}-${arch}`;
152
- }
153
-
154
- const image = `docker.useairfoil.com/airfoil/wings:${tag}`;
155
-
156
- p.log.info(`Using Docker image: ${image}`);
157
- p.log.info("Ports: 7777 (metadata), 7780 (http)");
158
-
159
- try {
160
- await Bun.$`docker --version`.quiet();
161
- } catch {
162
- p.cancel(
163
- "Docker is not installed or not running. Please install Docker first.",
164
- );
165
- process.exit(1);
166
- }
167
-
168
- let imageExists = false;
169
- if (!options.forcePull) {
170
- try {
171
- const result = await Bun.$`docker images -q ${image}`.text();
172
- imageExists = result.trim().length > 0;
173
- } catch {
174
- imageExists = false;
102
+ if (exitCode !== 0) {
103
+ yield* Effect.fail(new Error(`Process exited with code ${exitCode}`));
175
104
  }
176
- }
177
-
178
- if (!imageExists || options.forcePull) {
179
- const s = p.spinner();
180
- s.start(
181
- options.forcePull
182
- ? "Pulling latest Docker image..."
183
- : "Downloading Docker image...",
184
- );
185
-
186
- try {
187
- await Bun.$`docker pull ${image}`.quiet();
188
- s.stop("Docker image ready");
189
- } catch {
190
- s.stop("Failed to pull image");
191
- p.cancel(
192
- "Could not download Docker image. Please check your internet connection and registry access.",
193
- );
194
- process.exit(1);
105
+ });
106
+
107
+ const runWithDocker = (options: { tag: string; forcePull: boolean }) =>
108
+ Effect.gen(function* () {
109
+ yield* Effect.logInfo("🐳 Airfoil Dev (Docker)");
110
+
111
+ let tag = options.tag;
112
+
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
+ if (tag !== "latest" && !tag.includes("aarch64") && !tag.includes("x86_64")) {
116
+ const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
117
+ // Remove 'v' prefix if present for docker tags
118
+ const cleanTag = tag.startsWith("v") ? tag.substring(1) : tag;
119
+ tag = `${cleanTag}-${arch}`;
195
120
  }
196
- } else {
197
- p.log.success("Using cached Docker image");
198
- }
199
-
200
- try {
201
- await Bun.$`docker volume create wings-data`.quiet();
202
- } catch {
203
- // volume may already exist, that's fine
204
- }
205
-
206
- p.log.info("Starting Wings dev server...");
207
-
208
- const proc = Bun.spawn(
209
- [
210
- "docker",
211
- "run",
212
- "-it",
213
- "--rm",
214
- "--name",
215
- "wings-dev",
216
- "-v",
217
- "wings-data:/tmp",
218
- "-p",
219
- "7777:7777",
220
- "-p",
221
- "7780:7780",
222
- image,
223
- "dev",
224
- "--http.address=0.0.0.0:7780",
225
- "--metadata.address=0.0.0.0:7777",
226
- ],
227
- {
228
- stdout: "inherit",
229
- stderr: "inherit",
230
- stdin: "inherit",
231
- },
232
- );
233
121
 
234
- await proc.exited;
235
- }
122
+ const image = `docker.useairfoil.com/airfoil/wings:${tag}`;
123
+
124
+ yield* Effect.logInfo(`Using Docker image: ${image}`);
125
+ yield* Effect.logInfo("Ports: 7777 (gRPC), 7780 (http)");
126
+
127
+ yield* checkDockerVersion();
128
+
129
+ yield* createDockerVolume("wings-data");
130
+
131
+ yield* runDockerContainer(image, "wings-data");
132
+ });
@@ -1,12 +1,10 @@
1
- import { readFileSync } from "node:fs";
2
- import * as p from "@clack/prompts";
3
1
  import { ArrowFlightSqlClient } from "@useairfoil/flight";
4
2
  import { printTable } from "console-table-printer";
5
- import { Effect, Option } from "effect";
3
+ import { Effect, FileSystem, Option } from "effect";
6
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";
6
+
7
+ import { hostOption, portOption } from "../utils/options";
10
8
 
11
9
  const queryArg = Argument.string("query").pipe(
12
10
  Argument.withDescription("SQL query to execute"),
@@ -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.catch(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,11 +1,13 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
3
  import { Effect } from "effect";
4
4
  import { Command } from "effect/unstable/cli";
5
+ import { FetchHttpClient } from "effect/unstable/http";
6
+
5
7
  import packageJson from "../package.json";
6
- import { clusterCommand } from "./commands/cluster/index.js";
7
- import { devCommand } from "./commands/dev.js";
8
- import { sqlCommand } from "./commands/sql.js";
8
+ import { clusterCommand } from "./commands/cluster/index";
9
+ import { devCommand } from "./commands/dev";
10
+ import { sqlCommand } from "./commands/sql";
9
11
 
10
12
  const version = packageJson.version;
11
13
 
@@ -17,4 +19,9 @@ const cli = Command.run(program, {
17
19
  version,
18
20
  });
19
21
 
20
- cli.pipe(Effect.provide(NodeServices.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
+ });
@@ -23,9 +23,7 @@ export const pageSizeOption = Flag.integer("page-size").pipe(
23
23
  );
24
24
 
25
25
  export const pageTokenOption = Flag.optional(
26
- Flag.string("page-token").pipe(
27
- Flag.withDescription("Continuation token for pagination"),
28
- ),
26
+ Flag.string("page-token").pipe(Flag.withDescription("Continuation token for pagination")),
29
27
  );
30
28
 
31
29
  /**
@@ -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
- );