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.
- package/dist/index.js +1181 -104238
- package/package.json +19 -24
- package/src/commands/cluster/data-lake/create-iceberg.ts +8 -10
- package/src/commands/cluster/data-lake/create-parquet.ts +8 -10
- package/src/commands/cluster/data-lake/create.ts +4 -10
- package/src/commands/cluster/data-lake/delete.ts +6 -10
- package/src/commands/cluster/data-lake/get.ts +7 -13
- package/src/commands/cluster/data-lake/list.ts +8 -18
- package/src/commands/cluster/index.ts +3 -6
- package/src/commands/cluster/namespace/create.ts +18 -28
- package/src/commands/cluster/namespace/delete.ts +6 -10
- package/src/commands/cluster/namespace/get.ts +6 -10
- package/src/commands/cluster/namespace/list.ts +7 -16
- package/src/commands/cluster/object-store/create-aws.ts +20 -24
- package/src/commands/cluster/object-store/create-azure.ts +17 -21
- package/src/commands/cluster/object-store/create-google.ts +19 -34
- package/src/commands/cluster/object-store/create-s3.ts +22 -34
- package/src/commands/cluster/object-store/create.ts +2 -1
- package/src/commands/cluster/object-store/delete.ts +6 -8
- package/src/commands/cluster/object-store/get.ts +6 -8
- package/src/commands/cluster/object-store/list.ts +8 -18
- package/src/commands/cluster/tenant/create.ts +6 -10
- package/src/commands/cluster/tenant/delete.ts +10 -16
- package/src/commands/cluster/tenant/get.ts +6 -10
- package/src/commands/cluster/tenant/list.ts +5 -12
- package/src/commands/cluster/topic/create.ts +43 -66
- package/src/commands/cluster/topic/delete.ts +9 -11
- package/src/commands/cluster/topic/get.ts +5 -5
- package/src/commands/cluster/topic/list.ts +7 -16
- package/src/commands/dev.ts +89 -192
- package/src/commands/sql.ts +38 -59
- package/src/index.ts +18 -9
- package/src/utils/docker.ts +43 -0
- package/src/utils/options.ts +16 -17
- package/src/utils/wings.ts +68 -64
- package/src/scripts/compile-all.ts +0 -21
- package/src/utils/effect.ts +0 -11
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as p from "@clack/prompts";
|
|
3
|
-
import { Command, Options } from "@effect/cli";
|
|
4
1
|
import type { FieldConfig } from "@useairfoil/wings";
|
|
2
|
+
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
5
4
|
import { WingsClusterMetadata } from "@useairfoil/wings";
|
|
6
5
|
import { printTable } from "console-table-printer";
|
|
7
6
|
import { Effect, Option } from "effect";
|
|
7
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
|
|
8
10
|
import { makeClusterMetadataLayer } from "../../../utils/client.js";
|
|
9
|
-
import { handleCliError } from "../../../utils/effect.js";
|
|
10
11
|
import { hostOption, portOption } from "../../../utils/options.js";
|
|
11
12
|
|
|
12
13
|
// Supported simple types (no config required)
|
|
@@ -40,55 +41,47 @@ const SUPPORTED_INLINE_TYPES = [
|
|
|
40
41
|
|
|
41
42
|
type SupportedInlineType = (typeof SUPPORTED_INLINE_TYPES)[number];
|
|
42
43
|
|
|
43
|
-
const parentOption =
|
|
44
|
-
|
|
45
|
-
"Parent namespace in format: tenants/{tenant}/namespaces/{namespace}",
|
|
46
|
-
),
|
|
44
|
+
const parentOption = Flag.string("parent").pipe(
|
|
45
|
+
Flag.withDescription("Parent namespace in format: tenants/{tenant}/namespaces/{namespace}"),
|
|
47
46
|
);
|
|
48
47
|
|
|
49
|
-
const topicIdOption =
|
|
50
|
-
|
|
48
|
+
const topicIdOption = Flag.string("topic-id").pipe(
|
|
49
|
+
Flag.withDescription("Unique identifier for the topic"),
|
|
51
50
|
);
|
|
52
51
|
|
|
53
|
-
const descriptionOption =
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const descriptionOption = Flag.string("description").pipe(
|
|
53
|
+
Flag.withDescription("Topic description"),
|
|
54
|
+
Flag.optional,
|
|
56
55
|
);
|
|
57
56
|
|
|
58
|
-
const fieldsOption =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
),
|
|
62
|
-
Options.repeated,
|
|
57
|
+
const fieldsOption = Flag.string("fields").pipe(
|
|
58
|
+
Flag.withDescription('Field definitions in format "name:Type" or "name:Type?" for nullable'),
|
|
59
|
+
Flag.atLeast(0),
|
|
63
60
|
);
|
|
64
61
|
|
|
65
|
-
const schemaFileOption =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
),
|
|
69
|
-
Options.optional,
|
|
62
|
+
const schemaFileOption = Flag.string("schema-file").pipe(
|
|
63
|
+
Flag.withDescription("Path to JSON file containing FieldConfig[] (for complex types)"),
|
|
64
|
+
Flag.optional,
|
|
70
65
|
);
|
|
71
66
|
|
|
72
|
-
const partitionKeyOption =
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
const partitionKeyOption = Flag.string("partition-key").pipe(
|
|
68
|
+
Flag.withDescription("Name of the field used for partitioning"),
|
|
69
|
+
Flag.optional,
|
|
75
70
|
);
|
|
76
71
|
|
|
77
|
-
const freshnessSecondsOption =
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
const freshnessSecondsOption = Flag.integer("freshness-seconds").pipe(
|
|
73
|
+
Flag.withDescription("How often to compact the topic (seconds)"),
|
|
74
|
+
Flag.withDefault(60),
|
|
80
75
|
);
|
|
81
76
|
|
|
82
|
-
const ttlSecondsOption =
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
const ttlSecondsOption = Flag.integer("ttl-seconds").pipe(
|
|
78
|
+
Flag.withDescription("How long to keep topic data (seconds)"),
|
|
79
|
+
Flag.optional,
|
|
85
80
|
);
|
|
86
81
|
|
|
87
|
-
const targetFileSizeBytesOption =
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
Options.withDescription("Target file size for compaction (bytes)"),
|
|
91
|
-
Options.withDefault(1024 * 1024),
|
|
82
|
+
const targetFileSizeBytesOption = Flag.integer("target-file-size-bytes").pipe(
|
|
83
|
+
Flag.withDescription("Target file size for compaction (bytes)"),
|
|
84
|
+
Flag.withDefault(1024 * 1024),
|
|
92
85
|
);
|
|
93
86
|
|
|
94
87
|
/**
|
|
@@ -131,7 +124,7 @@ function parseFieldString(fieldStr: string, index: number): FieldConfig {
|
|
|
131
124
|
/**
|
|
132
125
|
* Parse multiple field strings into FieldConfig array
|
|
133
126
|
*/
|
|
134
|
-
function parseFieldsFromArgs(fields: string
|
|
127
|
+
function parseFieldsFromArgs(fields: ReadonlyArray<string>): FieldConfig[] {
|
|
135
128
|
return fields.map((field, index) => parseFieldString(field, index));
|
|
136
129
|
}
|
|
137
130
|
|
|
@@ -153,15 +146,11 @@ function loadFieldsFromFile(filePath: string): FieldConfig[] {
|
|
|
153
146
|
}
|
|
154
147
|
|
|
155
148
|
if (!Array.isArray(parsed)) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Schema file must contain an array of fields. Got: ${typeof parsed}`,
|
|
158
|
-
);
|
|
149
|
+
throw new Error(`Schema file must contain an array of fields. Got: ${typeof parsed}`);
|
|
159
150
|
}
|
|
160
151
|
|
|
161
152
|
if (parsed.length === 0) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
"Schema file contains an empty array. At least one field is required.",
|
|
164
|
-
);
|
|
153
|
+
throw new Error("Schema file contains an empty array. At least one field is required.");
|
|
165
154
|
}
|
|
166
155
|
|
|
167
156
|
return (parsed as FieldConfig[]).map((field, index) => {
|
|
@@ -208,9 +197,7 @@ export const createTopicCommand = Command.make(
|
|
|
208
197
|
const hasFields = fields.length > 0;
|
|
209
198
|
|
|
210
199
|
if (!hasFields && !schemaFilePath) {
|
|
211
|
-
return yield* Effect.fail(
|
|
212
|
-
new Error("Either --fields or --schema-file must be provided"),
|
|
213
|
-
);
|
|
200
|
+
return yield* Effect.fail(new Error("Either --fields or --schema-file must be provided"));
|
|
214
201
|
}
|
|
215
202
|
|
|
216
203
|
if (hasFields && schemaFilePath) {
|
|
@@ -221,11 +208,8 @@ export const createTopicCommand = Command.make(
|
|
|
221
208
|
|
|
222
209
|
const topicFields = yield* Effect.try({
|
|
223
210
|
try: () =>
|
|
224
|
-
schemaFilePath
|
|
225
|
-
|
|
226
|
-
: parseFieldsFromArgs(fields),
|
|
227
|
-
catch: (error) =>
|
|
228
|
-
error instanceof Error ? error : new Error("Failed to parse fields"),
|
|
211
|
+
schemaFilePath ? loadFieldsFromFile(schemaFilePath) : parseFieldsFromArgs(fields),
|
|
212
|
+
catch: (error) => (error instanceof Error ? error : new Error("Failed to parse fields")),
|
|
229
213
|
});
|
|
230
214
|
|
|
231
215
|
if (topicFields.length === 0) {
|
|
@@ -235,9 +219,7 @@ export const createTopicCommand = Command.make(
|
|
|
235
219
|
const partitionKeyName = Option.getOrUndefined(partitionKey);
|
|
236
220
|
let partitionKeyId: bigint | undefined;
|
|
237
221
|
if (partitionKeyName) {
|
|
238
|
-
const index = topicFields.findIndex(
|
|
239
|
-
(field) => field.name === partitionKeyName,
|
|
240
|
-
);
|
|
222
|
+
const index = topicFields.findIndex((field) => field.name === partitionKeyName);
|
|
241
223
|
if (index === -1) {
|
|
242
224
|
return yield* Effect.fail(
|
|
243
225
|
new Error(
|
|
@@ -269,16 +251,12 @@ export const createTopicCommand = Command.make(
|
|
|
269
251
|
partitionKey: partitionKeyId,
|
|
270
252
|
compaction: {
|
|
271
253
|
freshnessSeconds: BigInt(freshnessSeconds),
|
|
272
|
-
ttlSeconds: Option.getOrUndefined(
|
|
273
|
-
Option.map(ttlSeconds, (ttl) => BigInt(ttl)),
|
|
274
|
-
),
|
|
254
|
+
ttlSeconds: Option.getOrUndefined(Option.map(ttlSeconds, (ttl) => BigInt(ttl))),
|
|
275
255
|
targetFileSizeBytes: BigInt(targetFileSizeBytes),
|
|
276
256
|
},
|
|
277
257
|
}).pipe(
|
|
278
258
|
Effect.provide(layer),
|
|
279
|
-
Effect.tapError(() =>
|
|
280
|
-
Effect.sync(() => s.stop("Failed to create topic")),
|
|
281
|
-
),
|
|
259
|
+
Effect.tapError(() => Effect.sync(() => s.stop("Failed to create topic"))),
|
|
282
260
|
);
|
|
283
261
|
|
|
284
262
|
s.stop("Topic created successfully");
|
|
@@ -290,13 +268,12 @@ export const createTopicCommand = Command.make(
|
|
|
290
268
|
description: topic.description || "-",
|
|
291
269
|
partition_key:
|
|
292
270
|
topic.partitionKey !== undefined
|
|
293
|
-
? topicFields.find((field) => field.id === topic.partitionKey)
|
|
294
|
-
|
|
271
|
+
? topicFields.find((field) => field.id === topic.partitionKey)?.name ||
|
|
272
|
+
topic.partitionKey.toString()
|
|
295
273
|
: "-",
|
|
296
274
|
freshness_seconds: topic.compaction.freshnessSeconds.toString(),
|
|
297
275
|
ttl_seconds: topic.compaction.ttlSeconds?.toString() || "-",
|
|
298
|
-
target_file_size_bytes:
|
|
299
|
-
topic.compaction.targetFileSizeBytes.toString(),
|
|
276
|
+
target_file_size_bytes: topic.compaction.targetFileSizeBytes.toString(),
|
|
300
277
|
fields_count: topic.schema.fields.length.toString(),
|
|
301
278
|
},
|
|
302
279
|
]);
|
|
@@ -315,5 +292,5 @@ export const createTopicCommand = Command.make(
|
|
|
315
292
|
|
|
316
293
|
p.outro("✓ Done");
|
|
317
294
|
});
|
|
318
|
-
})
|
|
295
|
+
}),
|
|
319
296
|
).pipe(Command.withDescription("Create a new topic belonging to a namespace"));
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { Command, Options } from "@effect/cli";
|
|
3
2
|
import { WingsClusterMetadata } from "@useairfoil/wings";
|
|
4
3
|
import { Effect } from "effect";
|
|
4
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
5
|
+
|
|
5
6
|
import { makeClusterMetadataLayer } from "../../../utils/client.js";
|
|
6
|
-
import { handleCliError } from "../../../utils/effect.js";
|
|
7
7
|
import { forceOption, hostOption, portOption } from "../../../utils/options.js";
|
|
8
8
|
|
|
9
|
-
const nameOption =
|
|
10
|
-
|
|
9
|
+
const nameOption = Flag.string("name").pipe(
|
|
10
|
+
Flag.withDescription(
|
|
11
11
|
"Topic name in format: tenants/{tenant}/namespaces/{namespace}/topics/{topic}",
|
|
12
12
|
),
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
const forceDeleteOption =
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const forceDeleteOption = Flag.boolean("force-delete").pipe(
|
|
16
|
+
Flag.withDescription("Also delete data associated with the topic"),
|
|
17
|
+
Flag.withDefault(false),
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
export const deleteTopicCommand = Command.make(
|
|
@@ -56,12 +56,10 @@ export const deleteTopicCommand = Command.make(
|
|
|
56
56
|
force: forceDelete,
|
|
57
57
|
}).pipe(
|
|
58
58
|
Effect.provide(layer),
|
|
59
|
-
Effect.tapError(() =>
|
|
60
|
-
Effect.sync(() => s.stop("Failed to delete topic")),
|
|
61
|
-
),
|
|
59
|
+
Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete topic"))),
|
|
62
60
|
);
|
|
63
61
|
|
|
64
62
|
s.stop("Topic deleted successfully");
|
|
65
63
|
p.outro("✓ Done");
|
|
66
|
-
})
|
|
64
|
+
}),
|
|
67
65
|
).pipe(Command.withDescription("Delete a topic from the cluster"));
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { Command, Options } from "@effect/cli";
|
|
3
2
|
import { WingsClusterMetadata } from "@useairfoil/wings";
|
|
4
3
|
import { printTable } from "console-table-printer";
|
|
5
4
|
import { Effect } from "effect";
|
|
5
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
6
|
+
|
|
6
7
|
import { makeClusterMetadataLayer } from "../../../utils/client.js";
|
|
7
|
-
import { handleCliError } from "../../../utils/effect.js";
|
|
8
8
|
import { hostOption, portOption } from "../../../utils/options.js";
|
|
9
9
|
|
|
10
|
-
const nameOption =
|
|
11
|
-
|
|
10
|
+
const nameOption = Flag.string("name").pipe(
|
|
11
|
+
Flag.withDescription(
|
|
12
12
|
"Topic name in format: tenants/{tenant}/namespaces/{namespace}/topics/{topic}",
|
|
13
13
|
),
|
|
14
14
|
);
|
|
@@ -47,5 +47,5 @@ export const getTopicCommand = Command.make(
|
|
|
47
47
|
|
|
48
48
|
p.outro("✓ Done");
|
|
49
49
|
});
|
|
50
|
-
})
|
|
50
|
+
}),
|
|
51
51
|
).pipe(Command.withDescription("Get details of a specific topic"));
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { Command, Options } from "@effect/cli";
|
|
3
2
|
import { WingsClusterMetadata } from "@useairfoil/wings";
|
|
4
3
|
import { printTable } from "console-table-printer";
|
|
5
4
|
import { Effect, Option } from "effect";
|
|
5
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
6
|
+
|
|
6
7
|
import { makeClusterMetadataLayer } from "../../../utils/client.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
hostOption,
|
|
10
|
-
pageSizeOption,
|
|
11
|
-
pageTokenOption,
|
|
12
|
-
portOption,
|
|
13
|
-
} from "../../../utils/options.js";
|
|
8
|
+
import { hostOption, pageSizeOption, pageTokenOption, portOption } from "../../../utils/options.js";
|
|
14
9
|
|
|
15
|
-
const parentOption =
|
|
16
|
-
|
|
17
|
-
"Parent namespace in format: tenants/{tenant}/namespaces/{namespace}",
|
|
18
|
-
),
|
|
10
|
+
const parentOption = Flag.string("parent").pipe(
|
|
11
|
+
Flag.withDescription("Parent namespace in format: tenants/{tenant}/namespaces/{namespace}"),
|
|
19
12
|
);
|
|
20
13
|
|
|
21
14
|
export const listTopicsCommand = Command.make(
|
|
@@ -40,9 +33,7 @@ export const listTopicsCommand = Command.make(
|
|
|
40
33
|
pageToken: Option.getOrUndefined(pageToken),
|
|
41
34
|
}).pipe(
|
|
42
35
|
Effect.provide(layer),
|
|
43
|
-
Effect.tapError(() =>
|
|
44
|
-
Effect.sync(() => s.stop("Failed to list topics")),
|
|
45
|
-
),
|
|
36
|
+
Effect.tapError(() => Effect.sync(() => s.stop("Failed to list topics"))),
|
|
46
37
|
);
|
|
47
38
|
|
|
48
39
|
s.stop(`Found ${response.topics.length} topic(s)`);
|
|
@@ -66,5 +57,5 @@ export const listTopicsCommand = Command.make(
|
|
|
66
57
|
|
|
67
58
|
p.outro("✓ Done");
|
|
68
59
|
});
|
|
69
|
-
})
|
|
60
|
+
}),
|
|
70
61
|
).pipe(Command.withDescription("List all topics belonging to a namespace"));
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Command,
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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";
|
|
7
|
+
|
|
8
|
+
const dockerOption = Flag.boolean("docker").pipe(
|
|
9
|
+
Flag.withDescription("Run Wings using Docker (recommended)"),
|
|
10
|
+
Flag.withDefault(false),
|
|
10
11
|
);
|
|
11
12
|
|
|
12
|
-
const versionOption =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
),
|
|
16
|
-
Options.withDefault("latest"),
|
|
13
|
+
const versionOption = Flag.string("version").pipe(
|
|
14
|
+
Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.11 or 'latest')"),
|
|
15
|
+
Flag.withDefault("latest"),
|
|
17
16
|
);
|
|
18
17
|
|
|
19
|
-
const tagOption =
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const tagOption = Flag.string("tag").pipe(
|
|
19
|
+
Flag.withDescription("Docker image tag (only with --docker)"),
|
|
20
|
+
Flag.withDefault("latest"),
|
|
22
21
|
);
|
|
23
22
|
|
|
24
|
-
const forcePullOption =
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const forcePullOption = Flag.boolean("force-pull").pipe(
|
|
24
|
+
Flag.withDescription("Force pull Docker image even if it exists locally"),
|
|
25
|
+
Flag.withDefault(false),
|
|
27
26
|
);
|
|
28
27
|
|
|
29
|
-
const stressOption =
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const stressOption = Flag.boolean("stress").pipe(
|
|
29
|
+
Flag.withDescription("Use Wings stress binary variant"),
|
|
30
|
+
Flag.withDefault(false),
|
|
32
31
|
);
|
|
33
32
|
|
|
34
|
-
const yesOption =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const yesOption = Flag.boolean("yes").pipe(
|
|
34
|
+
Flag.withAlias("y"),
|
|
35
|
+
Flag.withDescription("Skip confirmation prompts"),
|
|
36
|
+
Flag.withDefault(false),
|
|
38
37
|
);
|
|
39
38
|
|
|
40
39
|
export const devCommand = Command.make(
|
|
@@ -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.catchAll(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
|
+
});
|