flightdeck 0.1.3 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flightdeck",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Jeremy Banka",
@@ -22,19 +22,20 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@t3-oss/env-core": "0.11.1",
25
+ "cron": "3.2.0",
25
26
  "zod": "3.23.8",
26
27
  "atom.io": "0.30.2",
27
28
  "comline": "0.1.6"
28
29
  },
29
30
  "devDependencies": {
30
- "@types/node": "22.7.9",
31
+ "@types/node": "22.9.0",
31
32
  "@types/tmp": "0.2.6",
32
- "bun-types": "1.1.33",
33
- "concurrently": "9.0.1",
33
+ "bun-types": "1.1.34",
34
+ "concurrently": "9.1.0",
34
35
  "rimraf": "6.0.1",
35
36
  "tmp": "0.2.3",
36
- "tsup": "8.3.0",
37
- "vitest": "2.1.3"
37
+ "tsup": "8.3.5",
38
+ "vitest": "2.1.4"
38
39
  },
39
40
  "scripts": {
40
41
  "build": "rimraf dist && concurrently \"bun:build:*\" && concurrently \"bun:schema:*\"",
@@ -0,0 +1,67 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ rmSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs"
10
+ import { resolve } from "node:path"
11
+
12
+ export type FilesystemStorageOptions = {
13
+ path: string
14
+ }
15
+
16
+ export class FilesystemStorage<
17
+ T extends Record<string, string> = Record<string, string>,
18
+ > implements Storage
19
+ {
20
+ public rootDir: string
21
+
22
+ public constructor(options: FilesystemStorageOptions) {
23
+ this.rootDir = options.path
24
+ if (!existsSync(this.rootDir)) {
25
+ mkdirSync(this.rootDir, { recursive: true })
26
+ }
27
+ }
28
+
29
+ public getItem<K extends string & keyof T>(key: K): T[K] | null {
30
+ const filePath = resolve(this.rootDir, key)
31
+ if (existsSync(filePath)) {
32
+ return readFileSync(filePath, `utf-8`) as T[K]
33
+ }
34
+ return null
35
+ }
36
+
37
+ public setItem<K extends string & keyof T>(key: K, value: T[K]): void {
38
+ const filePath = resolve(this.rootDir, key)
39
+ writeFileSync(filePath, value)
40
+ }
41
+
42
+ public removeItem<K extends string & keyof T>(key: K): void {
43
+ const filePath = resolve(this.rootDir, key)
44
+ if (existsSync(filePath)) {
45
+ rmSync(filePath)
46
+ }
47
+ }
48
+
49
+ public key(index: number): (string & keyof T) | null {
50
+ const filePaths = readdirSync(this.rootDir)
51
+ const filePathsByDateCreated = filePaths.sort((a, b) => {
52
+ const aStat = statSync(a)
53
+ const bStat = statSync(b)
54
+ return bStat.ctimeMs - aStat.ctimeMs
55
+ })
56
+ return (filePathsByDateCreated[index] as string & keyof T) ?? null
57
+ }
58
+
59
+ public clear(): void {
60
+ rmSync(this.rootDir, { recursive: true })
61
+ mkdirSync(this.rootDir, { recursive: true })
62
+ }
63
+
64
+ public get length(): number {
65
+ return readdirSync(this.rootDir).length
66
+ }
67
+ }
@@ -18,6 +18,7 @@ const FLIGHTDECK_MANUAL = {
18
18
  scripts: z.object({
19
19
  download: z.string(),
20
20
  install: z.string(),
21
+ checkAvailability: z.string(),
21
22
  }),
22
23
  }),
