@serverkgg/bridge 0.0.21 → 0.1.2

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.
Files changed (119) hide show
  1. package/package.json +3 -1
  2. package/src/bot/botDeploy.test.ts +363 -0
  3. package/src/bot/botDeploy.ts +136 -0
  4. package/src/bot/botEnvironment.test.ts +189 -0
  5. package/src/bot/botEnvironment.ts +141 -0
  6. package/src/bot/botIdentity.test.ts +251 -0
  7. package/src/bot/botIdentity.ts +140 -0
  8. package/src/bot/botPanel.test.ts +246 -0
  9. package/src/bot/botPanel.ts +129 -0
  10. package/src/bot/botState.test.ts +45 -0
  11. package/src/bot/botState.ts +54 -0
  12. package/src/bot/botTerminal.test.ts +79 -0
  13. package/src/bot/botTerminal.ts +62 -0
  14. package/src/bot/botVariables.test.ts +193 -0
  15. package/src/bot/botVariables.ts +73 -0
  16. package/src/bot/detect/botFramework.ts +23 -0
  17. package/src/bot/detect/botFrameworks.ts +198 -0
  18. package/src/bot/detect/botInspectWarning.ts +15 -0
  19. package/src/bot/detect/botPackageManager.ts +6 -0
  20. package/src/bot/detect/botPlatform.ts +10 -0
  21. package/src/bot/detect/botRuntime.ts +15 -0
  22. package/src/bot/detect/botToken.test.ts +71 -0
  23. package/src/bot/detect/botToken.ts +126 -0
  24. package/src/bot/detect/detect.test.ts +543 -0
  25. package/src/bot/detect/detect.ts +842 -0
  26. package/src/bot/detect/detectResult.ts +40 -0
  27. package/src/bot/detect/detectTable.test.ts +72 -0
  28. package/src/bot/detect/detectTable.ts +166 -0
  29. package/src/bot/detect/envExample.test.ts +29 -0
  30. package/src/bot/detect/envExample.ts +48 -0
  31. package/src/bot/detect/envKey.ts +7 -0
  32. package/src/bot/detect/envScan.test.ts +39 -0
  33. package/src/bot/detect/envScan.ts +87 -0
  34. package/src/bot/detect/index.ts +17 -0
  35. package/src/bot/detect/inferBotPlatform.test.ts +60 -0
  36. package/src/bot/detect/inferBotPlatform.ts +28 -0
  37. package/src/bot/detect/keys.test.ts +28 -0
  38. package/src/bot/detect/keys.ts +64 -0
  39. package/src/bot/detect/sensitiveKey.test.ts +91 -0
  40. package/src/bot/detect/sensitiveKey.ts +44 -0
  41. package/src/bot/detect/serverkJson.test.ts +88 -0
  42. package/src/bot/detect/serverkJson.ts +174 -0
  43. package/src/bot/index.ts +8 -0
  44. package/src/cli/detectTable.test.ts +66 -0
  45. package/src/cli/detectTable.ts +43 -0
  46. package/src/cli/main.ts +12 -4
  47. package/src/cli/renderedFile.ts +35 -0
  48. package/src/cli/schema.ts +7 -26
  49. package/src/cli/validate.test.ts +283 -3
  50. package/src/cli/validate.ts +105 -7
  51. package/src/context/bridgeContext.ts +17 -1
  52. package/src/core/bridgeAction.ts +2 -0
  53. package/src/core/bridgeActivity.ts +9 -0
  54. package/src/core/bridgeAvailability.ts +10 -0
  55. package/src/core/bridgePort.test.ts +45 -0
  56. package/src/core/bridgePort.ts +26 -0
  57. package/src/core/bridgeUserError.test.ts +10 -0
  58. package/src/core/bridgeUserError.ts +6 -2
  59. package/src/core/describeDownloadFailure.test.ts +58 -0
  60. package/src/core/describeDownloadFailure.ts +69 -0
  61. package/src/core/index.ts +3 -0
  62. package/src/guides/compile.ts +2 -2
  63. package/src/manifest/index.ts +1 -0
  64. package/src/manifest/kind.ts +5 -0
  65. package/src/manifest/manifest.test.ts +630 -3
  66. package/src/manifest/manifest.ts +391 -87
  67. package/src/manifest/runtime.ts +2 -2
  68. package/src/manifest/source.ts +4 -4
  69. package/src/modules/bridge.ts +8 -1
  70. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  71. package/src/modules/bridgeModule.ts +19 -1
  72. package/src/modules/bridgeStopSignal.ts +13 -0
  73. package/src/modules/index.ts +2 -0
  74. package/src/panel/bridgeDetail.ts +4 -1
  75. package/src/protocol/bridgeActivity.ts +54 -0
  76. package/src/protocol/bridgeConfig.ts +2 -0
  77. package/src/protocol/bridgeEvents.ts +30 -0
  78. package/src/protocol/bridgeFrame.ts +10 -1
  79. package/src/protocol/index.ts +1 -0
  80. package/src/runtime/bridgeActions.ts +19 -2
  81. package/src/runtime/bridgeActivity.test.ts +318 -0
  82. package/src/runtime/bridgeActivity.ts +268 -0
  83. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  84. package/src/runtime/bridgeConfigReader.ts +20 -0
  85. package/src/runtime/bridgeContext.test.ts +247 -0
  86. package/src/runtime/bridgeContext.ts +43 -10
  87. package/src/runtime/bridgeDriver.test.ts +153 -1
  88. package/src/runtime/bridgeDriver.ts +64 -8
  89. package/src/runtime/bridgeFiles.test.ts +62 -0
  90. package/src/runtime/bridgeFiles.ts +12 -2
  91. package/src/runtime/bridgeLiveConfig.ts +18 -0
  92. package/src/runtime/bridgeLog.ts +10 -0
  93. package/src/runtime/bridgeRedaction.test.ts +410 -0
  94. package/src/runtime/bridgeRedaction.ts +214 -0
  95. package/src/runtime/bridgeRefusalError.ts +13 -0
  96. package/src/runtime/bridgeRegistry.test.ts +1056 -10
  97. package/src/runtime/bridgeRegistry.ts +464 -50
  98. package/src/runtime/bridgeScopedContext.ts +55 -0
  99. package/src/runtime/bridgeServer.test.ts +534 -4
  100. package/src/runtime/bridgeServer.ts +145 -38
  101. package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
  102. package/src/runtime/bridgeSetupHandlers.ts +59 -14
  103. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  104. package/src/runtime/bridgeSupervisor.ts +219 -67
  105. package/src/runtime/serve.ts +87 -11
  106. package/src/steam/steamcmd.test.ts +32 -1
  107. package/src/steam/steamcmd.ts +27 -4
  108. package/src/utils/changedSettingKeys.test.ts +65 -0
  109. package/src/utils/changedSettingKeys.ts +6 -0
  110. package/src/utils/describeExecFailure.test.ts +34 -0
  111. package/src/utils/describeExecFailure.ts +9 -0
  112. package/src/utils/index.ts +3 -0
  113. package/src/utils/sameSettingValue.test.ts +35 -0
  114. package/src/utils/sameSettingValue.ts +40 -0
  115. package/src/wire/activity.test.ts +95 -0
  116. package/src/wire/activity.ts +41 -0
  117. package/src/wire/index.ts +1 -0
  118. package/src/wire/panel.test.ts +67 -1
  119. package/src/wire/panel.ts +11 -0
