flightdeck 0.0.14 → 0.1.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/flightdeck.$configPath.schema.json +2 -3
- package/dist/flightdeck.bin.js +170 -88
- package/dist/flightdeck.main.schema.json +2 -3
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +150 -70
- package/package.json +5 -4
- package/src/flightdeck.bin.ts +10 -8
- package/src/flightdeck.env.ts +10 -0
- package/src/flightdeck.lib.ts +94 -86
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "object",
|
|
3
3
|
"properties": {
|
|
4
|
-
"
|
|
5
|
-
"type": "
|
|
4
|
+
"port": {
|
|
5
|
+
"type": "number"
|
|
6
6
|
},
|
|
7
7
|
"packageName": {
|
|
8
8
|
"type": "string"
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"required": [
|
|
50
|
-
"secret",
|
|
51
50
|
"packageName",
|
|
52
51
|
"services",
|
|
53
52
|
"flightdeckRootDir",
|
package/dist/flightdeck.bin.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/flightdeck.bin.ts
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { cli, optional } from "comline";
|
|
6
|
-
import { z } from "zod";
|
|
5
|
+
import { cli, optional, parseNumberOption } from "comline";
|
|
6
|
+
import { z as z2 } from "zod";
|
|
7
7
|
|
|
8
8
|
// src/flightdeck.lib.ts
|
|
9
9
|
import { execSync, spawn } from "node:child_process";
|
|
@@ -14,9 +14,82 @@ import { resolve } from "node:path";
|
|
|
14
14
|
import { Future } from "atom.io/internal";
|
|
15
15
|
import { fromEntries, toEntries } from "atom.io/json";
|
|
16
16
|
import { ChildSocket } from "atom.io/realtime-server";
|
|
17
|
-
var PORT = process.env.PORT ?? 8080;
|
|
18
|
-
var ORIGIN = `http://localhost:${PORT}`;
|
|
19
17
|
|
|
18
|
+
// /home/runner/work/wayforge/wayforge/packages/flightdeck/node_modules/@t3-oss/env-core/dist/index.js
|
|
19
|
+
import { object } from "zod";
|
|
20
|
+
function createEnv(opts) {
|
|
21
|
+
const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
|
|
22
|
+
const emptyStringAsUndefined = opts.emptyStringAsUndefined ?? false;
|
|
23
|
+
if (emptyStringAsUndefined) {
|
|
24
|
+
for (const [key, value] of Object.entries(runtimeEnv)) {
|
|
25
|
+
if (value === "") {
|
|
26
|
+
delete runtimeEnv[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const skip = !!opts.skipValidation;
|
|
31
|
+
if (skip)
|
|
32
|
+
return runtimeEnv;
|
|
33
|
+
const _client = typeof opts.client === "object" ? opts.client : {};
|
|
34
|
+
const _server = typeof opts.server === "object" ? opts.server : {};
|
|
35
|
+
const _shared = typeof opts.shared === "object" ? opts.shared : {};
|
|
36
|
+
const client = object(_client);
|
|
37
|
+
const server = object(_server);
|
|
38
|
+
const shared = object(_shared);
|
|
39
|
+
const isServer = opts.isServer ?? (typeof window === "undefined" || ("Deno" in window));
|
|
40
|
+
const allClient = client.merge(shared);
|
|
41
|
+
const allServer = server.merge(shared).merge(client);
|
|
42
|
+
const parsed = isServer ? allServer.safeParse(runtimeEnv) : allClient.safeParse(runtimeEnv);
|
|
43
|
+
const onValidationError = opts.onValidationError ?? ((error) => {
|
|
44
|
+
console.error("\u274C Invalid environment variables:", error.flatten().fieldErrors);
|
|
45
|
+
throw new Error("Invalid environment variables");
|
|
46
|
+
});
|
|
47
|
+
const onInvalidAccess = opts.onInvalidAccess ?? ((_variable) => {
|
|
48
|
+
throw new Error("\u274C Attempted to access a server-side environment variable on the client");
|
|
49
|
+
});
|
|
50
|
+
if (parsed.success === false) {
|
|
51
|
+
return onValidationError(parsed.error);
|
|
52
|
+
}
|
|
53
|
+
const isServerAccess = (prop) => {
|
|
54
|
+
if (!opts.clientPrefix)
|
|
55
|
+
return true;
|
|
56
|
+
return !prop.startsWith(opts.clientPrefix) && !(prop in shared.shape);
|
|
57
|
+
};
|
|
58
|
+
const isValidServerAccess = (prop) => {
|
|
59
|
+
return isServer || !isServerAccess(prop);
|
|
60
|
+
};
|
|
61
|
+
const ignoreProp = (prop) => {
|
|
62
|
+
return prop === "__esModule" || prop === "$$typeof";
|
|
63
|
+
};
|
|
64
|
+
const extendedObj = (opts.extends ?? []).reduce((acc, curr) => {
|
|
65
|
+
return Object.assign(acc, curr);
|
|
66
|
+
}, {});
|
|
67
|
+
const fullObj = Object.assign(parsed.data, extendedObj);
|
|
68
|
+
const env = new Proxy(fullObj, {
|
|
69
|
+
get(target, prop) {
|
|
70
|
+
if (typeof prop !== "string")
|
|
71
|
+
return;
|
|
72
|
+
if (ignoreProp(prop))
|
|
73
|
+
return;
|
|
74
|
+
if (!isValidServerAccess(prop))
|
|
75
|
+
return onInvalidAccess(prop);
|
|
76
|
+
return Reflect.get(target, prop);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return env;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/flightdeck.env.ts
|
|
83
|
+
import { z } from "zod";
|
|
84
|
+
var env = createEnv({
|
|
85
|
+
server: { FLIGHTDECK_SECRET: z.string().optional() },
|
|
86
|
+
clientPrefix: `NEVER`,
|
|
87
|
+
client: {},
|
|
88
|
+
runtimeEnv: import.meta.env,
|
|
89
|
+
emptyStringAsUndefined: true
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// src/flightdeck.lib.ts
|
|
20
93
|
class FlightDeck {
|
|
21
94
|
options;
|
|
22
95
|
safety = 0;
|
|
@@ -38,7 +111,10 @@ class FlightDeck {
|
|
|
38
111
|
persistentStateDir;
|
|
39
112
|
constructor(options) {
|
|
40
113
|
this.options = options;
|
|
41
|
-
const {
|
|
114
|
+
const { FLIGHTDECK_SECRET } = env;
|
|
115
|
+
const { flightdeckRootDir = resolve(homedir(), `services`) } = options;
|
|
116
|
+
const port = options.port ?? 8080;
|
|
117
|
+
const origin = `http://localhost:${port}`;
|
|
42
118
|
const servicesEntries = toEntries(options.services);
|
|
43
119
|
this.services = fromEntries(servicesEntries.map(([serviceName]) => [serviceName, null]));
|
|
44
120
|
this.serviceIdx = fromEntries(servicesEntries.map(([serviceName], idx) => [serviceName, idx]));
|
|
@@ -83,79 +159,83 @@ class FlightDeck {
|
|
|
83
159
|
if (!existsSync(this.persistentStateDir)) {
|
|
84
160
|
mkdirSync(this.persistentStateDir, { recursive: true });
|
|
85
161
|
}
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
if (FLIGHTDECK_SECRET === undefined) {
|
|
163
|
+
this.logger.warn(`No FLIGHTDECK_SECRET environment variable found. FlightDeck will not run an update server.`);
|
|
164
|
+
} else {
|
|
165
|
+
createServer((req, res) => {
|
|
166
|
+
let data = [];
|
|
167
|
+
req.on(`data`, (chunk) => {
|
|
168
|
+
data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
|
|
169
|
+
}).on(`end`, () => {
|
|
170
|
+
const authHeader = req.headers.authorization;
|
|
171
|
+
try {
|
|
172
|
+
if (typeof req.url === `undefined`)
|
|
173
|
+
throw 400;
|
|
174
|
+
const expectedAuthHeader = `Bearer ${FLIGHTDECK_SECRET}`;
|
|
175
|
+
if (authHeader !== `Bearer ${FLIGHTDECK_SECRET}`) {
|
|
176
|
+
this.logger.info(`Unauthorized: needed \`${expectedAuthHeader}\`, got \`${authHeader}\``);
|
|
177
|
+
throw 401;
|
|
178
|
+
}
|
|
179
|
+
const url = new URL(req.url, origin);
|
|
180
|
+
this.logger.info(req.method, url.pathname);
|
|
181
|
+
switch (req.method) {
|
|
182
|
+
case `POST`:
|
|
183
|
+
{
|
|
184
|
+
switch (url.pathname) {
|
|
185
|
+
case `/`:
|
|
186
|
+
{
|
|
187
|
+
res.writeHead(200);
|
|
188
|
+
res.end();
|
|
189
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
190
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
191
|
+
if (!existsSync(installFile)) {
|
|
192
|
+
this.logger.info(`Install file does not exist yet. Creating...`);
|
|
193
|
+
writeFileSync(installFile, ``);
|
|
194
|
+
}
|
|
195
|
+
if (existsSync(readyFile)) {
|
|
196
|
+
this.logger.info(`Ready file exists. Removing...`);
|
|
197
|
+
rmSync(readyFile);
|
|
198
|
+
}
|
|
199
|
+
this.getLatestRelease();
|
|
200
|
+
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
201
|
+
this.logger.info(`All services are ready to update!`);
|
|
202
|
+
this.stopAllServices();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const entry of toEntries(this.services)) {
|
|
206
|
+
const [serviceName, service] = entry;
|
|
207
|
+
if (service) {
|
|
208
|
+
if (this.options.services[serviceName].waitFor) {
|
|
209
|
+
service.emit(`updatesReady`);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
this.startService(serviceName);
|
|
131
213
|
}
|
|
132
|
-
} else {
|
|
133
|
-
this.startService(serviceName);
|
|
134
214
|
}
|
|
135
215
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
throw 404;
|
|
219
|
+
}
|
|
140
220
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
throw 405;
|
|
224
|
+
}
|
|
225
|
+
} catch (thrown) {
|
|
226
|
+
this.logger.error(thrown, req.url);
|
|
227
|
+
if (typeof thrown === `number`) {
|
|
228
|
+
res.writeHead(thrown);
|
|
229
|
+
res.end();
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
data = [];
|
|
151
233
|
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
}
|
|
234
|
+
});
|
|
235
|
+
}).listen(port, () => {
|
|
236
|
+
this.logger.info(`Server started on port ${port}`);
|
|
155
237
|
});
|
|
156
|
-
}
|
|
157
|
-
this.logger.info(`Server started on port ${PORT}`);
|
|
158
|
-
});
|
|
238
|
+
}
|
|
159
239
|
this.startAllServices();
|
|
160
240
|
}
|
|
161
241
|
startAllServices() {
|
|
@@ -296,25 +376,26 @@ class FlightDeck {
|
|
|
296
376
|
|
|
297
377
|
// src/flightdeck.bin.ts
|
|
298
378
|
var FLIGHTDECK_MANUAL = {
|
|
299
|
-
optionsSchema:
|
|
300
|
-
|
|
301
|
-
packageName:
|
|
302
|
-
services:
|
|
303
|
-
flightdeckRootDir:
|
|
304
|
-
scripts:
|
|
305
|
-
download:
|
|
306
|
-
install:
|
|
379
|
+
optionsSchema: z2.object({
|
|
380
|
+
port: z2.number().optional(),
|
|
381
|
+
packageName: z2.string(),
|
|
382
|
+
services: z2.record(z2.object({ run: z2.string(), waitFor: z2.boolean() })),
|
|
383
|
+
flightdeckRootDir: z2.string(),
|
|
384
|
+
scripts: z2.object({
|
|
385
|
+
download: z2.string(),
|
|
386
|
+
install: z2.string()
|
|
307
387
|
})
|
|
308
388
|
}),
|
|
309
389
|
options: {
|
|
310
|
-
|
|
311
|
-
flag: `
|
|
312
|
-
required:
|
|
313
|
-
description: `
|
|
314
|
-
example: `--
|
|
390
|
+
port: {
|
|
391
|
+
flag: `p`,
|
|
392
|
+
required: false,
|
|
393
|
+
description: `Port to run the flightdeck server on.`,
|
|
394
|
+
example: `--port=8080`,
|
|
395
|
+
parse: parseNumberOption
|
|
315
396
|
},
|
|
316
397
|
packageName: {
|
|
317
|
-
flag: `
|
|
398
|
+
flag: `n`,
|
|
318
399
|
required: true,
|
|
319
400
|
description: `Name of the package.`,
|
|
320
401
|
example: `--packageName=\"my-app\"`
|
|
@@ -342,8 +423,8 @@ var FLIGHTDECK_MANUAL = {
|
|
|
342
423
|
}
|
|
343
424
|
};
|
|
344
425
|
var SCHEMA_MANUAL = {
|
|
345
|
-
optionsSchema:
|
|
346
|
-
outdir:
|
|
426
|
+
optionsSchema: z2.object({
|
|
427
|
+
outdir: z2.string().optional()
|
|
347
428
|
}),
|
|
348
429
|
options: {
|
|
349
430
|
outdir: {
|
|
@@ -362,6 +443,7 @@ var parse = cli({
|
|
|
362
443
|
$configPath: FLIGHTDECK_MANUAL,
|
|
363
444
|
schema: SCHEMA_MANUAL
|
|
364
445
|
},
|
|
446
|
+
debugOutput: true,
|
|
365
447
|
discoverConfigPath: (args) => {
|
|
366
448
|
if (args[0] === `schema`) {
|
|
367
449
|
return;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "object",
|
|
3
3
|
"properties": {
|
|
4
|
-
"
|
|
5
|
-
"type": "
|
|
4
|
+
"port": {
|
|
5
|
+
"type": "number"
|
|
6
6
|
},
|
|
7
7
|
"packageName": {
|
|
8
8
|
"type": "string"
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"required": [
|
|
50
|
-
"secret",
|
|
51
50
|
"packageName",
|
|
52
51
|
"services",
|
|
53
52
|
"flightdeckRootDir",
|
package/dist/lib.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { Future } from 'atom.io/internal';
|
|
|
3
3
|
import { ChildSocket } from 'atom.io/realtime-server';
|
|
4
4
|
|
|
5
5
|
type FlightDeckOptions<S extends string = string> = {
|
|
6
|
-
secret: string;
|
|
7
6
|
packageName: string;
|
|
8
7
|
services: {
|
|
9
8
|
[service in S]: {
|
|
@@ -15,6 +14,7 @@ type FlightDeckOptions<S extends string = string> = {
|
|
|
15
14
|
download: string;
|
|
16
15
|
install: string;
|
|
17
16
|
};
|
|
17
|
+
port?: number | undefined;
|
|
18
18
|
flightdeckRootDir?: string | undefined;
|
|
19
19
|
};
|
|
20
20
|
declare class FlightDeck<S extends string = string> {
|
package/dist/lib.js
CHANGED
|
@@ -18,9 +18,82 @@ import { resolve } from "node:path";
|
|
|
18
18
|
import { Future } from "atom.io/internal";
|
|
19
19
|
import { fromEntries, toEntries } from "atom.io/json";
|
|
20
20
|
import { ChildSocket } from "atom.io/realtime-server";
|
|
21
|
-
var PORT = process.env.PORT ?? 8080;
|
|
22
|
-
var ORIGIN = `http://localhost:${PORT}`;
|
|
23
21
|
|
|
22
|
+
// /home/runner/work/wayforge/wayforge/packages/flightdeck/node_modules/@t3-oss/env-core/dist/index.js
|
|
23
|
+
import { object } from "zod";
|
|
24
|
+
function createEnv(opts) {
|
|
25
|
+
const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
|
|
26
|
+
const emptyStringAsUndefined = opts.emptyStringAsUndefined ?? false;
|
|
27
|
+
if (emptyStringAsUndefined) {
|
|
28
|
+
for (const [key, value] of Object.entries(runtimeEnv)) {
|
|
29
|
+
if (value === "") {
|
|
30
|
+
delete runtimeEnv[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const skip = !!opts.skipValidation;
|
|
35
|
+
if (skip)
|
|
36
|
+
return runtimeEnv;
|
|
37
|
+
const _client = typeof opts.client === "object" ? opts.client : {};
|
|
38
|
+
const _server = typeof opts.server === "object" ? opts.server : {};
|
|
39
|
+
const _shared = typeof opts.shared === "object" ? opts.shared : {};
|
|
40
|
+
const client = object(_client);
|
|
41
|
+
const server = object(_server);
|
|
42
|
+
const shared = object(_shared);
|
|
43
|
+
const isServer = opts.isServer ?? (typeof window === "undefined" || ("Deno" in window));
|
|
44
|
+
const allClient = client.merge(shared);
|
|
45
|
+
const allServer = server.merge(shared).merge(client);
|
|
46
|
+
const parsed = isServer ? allServer.safeParse(runtimeEnv) : allClient.safeParse(runtimeEnv);
|
|
47
|
+
const onValidationError = opts.onValidationError ?? ((error) => {
|
|
48
|
+
console.error("\u274C Invalid environment variables:", error.flatten().fieldErrors);
|
|
49
|
+
throw new Error("Invalid environment variables");
|
|
50
|
+
});
|
|
51
|
+
const onInvalidAccess = opts.onInvalidAccess ?? ((_variable) => {
|
|
52
|
+
throw new Error("\u274C Attempted to access a server-side environment variable on the client");
|
|
53
|
+
});
|
|
54
|
+
if (parsed.success === false) {
|
|
55
|
+
return onValidationError(parsed.error);
|
|
56
|
+
}
|
|
57
|
+
const isServerAccess = (prop) => {
|
|
58
|
+
if (!opts.clientPrefix)
|
|
59
|
+
return true;
|
|
60
|
+
return !prop.startsWith(opts.clientPrefix) && !(prop in shared.shape);
|
|
61
|
+
};
|
|
62
|
+
const isValidServerAccess = (prop) => {
|
|
63
|
+
return isServer || !isServerAccess(prop);
|
|
64
|
+
};
|
|
65
|
+
const ignoreProp = (prop) => {
|
|
66
|
+
return prop === "__esModule" || prop === "$$typeof";
|
|
67
|
+
};
|
|
68
|
+
const extendedObj = (opts.extends ?? []).reduce((acc, curr) => {
|
|
69
|
+
return Object.assign(acc, curr);
|
|
70
|
+
}, {});
|
|
71
|
+
const fullObj = Object.assign(parsed.data, extendedObj);
|
|
72
|
+
const env = new Proxy(fullObj, {
|
|
73
|
+
get(target, prop) {
|
|
74
|
+
if (typeof prop !== "string")
|
|
75
|
+
return;
|
|
76
|
+
if (ignoreProp(prop))
|
|
77
|
+
return;
|
|
78
|
+
if (!isValidServerAccess(prop))
|
|
79
|
+
return onInvalidAccess(prop);
|
|
80
|
+
return Reflect.get(target, prop);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return env;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/flightdeck.env.ts
|
|
87
|
+
import { z } from "zod";
|
|
88
|
+
var env = createEnv({
|
|
89
|
+
server: { FLIGHTDECK_SECRET: z.string().optional() },
|
|
90
|
+
clientPrefix: `NEVER`,
|
|
91
|
+
client: {},
|
|
92
|
+
runtimeEnv: import.meta.env,
|
|
93
|
+
emptyStringAsUndefined: true
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// src/flightdeck.lib.ts
|
|
24
97
|
class FlightDeck {
|
|
25
98
|
options;
|
|
26
99
|
safety = 0;
|
|
@@ -42,7 +115,10 @@ class FlightDeck {
|
|
|
42
115
|
persistentStateDir;
|
|
43
116
|
constructor(options) {
|
|
44
117
|
this.options = options;
|
|
45
|
-
const {
|
|
118
|
+
const { FLIGHTDECK_SECRET } = env;
|
|
119
|
+
const { flightdeckRootDir = resolve(homedir(), `services`) } = options;
|
|
120
|
+
const port = options.port ?? 8080;
|
|
121
|
+
const origin = `http://localhost:${port}`;
|
|
46
122
|
const servicesEntries = toEntries(options.services);
|
|
47
123
|
this.services = fromEntries(servicesEntries.map(([serviceName]) => [serviceName, null]));
|
|
48
124
|
this.serviceIdx = fromEntries(servicesEntries.map(([serviceName], idx) => [serviceName, idx]));
|
|
@@ -87,79 +163,83 @@ class FlightDeck {
|
|
|
87
163
|
if (!existsSync(this.persistentStateDir)) {
|
|
88
164
|
mkdirSync(this.persistentStateDir, { recursive: true });
|
|
89
165
|
}
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
if (FLIGHTDECK_SECRET === undefined) {
|
|
167
|
+
this.logger.warn(`No FLIGHTDECK_SECRET environment variable found. FlightDeck will not run an update server.`);
|
|
168
|
+
} else {
|
|
169
|
+
createServer((req, res) => {
|
|
170
|
+
let data = [];
|
|
171
|
+
req.on(`data`, (chunk) => {
|
|
172
|
+
data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
|
|
173
|
+
}).on(`end`, () => {
|
|
174
|
+
const authHeader = req.headers.authorization;
|
|
175
|
+
try {
|
|
176
|
+
if (typeof req.url === `undefined`)
|
|
177
|
+
throw 400;
|
|
178
|
+
const expectedAuthHeader = `Bearer ${FLIGHTDECK_SECRET}`;
|
|
179
|
+
if (authHeader !== `Bearer ${FLIGHTDECK_SECRET}`) {
|
|
180
|
+
this.logger.info(`Unauthorized: needed \`${expectedAuthHeader}\`, got \`${authHeader}\``);
|
|
181
|
+
throw 401;
|
|
182
|
+
}
|
|
183
|
+
const url = new URL(req.url, origin);
|
|
184
|
+
this.logger.info(req.method, url.pathname);
|
|
185
|
+
switch (req.method) {
|
|
186
|
+
case `POST`:
|
|
187
|
+
{
|
|
188
|
+
switch (url.pathname) {
|
|
189
|
+
case `/`:
|
|
190
|
+
{
|
|
191
|
+
res.writeHead(200);
|
|
192
|
+
res.end();
|
|
193
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
194
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
195
|
+
if (!existsSync(installFile)) {
|
|
196
|
+
this.logger.info(`Install file does not exist yet. Creating...`);
|
|
197
|
+
writeFileSync(installFile, ``);
|
|
198
|
+
}
|
|
199
|
+
if (existsSync(readyFile)) {
|
|
200
|
+
this.logger.info(`Ready file exists. Removing...`);
|
|
201
|
+
rmSync(readyFile);
|
|
202
|
+
}
|
|
203
|
+
this.getLatestRelease();
|
|
204
|
+
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
205
|
+
this.logger.info(`All services are ready to update!`);
|
|
206
|
+
this.stopAllServices();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const entry of toEntries(this.services)) {
|
|
210
|
+
const [serviceName, service] = entry;
|
|
211
|
+
if (service) {
|
|
212
|
+
if (this.options.services[serviceName].waitFor) {
|
|
213
|
+
service.emit(`updatesReady`);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
this.startService(serviceName);
|
|
135
217
|
}
|
|
136
|
-
} else {
|
|
137
|
-
this.startService(serviceName);
|
|
138
218
|
}
|
|
139
219
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
throw 404;
|
|
223
|
+
}
|
|
144
224
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
225
|
+
break;
|
|
226
|
+
default:
|
|
227
|
+
throw 405;
|
|
228
|
+
}
|
|
229
|
+
} catch (thrown) {
|
|
230
|
+
this.logger.error(thrown, req.url);
|
|
231
|
+
if (typeof thrown === `number`) {
|
|
232
|
+
res.writeHead(thrown);
|
|
233
|
+
res.end();
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
data = [];
|
|
149
237
|
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
res.writeHead(thrown);
|
|
154
|
-
res.end();
|
|
155
|
-
}
|
|
156
|
-
} finally {
|
|
157
|
-
data = [];
|
|
158
|
-
}
|
|
238
|
+
});
|
|
239
|
+
}).listen(port, () => {
|
|
240
|
+
this.logger.info(`Server started on port ${port}`);
|
|
159
241
|
});
|
|
160
|
-
}
|
|
161
|
-
this.logger.info(`Server started on port ${PORT}`);
|
|
162
|
-
});
|
|
242
|
+
}
|
|
163
243
|
this.startAllServices();
|
|
164
244
|
}
|
|
165
245
|
startAllServices() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flightdeck",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jeremy Banka",
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
"klaxon": "./dist/klaxon.bin.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@t3-oss/env-core": "0.11.1",
|
|
24
25
|
"zod": "3.23.8",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
26
|
+
"comline": "0.1.4",
|
|
27
|
+
"atom.io": "0.30.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/node": "22.7.6",
|
|
30
31
|
"@types/tmp": "0.2.6",
|
|
31
|
-
"bun-types": "1.1.
|
|
32
|
+
"bun-types": "1.1.31",
|
|
32
33
|
"concurrently": "9.0.1",
|
|
33
34
|
"rimraf": "6.0.1",
|
|
34
35
|
"tmp": "0.2.3",
|
package/src/flightdeck.bin.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as path from "node:path"
|
|
4
4
|
|
|
5
5
|
import type { OptionsGroup } from "comline"
|
|
6
|
-
import { cli, optional,
|
|
6
|
+
import { cli, optional, parseNumberOption } from "comline"
|
|
7
7
|
import { z } from "zod"
|
|
8
8
|
|
|
9
9
|
import type { FlightDeckOptions } from "./flightdeck.lib"
|
|
@@ -11,7 +11,7 @@ import { FlightDeck } from "./flightdeck.lib"
|
|
|
11
11
|
|
|
12
12
|
const FLIGHTDECK_MANUAL = {
|
|
13
13
|
optionsSchema: z.object({
|
|
14
|
-
|
|
14
|
+
port: z.number().optional(),
|
|
15
15
|
packageName: z.string(),
|
|
16
16
|
services: z.record(z.object({ run: z.string(), waitFor: z.boolean() })),
|
|
17
17
|
flightdeckRootDir: z.string(),
|
|
@@ -21,14 +21,15 @@ const FLIGHTDECK_MANUAL = {
|
|
|
21
21
|
}),
|
|
22
22
|
}),
|
|
23
23
|
options: {
|
|
24
|
-
|
|
25
|
-
flag: `
|
|
26
|
-
required:
|
|
27
|
-
description: `
|
|
28
|
-
example: `--
|
|
24
|
+
port: {
|
|
25
|
+
flag: `p`,
|
|
26
|
+
required: false,
|
|
27
|
+
description: `Port to run the flightdeck server on.`,
|
|
28
|
+
example: `--port=8080`,
|
|
29
|
+
parse: parseNumberOption,
|
|
29
30
|
},
|
|
30
31
|
packageName: {
|
|
31
|
-
flag: `
|
|
32
|
+
flag: `n`,
|
|
32
33
|
required: true,
|
|
33
34
|
description: `Name of the package.`,
|
|
34
35
|
example: `--packageName=\"my-app\"`,
|
|
@@ -79,6 +80,7 @@ const parse = cli(
|
|
|
79
80
|
$configPath: FLIGHTDECK_MANUAL,
|
|
80
81
|
schema: SCHEMA_MANUAL,
|
|
81
82
|
},
|
|
83
|
+
debugOutput: true,
|
|
82
84
|
discoverConfigPath: (args) => {
|
|
83
85
|
if (args[0] === `schema`) {
|
|
84
86
|
return
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createEnv } from "@t3-oss/env-core"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
server: { FLIGHTDECK_SECRET: z.string().optional() },
|
|
6
|
+
clientPrefix: `NEVER`,
|
|
7
|
+
client: {},
|
|
8
|
+
runtimeEnv: import.meta.env,
|
|
9
|
+
emptyStringAsUndefined: true,
|
|
10
|
+
})
|
package/src/flightdeck.lib.ts
CHANGED
|
@@ -9,19 +9,19 @@ import { Future } from "atom.io/internal"
|
|
|
9
9
|
import { fromEntries, toEntries } from "atom.io/json"
|
|
10
10
|
import { ChildSocket } from "atom.io/realtime-server"
|
|
11
11
|
|
|
12
|
+
import { env } from "./flightdeck.env"
|
|
13
|
+
|
|
12
14
|
export type FlightDeckOptions<S extends string = string> = {
|
|
13
|
-
secret: string
|
|
14
15
|
packageName: string
|
|
15
16
|
services: { [service in S]: { run: string; waitFor: boolean } }
|
|
16
17
|
scripts: {
|
|
17
18
|
download: string
|
|
18
19
|
install: string
|
|
19
20
|
}
|
|
21
|
+
port?: number | undefined
|
|
20
22
|
flightdeckRootDir?: string | undefined
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const PORT = process.env.PORT ?? 8080
|
|
24
|
-
const ORIGIN = `http://localhost:${PORT}`
|
|
25
25
|
export class FlightDeck<S extends string = string> {
|
|
26
26
|
protected safety = 0
|
|
27
27
|
|
|
@@ -52,8 +52,10 @@ export class FlightDeck<S extends string = string> {
|
|
|
52
52
|
protected persistentStateDir: string
|
|
53
53
|
|
|
54
54
|
public constructor(public readonly options: FlightDeckOptions<S>) {
|
|
55
|
-
const {
|
|
56
|
-
|
|
55
|
+
const { FLIGHTDECK_SECRET } = env
|
|
56
|
+
const { flightdeckRootDir = resolve(homedir(), `services`) } = options
|
|
57
|
+
const port = options.port ?? 8080
|
|
58
|
+
const origin = `http://localhost:${port}`
|
|
57
59
|
|
|
58
60
|
const servicesEntries = toEntries(options.services)
|
|
59
61
|
this.services = fromEntries(
|
|
@@ -116,96 +118,102 @@ export class FlightDeck<S extends string = string> {
|
|
|
116
118
|
mkdirSync(this.persistentStateDir, { recursive: true })
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!existsSync(installFile)) {
|
|
155
|
-
this.logger.info(
|
|
156
|
-
`Install file does not exist yet. Creating...`,
|
|
121
|
+
if (FLIGHTDECK_SECRET === undefined) {
|
|
122
|
+
this.logger.warn(
|
|
123
|
+
`No FLIGHTDECK_SECRET environment variable found. FlightDeck will not run an update server.`,
|
|
124
|
+
)
|
|
125
|
+
} else {
|
|
126
|
+
createServer((req, res) => {
|
|
127
|
+
let data: Uint8Array[] = []
|
|
128
|
+
req
|
|
129
|
+
.on(`data`, (chunk) => {
|
|
130
|
+
data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk))
|
|
131
|
+
})
|
|
132
|
+
.on(`end`, () => {
|
|
133
|
+
const authHeader = req.headers.authorization
|
|
134
|
+
try {
|
|
135
|
+
if (typeof req.url === `undefined`) throw 400
|
|
136
|
+
const expectedAuthHeader = `Bearer ${FLIGHTDECK_SECRET}`
|
|
137
|
+
if (authHeader !== `Bearer ${FLIGHTDECK_SECRET}`) {
|
|
138
|
+
this.logger.info(
|
|
139
|
+
`Unauthorized: needed \`${expectedAuthHeader}\`, got \`${authHeader}\``,
|
|
140
|
+
)
|
|
141
|
+
throw 401
|
|
142
|
+
}
|
|
143
|
+
const url = new URL(req.url, origin)
|
|
144
|
+
this.logger.info(req.method, url.pathname)
|
|
145
|
+
switch (req.method) {
|
|
146
|
+
case `POST`:
|
|
147
|
+
{
|
|
148
|
+
switch (url.pathname) {
|
|
149
|
+
case `/`:
|
|
150
|
+
{
|
|
151
|
+
res.writeHead(200)
|
|
152
|
+
res.end()
|
|
153
|
+
const installFile = resolve(
|
|
154
|
+
this.persistentStateDir,
|
|
155
|
+
`install`,
|
|
157
156
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.logger.info(`Ready file exists. Removing...`)
|
|
162
|
-
rmSync(readyFile)
|
|
163
|
-
}
|
|
164
|
-
this.getLatestRelease()
|
|
165
|
-
if (
|
|
166
|
-
toEntries(this.servicesReadyToUpdate).every(
|
|
167
|
-
([, isReady]) => isReady,
|
|
157
|
+
const readyFile = resolve(
|
|
158
|
+
this.persistentStateDir,
|
|
159
|
+
`ready`,
|
|
168
160
|
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
161
|
+
if (!existsSync(installFile)) {
|
|
162
|
+
this.logger.info(
|
|
163
|
+
`Install file does not exist yet. Creating...`,
|
|
164
|
+
)
|
|
165
|
+
writeFileSync(installFile, ``)
|
|
166
|
+
}
|
|
167
|
+
if (existsSync(readyFile)) {
|
|
168
|
+
this.logger.info(`Ready file exists. Removing...`)
|
|
169
|
+
rmSync(readyFile)
|
|
170
|
+
}
|
|
171
|
+
this.getLatestRelease()
|
|
172
|
+
if (
|
|
173
|
+
toEntries(this.servicesReadyToUpdate).every(
|
|
174
|
+
([, isReady]) => isReady,
|
|
175
|
+
)
|
|
176
|
+
) {
|
|
177
|
+
this.logger.info(`All services are ready to update!`)
|
|
178
|
+
this.stopAllServices()
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
for (const entry of toEntries(this.services)) {
|
|
182
|
+
const [serviceName, service] = entry
|
|
183
|
+
if (service) {
|
|
184
|
+
if (this.options.services[serviceName].waitFor) {
|
|
185
|
+
service.emit(`updatesReady`)
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
this.startService(serviceName)
|
|
179
189
|
}
|
|
180
|
-
} else {
|
|
181
|
-
this.startService(serviceName)
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
|
-
|
|
185
|
-
break
|
|
192
|
+
break
|
|
186
193
|
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
default:
|
|
195
|
+
throw 404
|
|
196
|
+
}
|
|
189
197
|
}
|
|
190
|
-
|
|
191
|
-
break
|
|
198
|
+
break
|
|
192
199
|
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
default:
|
|
201
|
+
throw 405
|
|
202
|
+
}
|
|
203
|
+
} catch (thrown) {
|
|
204
|
+
this.logger.error(thrown, req.url)
|
|
205
|
+
if (typeof thrown === `number`) {
|
|
206
|
+
res.writeHead(thrown)
|
|
207
|
+
res.end()
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
data = []
|
|
195
211
|
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
} finally {
|
|
203
|
-
data = []
|
|
204
|
-
}
|
|
205
|
-
})
|
|
206
|
-
}).listen(PORT, () => {
|
|
207
|
-
this.logger.info(`Server started on port ${PORT}`)
|
|
208
|
-
})
|
|
212
|
+
})
|
|
213
|
+
}).listen(port, () => {
|
|
214
|
+
this.logger.info(`Server started on port ${port}`)
|
|
215
|
+
})
|
|
216
|
+
}
|
|
209
217
|
|
|
210
218
|
this.startAllServices()
|
|
211
219
|
}
|