@fastagent-sh/fastagent 0.29.0 → 0.30.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.
Files changed (310) hide show
  1. package/README.md +9 -4
  2. package/dist/atomic-write.d.ts +4 -5
  3. package/dist/atomic-write.js +4 -5
  4. package/dist/authoring.d.ts +32 -15
  5. package/dist/authoring.js +147 -127
  6. package/dist/channels/agentcore-protocol.d.ts +8 -3
  7. package/dist/channels/agentcore-protocol.js +3 -1
  8. package/dist/channels/agentcore-service.d.ts +1 -0
  9. package/dist/channels/agentcore-service.js +14 -4
  10. package/dist/channels/agentcore.d.ts +9 -0
  11. package/dist/channels/agentcore.js +29 -4
  12. package/dist/channels/context.d.ts +2 -0
  13. package/dist/channels/context.js +5 -1
  14. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -3
  16. package/dist/channels/feishu/feishu.js +10 -10
  17. package/dist/channels/feishu/register-app.js +3 -5
  18. package/dist/channels/feishu/scaffold/channel.ts +32 -34
  19. package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
  20. package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
  21. package/dist/channels/feishu/setup-mode.d.ts +2 -12
  22. package/dist/channels/feishu/setup-mode.js +2 -22
  23. package/dist/channels/feishu/setup.d.ts +12 -0
  24. package/dist/channels/feishu/setup.js +197 -0
  25. package/dist/channels/kit/abortable.d.ts +8 -0
  26. package/dist/channels/kit/abortable.js +22 -0
  27. package/dist/channels/lark/lark.d.ts +0 -3
  28. package/dist/channels/lark/lark.js +0 -5
  29. package/dist/channels/lark/scaffold/channel.ts +33 -35
  30. package/dist/channels/lark/scaffold/lark-send.ts +5 -5
  31. package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
  32. package/dist/channels/lark/setup.d.ts +9 -0
  33. package/dist/channels/lark/setup.js +57 -0
  34. package/dist/channels/setup.d.ts +112 -0
  35. package/dist/channels/setup.js +31 -0
  36. package/dist/channels/slack/history.js +1 -1
  37. package/dist/channels/slack/manifest.d.ts +17 -7
  38. package/dist/channels/slack/manifest.js +12 -16
  39. package/dist/channels/slack/scaffold/channel.ts +44 -30
  40. package/dist/channels/slack/scaffold/slack-send.ts +8 -8
  41. package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
  42. package/dist/channels/slack/setup.d.ts +7 -0
  43. package/dist/channels/slack/setup.js +119 -0
  44. package/dist/channels/slack/slack-api.d.ts +2 -0
  45. package/dist/channels/slack/slack-api.js +4 -0
  46. package/dist/channels/slack/slack.d.ts +21 -2
  47. package/dist/channels/slack/slack.js +398 -338
  48. package/dist/channels/slack/socket.d.ts +57 -0
  49. package/dist/channels/slack/socket.js +305 -0
  50. package/dist/channels/telegram/polling.d.ts +27 -0
  51. package/dist/channels/telegram/polling.js +105 -0
  52. package/dist/channels/telegram/scaffold/channel.ts +52 -32
  53. package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
  54. package/dist/channels/telegram/setup.d.ts +2 -0
  55. package/dist/channels/telegram/setup.js +42 -0
  56. package/dist/channels/telegram/telegram-api.d.ts +27 -1
  57. package/dist/channels/telegram/telegram-api.js +7 -6
  58. package/dist/channels/telegram/telegram.d.ts +11 -3
  59. package/dist/channels/telegram/telegram.js +184 -157
  60. package/dist/channels/webhook-evidence.d.ts +11 -0
  61. package/dist/channels/webhook-evidence.js +71 -0
  62. package/dist/channels/webhooks.d.ts +37 -0
  63. package/dist/channels/webhooks.js +70 -0
  64. package/dist/child-env.d.ts +16 -0
  65. package/dist/child-env.js +30 -0
  66. package/dist/cli/box-login.js +4 -1
  67. package/dist/cli/channel-app.d.ts +3 -0
  68. package/dist/cli/channel-app.js +35 -0
  69. package/dist/cli/channel-setup.d.ts +38 -0
  70. package/dist/cli/channel-setup.js +195 -0
  71. package/dist/cli/commands/add.d.ts +6 -14
  72. package/dist/cli/commands/add.js +45 -303
  73. package/dist/cli/commands/auth.js +8 -2
  74. package/dist/cli/commands/build.d.ts +5 -0
  75. package/dist/cli/commands/build.js +16 -0
  76. package/dist/cli/commands/channels.d.ts +19 -0
  77. package/dist/cli/commands/channels.js +230 -0
  78. package/dist/cli/commands/content.d.ts +7 -3
  79. package/dist/cli/commands/content.js +16 -10
  80. package/dist/cli/commands/deploy/agentcore.js +55 -23
  81. package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
  82. package/dist/cli/commands/deploy/channel-apps.js +30 -0
  83. package/dist/cli/commands/deploy/docker.js +32 -65
  84. package/dist/cli/commands/deploy/fly.js +28 -12
  85. package/dist/cli/commands/deploy/railway.js +38 -13
  86. package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
  87. package/dist/cli/commands/deploy/secrets-step.js +66 -0
  88. package/dist/cli/commands/deploy/shared.d.ts +16 -5
  89. package/dist/cli/commands/deploy/shared.js +25 -8
  90. package/dist/cli/commands/deploy.js +53 -60
  91. package/dist/cli/commands/dev.d.ts +2 -0
  92. package/dist/cli/commands/dev.js +15 -3
  93. package/dist/cli/commands/env.js +1 -1
  94. package/dist/cli/commands/info.d.ts +2 -0
  95. package/dist/cli/commands/info.js +60 -25
  96. package/dist/cli/commands/init.js +3 -1
  97. package/dist/cli/commands/invoke.d.ts +2 -0
  98. package/dist/cli/commands/invoke.js +20 -7
  99. package/dist/cli/commands/login.d.ts +0 -7
  100. package/dist/cli/commands/login.js +22 -10
  101. package/dist/cli/commands/logout.js +1 -1
  102. package/dist/cli/commands/mcp.js +14 -6
  103. package/dist/cli/commands/models.js +3 -2
  104. package/dist/cli/commands/schedules.d.ts +4 -2
  105. package/dist/cli/commands/schedules.js +14 -10
  106. package/dist/cli/commands/secrets.d.ts +14 -0
  107. package/dist/cli/commands/secrets.js +213 -0
  108. package/dist/cli/commands/start.d.ts +14 -14
  109. package/dist/cli/commands/start.js +46 -60
  110. package/dist/cli/content-view.d.ts +4 -6
  111. package/dist/cli/content-view.js +5 -11
  112. package/dist/cli/first-run-channels.d.ts +3 -0
  113. package/dist/cli/first-run-channels.js +63 -0
  114. package/dist/cli/instance.d.ts +41 -0
  115. package/dist/cli/instance.js +155 -0
  116. package/dist/cli/invoke-stream.d.ts +5 -2
  117. package/dist/cli/invoke-stream.js +7 -2
  118. package/dist/cli/program.js +282 -110
  119. package/dist/cli/serve.d.ts +4 -5
  120. package/dist/cli/serve.js +4 -7
  121. package/dist/cli/shared.d.ts +11 -2
  122. package/dist/cli/shared.js +32 -19
  123. package/dist/cli/ticket.js +50 -6
  124. package/dist/content/declare.d.ts +31 -37
  125. package/dist/content/declare.js +59 -60
  126. package/dist/content/file.d.ts +23 -12
  127. package/dist/content/file.js +48 -35
  128. package/dist/content/git.d.ts +0 -10
  129. package/dist/content/git.js +5 -39
  130. package/dist/content/resolve.d.ts +18 -22
  131. package/dist/content/resolve.js +32 -66
  132. package/dist/content/source.d.ts +7 -8
  133. package/dist/content/source.js +10 -28
  134. package/dist/credentials-page.d.ts +26 -0
  135. package/dist/credentials-page.js +185 -0
  136. package/dist/declared-secrets.d.ts +10 -3
  137. package/dist/declared-secrets.js +8 -4
  138. package/dist/deploy/agentcore/destroy.d.ts +4 -1
  139. package/dist/deploy/agentcore/destroy.js +25 -1
  140. package/dist/deploy/agentcore/forwarder.js +1 -1
  141. package/dist/deploy/agentcore/plan.d.ts +7 -21
  142. package/dist/deploy/agentcore/plan.js +47 -49
  143. package/dist/deploy/agentcore/run.d.ts +18 -10
  144. package/dist/deploy/agentcore/run.js +93 -70
  145. package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
  146. package/dist/deploy/agentcore/runtime-secrets.js +108 -0
  147. package/dist/deploy/agentcore/shell.js +12 -19
  148. package/dist/deploy/agentcore/sigv4.d.ts +7 -0
  149. package/dist/deploy/agentcore/sigv4.js +15 -0
  150. package/dist/deploy/agentcore/slr.d.ts +18 -0
  151. package/dist/deploy/agentcore/slr.js +27 -0
  152. package/dist/deploy/agentcore/store.d.ts +7 -0
  153. package/dist/deploy/agentcore/store.js +115 -0
  154. package/dist/deploy/api.d.ts +14 -14
  155. package/dist/deploy/api.js +26 -24
  156. package/dist/deploy/box-shell.d.ts +3 -1
  157. package/dist/deploy/build-context.d.ts +4 -4
  158. package/dist/deploy/build-context.js +11 -15
  159. package/dist/deploy/channel-ingress.d.ts +35 -9
  160. package/dist/deploy/channel-ingress.js +119 -22
  161. package/dist/deploy/container.d.ts +35 -10
  162. package/dist/deploy/container.js +79 -37
  163. package/dist/deploy/delivered-text.d.ts +13 -0
  164. package/dist/deploy/delivered-text.js +86 -0
  165. package/dist/deploy/docker/plan.d.ts +7 -8
  166. package/dist/deploy/docker/plan.js +31 -52
  167. package/dist/deploy/docker/run.d.ts +5 -43
  168. package/dist/deploy/docker/run.js +9 -84
  169. package/dist/deploy/file-store.d.ts +2 -0
  170. package/dist/deploy/file-store.js +17 -0
  171. package/dist/deploy/fly/plan.d.ts +3 -2
  172. package/dist/deploy/fly/plan.js +20 -9
  173. package/dist/deploy/fly/run.d.ts +18 -8
  174. package/dist/deploy/fly/run.js +24 -42
  175. package/dist/deploy/fly/store.d.ts +12 -0
  176. package/dist/deploy/fly/store.js +83 -0
  177. package/dist/deploy/hosts.d.ts +1 -2
  178. package/dist/deploy/hosts.js +1 -2
  179. package/dist/deploy/preflight.d.ts +18 -5
  180. package/dist/deploy/preflight.js +135 -63
  181. package/dist/deploy/railway/ignore.d.ts +8 -0
  182. package/dist/deploy/railway/ignore.js +57 -0
  183. package/dist/deploy/railway/plan.d.ts +1 -2
  184. package/dist/deploy/railway/plan.js +8 -6
  185. package/dist/deploy/railway/run.d.ts +21 -15
  186. package/dist/deploy/railway/run.js +32 -78
  187. package/dist/deploy/railway/store.d.ts +29 -0
  188. package/dist/deploy/railway/store.js +143 -0
  189. package/dist/deploy/runner.d.ts +5 -2
  190. package/dist/deploy/runner.js +4 -3
  191. package/dist/deploy/secret-store.d.ts +63 -0
  192. package/dist/deploy/secret-store.js +98 -0
  193. package/dist/deploy/secrets.d.ts +29 -46
  194. package/dist/deploy/secrets.js +49 -122
  195. package/dist/deploy/workspace.d.ts +6 -12
  196. package/dist/deploy/workspace.js +18 -94
  197. package/dist/dev-supervisor.js +6 -8
  198. package/dist/environment/declare.js +3 -2
  199. package/dist/environment/mise.d.ts +3 -1
  200. package/dist/environment/mise.js +4 -2
  201. package/dist/feishu.d.ts +1 -1
  202. package/dist/feishu.js +1 -1
  203. package/dist/harnesses/models/catalog.d.ts +1 -0
  204. package/dist/harnesses/models/catalog.js +1 -2
  205. package/dist/harnesses/models/environment.d.ts +7 -2
  206. package/dist/harnesses/models/environment.js +9 -5
  207. package/dist/harnesses/models/host.d.ts +0 -7
  208. package/dist/harnesses/models/host.js +10 -1
  209. package/dist/harnesses/models/login.d.ts +0 -8
  210. package/dist/harnesses/models/login.js +5 -4
  211. package/dist/harnesses/models/models.d.ts +7 -2
  212. package/dist/harnesses/models/models.js +28 -4
  213. package/dist/harnesses/pi-durable/agent.js +59 -38
  214. package/dist/harnesses/pi-durable/build.d.ts +12 -0
  215. package/dist/harnesses/pi-durable/build.js +270 -0
  216. package/dist/harnesses/pi-durable/built.d.ts +24 -0
  217. package/dist/harnesses/pi-durable/built.js +136 -0
  218. package/dist/harnesses/pi-durable/config.d.ts +81 -0
  219. package/dist/harnesses/pi-durable/config.js +266 -0
  220. package/dist/harnesses/pi-durable/define.d.ts +63 -13
  221. package/dist/harnesses/pi-durable/define.js +23 -2
  222. package/dist/harnesses/pi-durable/definition.d.ts +72 -0
  223. package/dist/harnesses/pi-durable/definition.js +143 -0
  224. package/dist/harnesses/pi-durable/events.d.ts +4 -3
  225. package/dist/harnesses/pi-durable/events.js +179 -136
  226. package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
  227. package/dist/harnesses/pi-durable/execution-env.js +11 -0
  228. package/dist/harnesses/pi-durable/mcp.js +1 -3
  229. package/dist/harnesses/pi-durable/open.d.ts +35 -37
  230. package/dist/harnesses/pi-durable/open.js +91 -153
  231. package/dist/harnesses/pi-durable/reads.d.ts +0 -1
  232. package/dist/harnesses/pi-durable/reads.js +1 -3
  233. package/dist/harnesses/pi-durable/registry.d.ts +4 -6
  234. package/dist/harnesses/pi-durable/registry.js +29 -26
  235. package/dist/harnesses/pi-durable/release.d.ts +2 -2
  236. package/dist/harnesses/pi-durable/release.js +10 -7
  237. package/dist/harnesses/pi-durable/serve.d.ts +91 -15
  238. package/dist/harnesses/pi-durable/serve.js +109 -47
  239. package/dist/harnesses/pi-durable/sessions.js +15 -12
  240. package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
  241. package/dist/harnesses/pi-durable/text-sources.js +55 -0
  242. package/dist/harnesses/pi-durable/text.d.ts +16 -4
  243. package/dist/harnesses/pi-durable/text.js +115 -59
  244. package/dist/harnesses/pi-durable/wake.js +8 -8
  245. package/dist/http/client.js +9 -3
  246. package/dist/http/contract.d.ts +3 -3
  247. package/dist/http/server.d.ts +1 -1
  248. package/dist/http/server.js +1 -1
  249. package/dist/index.d.ts +8 -5
  250. package/dist/index.js +4 -1
  251. package/dist/instance-name.d.ts +7 -0
  252. package/dist/instance-name.js +26 -0
  253. package/dist/lark.d.ts +1 -1
  254. package/dist/lark.js +1 -1
  255. package/dist/mcp/client.js +11 -2
  256. package/dist/mcp/config.d.ts +70 -18
  257. package/dist/mcp/config.js +136 -53
  258. package/dist/mcp/host.d.ts +5 -2
  259. package/dist/mcp/host.js +14 -6
  260. package/dist/mcp/sign-in.js +2 -2
  261. package/dist/mcp-api.d.ts +14 -0
  262. package/dist/paths.d.ts +22 -8
  263. package/dist/paths.js +44 -11
  264. package/dist/runtime-websocket.d.ts +14 -0
  265. package/dist/runtime-websocket.js +39 -0
  266. package/dist/scaffold/add-channel.d.ts +9 -16
  267. package/dist/scaffold/add-channel.js +13 -83
  268. package/dist/scaffold/templates/env.example +4 -5
  269. package/dist/scaffold/templates/fastagent.config.ts +20 -14
  270. package/dist/scaffold/templates/gitignore +9 -6
  271. package/dist/scaffold/templates.d.ts +6 -2
  272. package/dist/scaffold/templates.js +7 -2
  273. package/dist/scaffold/vendor-skill.js +4 -2
  274. package/dist/service.d.ts +44 -6
  275. package/dist/service.js +110 -10
  276. package/dist/serving-command.d.ts +10 -0
  277. package/dist/serving-command.js +1 -0
  278. package/dist/slack.d.ts +1 -1
  279. package/dist/slack.js +1 -1
  280. package/dist/strip-edges.d.ts +10 -0
  281. package/dist/strip-edges.js +22 -0
  282. package/dist/telegram.d.ts +1 -1
  283. package/dist/telegram.js +1 -1
  284. package/dist/tunnel.d.ts +0 -1
  285. package/dist/tunnel.js +2 -8
  286. package/dist/value-file.d.ts +11 -6
  287. package/dist/value-file.js +21 -9
  288. package/dist/version.d.ts +7 -1
  289. package/dist/version.js +9 -3
  290. package/package.json +6 -5
  291. package/dist/channels/lark/credentials-page.d.ts +0 -25
  292. package/dist/channels/lark/credentials-page.js +0 -206
  293. package/dist/channels/lark/onboard.d.ts +0 -52
  294. package/dist/channels/lark/onboard.js +0 -86
  295. package/dist/channels/slack/config-api.d.ts +0 -69
  296. package/dist/channels/slack/config-api.js +0 -162
  297. package/dist/channels/slack/onboard.d.ts +0 -40
  298. package/dist/channels/slack/onboard.js +0 -136
  299. package/dist/channels/slack/onboarding-state.d.ts +0 -31
  300. package/dist/channels/slack/onboarding-state.js +0 -64
  301. package/dist/channels/slack/register-webhook.d.ts +0 -12
  302. package/dist/channels/slack/register-webhook.js +0 -67
  303. package/dist/channels/slack/setup-server.d.ts +0 -32
  304. package/dist/channels/slack/setup-server.js +0 -246
  305. package/dist/cli/add-feishu.d.ts +0 -41
  306. package/dist/cli/add-feishu.js +0 -391
  307. package/dist/cli/add-slack.d.ts +0 -18
  308. package/dist/cli/add-slack.js +0 -216
  309. package/dist/harnesses/pi-durable/parked.d.ts +0 -18
  310. package/dist/harnesses/pi-durable/parked.js +0 -27