@@ -1,5 +1,5 @@
1
1
  import type { BridgeContext } from "../context";
2
- import type { BridgeValues } from "../core";
2
+ import type { BridgeActivity, BridgeValues } from "../core";
3
3
  import type { BridgeConfig, BridgeSetupRuntime } from "../protocol";
4
4
  import { stripAnsi } from "../utils";
5
5
  import { createBridgeCodecs } from "./bridgeCodec";
@@ -7,7 +7,7 @@ import { configPath } from "./bridgeConfigReader";
7
7
  import { createBridgeEgress } from "./bridgeEgress";
8
8
  import { createBridgeExec } from "./bridgeExec";
9
9
  import { createBridgeFiles } from "./bridgeFiles";
10
- import { createBridgeLiveConfig } from "./bridgeLiveConfig";
10
+ import { type BridgeLiveConfig, createBridgeLiveConfig } from "./bridgeLiveConfig";
11
11
  import type { BridgeLeveledLogger } from "./bridgeLog";
12
12
  import { createBridgeNet } from "./bridgeNet";
13
13
  import { createBridgeProbes } from "./bridgeProbe";
@@ -18,17 +18,18 @@ export interface ContextHooks {
18
18
  onLog: BridgeLeveledLogger;
19
19
  onEmit: (event: string, payload?: BridgeValues) => void;
20
20
  onSetup: (runtime: BridgeSetupRuntime | null) => void;
21
+ onActivity: (entry: BridgeActivity) => void;
22
+ onRestart: () => Promise<void>;
21
23
  }