23
24
  options: {
@@ -105,8 +106,7 @@ switch (inputs.case) {
105
106
  default: {
106
107
  const flightDeck = new FlightDeck(inputs.opts)
107
108
  process.on(`close`, async () => {
108
- flightDeck.stopAllServices()
109
- await flightDeck.dead
109
+ await flightDeck.stopAllServices()
110
110
  })
111
111
  }
112
112
  }
@@ -1,22 +1,39 @@
1
1
  import { execSync, spawn } from "node:child_process"
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
3
2
  import type { Server } from "node:http"
4
3
  import { createServer } from "node:http"
5
4
  import { homedir } from "node:os"
6
5
  import { resolve } from "node:path"
7
6
 
8
7
  import { Future } from "atom.io/internal"
8
+ import { discoverType } from "atom.io/introspection"
9
9
  import { fromEntries, toEntries } from "atom.io/json"
10
10
  import { ChildSocket } from "atom.io/realtime-server"
11
+ import { CronJob } from "cron"
11
12
 
13
+ import { FilesystemStorage } from "./filesystem-storage"
12
14
  import { env } from "./flightdeck.env"
13
15
 
16
+ export const FLIGHTDECK_SETUP_PHASES = [`downloaded`, `installed`] as const
17
+
18
+ export type FlightDeckSetupPhase = (typeof FLIGHTDECK_SETUP_PHASES)[number]
19
+
20
+ export const FLIGHTDECK_UPDATE_PHASES = [`notified`, `confirmed`] as const
21
+
22
+ export type FlightDeckUpdatePhase = (typeof FLIGHTDECK_UPDATE_PHASES)[number]
23
+
24
+ export function isVersionNumber(version: string): boolean {
25
+ return (
26
+ /^\d+\.\d+\.\d+$/.test(version) || !Number.isNaN(Number.parseFloat(version))
27
+ )
28
+ }
29
+
14
30
  export type FlightDeckOptions<S extends string = string> = {
15
31
  packageName: string
16
32
  services: { [service in S]: { run: string; waitFor: boolean } }
17
33
  scripts: {
18
34
  download: string
19
35
  install: string
36
+ checkAvailability?: string
20
37
  }
21
38
  port?: number | undefined
22
39
  flightdeckRootDir?: string | undefined
@@ -25,23 +42,30 @@ export type FlightDeckOptions<S extends string = string> = {
25
42
  export class FlightDeck<S extends string = string> {
26
43
  protected safety = 0
27
44
 
45
+ protected storage: FilesystemStorage<{
46
+ setupPhase: FlightDeckSetupPhase
47
+ updatePhase: FlightDeckUpdatePhase
48
+ updateAwaitedVersion: string
49
+ }>
28
50
  protected webhookServer: Server
29
51
  protected services: {
30
52
  [service in S]: ChildSocket<
31
- { updatesReady: [] },
53
+ { timeToStop: []; updatesReady: [] },
32
54
  { readyToUpdate: []; alive: [] }
33
55
  > | null
34
56
  }
35
57
  protected serviceIdx: { readonly [service in S]: number }
36
58
  public defaultServicesReadyToUpdate: { readonly [service in S]: boolean }
37
59
  public servicesReadyToUpdate: { [service in S]: boolean }
38
- public servicesShouldRestart: boolean
60
+ public autoRespawnDeadServices: boolean
39
61
 
40
62
  protected logger: Pick<Console, `error` | `info` | `warn`>
41
63
  protected serviceLoggers: {
42
64
  readonly [service in S]: Pick<Console, `error` | `info` | `warn`>
43
65
  }
44
66
 
67
+ protected updateAvailabilityChecker: CronJob | null = null
68
+
45
69
  public servicesLive: Future<void>[]
46
70
  public servicesDead: Future<void>[]
47
71
  public live = new Future(() => {})
@@ -49,11 +73,9 @@ export class FlightDeck<S extends string = string> {
49
73
 
50
74
  protected restartTimes: number[] = []
51
75
 
52
- protected persistentStateDir: string
53
-
54
76
  public constructor(public readonly options: FlightDeckOptions<S>) {
55
77
  const { FLIGHTDECK_SECRET } = env
56
- const { flightdeckRootDir = resolve(homedir(), `services`) } = options
78
+ const { flightdeckRootDir = resolve(homedir(), `.flightdeck`) } = options
57
79
  const port = options.port ?? 8080
58
80
  const origin = `http://localhost:${port}`
59
81
 
@@ -71,7 +93,7 @@ export class FlightDeck<S extends string = string> {
71
93
  ]),
72
94
  )
73
95
  this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate }
74
- this.servicesShouldRestart = true
96
+ this.autoRespawnDeadServices = true
75
97
 
76
98
  this.logger = {
77
99
  info: (...args: any[]) => {
@@ -109,14 +131,9 @@ export class FlightDeck<S extends string = string> {
109
131
  this.live.use(Promise.all(this.servicesLive))
110
132
  this.dead.use(Promise.all(this.servicesDead))
111
133
 
112
- this.persistentStateDir = resolve(
113
- flightdeckRootDir,
114
- `.state`,
115
- options.packageName,
116
- )
117
- if (!existsSync(this.persistentStateDir)) {
118
- mkdirSync(this.persistentStateDir, { recursive: true })
119
- }
134
+ this.storage = new FilesystemStorage({
135
+ path: resolve(flightdeckRootDir, `storage`, options.packageName),
136
+ })
120
137
 
121
138
  if (FLIGHTDECK_SECRET === undefined) {
122
139
  this.logger.warn(
@@ -142,63 +159,34 @@ export class FlightDeck<S extends string = string> {
142
159
  }
143
160
  const url = new URL(req.url, origin)
144
161
  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`,
156
- )
157
- const readyFile = resolve(
158
- this.persistentStateDir,
159
- `ready`,
160
- )
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)
189
- }
190
- }
191
- }
192
- break
193
-
194
- default:
195
- throw 404
196
- }
197
- }
198
- break
199
-
200
- default:
201
- throw 405
162
+
163
+ const versionForeignInput = Buffer.concat(data).toString()
164
+ if (!isVersionNumber(versionForeignInput)) {
165
+ throw 400
166
+ }
167
+
168
+ res.writeHead(200)
169
+ res.end()
170
+
171
+ this.storage.setItem(`updatePhase`, `notified`)
172
+ this.storage.setItem(`updateAwaitedVersion`, versionForeignInput)
173
+ const { checkAvailability } = options.scripts
174
+ if (checkAvailability) {
175
+ this.updateAvailabilityChecker?.stop()
176
+ this.seekUpdate(versionForeignInput)
177
+ const updatePhase = this.storage.getItem(`updatePhase`)
178
+ this.logger.info(`> storage("updatePhase") >`, updatePhase)
179
+ if (updatePhase === `notified`) {
180
+ this.updateAvailabilityChecker = new CronJob(
181
+ `30 * * * * *`,
182
+ () => {
183
+ this.seekUpdate(versionForeignInput)
184
+ },
185
+ )
186
+ this.updateAvailabilityChecker.start()
187
+ }
188
+ } else {
189
+ this.downloadPackage()
202
190
  }
203
191
  } catch (thrown) {
204
192
  this.logger.error(thrown, req.url)
@@ -216,12 +204,101 @@ export class FlightDeck<S extends string = string> {
216
204
  }
217
205
 
218
206
  this.startAllServices()
207
+ .then(() => {
208
+ this.logger.info(`All services started.`)
209
+ })
210
+ .catch((thrown) => {
211
+ if (thrown instanceof Error) {
212
+ this.logger.error(`Failed to start all services:`, thrown.message)
213
+ }
214
+ })
215
+ }
216
+
217
+ protected seekUpdate(version: string): void {
218
+ this.logger.info(`Checking for updates...`)
219
+ const { checkAvailability } = this.options.scripts
220
+ if (!checkAvailability) {
221
+ this.logger.info(`No checkAvailability script found.`)
222
+ return
223
+ }
224
+ try {
225
+ const out = execSync(`${checkAvailability} ${version}`)
226
+ this.logger.info(`Check stdout:`, out.toString())
227
+ this.updateAvailabilityChecker?.stop()
228
+ this.storage.setItem(`updatePhase`, `confirmed`)
229
+ this.downloadPackage()
230
+ this.announceUpdate()
231
+ } catch (thrown) {
232
+ if (thrown instanceof Error) {
233
+ this.logger.error(`Check failed:`, thrown.message)
234
+ } else {
235
+ const thrownType = discoverType(thrown)
236
+ this.logger.error(`Check threw`, thrownType, thrown)
237
+ }
238
+ }
239
+ }
240
+
241
+ protected announceUpdate(): void {
242
+ for (const entry of toEntries(this.services)) {
243
+ const [serviceName, service] = entry
244
+ if (service) {
245
+ if (this.options.services[serviceName].waitFor) {
246
+ service.emit(`updatesReady`)
247
+ }
248
+ } else {
249
+ this.startService(serviceName)
250
+ }
251
+ }
219
252
  }
220
253
 
221
- protected startAllServices(): void {
254
+ protected tryUpdate(): void {
255
+ if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
256
+ this.logger.info(`All services are ready to update.`)
257
+ this.stopAllServices()
258
+ .then(() => {
259
+ this.logger.info(`All services stopped; starting up fresh...`)
260
+ this.startAllServices()
261
+ .then(() => {
262
+ this.logger.info(`All services started; we're back online.`)
263
+ })
264
+ .catch((thrown) => {
265
+ if (thrown instanceof Error) {
266
+ this.logger.error(
267
+ `Failed to start all services:`,
268
+ thrown.message,
269
+ )
270
+ }
271
+ })
272
+ })
273
+ .catch((thrown) => {
274
+ if (thrown instanceof Error) {
275
+ this.logger.error(`Failed to stop all services:`, thrown.message)
276
+ }
277
+ })
278
+ }
279
+ }
280
+
281
+ protected startAllServices(): Future<unknown> {
222
282
  this.logger.info(`Starting all services...`)
223
- for (const [serviceName] of toEntries(this.services)) {
224
- this.startService(serviceName)
283
+ this.autoRespawnDeadServices = true
284
+ const setupPhase = this.storage.getItem(`setupPhase`)
285
+ this.logger.info(`> storage("setupPhase") >`, setupPhase)
286
+ switch (setupPhase) {
287
+ case null:
288
+ this.logger.info(`Starting from scratch.`)
289
+ this.downloadPackage()
290
+ this.installPackage()
291
+ return this.startAllServices()
292
+ case `downloaded`:
293
+ this.logger.info(`Found package downloaded but not installed.`)
294
+ this.installPackage()
295
+ return this.startAllServices()
296
+ case `installed`: {
297
+ for (const [serviceName] of toEntries(this.services)) {
298
+ this.startService(serviceName)
299
+ }
300
+ return this.live
301
+ }
225
302
  }
226
303
  }
227
304
 
@@ -233,17 +310,6 @@ export class FlightDeck<S extends string = string> {
233
310
  throw new Error(`Out of tries...`)
234
311
  }
235
312
  this.safety++
236
- const readyFile = resolve(this.persistentStateDir, `ready`)
237
- if (!existsSync(readyFile)) {
238
- this.logger.info(
239
- `Tried to start service but failed: could not find readyFile: ${readyFile}`,
240
- )
241
- this.getLatestRelease()
242
- this.applyUpdate()
243
- this.startService(serviceName)
244
-
245
- return
246
- }
247
313
 
248
314
  const [exe, ...args] = this.options.services[serviceName].run.split(` `)
249
315
  const serviceProcess = spawn(exe, args, {
@@ -256,17 +322,12 @@ export class FlightDeck<S extends string = string> {
256
322
  console,
257
323
  )
258
324
  this.services[serviceName].onAny((...messages) => {
259
- this.logger.info(`💬`, ...messages)
325
+ this.serviceLoggers[serviceName].info(`💬`, ...messages)
260
326
  })
261
327
  this.services[serviceName].on(`readyToUpdate`, () => {
262
- this.serviceLoggers[serviceName].info(`Ready to update.`)
328
+ this.logger.info(`Service "${serviceName}" is ready to update.`)
263
329
  this.servicesReadyToUpdate[serviceName] = true
264
- if (
265
- toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)
266
- ) {
267
- this.logger.info(`All services are ready to update.`)
268
- this.stopAllServices()
269
- }
330
+ this.tryUpdate()
270
331
  })
271
332
  this.services[serviceName].on(`alive`, () => {
272
333
  this.servicesLive[this.serviceIdx[serviceName]].use(Promise.resolve())
@@ -276,19 +337,22 @@ export class FlightDeck<S extends string = string> {
276
337
  }
277
338
  this.dead.use(Promise.all(this.servicesDead))
278
339
  })
279
- this.services[serviceName].process.on(`close`, (exitCode) => {
280
- this.serviceLoggers[serviceName].info(`Exited with code ${exitCode}`)
340
+ this.services[serviceName].process.once(`close`, (exitCode) => {
341
+ this.logger.info(
342
+ `Auto-respawn saw "${serviceName}" exit with code ${exitCode}`,
343
+ )
281
344
  this.services[serviceName] = null
282
- if (!this.servicesShouldRestart) {
283
- this.serviceLoggers[serviceName].info(`Will not be restarted.`)
345
+ if (!this.autoRespawnDeadServices) {
346
+ this.logger.info(`Auto-respawn is off; "${serviceName}" rests.`)
284
347
  return
285
348
  }
286
- const installFile = resolve(this.persistentStateDir, `install`)
287
- const updatesAreReady = existsSync(installFile)
349
+ const updatePhase = this.storage.getItem(`updatePhase`)
350
+ this.logger.info(`> storage("updatePhase") >`, updatePhase)
351
+ const updatesAreReady = updatePhase === `confirmed`
288
352
  if (updatesAreReady) {
289
353
  this.serviceLoggers[serviceName].info(`Updating before startup...`)
290
354
  this.restartTimes = []
291
- this.applyUpdate()
355
+ this.installPackage()
292
356
  this.startService(serviceName)
293
357
  } else {
294
358
  const now = Date.now()
@@ -311,18 +375,13 @@ export class FlightDeck<S extends string = string> {
311
375
  this.safety = 0
312
376
  }
313
377
 
314
- protected applyUpdate(): void {
315
- this.logger.info(`Installing...`)
316
-
378
+ protected downloadPackage(): void {
379
+ this.logger.info(`Downloading...`)
317
380
  try {
318
- execSync(this.options.scripts.install)
319
- const installFile = resolve(this.persistentStateDir, `install`)
320
- if (existsSync(installFile)) {
321
- rmSync(installFile)
322
- }
323
- const readyFile = resolve(this.persistentStateDir, `ready`)
324
- writeFileSync(readyFile, ``)
325
- this.logger.info(`Installed!`)
381
+ const out = execSync(this.options.scripts.download)
382
+ this.logger.info(`Download stdout:`, out.toString())
383
+ this.storage.setItem(`setupPhase`, `downloaded`)
384
+ this.logger.info(`Downloaded!`)
326
385
  } catch (thrown) {
327
386
  if (thrown instanceof Error) {
328
387
  this.logger.error(`Failed to get the latest release: ${thrown.message}`)
@@ -331,12 +390,14 @@ export class FlightDeck<S extends string = string> {
331
390
  }
332
391
  }
333
392
 
334
- protected getLatestRelease(): void {
335
- this.logger.info(`Downloading...`)
393
+ protected installPackage(): void {
394
+ this.logger.info(`Installing...`)
336
395
 
337
396
  try {
338
- execSync(this.options.scripts.download)
339
- this.logger.info(`Downloaded!`)
397
+ const out = execSync(this.options.scripts.install)
398
+ this.logger.info(`Install stdout:`, out.toString())
399
+ this.storage.setItem(`setupPhase`, `installed`)
400
+ this.logger.info(`Installed!`)
340
401
  } catch (thrown) {
341
402
  if (thrown instanceof Error) {
342
403
  this.logger.error(`Failed to get the latest release: ${thrown.message}`)
@@ -345,19 +406,32 @@ export class FlightDeck<S extends string = string> {
345
406
  }
346
407
  }
347
408
 
348
- public stopAllServices(): void {
349
- this.logger.info(`Stopping all services...`)
409
+ public stopAllServices(): Future<unknown> {
410
+ this.logger.info(`Stopping all services... auto-respawn disabled.`)
411
+ this.autoRespawnDeadServices = false
350
412
  for (const [serviceName] of toEntries(this.services)) {
351
413
  this.stopService(serviceName)
352
414
  }
415
+ return this.dead
353
416
  }
354
417
 
355
418
  public stopService(serviceName: S): void {
356
- if (this.services[serviceName]) {
357
- this.serviceLoggers[serviceName].info(`Stopping service...`)
358
- this.services[serviceName].process.kill(`SIGINT`)
359
- this.services[serviceName] = null
360
- this.servicesDead[this.serviceIdx[serviceName]].use(Promise.resolve())
419
+ const service = this.services[serviceName]
420
+ if (service) {
421
+ this.logger.info(`Stopping service "${serviceName}"...`)
422
+ this.servicesDead[this.serviceIdx[serviceName]].use(
423
+ new Promise((pass) => {
424
+ service.emit(`timeToStop`)
425
+ service.process.once(`close`, (exitCode) => {
426
+ this.logger.info(
427
+ `Stopped service "${serviceName}"; exited with code ${exitCode}`,
428
+ )
429
+ this.services[serviceName] = null
430
+ pass()
431
+ })
432
+ }),
433
+ )
434
+ this.dead.use(Promise.all(this.servicesDead))
361
435
  this.servicesLive[this.serviceIdx[serviceName]] = new Future(() => {})
362
436
  if (this.live.done) {
363
437
  this.live = new Future(() => {})
@@ -369,10 +443,4 @@ export class FlightDeck<S extends string = string> {
369
443
  )
370
444
  }
371
445
  }
372
-
373
- public shutdown(): void {
374
- this.logger.info(`Shutting down...`)
375
- this.servicesShouldRestart = false
376
- this.stopAllServices()
377
- }
378
446
  }
package/src/klaxon.lib.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  export type AlertOptions = {
2
2
  secret: string
3
3
  endpoint: string
4
+ version: string
4
5
  }
5
6
 
6
7
  export async function alert({
7
8
  secret,
8
9
  endpoint,
10
+ version,
9
11
  }: AlertOptions): Promise<Response> {
10
12
  const response = await fetch(endpoint, {
11
13
  method: `POST`,
12
14
  headers: {
13
- "Content-Type": `application/json`,
15
+ "Content-Type": `text/plain;charset=UTF-8`,
14
16
  Authorization: `Bearer ${secret}`,
15
17
  },
18
+ body: version,
16
19
  })
17
20
 
18
21
  return response
@@ -64,7 +67,8 @@ export async function scramble<K extends string = string>({
64
67
  const name = publishedPackage.name as K
65
68
  const { endpoint } = packageConfig[name]
66
69
  const secret = secretsConfig[name]
67
- const alertResultPromise = alert({ secret, endpoint }).then(
70
+ const version = publishedPackage.version
71
+ const alertResultPromise = alert({ secret, endpoint, version }).then(
68
72
  (alertResult) => [name, alertResult] as const,
69
73
  )
70
74
  alertResults.push(alertResultPromise)
package/src/lib.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./filesystem-storage"
1
2
  export * from "./flightdeck.lib"
2
3
  import * as Klaxon from "./klaxon.lib"
3
4