@@ -1,6 +1,4 @@
1
1
  import type { BoxLoginStep } from "../box-shell.ts";
2
- import type { DeclaredChannel } from "../../channels/discover.ts";
3
- import type { RegistrationOutcome } from "../../channels/registration.ts";
4
2
  import { type CliRunner } from "../runner.ts";
5
3
  export interface DockerRunPlan {
6
4
  /** Compose file relative to the runner cwd (the agent directory). */
@@ -8,20 +6,10 @@ export interface DockerRunPlan {
8
6
  /** Container port from config; used to ask Compose for the effective published host port. */
9
7
  port: number;
10
8
  /**
11
- * What `--run` assembled. The container reads the value file itself through the generated `env_file`, so nothing
12
- * travels through this process: the names are reported to the operator and otherwise unused.
9
+ * Make sure the value file Compose reads by `env_file:` holds a value for every name the deployment needs, asking for
10
+ * the ones it lacks: resolves a gate line, or undefined. Nothing travels through this process.
13
11
  */
14
- secrets: Record<string, string>;
15
- /** Declared names the value file supplies no value for — the run gates on these before any side effect. */
16
- missingSecrets: string[];
17
- /** That value file, agent-dir-relative, so the gate names the file this deploy actually read. */
18
- valueFile: string;
19
- /** Register the deployment's webhooks against the tunnel URL, reporting what each registrar answered. */
20
- announce: DockerAnnounce;
21
- /** Every declared channel and its ingress: which of them a failed login leaves unregistered. */
22
- channels: readonly DeclaredChannel[];
23
- /** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
24
- requireTunnel: boolean;
12
+ provideSecrets: () => Promise<string | undefined>;
25
13
  /**
26
14
  * Log the box in (`fastagent login --deployment`) once it is up and before any webhook is pointed at it: a channel pointed at
27
15
  * a box with no model credential answers every message with a failure. Resolves a gate line, or undefined.
@@ -31,23 +19,13 @@ export interface DockerRunPlan {
31
19
  export type DockerRunOutcome = {
32
20
  ok: true;
33
21
  url?: string;
34
- tunnelUrl?: string;
35
22
  }
36
- /**
37
- * `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know where it is and what
38
- * to re-run.
39
- */
23
+ /** `url` travels with a gate too: Compose is up, so the operator still needs to know where. */
40
24
  | {
41
25
  ok: false;
42
26
  gate: string;
43
27
  url?: string;
44
- tunnelUrl?: string;
45
28
  };
46
- /** Register the deployment's webhooks against its public URL, reporting what each registrar answered. */
47
- type DockerAnnounce = (baseUrl: string) => Promise<{
48
- kind: string;
49
- outcome: RegistrationOutcome;
50
- }[]>;
51
29
  /**
52
30
  * `stillStarting` answers whether the agent container is still up; a probe that ignores it simply waits out its whole
53
31
  * budget.
@@ -58,12 +36,6 @@ export type DockerHealthProbe = (healthUrl: string, stillStarting: () => Promise
58
36
  * publish free", asked before `up`, because afterwards a `/health` that answers cannot say whose it is.
59
37
  */
60
38
  export type HostPortProbe = (host: string, port: number) => Promise<boolean>;
61
- /** A published Quick Tunnel URL, and whether its tunnel ever reported an edge connection. */
62
- export interface ComposeTunnel {
63
- url: string;
64
- connected: boolean;
65
- }
66
- export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string) => Promise<ComposeTunnel | undefined>;
67
39
  /** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
68
40
  export declare function localUrlFromComposePort(stdout: string): string | undefined;
69
41
  /**
@@ -75,14 +47,4 @@ export declare function agentPublishedPorts(stdout: string): {
75
47
  host: string;
76
48
  port: number;
77
49
  }[];
78
- /**
79
- * Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
80
- * connection.
81
- */
82
- export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile: string, options?: {
83
- attempts?: number;
84
- intervalMs?: number;
85
- sleep?: (ms: number) => Promise<void>;
86
- }): Promise<ComposeTunnel | undefined>;
87
- export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe, hostPortBusy?: HostPortProbe): Promise<DockerRunOutcome>;
88
- export {};
50
+ export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, hostPortBusy?: HostPortProbe): Promise<DockerRunOutcome>;
@@ -1,12 +1,8 @@
1
1
  /** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
2
2
  import { connect } from "node:net";
3
3
  import { waitForHealth } from "../../channels/wait-health.js";
4
- import { loginGate } from "../channel-ingress.js";
5
- import { TUNNEL_DNS_LAG_MS, hasTunnelConnection, parseTunnelUrl } from "../../tunnel.js";
6
- import { registrationGate } from "../registration-gate.js";
7
4
  import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
8
5
  import { readOutput } from "../runner.js";
9
- import { missingValuesGate } from "../secrets.js";
10
6
  /** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
11
7
  export function localUrlFromComposePort(stdout) {
12
8
  const line = stdout
@@ -52,29 +48,6 @@ export function agentPublishedPorts(stdout) {
52
48
  });
53
49
  }
54
50
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
55
- /**
56
- * Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
57
- * connection.
58
- */
59
- export async function waitForComposeTunnelUrl(docker, composeFile, options = {}) {
60
- const compose = ["compose", "-f", composeFile];
61
- const attempts = options.attempts ?? 60;
62
- let assigned;
63
- for (let attempt = 0; attempt < attempts; attempt++) {
64
- const logs = await docker([...compose, "logs", "--no-color", "tunnel"], { capture: true });
65
- if (logs.code === 0) {
66
- assigned ??= parseTunnelUrl(logs.stdout);
67
- if (assigned && hasTunnelConnection(logs.stdout)) {
68
- await (options.sleep ?? sleep)(TUNNEL_DNS_LAG_MS);
69
- return { url: assigned, connected: true };
70
- }
71
- }
72
- if (attempt + 1 < attempts)
73
- await (options.sleep ?? sleep)(options.intervalMs ?? 500);
74
- }
75
- return assigned ? { url: assigned, connected: false } : undefined;
76
- }
77
- const defaultTunnelUrlProbe = (docker, composeFile) => waitForComposeTunnelUrl(docker, composeFile);
78
51
  /**
79
52
  * `/health` asked from inside the agent container, for a topology that publishes no host port. The image always has
80
53
  * node or bun (the generated Dockerfile's base), and both have `fetch`; nothing else is assumed installed.
@@ -92,7 +65,7 @@ async function containerHealthy(docker, compose, port, stillStarting) {
92
65
  }
93
66
  return false;
94
67
  }
95
- export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, tunnelUrlProbe = defaultTunnelUrlProbe, hostPortBusy = defaultHostPortBusy) {
68
+ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, hostPortBusy = defaultHostPortBusy) {
96
69
  const gate = (message) => ({ ok: false, gate: message });
97
70
  const compose = ["compose", "-f", plan.composeFile];
98
71
  // CLI/plugin gate first: unlike a daemon error, spawn ENOENT becomes 127 at the shared runner seam.
@@ -103,17 +76,10 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
103
76
  if (version.code !== 0) {
104
77
  return gate("Docker Compose plugin is unavailable — install/enable `docker compose`, then re-run");
105
78
  }
106
- // The values gate precedes the first side effect (build/create).
107
- const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
108
- if (missingValues)
109
- return gate(missingValues);
110
- // Name what the container must find, and WHERE — the generated Compose reads the value file itself, so this run
111
- // hands Compose nothing. Saying "passing N secrets to Compose" would be false, and doubly so on a hand-owned
112
- // Compose file that has no `env_file` entry at all.
113
- const secretNames = Object.keys(plan.secrets);
114
- if (secretNames.length > 0) {
115
- log(`${secretNames.length} value(s) the container reads from ${plan.valueFile}: ${secretNames.join(", ")}`);
116
- }
79
+ // The values come before the first side effect (build/create).
80
+ const secretsGate = await plan.provideSecrets();
81
+ if (secretsGate !== undefined)
82
+ return gate(secretsGate);
117
83
  if ((await docker(["info"], { capture: true })).code !== 0) {
118
84
  return gate("Docker daemon is unavailable — start Docker Engine/Desktop, then re-run");
119
85
  }
@@ -127,11 +93,6 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
127
93
  if (!services.includes("agent")) {
128
94
  return gate(`Compose file must keep the "agent" service for \`fastagent deploy docker --run\``);
129
95
  }
130
- const hasTunnel = services.includes("tunnel");
131
- if (plan.requireTunnel && !hasTunnel) {
132
- return gate(`--tunnel was requested but the kept ${plan.composeFile} has no "tunnel" service — ` +
133
- `edit it, delete it and regenerate, or pass --force`);
134
- }
135
96
  // A port something else already serves would answer the `/health` this run waits for, and the run would report that
136
97
  // process's answer as the agent's. A container this project already runs holds its own port, so only a first start
137
98
  // (or a start after `down`) asks. Before the first side effect: a refusal leaves nothing to undo.
@@ -146,19 +107,12 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
146
107
  if (await hostPortBusy(host, port)) {
147
108
  return gate(`${host}:${port} is already in use by another process, so the agent's published port is not free and a ` +
148
109
  `health check there would answer for that process, not for the agent — stop it, or move the agent: ` +
149
- `edit the published port in ${plan.composeFile}, or change http.port in fastagent.config.ts and re-run ` +
110
+ `edit the published port in ${plan.composeFile}, or change defaults.http.port in fastagent.config.ts and re-run ` +
150
111
  `with --force (a generated Compose file is kept otherwise)`);
151
112
  }
152
113
  }
153
114
  }
154
115
  }
155
- // Quick Tunnel logs are the control-plane output (the assigned URL).
156
- if (hasTunnel) {
157
- log("recreating the ephemeral tunnel service…");
158
- if ((await docker([...compose, "rm", "-s", "-f", "tunnel"])).code !== 0) {
159
- return gate(`could not recreate the tunnel service — inspect \`docker compose -f ${plan.composeFile} ps\``);
160
- }
161
- }
162
116
  log(`building and reconciling ${plan.composeFile}…`);
163
117
  // `--pull`: the base image is a floating tag, and a copy cached here may be older than the runtime floor.
164
118
  if ((await docker([...compose, "build", "--pull"])).code !== 0) {
@@ -173,9 +127,6 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
173
127
  if (running.code !== 0 || !runningServices.includes("agent")) {
174
128
  return gate(`the Compose service "agent" is not running — inspect with \`docker compose -f ${plan.composeFile} logs agent\``);
175
129
  }
176
- if (hasTunnel && !runningServices.includes("tunnel")) {
177
- return gate(`the Compose service "tunnel" is not running — inspect with \`docker compose -f ${plan.composeFile} logs tunnel\``);
178
- }
179
130
  // A user-owned topology may deliberately remove the host port and expose only through its own ingress.
180
131
  const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true });
181
132
  const url = published.code === 0 ? localUrlFromComposePort(published.stdout) : undefined;
@@ -208,33 +159,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
208
159
  }
209
160
  // `url` travels with this gate: Compose is up, and the operator needs to know where.
210
161
  const notLoggedIn = await plan.boxLogin?.run();
211
- if (notLoggedIn) {
212
- // Only a tunnel is ours to announce; without one, the webhooks were never this run's to register.
213
- const afterLogin = "re-run `fastagent deploy docker --run` (it keeps the login and announces the tunnel)";
214
- const channels = hasTunnel ? plan.channels : [];
215
- return { ok: false, gate: loginGate({ notLoggedIn, channels, log, afterLogin }), url };
216
- }
217
- if (!hasTunnel)
218
- return { ok: true, url };
219
- log("waiting for the Compose tunnel service to publish its Quick Tunnel URL…");
220
- const tunnel = await tunnelUrlProbe(docker, plan.composeFile);
221
- if (!tunnel) {
222
- return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
223
- }
224
- // The same sentence `startCloudflareTunnel` prints for the same state, and needed MORE here.
225
- if (!tunnel.connected) {
226
- log(`warn: the tunnel service never reported an edge connection for ${tunnel.url} — announcing it anyway. ` +
227
- `Nothing reaches a tunnel that has not connected, so a webhook registration that cannot resolve the ` +
228
- `host is this, not the platform. Inspect \`docker compose -f ${plan.composeFile} logs tunnel\`.`);
229
- }
230
- const tunnelUrl = tunnel.url;
231
- // Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer that owns the
232
- // outcome, so it is the layer that can gate on one.
233
- const reg = registrationGate(log, `re-run this deploy to retry registration (Compose is already up)`);
234
- for (const { kind, outcome } of await plan.announce(tunnelUrl))
235
- reg.track(kind, outcome);
236
- const blocked = reg.gate();
237
- if (blocked)
238
- return { ok: false, gate: blocked, url, tunnelUrl };
239
- return { ok: true, url, tunnelUrl };
162
+ if (notLoggedIn)
163
+ return { ok: false, gate: notLoggedIn, url };
164
+ return { ok: true, url };
240
165
  }
@@ -0,0 +1,2 @@
1
+ import type { SecretStore } from "./secret-store.ts";
2
+ export declare function fileStore(agentDir: string, file: string): SecretStore;
@@ -0,0 +1,17 @@
1
+ /** A value file as a deployment's store: Docker's (Compose reads it by `env_file:`), and what `start` reads. */
2
+ import { loadEnvValues, writeValues } from "../value-file.js";
3
+ export function fileStore(agentDir, file) {
4
+ return {
5
+ async exists() {
6
+ return true;
7
+ },
8
+ async ensure() { },
9
+ async names() {
10
+ return new Set([...loadEnvValues(file)].filter(([, value]) => value.trim() !== "").map(([name]) => name));
11
+ },
12
+ async write(values) {
13
+ const names = Object.keys(values);
14
+ await writeValues(agentDir, "deployment", names.map((name) => ({ name, value: values[name] })), names, file);
15
+ },
16
+ };
17
+ }
@@ -18,12 +18,13 @@ export interface FlyPlanInput extends ContainerInput {
18
18
  */
19
19
  channels: readonly DeclaredChannel[];
20
20
  /**
21
- * The runbook's variable list (`deploymentSecrets`): what must have a value, then everything else the value file
22
- * carries.
21
+ * The runbook's variable list (`deploymentSecrets`): the declared names, each hinted by the file that declared it.
23
22
  */
24
23
  secrets?: readonly DeploymentSecret[];
25
24
  /** `schedules/` declares a schedule — one of the things that forces a machine up (deploy/residency.ts). */
26
25
  hasCron: boolean;
26
+ /** The `http.publicUrl` the image delivers, where the platforms reach the box; default the app's own URL. */
27
+ publicUrl?: string;
27
28
  }
