flightdeck 0.0.1 → 0.0.3

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/lib.js CHANGED
@@ -1,24 +1,39 @@
1
- // src/flightdeck.ts
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, {
5
+ get: all[name],
6
+ enumerable: true,
7
+ configurable: true,
8
+ set: (newValue) => all[name] = () => newValue
9
+ });
10
+ };
11
+
12
+ // src/flightdeck.lib.ts
2
13
  import {execSync, spawn} from "node:child_process";
3
14
  import {existsSync, mkdirSync, renameSync, rmSync} from "node:fs";
4
- import {createServer} from "node:http2";
15
+ import {createServer} from "node:http";
5
16
  import {homedir} from "node:os";
6
17
  import {resolve} from "node:path";
7
18
  import {Future} from "atom.io/internal";
19
+ import {fromEntries, toEntries} from "atom.io/json";
8
20
  import {ChildSocket} from "atom.io/realtime-server";
9
- var safety = 0;
10
21
  var PORT = process.env.PORT ?? 8080;
11
22
  var ORIGIN = `http://localhost:${PORT}`;
12
23
 
13
24
  class FlightDeck {
14
25
  options;
15
- get serviceName() {
16
- return `${this.options.repo}/${this.options.app}`;
17
- }
26
+ safety = 0;
18
27
  webhookServer;
19
- service = null;
28
+ services;
29
+ serviceIdx;
30
+ defaultServicesReadyToUpdate;
31
+ servicesReadyToUpdate;
32
+ servicesShouldRestart;
20
33
  restartTimes = [];
21
- alive = new Future(() => {
34
+ servicesLive;
35
+ servicesDead;
36
+ live = new Future(() => {
22
37
  });
23
38
  dead = new Future(() => {
24
39
  });
@@ -27,13 +42,25 @@ class FlightDeck {
27
42
  backupServiceDir;
28
43
  constructor(options) {
29
44
  this.options = options;
30
- const {
31
- secret,
32
- serviceDir = resolve(homedir(), `services`, `sample/repo`, `my-app`, `current`)
33
- } = options;
34
- this.currentServiceDir = resolve(serviceDir, `current`);
35
- this.backupServiceDir = resolve(serviceDir, `backup`);
36
- this.updateServiceDir = resolve(serviceDir, `update`);
45
+ const { secret, flightdeckRootDir = resolve(homedir(), `services`) } = options;
46
+ const servicesEntries = toEntries(options.services);
47
+ this.services = fromEntries(servicesEntries.map(([serviceName]) => [serviceName, null]));
48
+ this.serviceIdx = fromEntries(servicesEntries.map(([serviceName], idx) => [serviceName, idx]));
49
+ this.defaultServicesReadyToUpdate = fromEntries(servicesEntries.map(([serviceName, { waitFor }]) => [
50
+ serviceName,
51
+ !waitFor
52
+ ]));
53
+ this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
54
+ this.servicesShouldRestart = true;
55
+ this.servicesLive = servicesEntries.map(() => new Future(() => {
56
+ }));
57
+ this.servicesDead = servicesEntries.map(() => new Future(() => {
58
+ }));
59
+ this.live.use(Promise.all(this.servicesLive));
60
+ this.dead.use(Promise.all(this.servicesDead));
61
+ this.currentServiceDir = resolve(flightdeckRootDir, options.packageName, `current`);
62
+ this.backupServiceDir = resolve(flightdeckRootDir, options.packageName, `backup`);
63
+ this.updateServiceDir = resolve(flightdeckRootDir, options.packageName, `update`);
37
64
  createServer((req, res) => {
38
65
  let data = [];
39
66
  req.on(`data`, (chunk) => {
@@ -42,6 +69,8 @@ class FlightDeck {
42
69
  console.log(req.headers);
43
70
  const authHeader = req.headers.authorization;
44
71
  try {
72
+ if (typeof req.url === `undefined`)
73
+ throw 400;
45
74
  if (authHeader !== `Bearer ${secret}`)
46
75
  throw 401;
47
76
  const url = new URL(req.url, ORIGIN);
@@ -56,11 +85,20 @@ class FlightDeck {
56
85
  res.writeHead(200);
57
86
  res.end();
58
87
  this.fetchLatestRelease();
59
- if (this.service) {
60
- this.service.emit(`updatesReady`);
61
- } else {
62
- this.applyUpdate();
63
- this.startService();
88
+ if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
89
+ console.log(`All services are ready to update!`);
90
+ this.stopAllServices();
91
+ return;
92
+ }
93
+ for (const entry of toEntries(this.services)) {
94
+ const [serviceName, service] = entry;
95
+ if (service) {
96
+ if (this.options.services[serviceName].waitFor) {
97
+ service.emit(`updatesReady`);
98
+ }
99
+ } else {
100
+ this.startService(serviceName);
101
+ }
64
102
  }
65
103
  }
66
104
  break;
@@ -85,68 +123,89 @@ class FlightDeck {
85
123
  }).listen(PORT, () => {
86
124
  console.log(`Server started on port ${PORT}`);
87
125
  });
88
- this.startService();
126
+ this.startAllServices();
127
+ }
128
+ startAllServices() {
129
+ console.log(`Starting all services...`);
130
+ for (const [serviceName] of toEntries(this.services)) {
131
+ this.startService(serviceName);
132
+ }
89
133
  }
90
- startService() {
91
- safety++;
92
- if (safety > 10) {
93
- throw new Error(`safety exceeded`);
134
+ startService(serviceName) {
135
+ console.log(`Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`);
136
+ if (this.safety > 2) {
137
+ throw new Error(`Out of tries...`);
94
138
  }
139
+ this.safety++;
95
140
  if (!existsSync(this.currentServiceDir)) {
96
- console.log(`Tried to start service but failed: Service ${this.serviceName} is not yet installed.`);
141
+ console.log(`Tried to start service but failed: Service ${this.options.packageName} is not yet installed.`);
97
142
  this.fetchLatestRelease();
98
143
  this.applyUpdate();
99
- this.startService();
144
+ this.startService(serviceName);
100
145
  return;
101
146
  }
102
- const [executable, ...args] = this.options.runCmd;
147
+ const [executable, ...args] = this.options.services[serviceName].run;
103
148
  const program = executable.startsWith(`./`) ? resolve(this.currentServiceDir, executable) : executable;
104
149
  const serviceProcess = spawn(program, args, {
105
150
  cwd: this.currentServiceDir,
106
151
  env: import.meta.env
107
152
  });
108
- this.service = new ChildSocket(serviceProcess, this.serviceName, console);
109
- this.service.onAny((...messages) => {
110
- console.log(`\uD83D\uDEF0 `, ...messages);
153
+ this.services[serviceName] = new ChildSocket(serviceProcess, `${this.options.packageName}::${serviceName}`, console);
154
+ this.services[serviceName].onAny((...messages) => {
155
+ console.log(`${this.options.packageName}::${serviceName} \uD83D\uDCAC`, ...messages);
111
156
  });
112
- this.service.on(`readyToUpdate`, () => {
113
- this.stopService();
157
+ this.services[serviceName].on(`readyToUpdate`, () => {
158
+ console.log(`Service ${this.options.packageName}::${serviceName} is ready to update.`);
159
+ this.servicesReadyToUpdate[serviceName] = true;
160
+ if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
161
+ console.log(`All services are ready to update!`);
162
+ this.stopAllServices();
163
+ }
114
164
  });
115
- this.service.on(`alive`, () => {
116
- this.alive.use(Promise.resolve());
117
- this.dead = new Future(() => {
165
+ this.services[serviceName].on(`alive`, () => {
166
+ this.servicesLive[this.serviceIdx[serviceName]].use(Promise.resolve());
167
+ this.servicesDead[this.serviceIdx[serviceName]] = new Future(() => {
118
168
  });
169
+ if (this.dead.done) {
170
+ this.dead = new Future(() => {
171
+ });
172
+ }
173
+ this.dead.use(Promise.all(this.servicesDead));
119
174
  });
120
- this.service.process.on(`close`, (exitCode) => {
121
- console.log(`Service ${this.serviceName} exited with code ${exitCode}`);
122
- this.service = null;
175
+ this.services[serviceName].process.on(`close`, (exitCode) => {
176
+ console.log(`${this.options.packageName}::${serviceName} exited with code ${exitCode}`);
177
+ this.services[serviceName] = null;
178
+ if (!this.servicesShouldRestart) {
179
+ console.log(`Service ${this.options.packageName}::${serviceName} will not be restarted.`);
180
+ return;
181
+ }
123
182
  const updatesAreReady = existsSync(this.updateServiceDir);
124
183
  if (updatesAreReady) {
125
- console.log(`Updates are ready; applying and restarting...`);
184
+ console.log(`${this.options.packageName}::${serviceName} will be updated before startup...`);
126
185
  this.restartTimes = [];
127
186
  this.applyUpdate();
128
- this.startService();
187
+ this.startService(serviceName);
129
188
  } else {
130
- if (exitCode !== 0) {
131
- const now = Date.now();
132
- const fiveMinutesAgo = now - 5 * 60 * 1000;
133
- this.restartTimes = this.restartTimes.filter((time) => time > fiveMinutesAgo);
134
- this.restartTimes.push(now);
135
- if (this.restartTimes.length < 5) {
136
- console.log(`Service ${this.serviceName} crashed. Restarting...`);
137
- this.startService();
138
- } else {
139
- console.log(`Service ${this.serviceName} crashed too many times. Not restarting.`);
140
- }
189
+ const now = Date.now();
190
+ const fiveMinutesAgo = now - 5 * 60 * 1000;
191
+ this.restartTimes = this.restartTimes.filter((time) => time > fiveMinutesAgo);
192
+ this.restartTimes.push(now);
193
+ if (this.restartTimes.length < 5) {
194
+ console.log(`Service ${this.options.packageName}::${serviceName} crashed. Restarting...`);
195
+ this.startService(serviceName);
196
+ } else {
197
+ console.log(`Service ${this.options.packageName}::${serviceName} crashed too many times. Not restarting.`);
141
198
  }
142
199
  }
143
200
  });
201
+ this.safety = 0;
144
202
  }
145
203
  applyUpdate() {
146
- console.log(`Installing latest version of service ${this.serviceName}...`);
204
+ console.log(`Installing latest version of service ${this.options.packageName}...`);
147
205
  if (existsSync(this.updateServiceDir)) {
148
- if (this.service) {
149
- console.log(`Tried to apply update but failed: Service ${this.serviceName} is currently running.`);
206
+ const runningServices = toEntries(this.services).filter(([, service]) => service);
207
+ if (runningServices.length > 0) {
208
+ console.log(`Tried to apply update to ${this.options.packageName} but failed. The following services are currently running: [${runningServices.map(([serviceName]) => serviceName).join(`, `)}]`);
150
209
  return;
151
210
  }
152
211
  if (existsSync(this.currentServiceDir)) {
@@ -159,14 +218,15 @@ class FlightDeck {
159
218
  }
160
219
  renameSync(this.updateServiceDir, this.currentServiceDir);
161
220
  this.restartTimes = [];
221
+ this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
162
222
  } else {
163
- console.log(`Service ${this.serviceName} is already up to date.`);
223
+ console.log(`Service ${this.options.packageName} is already up to date.`);
164
224
  }
165
225
  }
166
226
  fetchLatestRelease() {
167
- console.log(`Downloading latest version of service ${this.serviceName}...`);
227
+ console.log(`Downloading latest version of service ${this.options.packageName}...`);
168
228
  try {
169
- execSync(this.options.updateCmd.join(` `));
229
+ execSync(this.options.downloadPackageToUpdatesCmd.join(` `));
170
230
  } catch (thrown) {
171
231
  if (thrown instanceof Error) {
172
232
  console.error(`Failed to fetch the latest release: ${thrown.message}`);
@@ -174,19 +234,73 @@ class FlightDeck {
174
234
  return;
175
235
  }
176
236
  }
177
- stopService() {
178
- if (this.service) {
179
- console.log(`Stopping service ${this.serviceName}...`);
180
- this.service.process.kill();
181
- this.service = null;
182
- this.dead.use(Promise.resolve());
183
- this.alive = new Future(() => {
237
+ stopAllServices() {
238
+ console.log(`Stopping all services...`);
239
+ for (const [serviceName] of toEntries(this.services)) {
240
+ this.stopService(serviceName);
241
+ }
242
+ }
243
+ stopService(serviceName) {
244
+ if (this.services[serviceName]) {
245
+ console.log(`Stopping service ${this.options.packageName}::${serviceName}...`);
246
+ this.services[serviceName].process.kill();
247
+ this.services[serviceName] = null;
248
+ this.servicesDead[this.serviceIdx[serviceName]].use(Promise.resolve());
249
+ this.servicesLive[this.serviceIdx[serviceName]] = new Future(() => {
184
250
  });
251
+ if (this.live.done) {
252
+ this.live = new Future(() => {
253
+ });
254
+ }
255
+ this.live.use(Promise.all(this.servicesLive));
185
256
  } else {
186
- console.error(`Failed to stop service ${this.serviceName}: Service is not running.`);
257
+ console.error(`Failed to stop service ${this.options.packageName}::${serviceName}: Service is not running.`);
258
+ }
259
+ }
260
+ shutdown() {
261
+ this.servicesShouldRestart = false;
262
+ this.stopAllServices();
263
+ }
264
+ }
265
+ // src/klaxon.lib.ts
266
+ var exports_klaxon_lib = {};
267
+ __export(exports_klaxon_lib, {
268
+ scramble: () => scramble,
269
+ alert: () => alert
270
+ });
271
+ async function alert({
272
+ secret,
273
+ endpoint
274
+ }) {
275
+ const response = await fetch(endpoint, {
276
+ method: `POST`,
277
+ headers: {
278
+ "Content-Type": `application/json`,
279
+ Authorization: `Bearer ${secret}`
280
+ }
281
+ });
282
+ return response;
283
+ }
284
+ async function scramble({
285
+ packageConfig,
286
+ secretsConfig,
287
+ publishedPackages
288
+ }) {
289
+ const alertResults = [];
290
+ for (const publishedPackage of publishedPackages) {
291
+ if (publishedPackage.name in packageConfig) {
292
+ const name = publishedPackage.name;
293
+ const { endpoint } = packageConfig[name];
294
+ const secret = secretsConfig[name];
295
+ const alertResultPromise = alert({ secret, endpoint }).then((alertResult) => [name, alertResult]);
296
+ alertResults.push(alertResultPromise);
187
297
  }
188
298
  }
299
+ const alertResultsResolved = await Promise.all(alertResults);
300
+ const scrambleResult = Object.fromEntries(alertResultsResolved);
301
+ return scrambleResult;
189
302
  }
190
303
  export {
304
+ exports_klaxon_lib as Klaxon,
191
305
  FlightDeck
192
306
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flightdeck",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Jeremy Banka",
@@ -17,29 +17,31 @@
17
17
  "main": "dist/lib.js",
18
18
  "types": "dist/lib.d.ts",
19
19
  "bin": {
20
- "flightdeck": "./dist/bin.js"
20
+ "flightdeck": "./dist/flightdeck.bin.js",
21
+ "klaxon": "./dist/klaxon.bin.js"
21
22
  },
22
23
  "dependencies": {
23
24
  "zod": "3.23.8",
24
- "atom.io": "0.29.0",
25
- "comline": "0.1.0"
25
+ "atom.io": "0.29.2",
26
+ "comline": "0.1.1"
26
27
  },
27
28
  "devDependencies": {
28
- "@types/bun": "1.1.8",
29
+ "@types/bun": "1.1.9",
29
30
  "@types/node": "20.16.5",
30
31
  "@types/tmp": "0.2.6",
31
- "concurrently": "8.2.2",
32
+ "concurrently": "9.0.1",
32
33
  "tmp": "0.2.3",
33
34
  "tsup": "8.2.4",
34
35
  "rimraf": "6.0.1",
35
- "vitest": "2.0.5"
36
+ "vitest": "2.1.1"
36
37
  },
37
38
  "scripts": {
38
- "build": "rimraf dist && concurrently \"bun:build:*\" && bun run schema",
39
+ "build": "rimraf dist && concurrently \"bun:build:*\" && concurrently \"bun:schema:*\"",
40
+ "build:bin:flightdeck": "bun build --outdir dist --target node --external flightdeck --external atom.io --external comline --external zod -- src/flightdeck.bin.ts",
41
+ "build:bin:klaxon": "bun build --outdir dist --target node --external flightdeck --external atom.io --external comline --external zod -- src/klaxon.bin.ts",
39
42
  "build:lib": "bun build --outdir dist --target node --external flightdeck --external atom.io --external comline --external zod -- src/lib.ts ",
40
- "build:bin": "bun build --outdir dist --target node --external flightdeck --external atom.io --external comline --external zod -- src/bin.ts",
41
43
  "build:dts": "tsup",
42
- "schema": "bun ./src/bin.ts --outdir=dist -- schema",
44
+ "schema:flightdeck": "bun ./src/flightdeck.bin.ts --outdir=dist -- schema",
43
45
  "lint:biome": "biome check -- .",
44
46
  "lint:eslint": "eslint --flag unstable_ts_config -- .",
45
47
  "lint:types": "tsc --noEmit",
@@ -4,56 +4,52 @@ import * as path from "node:path"
4
4
 
5
5
  import type { OptionsGroup } from "comline"
6
6
  import { cli, optional, parseArrayOption } from "comline"
7
- import type { FlightDeckOptions } from "flightdeck"
8
- import { FlightDeck } from "flightdeck"
9
7
  import { z } from "zod"
10
8
 
9
+ import type { FlightDeckOptions } from "~/packages/flightdeck/src/flightdeck.lib"
10
+ import { FlightDeck } from "~/packages/flightdeck/src/flightdeck.lib"
11
+
11
12
  const FLIGHTDECK_MANUAL = {
12
13
  optionsSchema: z.object({
13
14
  secret: z.string(),
14
- repo: z.string(),
15
- app: z.string(),
16
- runCmd: z.array(z.string()),
17
- serviceDir: z.string(),
18
- updateCmd: z.array(z.string()),
15
+ packageName: z.string(),
16
+ services: z.record(
17
+ z.object({ run: z.array(z.string()), waitFor: z.boolean() }),
18
+ ),
19
+ flightDeckRootDir: z.string(),
20
+ downloadPackageToUpdatesCmd: z.array(z.string()),
19
21
  }),
20
22
  options: {
21
23
  secret: {
22
- flag: `s`,
24
+ flag: `x`,
23
25
  required: true,
24
26
  description: `Secret used to authenticate with the service.`,
25
27
  example: `--secret=\"secret\"`,
26
28
  },
27
- repo: {
28
- flag: `r`,
29
- required: true,
30
- description: `Name of the repository.`,
31
- example: `--repo=\"sample/repo\"`,
32
- },
33
- app: {
34
- flag: `a`,
29
+ packageName: {
30
+ flag: `p`,
35
31
  required: true,
36
- description: `Name of the application.`,
37
- example: `--app=\"my-app\"`,
32
+ description: `Name of the package.`,
33
+ example: `--packageName=\"my-app\"`,
38
34
  },
39
- runCmd: {
40
- flag: `r`,
35
+ services: {
36
+ flag: `s`,
41
37
  required: true,
42
- description: `Command to run the application.`,
43
- example: `--runCmd=\"./app\"`,
44
- parse: parseArrayOption,
38
+ description: `Map of service names to executables.`,
39
+ example: `--services="{\\"frontend\\":{\\"run\\":[\\"./app\\"],\\"waitFor\\":false},\\"backend\\":{\\"run\\":[\\"./backend\\"],\\"waitFor\\":true}}"`,
40
+ parse: JSON.parse,
45
41
  },
46
- serviceDir: {
42
+ flightdeckRootDir: {
47
43
  flag: `d`,
48
44
  required: true,
49
45
  description: `Directory where the service is stored.`,
50
- example: `--serviceDir=\"./services/sample/repo/my-app/current\"`,
46
+ example: `--flightdeckRootDir=\"./services/sample/repo/my-app/current\"`,
51
47
  },
52
- updateCmd: {
48
+ downloadPackageToUpdatesCmd: {
53
49
  flag: `u`,
54
50
  required: true,
55
51
  description: `Command to update the service.`,
56
- example: `--updateCmd=\"./app\"`,
52
+ example: `--downloadPackageToUpdatesCmd=\"./app\"`,
57
53
  parse: parseArrayOption,
58
54
  },
59
55
  },
@@ -106,7 +102,7 @@ switch (inputs.case) {
106
102
  default: {
107
103
  const flightDeck = new FlightDeck(inputs.opts)
108
104
  process.on(`close`, async () => {
109
- flightDeck.stopService()
105
+ flightDeck.stopAllServices()
110
106
  await flightDeck.dead
111
107
  })
112
108
  }