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.
- package/dist/index.js +1182 -99651
- package/package.json +19 -23
- package/src/commands/cluster/data-lake/create-iceberg.ts +3 -5
- package/src/commands/cluster/data-lake/create-parquet.ts +3 -5
- package/src/commands/cluster/data-lake/create.ts +3 -9
- package/src/commands/cluster/data-lake/delete.ts +4 -8
- package/src/commands/cluster/data-lake/get.ts +5 -11
- package/src/commands/cluster/data-lake/list.ts +5 -15
- package/src/commands/cluster/index.ts +2 -5
- package/src/commands/cluster/namespace/create.ts +7 -17
- package/src/commands/cluster/namespace/delete.ts +4 -8
- package/src/commands/cluster/namespace/get.ts +4 -8
- package/src/commands/cluster/namespace/list.ts +5 -14
- package/src/commands/cluster/object-store/create-aws.ts +3 -5
- package/src/commands/cluster/object-store/create-azure.ts +3 -7
- package/src/commands/cluster/object-store/create-google.ts +5 -20
- package/src/commands/cluster/object-store/create-s3.ts +6 -18
- package/src/commands/cluster/object-store/create.ts +1 -0
- package/src/commands/cluster/object-store/delete.ts +3 -5
- package/src/commands/cluster/object-store/get.ts +3 -5
- package/src/commands/cluster/object-store/list.ts +5 -15
- package/src/commands/cluster/tenant/create.ts +3 -5
- package/src/commands/cluster/tenant/delete.ts +7 -13
- package/src/commands/cluster/tenant/get.ts +4 -8
- package/src/commands/cluster/tenant/list.ts +4 -11
- package/src/commands/cluster/topic/create.ts +19 -40
- package/src/commands/cluster/topic/delete.ts +3 -5
- package/src/commands/cluster/topic/get.ts +2 -2
- package/src/commands/cluster/topic/list.ts +5 -14
- package/src/commands/dev.ts +70 -173
- package/src/commands/sql.ts +23 -44
- package/src/index.ts +12 -5
- package/src/utils/docker.ts +43 -0
- package/src/utils/options.ts +1 -3
- package/src/utils/wings.ts +68 -64
- package/src/scripts/compile-all.ts +0 -21
- package/src/utils/effect.ts +0 -11
package/src/commands/dev.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
93
|
-
s.start(`Downloading Wings${isStress ? " stress" : ""} ${version}...`);
|
|
61
|
+
yield* Effect.logInfo("šŖ½ Airfoil Dev");
|
|
94
62
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
68
|
+
if (!exists) {
|
|
69
|
+
yield* Effect.logWarning(
|
|
70
|
+
`Wings${isStress ? " stress" : ""} binary not found for version ${version}`,
|
|
71
|
+
);
|
|
105
72
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
98
|
+
const handle = yield* spawner.spawn(proc);
|
|
130
99
|
|
|
131
|
-
|
|
132
|
-
stdout: "inherit",
|
|
133
|
-
stderr: "inherit",
|
|
134
|
-
stdin: "inherit",
|
|
135
|
-
});
|
|
100
|
+
const exitCode = yield* handle.exitCode;
|
|
136
101
|
|
|
137
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/src/commands/sql.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
import { hostOption, portOption } from "../utils/options
|
|
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*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
7
|
-
import { devCommand } from "./commands/dev
|
|
8
|
-
import { sqlCommand } from "./commands/sql
|
|
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(
|
|
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
|
+
});
|
package/src/utils/options.ts
CHANGED
|
@@ -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
|
/**
|
package/src/utils/wings.ts
CHANGED
|
@@ -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(), ".
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
version === "latest" ? "latest/download" : `download/${version}`;
|
|
60
|
-
const url = `${GITHUB_RELEASES_URL}/${downloadPath}/${filename}`;
|
|
58
|
+
const filename = getBinaryFilename(isStress);
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
const downloadPath = version === "latest" ? "latest/download" : `download/${version}`;
|
|
61
|
+
const url = `${GITHUB_RELEASES_URL}/${downloadPath}/${filename}`;
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
yield* fs.makeDirectory(WINGS_DIR, { recursive: true });
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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!");
|
package/src/utils/effect.ts
DELETED
|
@@ -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
|
-
);
|