28
29
  export interface FlyPlan {
29
30
  /** fly.toml / Dockerfile / .dockerignore — written by the CLI (skipped if present unless --force). */
@@ -1,7 +1,9 @@
1
+ import { webhookKinds } from "../../channels/webhooks.js";
1
2
  import { webhookRunbook } from "../channel-ingress.js";
2
3
  import { containerArtifacts, imageHasGit } from "../container.js";
3
4
  import { deploymentLoginCommand } from "../box-shell.js";
4
5
  import { WAKEUPS_WHEN_ASLEEP, residencyFor } from "../residency.js";
6
+ import { stripEdges } from "../../strip-edges.js";
5
7
  function flyToml(appName, port, residency) {
6
8
  // WHY is residency.ts's to decide; the SETTING and its wording are Fly's. Anything else is an edit to the
7
9
  // generated file, which is the artifact's job.
@@ -65,7 +67,7 @@ export function planFlyDeploy(input) {
65
67
  `# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
66
68
  `# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
67
69
  `fly apps create ${appName}`,
68
- `# The volume (definition, state, credentials — kept across stop/suspend/redeploy) is created by the deploy`,
70
+ `# The volume (the agent's working directory, state, credentials — kept across stop/suspend/redeploy) is created by the deploy`,
69
71
  `# below, in primary_region and sized by [mounts].initial_size. Creating it yourself first pins it to a host`,
70
72
  `# picked without the machine, which can fail the deploy with "insufficient resources … existing volume".`,
71
73
  ``,
@@ -77,18 +79,30 @@ export function planFlyDeploy(input) {
77
79
  `fly ips allocate-v6 --app ${appName}`,
78
80
  ];
79
81
  if (secrets.length > 0) {
80
- runbook.push(``, `# Secrets (replace each <value>):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`), `fly secrets set --app ${appName} ${secrets.map((s) => `${s.name}=<value>`).join(" ")}`);
82
+ runbook.push(``, `# Secrets the app needs a value for. \`fastagent secrets push fly\` sets the ones a value file holds and`, `# \`fastagent secrets set fly <NAME>\` one typed without echo; or with flyctl (replace each <value>):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`), `fly secrets set --app ${appName} ${secrets.map((s) => `${s.name}=<value>`).join(" ")}`);
81
83
  }
82
84
  runbook.push(``, `# The build context is the agent directory, baked as the definition.`, `# Before a new definition release, run \`fastagent deploy fly\` to refresh the release manifest.`, `# Run this from the agent directory:`, deployCmd);
83
- runbook.push(``, `# The volume keeps .state and .secrets across restarts and deploys; each release replaces /data/definition.`, imageHasGit(input)
85
+ runbook.push(``, `# The volume is the agent's working directory: it keeps its work, .state and .secrets across restarts and deploys.`, imageHasGit(input)
84
86
  ? `# Git is installed (the definition ships its .git, github content is cloned here, or context.json declares it in aptPackages).`
85
87
  : `# To give the agent git, declare it: fastagent env add apt:git`);
86
88
  // The credential is created on the box, never carried: the box is then the only holder of its grant.
87
89
  if (input.boxLogin) {
88
90
  runbook.push(``, `# Model auth: once it is up, the deployment logs in to ${input.boxLogin} itself (from the agent directory):`, `${deploymentLoginCommand("fly", input.boxLogin)}`);
89
91
  }
90
- // The fastagent-only post-step: point each channel at the live URL.
91
- const steps = webhookRunbook(`https://${appName}.fly.dev`, channels);
92
+ // The box points the channels its own credentials can at its `http.publicUrl`; only Slack's is a person's step.
93
+ const appUrl = `https://${appName}.fly.dev`;
94
+ const publicUrl = input.publicUrl ?? appUrl;
95
+ const own = webhookKinds(channels).filter((kind) => kind !== "slack");
96
+ const steps = [
97
+ ...(own.length > 0
98
+ ? [
99
+ `# The box points ${own.join(", ")} at ${publicUrl} itself, on every start; its /health says how each`,
100
+ `# one went (a step only a person can take, Lark's console where its config API answers 404, is in its log):`,
101
+ `curl -fsS ${appUrl}/health`,
102
+ ]
103
+ : []),
104
+ ...webhookRunbook(publicUrl, channels.filter((channel) => channel.name === "slack")),
105
+ ];
92
106
  const post = steps.length > 0 ? [`# After deploy:`, ...steps] : [];
93
107
  if (post.length > 0)
94
108
  runbook.push(``, ...post);
@@ -107,9 +121,6 @@ export function parseFlyMinMachines(toml) {
107
121
  }
108
122
  /** Sanitize a directory basename into a Fly app name: lowercase, [a-z0-9-], must start with a letter. */
109
123
  export function toFlyAppName(basename) {
110
- const slug = basename
111
- .toLowerCase()
112
- .replace(/[^a-z0-9]+/g, "-")
113
- .replace(/^-+|-+$/g, "");
124
+ const slug = stripEdges(basename.toLowerCase().replace(/[^a-z0-9]+/g, "-"), (char) => char === "-");
114
125
  return /^[a-z]/.test(slug) ? slug : `app-${slug || "agent"}`;
115
126
  }
@@ -1,16 +1,21 @@
1
1
  /** `fastagent deploy fly --run` — drive flyctl to completion. */
2
2
  import type { BoxLoginStep } from "../box-shell.ts";
3
- import { type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
3
+ import { type BoxWebhooksProbe, type UrlRecorder, type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
4
4
  import type { DeclaredChannel } from "../../channels/discover.ts";
5
5
  import { type CliRunner } from "../runner.ts";
6
+ import type { SecretStore } from "../secret-store.ts";
6
7
  export interface FlyRunPlan {
8
+ /** At a terminal with a `--config` file: record each URL a person set once the box confirms it. */
9
+ recordUrl?: UrlRecorder;
10
+ /** The `http.publicUrl` the image delivers, where the platforms reach the box; default the app's own URL. */
11
+ publicUrl?: string;
7
12
  appName: string;
8
- /** `KEY=value` secrets to set on Fly: the value file's variables. */
9
- secrets: Record<string, string>;
10
- /** Declared names the value file supplies no value for — the run gates on these before any side effect. */
11
- missingSecrets: string[];
12
- /** That value file, agent-dir-relative, so the gate names the file this deploy actually read. */
13
- valueFile: string;
13
+ /**
14
+ * Make sure the app holds a value for every name the deployment needs, through whoever can supply one
15
+ * (secret-store.ts): resolves a gate line, or undefined. It runs once the app exists, since only then can Fly say
16
+ * which names it has.
17
+ */
18
+ provideSecrets: (store: SecretStore) => Promise<string | undefined>;
14
19
  /** Every declared channel and its ingress — the driver asks which of them have a webhook. */
15
20
  channels: readonly DeclaredChannel[];
16
21
  /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the agent directory). */
@@ -37,4 +42,9 @@ export type FlyRunOutcome = {
37
42
  ok: false;
38
43
  gate: string;
39
44
  };
40
- export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars, healthProbe?: PublicHealthProbe): Promise<FlyRunOutcome>;
45
+ export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void,
46
+ /** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
47
+ registrars: Pick<Registrars, "leave">, healthProbe?: PublicHealthProbe, boxWebhooks?: {
48
+ probe: BoxWebhooksProbe;
49
+ intervalMs?: number;
50
+ }): Promise<FlyRunOutcome>;
@@ -1,6 +1,7 @@
1
- import { loginGate, publicHealthGate, registerWebhooks, } from "../channel-ingress.js";
1
+ import { confirmWebhooks, loginGate, publicHealthGate, } from "../channel-ingress.js";
2
2
  import { readOutput } from "../runner.js";
3
- import { hostSignedOutGate, missingValuesGate } from "../secrets.js";
3
+ import { hostSignedOutGate } from "../secrets.js";
4
+ import { ensureFlyApp, flyStore } from "./store.js";
4
5
  /**
5
6
  * The `Type` values that put an app on `https://<app>.fly.dev`, BY FAMILY — one entry per allocate command, because
6
7
  * that is the granularity the answer is acted on at.
@@ -17,23 +18,10 @@ function ingressAddresses(stdout) {
17
18
  const has = (types) => entries.some((entry) => typeof entry?.Address === "string" && entry.Address !== "" && types.includes(entry.Type));
18
19
  return { v4: has(INGRESS_TYPES.v4), v6: has(INGRESS_TYPES.v6) };
19
20
  }
20
- /**
21
- * Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field, and an
22
- * offline fixture can only repeat what we already believe, so a change in flyctl's output is not caught here.
23
- */
24
- function listHasName(stdout, name) {
25
- const entries = JSON.parse(stdout);
26
- if (!Array.isArray(entries))
27
- throw new Error(`expected a JSON array, got ${typeof entries}`);
28
- return entries.some((o) => o.Name === name);
29
- }
30
- export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
21
+ export async function deployFlyRun(plan, fly, log,
22
+ /** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
23
+ registrars, healthProbe, boxWebhooks) {
31
24
  const gate = (g) => ({ ok: false, gate: g });
32
- // 1. Gate missing required secret VALUES before any side effect (no half-created infra), and before asking a
33
- // person to sign in for a deploy that would stop here anyway.
34
- const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
35
- if (missingValues)
36
- return gate(missingValues);
37
25
  // 2.
38
26
  if ((await fly(["auth", "whoami"], { capture: true })).code !== 0) {
39
27
  const failed = plan.hostLogin
@@ -43,19 +31,14 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
43
31
  return gate(failed);
44
32
  }
45
33
  // 3.
46
- const appExists = await readOutput(fly, "fly", ["apps", "list", "--json"], (out) => listHasName(out, plan.appName));
47
- if ("gate" in appExists)
48
- return gate(appExists.gate);
49
- if (appExists.value) {
50
- log(`app ${plan.appName} exists — skipping create`);
51
- }
52
- else {
53
- log(`creating app ${plan.appName}…`);
54
- if ((await fly(["apps", "create", plan.appName])).code !== 0) {
55
- return gate(`\`fly apps create ${plan.appName}\` failed — Fly app names are globally unique and it may be taken. ` +
56
- `Set a unique \`app\` in fly.toml and re-run.`);
57
- }
58
- }
34
+ const appGate = await ensureFlyApp(fly, plan.appName, log);
35
+ if (appGate !== undefined)
36
+ return gate(appGate);
37
+ // 3b. Secrets, before anything else is set up for an app that cannot run without them: the names the app holds, the
38
+ // gaps filled (staged: the deploy below applies them).
39
+ const secretsGate = await plan.provideSecrets(flyStore(fly, plan.appName, log, { stage: true }));
40
+ if (secretsGate !== undefined)
41
+ return gate(secretsGate);
59
42
  // 4. NO volume step: `fly deploy` creates it from [mounts] on a first deploy, and only it can tell the scheduler
60
43
  // which machine (guest size + image) must fit on the host the volume is pinned to. Pre-creating one here placed it
61
44
  // blind, and a host with disk but no compute failed the deploy with `insufficient resources … with existing volume`.
@@ -78,15 +61,6 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
78
61
  return gate(`\`fly ${allocate.join(" ")}\` failed — see the flyctl output above`);
79
62
  }
80
63
  }
81
- // 6. Secrets — staged (no deploy yet; we deploy with fly.toml next). Values over stdin, not argv.
82
- const keys = Object.keys(plan.secrets);
83
- if (keys.length > 0) {
84
- log(`setting ${keys.length} secret(s): ${keys.join(", ")}`);
85
- const input = `${keys.map((k) => `${k}=${plan.secrets[k]}`).join("\n")}\n`;
86
- if ((await fly(["secrets", "import", "--stage", "-a", plan.appName], { input })).code !== 0) {
87
- return gate("`fly secrets import` failed — see the flyctl output above");
88
- }
89
- }
90
64
  // 7.
91
65
  log("deploying (remote build)…");
92
66
  const deployArgs = [
@@ -128,13 +102,21 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
128
102
  afterLogin: "re-run `fastagent deploy fly --run` (it keeps the login)",
129
103
  }));
130
104
  }
131
- // 9.
132
- const registrationGateMsg = await registerWebhooks({
105
+ // 9. The box points its own channels at the `http.publicUrl` this deploy delivered; this reads what it made of them.
106
+ const registrationGateMsg = await confirmWebhooks({
133
107
  baseUrl,
108
+ publicUrl: plan.publicUrl ?? baseUrl,
134
109
  channels: plan.channels,
135
110
  registrars,
136
111
  log,
112
+ ...(plan.recordUrl === undefined ? {} : { record: plan.recordUrl }),
137
113
  retryHint: "re-run to retry registration (steps already done are skipped)",
114
+ ...(boxWebhooks === undefined
115
+ ? {}
116
+ : {
117
+ probe: boxWebhooks.probe,
118
+ ...(boxWebhooks.intervalMs === undefined ? {} : { intervalMs: boxWebhooks.intervalMs }),
119
+ }),
138
120
  });
139
121
  if (registrationGateMsg)
140
122
  return gate(registrationGateMsg);
@@ -0,0 +1,12 @@
1
+ /** Fly's secrets as a deployment's store: `fly secrets list` for the names, `fly secrets set NAME=-` to set each. */
2
+ import type { SecretStore } from "../secret-store.ts";
3
+ import { type CliRunner } from "../runner.ts";
4
+ /** Create the app when `fly apps list` does not show it. Resolves the gate line when it cannot be made. */
5
+ export declare function ensureFlyApp(fly: CliRunner, app: string, log: (message: string) => void): Promise<string | undefined>;
6
+ /**
7
+ * `stage`: set the secrets without deploying them, for a deploy that follows (`fly deploy` applies staged ones); off, a
8
+ * running app restarts on them at once.
9
+ */
10
+ export declare function flyStore(fly: CliRunner, app: string, log: (message: string) => void, options: {
11
+ stage: boolean;
12
+ }): SecretStore;
@@ -0,0 +1,83 @@
1
+ import { readOutput } from "../runner.js";
2
+ /**
3
+ * Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field, and an
4
+ * offline fixture can only repeat what we already believe, so a change in flyctl's output is not caught here.
5
+ */
6
+ function listHasName(stdout, name) {
7
+ const entries = JSON.parse(stdout);
8
+ if (!Array.isArray(entries))
9
+ throw new Error(`expected a JSON array, got ${typeof entries}`);
10
+ return entries.some((o) => o.Name === name);
11
+ }
12
+ /** The names `fly secrets list --json` shows (it never prints a value): each entry's `Name`. */
13
+ function secretNames(stdout) {
14
+ const entries = JSON.parse(stdout);
15
+ if (!Array.isArray(entries))
16
+ throw new Error(`expected a JSON array, got ${typeof entries}`);
17
+ return new Set(entries.flatMap((entry) => {
18
+ const { Name, name } = entry;
19
+ const found = Name ?? name;
20
+ return typeof found === "string" ? [found] : [];
21
+ }));
22
+ }
23
+ /** Create the app when `fly apps list` does not show it. Resolves the gate line when it cannot be made. */
24
+ export async function ensureFlyApp(fly, app, log) {
25
+ const exists = await readOutput(fly, "fly", ["apps", "list", "--json"], (out) => listHasName(out, app));
26
+ if ("gate" in exists)
27
+ return exists.gate;
28
+ if (exists.value) {
29
+ log(`app ${app} exists — skipping create`);
30
+ return undefined;
31
+ }
32
+ log(`creating app ${app}…`);
33
+ if ((await fly(["apps", "create", app])).code !== 0) {
34
+ return (`\`fly apps create ${app}\` failed — see the flyctl output above: the name may be taken (Fly app names are ` +
35
+ `globally unique: set a unique \`app\` in fly.toml) or the account may need payment information. Then re-run.`);
36
+ }
37
+ return undefined;
38
+ }
39
+ /** What `fly secrets set NAME=-` reads from standard input. */
40
+ const FLY_STDIN_LIMIT = 64 * 1024;
41
+ /**
42
+ * `stage`: set the secrets without deploying them, for a deploy that follows (`fly deploy` applies staged ones); off, a
43
+ * running app restarts on them at once.
44
+ */
45
+ export function flyStore(fly, app, log, options) {
46
+ return {
47
+ async exists() {
48
+ const listed = await readOutput(fly, "fly", ["apps", "list", "--json"], (out) => listHasName(out, app));
49
+ if ("gate" in listed)
50
+ throw new Error(listed.gate);
51
+ return listed.value;
52
+ },
53
+ async ensure() {
54
+ const gate = await ensureFlyApp(fly, app, log);
55
+ if (gate !== undefined)
56
+ throw new Error(gate);
57
+ },
58
+ async names() {
59
+ const listed = await readOutput(fly, "fly", ["secrets", "list", "-a", app, "--json"], secretNames);
60
+ if ("gate" in listed)
61
+ throw new Error(listed.gate);
62
+ return listed.value;
63
+ },
64
+ async write(values) {
65
+ const names = Object.keys(values);
66
+ const tooLong = names.find((name) => Buffer.byteLength(values[name]) > FLY_STDIN_LIMIT);
67
+ if (tooLong !== undefined) {
68
+ throw new Error(`${tooLong} is longer than the ${FLY_STDIN_LIMIT} bytes \`fly secrets set\` reads from standard input`);
69
+ }
70
+ log(`setting ${names.length} secret(s) on ${app}: ${names.join(", ")}`);
71
+ // `NAME=-` reads the value from standard input exactly as it is, and never from argv. `import` reads a dotenv-like
72
+ // line instead and changes a value that holds `#` or quotes, so it is not used. Staged, a running app restarts once,
73
+ // on the last.
74
+ for (const [index, name] of names.entries()) {
75
+ const last = index === names.length - 1;
76
+ const args = ["secrets", "set", ...(options.stage || !last ? ["--stage"] : []), "-a", app, `${name}=-`];
77
+ if ((await fly(args, { input: values[name] })).code !== 0) {
78
+ throw new Error(`\`fly secrets set ${name}=-\` failed — see the flyctl output above`);
79
+ }
80
+ }
81
+ },
82
+ };
83
+ }
@@ -11,8 +11,7 @@
11
11
  * below. Locally known refusals belong in `HostDeploy.validate`: the dispatcher runs it before app onboarding
12
12
  * and again on the facts onboarding produces. `deploy/` itself is a neutral kernel plus one directory per host; nothing host-specific
13
13
  * belongs in the kernel, and no host may hold a fact about another one (a host-only flag is a row in
14
- * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere; `--tunnel` stays a usage GATE in `runDeploy`
15
- * because a refusal is not a row).
14
+ * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere).
16
15
  *
17
16
  * BEFORE writing one, read that host's docs for what it does NOT do implicitly — above all, whether
18
17
  * a created resource is REACHABLE without a further step. On Fly, `[http_service]` declares a service and allocates
@@ -11,8 +11,7 @@
11
11
  * below. Locally known refusals belong in `HostDeploy.validate`: the dispatcher runs it before app onboarding
12
12
  * and again on the facts onboarding produces. `deploy/` itself is a neutral kernel plus one directory per host; nothing host-specific
13
13
  * belongs in the kernel, and no host may hold a fact about another one (a host-only flag is a row in
14
- * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere; `--tunnel` stays a usage GATE in `runDeploy`
15
- * because a refusal is not a row).
14
+ * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere).
16
15
  *
17
16
  * BEFORE writing one, read that host's docs for what it does NOT do implicitly — above all, whether
18
17
  * a created resource is REACHABLE without a further step. On Fly, `[http_service]` declares a service and allocates