22
24
 
23
25
  export const createContext = (
24
26
  config: BridgeConfig,
25
27
  supervisor: BridgeSupervisor,
26
28
  hooks: ContextHooks,
29
+ live: BridgeLiveConfig = createBridgeLiveConfig(configPath(), config),
27
30
  ): BridgeContext => {
28
31
  const egress = createBridgeEgress(config);
29
32
 
30
- const live = createBridgeLiveConfig(configPath(), config);
31
-
32
33
  const files = createBridgeFiles({
33
34
  root: config.cwd,
34
35
  uid: config.gameUid,
@@ -37,6 +38,16 @@ export const createContext = (
37
38
  onLog: hooks.onLog,
38
39
  });
39
40
 
41
+ const portOf = (key: string) => {
42
+ const value = config.ports[key];
43
+
44
+ if (value === undefined) {
45
+ throw new Error(`this game declares no port named "${key}"`);
46
+ }
47
+
48
+ return value;
49
+ };
50
+
40
51
  return {
41
52
  server: {
42
53
  code: config.code,
@@ -49,24 +60,36 @@ export const createContext = (
49
60
  get running() {
50
61
  return supervisor.running;
51
62
  },
63
+ get publicAddress() {
64
+ return live.publicAddress();
65
+ },
52
66
  },
53
67
 
54
68
  variable(key) {
55
69
  return live.variable(key);
56
70
  },
57
71
 
72
+ variables() {
73
+ return live.variables();
74
+ },
75
+
58
76
  secret(key) {
59
77
  return live.secret(key);
60
78
  },
61
79
 
62
- port(key) {
63
- const value = config.ports[key];
80
+ secrets() {
81
+ return live.secrets();
82
+ },
64
83
 
65
- if (value === undefined) {
66
- throw new Error(`this game declares no port named "${key}"`);
67
- }
84
+ port(key) {
85
+ return portOf(key);
86
+ },
68
87
 
69
- return value;
88
+ portBlock(key) {
89
+ return {
90
+ first: portOf(key),
91
+ count: config.portCounts?.[key] ?? 1,
92
+ };
70
93
  },
71
94
 
72
95
  files,
@@ -119,10 +142,20 @@ export const createContext = (
119
142
  },
120
143
  },
121
144
 
145
+ activity: {
146
+ report(entry) {
147
+ hooks.onActivity(entry);
148
+ },
149
+ },
150
+
122
151
  emit(event, payload) {
123
152
  hooks.onEmit(event, payload);
124
153
  },
125
154
 
155
+ async restart() {
156
+ await hooks.onRestart();
157
+ },
158
+
126
159
  log: Object.assign(
127
160
  (message: string, fields?: BridgeValues) => {
128
161
  hooks.onLog(message, {
@@ -2,7 +2,13 @@ import { describe, expect, test } from "bun:test";
2
2
  import { BridgeKind, BridgeSetupStepKind, PortJoin, PortProtocol } from "../core";
3
3
  import { GuideOpenTab } from "../guides";
4
4
  import type { ManifestPort, ManifestPortActivation } from "../manifest";
5
- import type { BridgeDriver, BridgePendingModule, BridgeSetupModule } from "../modules";
5
+ import {
6
+ type BridgeDriver,
7
+ BridgeLifecycleSubject,
8
+ type BridgePendingModule,
9
+ type BridgeSetupModule,
10
+ BridgeStopSignal,
11
+ } from "../modules";
6
12
  import {
7
13
  BridgeConfirm,
8
14
  BridgeControl,
@@ -235,6 +241,7 @@ const port = (activeWhen: ManifestPortActivation[]): ManifestPort => ({
235
241
  join: PortJoin.Combined,
236
242
  activeWhen,
237
243
  mirror: false,
244
+ count: 1,
238
245
  });
239
246
 
240
247
  const driverWithTabs = (tabs: number, sections: number): BridgeDriver => ({
@@ -268,6 +275,27 @@ const driverWithTabs = (tabs: number, sections: number): BridgeDriver => ({
268
275
  });
269
276
 
270
277
  describe("panel caps", () => {
278
+ test("rejects a collection row action named after a read action", () => {
279
+ const driver = driverWithTabs(1, 1);
280
+
281
+ driver.modules = {
282
+ ...driver.modules,
283
+ worlds: {
284
+ kind: BridgeKind.Collection,
285
+ list: async () => [],
286
+ actions: {
287
+ preview: async () => {
288
+ return;
289
+ },
290
+ },
291
+ },
292
+ };
293
+
294
+ expect(() => validateDriver(driver)).toThrow(
295
+ 'module "worlds" declares action "preview", which is reserved for reads',
296
+ );
297
+ });
298
+
271
299
  test("takes the tab and section counts the agent mirror accepts", () => {
272
300
  expect(() => validateDriver(driverWithTabs(8, 8))).not.toThrow();
273
301
  });
@@ -1781,3 +1809,127 @@ describe("section placement", () => {
1781
1809
  );
1782
1810
  });
1783
1811
  });
1812
+
1813
+ describe("the lifecycle module", () => {
1814
+ const lifecycleDriver = (lifecycle: Record<string, unknown>): BridgeDriver => ({
1815
+ install: {
1816
+ kind: BridgeKind.Install,
1817
+ run: async () => {
1818
+ return;
1819
+ },
1820
+ },
1821
+ lifecycle: {
1822
+ kind: BridgeKind.Lifecycle,
1823
+ command: async () => [
1824
+ "run",
1825
+ ],
1826
+ ...lifecycle,
1827
+ },
1828
+ });
1829
+
1830
+ test("accepts an environment resolver", () => {
1831
+ expect(() =>
1832
+ validateDriver(
1833
+ lifecycleDriver({
1834
+ environment: async () => ({
1835
+ BOT_TOKEN: "token",
1836
+ }),
1837
+ }),
1838
+ ),
1839
+ ).not.toThrow();
1840
+ });
1841
+
1842
+ test("accepts a lifecycle that declares no environment", () => {
1843
+ expect(() => validateDriver(lifecycleDriver({}))).not.toThrow();
1844
+ });
1845
+
1846
+ test("accepts each stop signal the bridge sends", () => {
1847
+ for (const stopSignal of Object.values(BridgeStopSignal)) {
1848
+ expect(() =>
1849
+ validateDriver(
1850
+ lifecycleDriver({
1851
+ stopSignal,
1852
+ }),
1853
+ ),
1854
+ ).not.toThrow();
1855
+ }
1856
+ });
1857
+
1858
+ test("refuses a stop signal the bridge does not send", () => {
1859
+ expect(() =>
1860
+ validateDriver(
1861
+ lifecycleDriver({
1862
+ stopSignal: "SIGHUP",
1863
+ }),
1864
+ ),
1865
+ ).toThrow('the lifecycle module declares stop signal "SIGHUP" — the bridge sends only SIGTERM or SIGINT');
1866
+ });
1867
+
1868
+ test("accepts a ready timeout from one second to an hour", () => {
1869
+ for (const readyTimeoutSeconds of [
1870
+ 1,
1871
+ 900,
1872
+ 3600,
1873
+ ]) {
1874
+ expect(() =>
1875
+ validateDriver(
1876
+ lifecycleDriver({
1877
+ readyTimeoutSeconds,
1878
+ }),
1879
+ ),
1880
+ ).not.toThrow();
1881
+ }
1882
+ });
1883
+
1884
+ test("refuses a ready timeout outside one second to an hour", () => {
1885
+ for (const readyTimeoutSeconds of [
1886
+ 0,
1887
+ 3601,
1888
+ 1.5,
1889
+ ]) {
1890
+ expect(() =>
1891
+ validateDriver(
1892
+ lifecycleDriver({
1893
+ readyTimeoutSeconds,
1894
+ }),
1895
+ ),
1896
+ ).toThrow(
1897
+ `the lifecycle module declares a ready timeout of ${readyTimeoutSeconds} seconds — it must be a whole number from 1 to 3600`,
1898
+ );
1899
+ }
1900
+ });
1901
+
1902
+ test("accepts each lifecycle subject the bridge names", () => {
1903
+ for (const subject of Object.values(BridgeLifecycleSubject)) {
1904
+ expect(() =>
1905
+ validateDriver(
1906
+ lifecycleDriver({
1907
+ subject,
1908
+ }),
1909
+ ),
1910
+ ).not.toThrow();
1911
+ }
1912
+ });
1913
+
1914
+ test("refuses a lifecycle subject the bridge does not name", () => {
1915
+ expect(() =>
1916
+ validateDriver(
1917
+ lifecycleDriver({
1918
+ subject: "server",
1919
+ }),
1920
+ ),
1921
+ ).toThrow('the lifecycle module declares subject "server" — the bridge names only game or bot');
1922
+ });
1923
+
1924
+ test("refuses an environment that is not a function", () => {
1925
+ expect(() =>
1926
+ validateDriver(
1927
+ lifecycleDriver({
1928
+ environment: {
1929
+ BOT_TOKEN: "token",
1930
+ },
1931
+ }),
1932
+ ),
1933
+ ).toThrow("the lifecycle module declares an environment that is not a function");
1934
+ });
1935
+ });
@@ -3,11 +3,19 @@ import {
3
3
  BRIDGE_MODULE_ID_PATTERN,
4
4
  BridgeKind,
5
5
  BridgeSetupStepKind,
6
+ isReadAction,
6
7
  type LocalizedText,
7
8
  } from "../core";
8
9
  import { GuideOpenTab } from "../guides";
9
10
  import type { ManifestPort } from "../manifest";
10
- import type { BridgeDriver, BridgePanelModule } from "../modules";
11
+ import {
12
+ type BridgeDriver,
13
+ BridgeLifecycleSubject,
14
+ type BridgePanelModule,
15
+ BridgeStopSignal,
16
+ isBridgeLifecycleSubject,
17
+ isBridgeStopSignal,
18
+ } from "../modules";
11
19
  import {
12
20
  BridgeControl,
13
21
  type BridgeField,
@@ -50,6 +58,10 @@ import { actionsOf } from "./bridgeActions";
50
58
 
51
59
  const UPLOAD_MODES = new Set<string>(Object.values(BridgeUploadMode));
52
60
 
61
+ const READY_TIMEOUT_MIN_SECONDS = 1;
62
+
63
+ const READY_TIMEOUT_MAX_SECONDS = 3600;
64
+
53
65
  const RESERVED_MODULE_IDS = new Set([
54
66
  "announce",
55
67
  "backup",
@@ -591,6 +603,47 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
591
603
  }
592
604
  };
593
605
 
606
+ const validateLifecycle = (driver: BridgeDriver) => {
607
+ const environment: unknown = driver.lifecycle.environment;
608
+
609
+ if (environment !== undefined && typeof environment !== "function") {
610
+ fail(
611
+ "the lifecycle module declares an environment that is not a function — it must resolve the variables the process starts with",
612
+ );
613
+ }
614
+
615
+ const stopSignal: unknown = driver.lifecycle.stopSignal;
616
+
617
+ if (stopSignal !== undefined && !isBridgeStopSignal(stopSignal)) {
618
+ fail(
619
+ `the lifecycle module declares stop signal "${String(stopSignal)}" — the bridge sends only ${Object.values(BridgeStopSignal).join(" or ")}`,
620
+ );
621
+ }
622
+
623
+ const readyTimeoutSeconds: unknown = driver.lifecycle.readyTimeoutSeconds;
624
+
625
+ if (
626
+ readyTimeoutSeconds !== undefined
627
+ && !(
628
+ Number.isInteger(readyTimeoutSeconds)
629
+ && (readyTimeoutSeconds as number) >= READY_TIMEOUT_MIN_SECONDS
630
+ && (readyTimeoutSeconds as number) <= READY_TIMEOUT_MAX_SECONDS
631
+ )
632
+ ) {
633
+ fail(
634
+ `the lifecycle module declares a ready timeout of ${String(readyTimeoutSeconds)} seconds — it must be a whole number from ${READY_TIMEOUT_MIN_SECONDS} to ${READY_TIMEOUT_MAX_SECONDS}`,
635
+ );
636
+ }
637
+
638
+ const subject: unknown = driver.lifecycle.subject;
639
+
640
+ if (subject !== undefined && !isBridgeLifecycleSubject(subject)) {
641
+ fail(
642
+ `the lifecycle module declares subject "${String(subject)}" — the bridge names only ${Object.values(BridgeLifecycleSubject).join(" or ")}`,
643
+ );
644
+ }
645
+ };
646
+
594
647
  const validatePending = (driver: BridgeDriver) => {
595
648
  const pending = driver.pending;
596
649
 
@@ -732,6 +785,7 @@ export const validateDriver = (driver: BridgeDriver) => {
732
785
 
733
786
  countIn(emitted.size, BRIDGE_EVENTS_LIMIT, "event names", "the driver");
734
787
 
788
+ validateLifecycle(driver);
735
789
  validatePending(driver);
736
790
  validateSetup(driver);
737
791
 
@@ -748,15 +802,17 @@ export const validateDriver = (driver: BridgeDriver) => {
748
802
  countIn((module.dependsOn ?? []).length, BRIDGE_DEPENDS_ON_LIMIT, "dependencies", `module "${id}"`);
749
803
  }
750
804
 
751
- if (module.kind === BridgeKind.Actions) {
752
- for (const action of Object.keys(module.actions)) {
753
- validateModuleId(action, `module "${id}" action`);
754
- }
755
- }
756
-
757
- if (module.kind === BridgeKind.Collection || module.kind === BridgeKind.Detail) {
805
+ if (
806
+ module.kind === BridgeKind.Actions
807
+ || module.kind === BridgeKind.Collection
808
+ || module.kind === BridgeKind.Detail
809
+ ) {
758
810
  for (const action of Object.keys(module.actions ?? {})) {
759
811
  validateModuleId(action, `module "${id}" action`);
812
+
813
+ if (isReadAction(action)) {
814
+ fail(`module "${id}" declares action "${action}", which is reserved for reads — pick another id`);
815
+ }
760
816
  }
761
817
  }
762
818
  }
@@ -564,6 +564,68 @@ describe("extract", () => {
564
564
  expect(await read("config/beta.json")).toBe("beta");
565
565
  });
566
566
 
567
+ test("keeps dotfiles in a tree when asked and never writes .git or .serverk", async () => {
568
+ const path = await archive([
569
+ {
570
+ name: "bot/.env.example",
571
+ content: "DISCORD_TOKEN=",
572
+ },
573
+ {
574
+ name: "bot/src/.config/settings.json",
575
+ content: "{}",
576
+ },
577
+ {
578
+ name: "bot/index.js",
579
+ content: "run()",
580
+ },
581
+ {
582
+ name: "bot/.git/config",
583
+ content: "[core]",
584
+ },
585
+ {
586
+ name: "bot/.serverk/deploy-requested",
587
+ content: "{}",
588
+ },
589
+ ]);
590
+
591
+ const written = await files.extract(path, "", {
592
+ tree: true,
593
+ dotfiles: true,
594
+ select: "bot/",
595
+ });
596
+
597
+ expect(written).toEqual([
598
+ ".env.example",
599
+ "src/.config/settings.json",
600
+ "index.js",
601
+ ]);
602
+
603
+ expect(await read(".env.example")).toBe("DISCORD_TOKEN=");
604
+ expect(await files.exists(".git/config")).toBe(false);
605
+ expect(await files.exists(".serverk/deploy-requested")).toBe(false);
606
+ });
607
+
608
+ test("still skips dotfiles in a tree unless asked", async () => {
609
+ const path = await archive([
610
+ {
611
+ name: ".env.example",
612
+ content: "DISCORD_TOKEN=",
613
+ },
614
+ {
615
+ name: "index.js",
616
+ content: "run()",
617
+ },
618
+ ]);
619
+
620
+ const written = await files.extract(path, "", {
621
+ tree: true,
622
+ });
623
+
624
+ expect(written).toEqual([
625
+ "index.js",
626
+ ]);
627
+ });
628
+
567
629
  test("takes a single named entry when the select carries no trailing slash", async () => {
568
630
  const path = await archive([
569
631
  {
@@ -152,6 +152,15 @@ const compileSelect = (select: string): ArchiveSelect => {
152
152
  return select.endsWith("/") ? prefixSelect(select) : entrySelect(select);
153
153
  };
154
154
 
155
+ const ALWAYS_SKIPPED_SEGMENTS: ReadonlySet<string> = new Set([
156
+ ".git",
157
+ ".serverk",
158
+ ]);
159
+
160
+ const hiddenEntry = (segments: string[], dotfiles: boolean) => {
161
+ return segments.some((segment) => ALWAYS_SKIPPED_SEGMENTS.has(segment) || (!dotfiles && segment.startsWith(".")));
162
+ };
163
+
155
164
  const compileSelects = (select: string | undefined, selects: string[] | undefined) => {
156
165
  const compiled: ArchiveSelect[] = [];
157
166
 
@@ -603,6 +612,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
603
612
  }
604
613
 
605
614
  const tree = extractOptions.tree === true;
615
+ const dotfiles = extractOptions.dotfiles === true;
606
616
  const allowed = extractOptions.extensions?.map((extension) => extension.toLowerCase());
607
617
  const selects = compileSelects(extractOptions.select, extractOptions.selects);
608
618
  const excluded = compileGlobs(extractOptions.exclude ?? []);
@@ -638,7 +648,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
638
648
  selected.split("/").at(-1) ?? "",
639
649
  ];
640
650
  const name = segments.at(-1) ?? "";
641
- if (!name || segments.some((segment) => segment.startsWith("."))) {
651
+ if (!name || hiddenEntry(segments, dotfiles)) {
642
652
  continue;
643
653
  }
644
654
  if (allowed !== undefined && !allowed.includes(name.split(".").at(-1)?.toLowerCase() ?? "")) {
@@ -677,7 +687,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
677
687
  const name = segments.at(-1) ?? "";
678
688
  const extension = name.split(".").at(-1)?.toLowerCase() ?? "";
679
689
 
680
- if (name.length === 0 || segments.some((segment) => segment.startsWith("."))) {
690
+ if (name.length === 0 || hiddenEntry(segments, dotfiles)) {
681
691
  continue;
682
692
  }
683
693
 
@@ -14,9 +14,13 @@ export const BRIDGE_CONFIG_REFRESH_MS = 2000;
14
14
  */
15
15
  export interface BridgeLiveConfig {
16
16
  variable: (key: string) => string | null;
17
+ variables: () => Readonly<Record<string, string>>;
17
18
  secret: (key: string) => string | null;
19
+ secrets: () => Readonly<Record<string, string>>;
18
20
  memoryMb: () => number;
19
21
  cpuPercent: () => number;
22
+ publicAddress: () => string | null;
23
+ reload: () => void;
20
24
  }
21
25
 
22
26
  export const createBridgeLiveConfig = (
@@ -50,14 +54,28 @@ export const createBridgeLiveConfig = (
50
54
  variable(key) {
51
55
  return refresh().variables[key] ?? null;
52
56
  },
57
+ variables() {
58
+ return refresh().variables;
59
+ },
53
60
  secret(key) {
54
61
  return refresh().secrets[key] ?? null;
55
62
  },
63
+ secrets() {
64
+ return refresh().secrets;
65
+ },
56
66
  memoryMb() {
57
67
  return refresh().memoryMb;
58
68
  },
59
69
  cpuPercent() {
60
70
  return refresh().cpuPercent;
61
71
  },
72
+ publicAddress() {
73
+ return refresh().publicAddress ?? null;
74
+ },
75
+ reload() {
76
+ checkedAt = null;
77
+
78
+ refresh();
79
+ },
62
80
  };
63
81
  };
@@ -36,3 +36,13 @@ export const log: BridgeLeveledLogger = Object.assign(
36
36
  export const errorText = (error: unknown) => {
37
37
  return error instanceof Error ? error.message : String(error);
38
38
  };
39
+
40
+ export const errorDetail = (error: unknown) => {
41
+ const text = errorText(error);
42
+
43
+ if (!(error instanceof Error) || error.cause === undefined) {
44
+ return text;
45
+ }
46
+
47
+ return `${text} — ${errorText(error.cause)}`;
48
+ };