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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "type": "object",
3
3
  "properties": {
4
- "secret": {
5
- "type": "string"
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",
@@ -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 { secret, flightdeckRootDir = resolve(homedir(), `services`) } = options;
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
- createServer((req, res) => {
87
- let data = [];
88
- req.on(`data`, (chunk) => {
89
- data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
90
- }).on(`end`, () => {
91
- const authHeader = req.headers.authorization;
92
- try {
93
- if (typeof req.url === `undefined`)
94
- throw 400;
95
- const expectedAuthHeader = `Bearer ${secret}`;
96
- if (authHeader !== `Bearer ${secret}`) {
97
- this.logger.info(`Unauthorized: needed \`${expectedAuthHeader}\`, got \`${authHeader}\``);
98
- throw 401;
99
- }
100
- const url = new URL(req.url, ORIGIN);
101
- this.logger.info(req.method, url.pathname);
102
- switch (req.method) {
103
- case `POST`:
104
- {
105
- switch (url.pathname) {
106
- case `/`:
107
- {
108
- res.writeHead(200);
109
- res.end();
110
- const installFile = resolve(this.persistentStateDir, `install`);
111
- const readyFile = resolve(this.persistentStateDir, `ready`);
112
- if (!existsSync(installFile)) {
113
- this.logger.info(`Install file does not exist yet. Creating...`);
114
- writeFileSync(installFile, ``);
115
- }
116
- if (existsSync(readyFile)) {
117
- this.logger.info(`Ready file exists. Removing...`);
118
- rmSync(readyFile);
119
- }
120
- this.getLatestRelease();
121
- if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
122
- this.logger.info(`All services are ready to update!`);
123
- this.stopAllServices();
124
- return;
125
- }
126
- for (const entry of toEntries(this.services)) {
127
- const [serviceName, service] = entry;
128
- if (service) {
129
- if (this.options.services[serviceName].waitFor) {
130
- service.emit(`updatesReady`);
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
- break;
138
- default:
139
- throw 404;
216
+ break;
217
+ default:
218
+ throw 404;
219
+ }
140
220
  }
141
- }
142
- break;
143
- default:
144
- throw 405;
145
- }
146
- } catch (thrown) {
147
- this.logger.error(thrown, req.url);
148
- if (typeof thrown === `number`) {
149
- res.writeHead(thrown);
150
- res.end();
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
- } finally {
153
- data = [];
154
- }
234
+ });
235
+ }).listen(port, () => {
236
+ this.logger.info(`Server started on port ${port}`);
155
237
  });
156
- }).listen(PORT, () => {
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: z.object({
300
- secret: z.string(),
301
- packageName: z.string(),
302
- services: z.record(z.object({ run: z.string(), waitFor: z.boolean() })),
303
- flightdeckRootDir: z.string(),
304
- scripts: z.object({
305
- download: z.string(),
306
- install: z.string()
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
- secret: {
311
- flag: `x`,
312
- required: true,
313
- description: `Secret used to authenticate with the service.`,
314
- example: `--secret=\"secret\"`
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: `p`,
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: z.object({
346
- outdir: z.string().optional()
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
- "secret": {
5
- "type": "string"
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 { secret, flightdeckRootDir = resolve(homedir(), `services`) } = options;
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
- createServer((req, res) => {
91
- let data = [];
92
- req.on(`data`, (chunk) => {
93
- data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
94
- }).on(`end`, () => {
95
- const authHeader = req.headers.authorization;
96
- try {
97
- if (typeof req.url === `undefined`)
98
- throw 400;
99
- const expectedAuthHeader = `Bearer ${secret}`;
100
- if (authHeader !== `Bearer ${secret}`) {
101
- this.logger.info(`Unauthorized: needed \`${expectedAuthHeader}\`, got \`${authHeader}\``);
102
- throw 401;
103
- }
104
- const url = new URL(req.url, ORIGIN);
105
- this.logger.info(req.method, url.pathname);
106
- switch (req.method) {
107
- case `POST`:
108
- {
109
- switch (url.pathname) {
110
- case `/`:
111
- {
112
- res.writeHead(200);
113
- res.end();
114
- const installFile = resolve(this.persistentStateDir, `install`);
115
- const readyFile = resolve(this.persistentStateDir, `ready`);
116
- if (!existsSync(installFile)) {
117
- this.logger.info(`Install file does not exist yet. Creating...`);
118
- writeFileSync(installFile, ``);
119
- }
120
- if (existsSync(readyFile)) {
121
- this.logger.info(`Ready file exists. Removing...`);
122
- rmSync(readyFile);
123
- }
124
- this.getLatestRelease();
125
- if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
126
- this.logger.info(`All services are ready to update!`);
127
- this.stopAllServices();
128
- return;
129
- }
130
- for (const entry of toEntries(this.services)) {
131
- const [serviceName, service] = entry;
132
- if (service) {
133
- if (this.options.services[serviceName].waitFor) {
134
- service.emit(`updatesReady`);
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
- break;
142
- default:
143
- throw 404;
220
+ break;
221
+ default:
222
+ throw 404;
223
+ }
144
224
  }
145
- }
146
- break;
147
- default:
148
- throw 405;
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
- } catch (thrown) {
151
- this.logger.error(thrown, req.url);
152
- if (typeof thrown === `number`) {
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
- }).listen(PORT, () => {
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.14",
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
- "atom.io": "0.30.0",
26
- "comline": "0.1.3"
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.30",
32
+ "bun-types": "1.1.31",
32
33
  "concurrently": "9.0.1",
33
34
  "rimraf": "6.0.1",
34
35
  "tmp": "0.2.3",
@@ -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, parseArrayOption } from "comline"
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
- secret: z.string(),
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
- secret: {
25
- flag: `x`,
26
- required: true,
27
- description: `Secret used to authenticate with the service.`,
28
- example: `--secret=\"secret\"`,
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: `p`,
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
+ })
@@ -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 { secret, flightdeckRootDir = resolve(homedir(), `services`) } =
56
- options
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
- createServer((req, res) => {
120
- let data: Uint8Array[] = []
121
- req
122
- .on(`data`, (chunk) => {
123
- data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk))
124
- })
125
- .on(`end`, () => {
126
- const authHeader = req.headers.authorization
127
- try {
128
- if (typeof req.url === `undefined`) throw 400
129
- const expectedAuthHeader = `Bearer ${secret}`
130
- if (authHeader !== `Bearer ${secret}`) {
131
- this.logger.info(
132
- `Unauthorized: needed \`${expectedAuthHeader}\`, got \`${authHeader}\``,
133
- )
134
- throw 401
135
- }
136
- const url = new URL(req.url, ORIGIN)
137
- this.logger.info(req.method, url.pathname)
138
- switch (req.method) {
139
- case `POST`:
140
- {
141
- switch (url.pathname) {
142
- case `/`:
143
- {
144
- res.writeHead(200)
145
- res.end()
146
- const installFile = resolve(
147
- this.persistentStateDir,
148
- `install`,
149
- )
150
- const readyFile = resolve(
151
- this.persistentStateDir,
152
- `ready`,
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
- writeFileSync(installFile, ``)
159
- }
160
- if (existsSync(readyFile)) {
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
- this.logger.info(`All services are ready to update!`)
171
- this.stopAllServices()
172
- return
173
- }
174
- for (const entry of toEntries(this.services)) {
175
- const [serviceName, service] = entry
176
- if (service) {
177
- if (this.options.services[serviceName].waitFor) {
178
- service.emit(`updatesReady`)
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
- default:
188
- throw 404
194
+ default:
195
+ throw 404
196
+ }
189
197
  }
190
- }
191
- break
198
+ break
192
199
 
193
- default:
194
- throw 405
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
- } catch (thrown) {
197
- this.logger.error(thrown, req.url)
198
- if (typeof thrown === `number`) {
199
- res.writeHead(thrown)
200
- res.end()
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
  }