@fastagent-sh/fastagent 0.12.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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/agent.d.ts +72 -0
- package/dist/agent.js +14 -0
- package/dist/channels/body.d.ts +9 -0
- package/dist/channels/body.js +29 -0
- package/dist/channels/github/github.d.ts +31 -0
- package/dist/channels/github/github.js +80 -0
- package/dist/channels/github/scaffold/channel.ts +25 -0
- package/dist/channels/http.d.ts +13 -0
- package/dist/channels/http.js +161 -0
- package/dist/channels/respond.d.ts +5 -0
- package/dist/channels/respond.js +3 -0
- package/dist/channels/telegram/context-buffer.d.ts +59 -0
- package/dist/channels/telegram/context-buffer.js +139 -0
- package/dist/channels/telegram/invoke-turn.d.ts +53 -0
- package/dist/channels/telegram/invoke-turn.js +127 -0
- package/dist/channels/telegram/parse.d.ts +160 -0
- package/dist/channels/telegram/parse.js +153 -0
- package/dist/channels/telegram/preview.d.ts +22 -0
- package/dist/channels/telegram/preview.js +266 -0
- package/dist/channels/telegram/register-webhook.d.ts +13 -0
- package/dist/channels/telegram/register-webhook.js +73 -0
- package/dist/channels/telegram/scaffold/channel.ts +25 -0
- package/dist/channels/telegram/scaffold/telegram-send.ts +107 -0
- package/dist/channels/telegram/state.d.ts +9 -0
- package/dist/channels/telegram/state.js +58 -0
- package/dist/channels/telegram/telegram-api.d.ts +77 -0
- package/dist/channels/telegram/telegram-api.js +345 -0
- package/dist/channels/telegram/telegram.d.ts +35 -0
- package/dist/channels/telegram/telegram.js +352 -0
- package/dist/channels/telegram/turn-queue.d.ts +21 -0
- package/dist/channels/telegram/turn-queue.js +47 -0
- package/dist/channels/telegram/turn-store.d.ts +38 -0
- package/dist/channels/telegram/turn-store.js +150 -0
- package/dist/cli-auth.d.ts +24 -0
- package/dist/cli-auth.js +30 -0
- package/dist/cli-models.d.ts +11 -0
- package/dist/cli-models.js +20 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1288 -0
- package/dist/collect.d.ts +20 -0
- package/dist/collect.js +27 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +8 -0
- package/dist/deploy/container.d.ts +56 -0
- package/dist/deploy/container.js +169 -0
- package/dist/deploy/fly/plan.d.ts +65 -0
- package/dist/deploy/fly/plan.js +142 -0
- package/dist/deploy/fly/run.d.ts +51 -0
- package/dist/deploy/fly/run.js +89 -0
- package/dist/deploy/preflight.d.ts +59 -0
- package/dist/deploy/preflight.js +191 -0
- package/dist/deploy/railway/plan.d.ts +48 -0
- package/dist/deploy/railway/plan.js +91 -0
- package/dist/deploy/railway/run.d.ts +81 -0
- package/dist/deploy/railway/run.js +176 -0
- package/dist/deploy/runner.d.ts +18 -0
- package/dist/deploy/runner.js +25 -0
- package/dist/deploy/secrets.d.ts +49 -0
- package/dist/deploy/secrets.js +88 -0
- package/dist/dev-supervisor.d.ts +14 -0
- package/dist/dev-supervisor.js +158 -0
- package/dist/engines/pi/auth.d.ts +17 -0
- package/dist/engines/pi/auth.js +118 -0
- package/dist/engines/pi/channel.d.ts +31 -0
- package/dist/engines/pi/channel.js +113 -0
- package/dist/engines/pi/chat.d.ts +19 -0
- package/dist/engines/pi/chat.js +200 -0
- package/dist/engines/pi/config.d.ts +110 -0
- package/dist/engines/pi/config.js +285 -0
- package/dist/engines/pi/create.d.ts +127 -0
- package/dist/engines/pi/create.js +222 -0
- package/dist/engines/pi/definition.d.ts +72 -0
- package/dist/engines/pi/definition.js +129 -0
- package/dist/engines/pi/harness.d.ts +50 -0
- package/dist/engines/pi/harness.js +41 -0
- package/dist/engines/pi/invoke.d.ts +28 -0
- package/dist/engines/pi/invoke.js +298 -0
- package/dist/engines/pi/login.d.ts +51 -0
- package/dist/engines/pi/login.js +125 -0
- package/dist/engines/pi/models.d.ts +41 -0
- package/dist/engines/pi/models.js +76 -0
- package/dist/engines/pi/report.d.ts +13 -0
- package/dist/engines/pi/report.js +21 -0
- package/dist/engines/pi/sessions.d.ts +15 -0
- package/dist/engines/pi/sessions.js +130 -0
- package/dist/engines/pi/tool-context.d.ts +17 -0
- package/dist/engines/pi/tool-context.js +13 -0
- package/dist/engines/pi/tool.d.ts +45 -0
- package/dist/engines/pi/tool.js +85 -0
- package/dist/engines/pi/wake-tool.d.ts +20 -0
- package/dist/engines/pi/wake-tool.js +110 -0
- package/dist/engines/pi/workspace.d.ts +53 -0
- package/dist/engines/pi/workspace.js +70 -0
- package/dist/env.d.ts +24 -0
- package/dist/env.js +57 -0
- package/dist/github.d.ts +3 -0
- package/dist/github.js +2 -0
- package/dist/host/node.d.ts +41 -0
- package/dist/host/node.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/invoke-stream.d.ts +10 -0
- package/dist/invoke-stream.js +26 -0
- package/dist/loader.d.ts +38 -0
- package/dist/loader.js +69 -0
- package/dist/log.d.ts +26 -0
- package/dist/log.js +51 -0
- package/dist/observe.d.ts +12 -0
- package/dist/observe.js +44 -0
- package/dist/pi.d.ts +16 -0
- package/dist/pi.js +12 -0
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.js +23 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.js +29 -0
- package/dist/scaffold/add-channel.d.ts +46 -0
- package/dist/scaffold/add-channel.js +227 -0
- package/dist/scaffold/init.d.ts +54 -0
- package/dist/scaffold/init.js +249 -0
- package/dist/scaffold/templates/env.example +24 -0
- package/dist/scaffold/templates/fastagent.config.mjs +19 -0
- package/dist/scaffold/templates/gitignore +8 -0
- package/dist/scaffold/templates/gitignore.agentdir-root +5 -0
- package/dist/scaffold/templates/gitignore.kit +2 -0
- package/dist/scaffold/templates/persona.md +10 -0
- package/dist/scaffold/templates/skills/writing-great-skills/GLOSSARY.md +195 -0
- package/dist/scaffold/templates/skills/writing-great-skills/LICENSE +21 -0
- package/dist/scaffold/templates/skills/writing-great-skills/SKILL.md +82 -0
- package/dist/scaffold/templates/tools/fetch-url.ts +27 -0
- package/dist/scaffold/templates.d.ts +17 -0
- package/dist/scaffold/templates.js +56 -0
- package/dist/scaffold/vendor-skill.d.ts +24 -0
- package/dist/scaffold/vendor-skill.js +157 -0
- package/dist/schedule/audit.d.ts +18 -0
- package/dist/schedule/audit.js +57 -0
- package/dist/schedule/cron.d.ts +7 -0
- package/dist/schedule/cron.js +33 -0
- package/dist/schedule/discover.d.ts +17 -0
- package/dist/schedule/discover.js +64 -0
- package/dist/schedule/schedule.d.ts +35 -0
- package/dist/schedule/schedule.js +23 -0
- package/dist/schedule/scheduler.d.ts +36 -0
- package/dist/schedule/scheduler.js +253 -0
- package/dist/schedule/state.d.ts +14 -0
- package/dist/schedule/state.js +56 -0
- package/dist/schedule/wakeups.d.ts +82 -0
- package/dist/schedule/wakeups.js +196 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +2 -0
- package/dist/tunnel.d.ts +31 -0
- package/dist/tunnel.js +139 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +13 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.js +45 -0
- package/package.json +97 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffered consumption helper (caller-side, SPEC §7): reduce an AgentEvent stream to a final value,
|
|
3
|
+
* encoding the terminal discipline (failed → throw, missing terminal → error). Streaming consumers
|
|
4
|
+
* for-await themselves.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentEvent, Json } from "./agent.ts";
|
|
7
|
+
/** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
|
|
8
|
+
* a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
|
|
9
|
+
export declare class AgentFailure extends Error {
|
|
10
|
+
readonly details: string;
|
|
11
|
+
readonly retryable: boolean;
|
|
12
|
+
/** Machine-readable failure subdivision (SPEC §8), when the engine set one. */
|
|
13
|
+
readonly code?: string;
|
|
14
|
+
constructor(details: string, retryable: boolean, code?: string);
|
|
15
|
+
}
|
|
16
|
+
export interface CollectResult {
|
|
17
|
+
text: string;
|
|
18
|
+
data?: Json;
|
|
19
|
+
}
|
|
20
|
+
export declare function collect(events: AsyncIterable<AgentEvent>): Promise<CollectResult>;
|
package/dist/collect.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
|
|
2
|
+
* a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
|
|
3
|
+
export class AgentFailure extends Error {
|
|
4
|
+
details;
|
|
5
|
+
retryable;
|
|
6
|
+
/** Machine-readable failure subdivision (SPEC §8), when the engine set one. */
|
|
7
|
+
code;
|
|
8
|
+
constructor(details, retryable, code) {
|
|
9
|
+
super(details);
|
|
10
|
+
this.name = "AgentFailure";
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.retryable = retryable;
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function collect(events) {
|
|
17
|
+
let text = "";
|
|
18
|
+
for await (const e of events) {
|
|
19
|
+
if (e.type === "text")
|
|
20
|
+
text += e.delta;
|
|
21
|
+
else if (e.type === "completed")
|
|
22
|
+
return { text, data: e.data };
|
|
23
|
+
else if (e.type === "failed")
|
|
24
|
+
throw new AgentFailure(e.details, e.retryable, e.code);
|
|
25
|
+
}
|
|
26
|
+
throw new Error("stream ended without a terminal event"); // violates SPEC MUST 1
|
|
27
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { Agent, AgentEvent, ImageRef, Json, Prompt, Scope } from "./agent.ts";
|
|
2
|
+
export { collect, AgentFailure, type CollectResult } from "./collect.ts";
|
|
3
|
+
export type { ModuleLoadFailure } from "./loader.ts";
|
|
4
|
+
export { createInvokeHandler, nodeListener } from "./channels/http.ts";
|
|
5
|
+
export { readBodyCapped } from "./channels/body.ts";
|
|
6
|
+
export { text, textHeaders } from "./channels/respond.ts";
|
|
7
|
+
export { type ChannelContext, type ChannelHandler, type ChannelModule, type Routes, router, serveNode, } from "./host/node.ts";
|
|
8
|
+
export { defineSchedule, type LoadedSchedule, type Schedule } from "./schedule/schedule.ts";
|
|
9
|
+
export { discoverScheduleFiles, loadSchedules } from "./schedule/discover.ts";
|
|
10
|
+
export { createScheduler, scheduleSession, type Scheduler, type SchedulerOptions } from "./schedule/scheduler.ts";
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { collect, AgentFailure } from "./collect.js";
|
|
2
|
+
export { createInvokeHandler, nodeListener } from "./channels/http.js";
|
|
3
|
+
export { readBodyCapped } from "./channels/body.js";
|
|
4
|
+
export { text, textHeaders } from "./channels/respond.js";
|
|
5
|
+
export { router, serveNode, } from "./host/node.js";
|
|
6
|
+
export { defineSchedule } from "./schedule/schedule.js";
|
|
7
|
+
export { discoverScheduleFiles, loadSchedules } from "./schedule/discover.js";
|
|
8
|
+
export { createScheduler, scheduleSession } from "./schedule/scheduler.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The portable container — Dockerfile + .dockerignore, host-neutral. Every deploy target ships these
|
|
3
|
+
* unchanged; only the host config (fly.toml / railway.json) and the CLI sequence differ. The image
|
|
4
|
+
* runs `fastagent start /app` on `$PORT` with state under `$FASTAGENT_STATE_DIR` — standard container
|
|
5
|
+
* conventions, no host coupling. Extracted from the Fly plan when Railway became the second target.
|
|
6
|
+
*/
|
|
7
|
+
export interface Artifact {
|
|
8
|
+
path: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
/** Fixed first-line prefix of a fastagent-generated Dockerfile — the ONE source both the generator (the
|
|
12
|
+
* `head` below) and the "is this ours?" check (cli deploy's kept-Dockerfile apt warn) share, so they
|
|
13
|
+
* can't drift into detecting a marker the generator no longer writes. */
|
|
14
|
+
export declare const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
|
|
15
|
+
/** The ONE predicate for "did fastagent generate this Dockerfile?" — a generated file opens with the
|
|
16
|
+
* marker, so a hand-written one (or a file where the marker was dropped to take ownership) reads false.
|
|
17
|
+
* Both the "keep vs flag-as-stale" decision and the apt warning share this; a split would let them
|
|
18
|
+
* disagree on the same file (e.g. a marker not at byte 0). */
|
|
19
|
+
export declare function isGeneratedDockerfile(content: string): boolean;
|
|
20
|
+
export interface ContainerInput {
|
|
21
|
+
/** Whether the workspace has a package.json (a code workspace); else a pure markdown/skills agent. */
|
|
22
|
+
hasPackageJson: boolean;
|
|
23
|
+
/** The package manager the generated image targets: `bun` (packageManager: bun / a bun lockfile) gets an
|
|
24
|
+
* `oven/bun` base + `bun install` + `bun run fastagent`; otherwise npm on `node:22-slim`. */
|
|
25
|
+
runtime: "node" | "bun";
|
|
26
|
+
/** Whether the runtime's lockfile exists (package-lock.json for npm, bun.lock/bun.lockb for bun) — a
|
|
27
|
+
* frozen/ci install needs it; without it the build resolves versions at build time (not reproducible). */
|
|
28
|
+
hasLockfile: boolean;
|
|
29
|
+
/** For `runtime: "bun"`, the `FROM oven/bun:<tag>` version (from packageManager `bun@x`), else `"1"`. */
|
|
30
|
+
bunVersion?: string;
|
|
31
|
+
/** This fastagent version, to PIN the global install on the markdown path (reproducible redeploys). */
|
|
32
|
+
version: string;
|
|
33
|
+
/** Extra apt packages (fastagent.config deploy.apt) baked in for the agent's tools — git, ripgrep, …. */
|
|
34
|
+
apt?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Repo-as-workspace layout: the agent kit's subdirectory relative to the repo root (e.g. "agent" —
|
|
37
|
+
* `config.agentDir` resolved). When set, the image bakes the WHOLE repo as the agent's cwd (§11 bake
|
|
38
|
+
* decision), installs the KIT's deps (not the host's — the agent installs those in its workspace at
|
|
39
|
+
* runtime when its job needs them), ships `.git` (write-back via commit/push needs it), and the
|
|
40
|
+
* artifacts are namespaced under the kit so they never collide with the host repo's own
|
|
41
|
+
* Dockerfile/.dockerignore. The runtime facts (hasPackageJson/runtime/hasLockfile) describe the KIT.
|
|
42
|
+
*/
|
|
43
|
+
kitDir?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The Dockerfile + ignore artifacts — spread into any host's artifact list. Kit layout ({@link
|
|
47
|
+
* ContainerInput.kitDir}): the Dockerfile is namespaced under the kit (`agent/Dockerfile`) so it never
|
|
48
|
+
* collides with the host repo's own. The ignore ships in TWO forms because context packing is
|
|
49
|
+
* host-CLI-owned and inconsistent: (1) a ROOT `.dockerignore` — the only form flyctl/railway's own
|
|
50
|
+
* context packers reliably read (kept if the host already has one — preflight then warns specifically
|
|
51
|
+
* about a .git exclude / missing recursive node_modules) — and (2) a
|
|
52
|
+
* per-Dockerfile `agent/Dockerfile.dockerignore` for plain docker/buildx builds. Whether `.git`
|
|
53
|
+
* actually reaches the box is host-CLI-dependent (some strip it from the upload regardless of any
|
|
54
|
+
* ignore file) — the runbook's write-back note carries the runtime-clone fallback.
|
|
55
|
+
*/
|
|
56
|
+
export declare function containerArtifacts(input: ContainerInput): Artifact[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The portable container — Dockerfile + .dockerignore, host-neutral. Every deploy target ships these
|
|
3
|
+
* unchanged; only the host config (fly.toml / railway.json) and the CLI sequence differ. The image
|
|
4
|
+
* runs `fastagent start /app` on `$PORT` with state under `$FASTAGENT_STATE_DIR` — standard container
|
|
5
|
+
* conventions, no host coupling. Extracted from the Fly plan when Railway became the second target.
|
|
6
|
+
*/
|
|
7
|
+
/** Fixed first-line prefix of a fastagent-generated Dockerfile — the ONE source both the generator (the
|
|
8
|
+
* `head` below) and the "is this ours?" check (cli deploy's kept-Dockerfile apt warn) share, so they
|
|
9
|
+
* can't drift into detecting a marker the generator no longer writes. */
|
|
10
|
+
export const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
|
|
11
|
+
/** The ONE predicate for "did fastagent generate this Dockerfile?" — a generated file opens with the
|
|
12
|
+
* marker, so a hand-written one (or a file where the marker was dropped to take ownership) reads false.
|
|
13
|
+
* Both the "keep vs flag-as-stale" decision and the apt warning share this; a split would let them
|
|
14
|
+
* disagree on the same file (e.g. a marker not at byte 0). */
|
|
15
|
+
export function isGeneratedDockerfile(content) {
|
|
16
|
+
return content.startsWith(GENERATED_DOCKERFILE_MARKER);
|
|
17
|
+
}
|
|
18
|
+
/** The repo-as-workspace Dockerfile: bake the repo as cwd, install the kit's deps, run from the kit. */
|
|
19
|
+
function kitDockerfile(input, kit) {
|
|
20
|
+
const apt = aptLayer(input.apt);
|
|
21
|
+
if (!input.hasPackageJson) {
|
|
22
|
+
// Markdown-only kit: no deps to install — the pinned global CLI serves the repo directly.
|
|
23
|
+
return `${GENERATED_DOCKERFILE_MARKER}. Repo-as-workspace: the whole repo is the agent's cwd; the kit lives in ${kit}/.
|
|
24
|
+
FROM node:22-slim
|
|
25
|
+
${apt}WORKDIR /app
|
|
26
|
+
RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
27
|
+
COPY . .
|
|
28
|
+
CMD ["fastagent", "start", "/app"]
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
const isBun = input.runtime === "bun";
|
|
32
|
+
const base = isBun ? `oven/bun:${input.bunVersion ?? "1"}` : "node:22-slim";
|
|
33
|
+
const head = `${GENERATED_DOCKERFILE_MARKER}. Repo-as-workspace: the whole repo is the agent's cwd; the kit lives in ${kit}/.
|
|
34
|
+
# Only the KIT's deps are installed — the host repo's own deps are the agent's runtime concern (it can
|
|
35
|
+
# install them in its workspace when its job needs them). .git IS shipped: write-back needs it.
|
|
36
|
+
FROM ${base}
|
|
37
|
+
${apt}WORKDIR /app
|
|
38
|
+
`;
|
|
39
|
+
if (isBun) {
|
|
40
|
+
const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
|
|
41
|
+
return `${head}COPY ${kit}/package.json ${kit}/bun.lock* ./${kit}/
|
|
42
|
+
RUN cd ${kit} && ${install}
|
|
43
|
+
COPY . .
|
|
44
|
+
CMD ["sh", "-c", "cd ${kit} && bun run fastagent start /app"]
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
const install = input.hasLockfile ? "npm ci" : "npm install";
|
|
48
|
+
return `${head}COPY ${kit}/package.json ${kit}/package-lock.json* ./${kit}/
|
|
49
|
+
RUN cd ${kit} && ${install}
|
|
50
|
+
COPY . .
|
|
51
|
+
CMD ["./${kit}/node_modules/.bin/fastagent", "start", "/app"]
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
/** The apt layer (cached right after FROM). Debian default repos only. */
|
|
55
|
+
function aptLayer(packages) {
|
|
56
|
+
return packages && packages.length > 0
|
|
57
|
+
? `RUN apt-get update \\
|
|
58
|
+
&& apt-get install -y --no-install-recommends ${packages.join(" ")} \\
|
|
59
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
60
|
+
`
|
|
61
|
+
: "";
|
|
62
|
+
}
|
|
63
|
+
function dockerfile(input) {
|
|
64
|
+
// apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc.,
|
|
65
|
+
// which node:22-slim lacks. Debian default repos only — a package needing a custom repo (gh) or a
|
|
66
|
+
// different base is the operator's own Dockerfile (kept if present). deploy.apt is package-name-validated.
|
|
67
|
+
const apt = aptLayer(input.apt);
|
|
68
|
+
// No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent`
|
|
69
|
+
// from PATH. That needs npm + a global bin, which live on node:22-slim, NOT on oven/bun — so this path
|
|
70
|
+
// pins node:22-slim regardless of input.runtime (a stray bun lockfile with no package.json would
|
|
71
|
+
// otherwise select a bun base here and crash the `npm i -g` build). input.runtime is meaningful only
|
|
72
|
+
// for a code workspace, so the bun-vs-node base below is scoped to that path.
|
|
73
|
+
if (!input.hasPackageJson) {
|
|
74
|
+
return `${GENERATED_DOCKERFILE_MARKER}. The directory IS the agent — no build step.
|
|
75
|
+
# npm-based (a markdown/skills agent installs the pinned CLI globally; node:22-slim has npm).
|
|
76
|
+
FROM node:22-slim
|
|
77
|
+
${apt}WORKDIR /app
|
|
78
|
+
RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
79
|
+
COPY . .
|
|
80
|
+
CMD ["fastagent", "start", "/app"]
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
const isBun = input.runtime === "bun";
|
|
84
|
+
const base = isBun ? `oven/bun:${input.bunVersion ?? "1"}` : "node:22-slim";
|
|
85
|
+
const note = isBun
|
|
86
|
+
? "# Bun-based (packageManager: bun / a bun lockfile detected). fastagent runs under Bun (bun run)."
|
|
87
|
+
: "# npm-based — for pnpm/yarn, adapt the install line + the lockfile COPY (corepack enable, etc.).";
|
|
88
|
+
const head = `${GENERATED_DOCKERFILE_MARKER}. The directory IS the agent — no build step.
|
|
89
|
+
${note}
|
|
90
|
+
FROM ${base}
|
|
91
|
+
${apt}WORKDIR /app
|
|
92
|
+
`;
|
|
93
|
+
// Install ALL deps (no --omit=dev / --production): a repo-as-agent (e.g. an Astro site it operates on)
|
|
94
|
+
// needs its full toolchain — the build/check tools that live in devDependencies — to do its work, and
|
|
95
|
+
// we can't tell a repo-as-agent from a purpose-built workspace, so the safe default keeps everything.
|
|
96
|
+
if (isBun) {
|
|
97
|
+
// `--frozen-lockfile` needs bun.lock and hard-fails without it; fall back to a plain `bun install`
|
|
98
|
+
// (resolves at build time — not reproducible; the CLI warns to commit the lockfile).
|
|
99
|
+
// `bun run` executes the LOCAL node_modules/.bin entry only — never the registry. A bare
|
|
100
|
+
// `bunx fastagent` would fall back to installing the npm package named `fastagent`, which is an
|
|
101
|
+
// unrelated third-party package (ours is the scoped @fastagent-sh/fastagent).
|
|
102
|
+
const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
|
|
103
|
+
return `${head}COPY package.json bun.lock* ./
|
|
104
|
+
RUN ${install}
|
|
105
|
+
COPY . .
|
|
106
|
+
CMD ["bun", "run", "fastagent", "start", "/app"]
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
// `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` workspace);
|
|
110
|
+
// fall back to `npm install` when there is none so the build never breaks. Caveat: `npm install`
|
|
111
|
+
// resolves caret ranges at build time, so THIS branch is NOT reproducible — unlike the pinned
|
|
112
|
+
// `npm ci` (lockfile) and pinned global (markdown) paths. The CLI warns to commit a lockfile.
|
|
113
|
+
// The explicit local bin, not `npx fastagent`: npx falls back to fetching the npm package named
|
|
114
|
+
// `fastagent` when the dep is absent — an unrelated third-party package (ours is scoped). The local
|
|
115
|
+
// path fails fast and visibly instead.
|
|
116
|
+
const install = input.hasLockfile ? "npm ci" : "npm install";
|
|
117
|
+
return `${head}COPY package.json package-lock.json* ./
|
|
118
|
+
RUN ${install}
|
|
119
|
+
COPY . .
|
|
120
|
+
CMD ["./node_modules/.bin/fastagent", "start", "/app"]
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
const DOCKERIGNORE = `node_modules
|
|
124
|
+
.fastagent
|
|
125
|
+
.env
|
|
126
|
+
.env.*
|
|
127
|
+
!.env.example
|
|
128
|
+
*.log
|
|
129
|
+
# .git is excluded to keep the image small. If your agent runs git on its OWN history
|
|
130
|
+
# (git log/blame over the repo it ships in), delete the next line so that history is in the image.
|
|
131
|
+
.git
|
|
132
|
+
`;
|
|
133
|
+
/** The kit-layout ignore: `.git` is deliberately NOT excluded (write-back wants it — though whether it
|
|
134
|
+
* survives is host-CLI-dependent; see containerArtifacts). Patterns are RECURSIVE (`**/`) on purpose —
|
|
135
|
+
* dockerignore patterns are root-anchored (unlike .gitignore), so a bare `node_modules` would NOT
|
|
136
|
+
* exclude `agent/node_modules`: the build machine's kit deps (macOS binaries!) would be uploaded by
|
|
137
|
+
* `COPY . .` and clobber the image's freshly-installed linux ones. */
|
|
138
|
+
const KIT_DOCKERIGNORE = `**/node_modules
|
|
139
|
+
**/.fastagent
|
|
140
|
+
**/.env
|
|
141
|
+
**/.env.*
|
|
142
|
+
!**/.env.example
|
|
143
|
+
**/*.log
|
|
144
|
+
# .git is DELIBERATELY shipped: the agent's write-back (commit/push) needs the repo's history+remote.
|
|
145
|
+
`;
|
|
146
|
+
/**
|
|
147
|
+
* The Dockerfile + ignore artifacts — spread into any host's artifact list. Kit layout ({@link
|
|
148
|
+
* ContainerInput.kitDir}): the Dockerfile is namespaced under the kit (`agent/Dockerfile`) so it never
|
|
149
|
+
* collides with the host repo's own. The ignore ships in TWO forms because context packing is
|
|
150
|
+
* host-CLI-owned and inconsistent: (1) a ROOT `.dockerignore` — the only form flyctl/railway's own
|
|
151
|
+
* context packers reliably read (kept if the host already has one — preflight then warns specifically
|
|
152
|
+
* about a .git exclude / missing recursive node_modules) — and (2) a
|
|
153
|
+
* per-Dockerfile `agent/Dockerfile.dockerignore` for plain docker/buildx builds. Whether `.git`
|
|
154
|
+
* actually reaches the box is host-CLI-dependent (some strip it from the upload regardless of any
|
|
155
|
+
* ignore file) — the runbook's write-back note carries the runtime-clone fallback.
|
|
156
|
+
*/
|
|
157
|
+
export function containerArtifacts(input) {
|
|
158
|
+
if (input.kitDir) {
|
|
159
|
+
return [
|
|
160
|
+
{ path: `${input.kitDir}/Dockerfile`, content: kitDockerfile(input, input.kitDir) },
|
|
161
|
+
{ path: ".dockerignore", content: KIT_DOCKERIGNORE },
|
|
162
|
+
{ path: `${input.kitDir}/Dockerfile.dockerignore`, content: KIT_DOCKERIGNORE },
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
return [
|
|
166
|
+
{ path: "Dockerfile", content: dockerfile(input) },
|
|
167
|
+
{ path: ".dockerignore", content: DOCKERIGNORE },
|
|
168
|
+
];
|
|
169
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent deploy fly` — the Fly.io deploy PLAN, computed from the resolved definition. Pure: facts
|
|
3
|
+
* in, artifact contents + an ordered runbook out; the CLI (deploy side effects live there) writes the
|
|
4
|
+
* files and prints the runbook.
|
|
5
|
+
*
|
|
6
|
+
* fastagent owns the two ends only it can know — generate definition-aware artifacts (state root →
|
|
7
|
+
* volume, autostop tuned to the turn model, the exact secret list) and the post-deploy webhook step —
|
|
8
|
+
* and GUIDES the middle (flyctl app/volume/secrets/deploy) as a precise, values-resolved runbook. By
|
|
9
|
+
* default a coding agent (or you) runs flyctl from that runbook; `deploy fly --run` drives it from the
|
|
10
|
+
* CLI instead. This module stays pure either way — it produces the plan, never runs flyctl. The runbook
|
|
11
|
+
* is a FIRST-deploy sequence: `apps`/`volumes create` are one-time (marked so — re-running would make a
|
|
12
|
+
* second volume, the state split it warns against); a redeploy is `fly deploy` alone.
|
|
13
|
+
*
|
|
14
|
+
* autostop = "suspend": the machine snapshots and suspends when idle (Fly Proxy sees inbound load 0),
|
|
15
|
+
* resumes on the next webhook in ~hundreds of ms. A long turn interrupted by an idle-suspend whose
|
|
16
|
+
* snapshot is discarded replays on the next start (the Telegram L1 turn store) — at-least-once, the
|
|
17
|
+
* documented floor. State on the /data volume survives stop/suspend on the same machine.
|
|
18
|
+
*/
|
|
19
|
+
import type { ChannelKind } from "../../scaffold/add-channel.ts";
|
|
20
|
+
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
21
|
+
export interface FlyPlanInput extends ContainerInput {
|
|
22
|
+
/** Fly app name — globally unique, lowercase; the CLI sanitizes it from the dir basename. */
|
|
23
|
+
appName: string;
|
|
24
|
+
/** The port the app listens on (config.http.port ?? 8787); fly.toml routes to it. */
|
|
25
|
+
port: number;
|
|
26
|
+
/**
|
|
27
|
+
* What satisfies model auth locally ({@link probeAuthSource}): an env-var name (`OPENAI_API_KEY`),
|
|
28
|
+
* `"OAuth"`/`"stored credential"` (a local login the server can't use), or undefined (unconfigured).
|
|
29
|
+
*/
|
|
30
|
+
modelAuth: string | undefined;
|
|
31
|
+
/** Channels discovered in the workspace — each contributes its required secrets + webhook step. */
|
|
32
|
+
channels: ChannelKind[];
|
|
33
|
+
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
34
|
+
extraSecrets?: string[];
|
|
35
|
+
/** `auto_stop_machines` — `"suspend"` (default, fast resume) or `"stop"` (cold start). CLI `--stop`. */
|
|
36
|
+
autostop: "suspend" | "stop";
|
|
37
|
+
/** Allow scaling to zero when idle (default true → `min_machines_running=0`). CLI `--no-scale-to-zero`
|
|
38
|
+
* forces one machine up; a github channel forces it too (fire-and-forget turns have no replay). */
|
|
39
|
+
scaleToZero: boolean;
|
|
40
|
+
/** Time triggers present (schedules/ or selfSchedule) — forces one machine up: cron/wake has no
|
|
41
|
+
* external wake-up, so a scaled-to-zero box would sleep through them. */
|
|
42
|
+
hasTimeTriggers: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface FlyPlan {
|
|
45
|
+
/** fly.toml / Dockerfile / .dockerignore — written by the CLI (skipped if present unless --force). */
|
|
46
|
+
artifacts: Artifact[];
|
|
47
|
+
/** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
|
|
48
|
+
runbook: string[];
|
|
49
|
+
}
|
|
50
|
+
/** Compute the Fly deploy plan from the resolved definition. */
|
|
51
|
+
export declare function planFlyDeploy(input: FlyPlanInput): FlyPlan;
|
|
52
|
+
/**
|
|
53
|
+
* The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent — the KEEP-mode
|
|
54
|
+
* single source (a user who renamed the app is not overridden by the basename guess). Accepts TOML's
|
|
55
|
+
* double OR single quotes; anything else (or no `app`) is undefined → the caller falls back to basename.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseFlyAppName(toml: string): string | undefined;
|
|
58
|
+
/** The `primary_region` from a fly.toml, or undefined — `--run` passes it to `fly volumes create` so the
|
|
59
|
+
* volume lands in the machine's region (fly.toml is the single source; see {@link parseFlyAppName}). */
|
|
60
|
+
export declare function parseFlyRegion(toml: string): string | undefined;
|
|
61
|
+
/** The `min_machines_running` from a fly.toml, or undefined — the KEEP-mode check reads it so a kept
|
|
62
|
+
* file that still scales to zero can be warned about when time triggers (schedules/wake) exist. */
|
|
63
|
+
export declare function parseFlyMinMachines(toml: string): number | undefined;
|
|
64
|
+
/** Sanitize a directory basename into a Fly app name: lowercase, [a-z0-9-], must start with a letter. */
|
|
65
|
+
export declare function toFlyAppName(basename: string): string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { containerArtifacts } from "../container.js";
|
|
2
|
+
import { isEnvKey, requiredSecrets } from "../secrets.js";
|
|
3
|
+
function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers) {
|
|
4
|
+
// min_machines_running: 1 (keep one up) when a github channel is present, TIME triggers exist, OR the
|
|
5
|
+
// operator opted out of scale-to-zero. GitHub's is a SAFETY default — its fire-and-forget turns have no
|
|
6
|
+
// replay, so scaling to zero could drop an in-flight review. Time triggers (schedules/wake) have no
|
|
7
|
+
// external wake-up at all — a scaled-to-zero box sleeps through the cron instant. Reason-tagged so the
|
|
8
|
+
// comment is honest.
|
|
9
|
+
const min = hasGithub
|
|
10
|
+
? ` min_machines_running = 1 # github turns have no replay — don't scale to zero (an in-flight review would be lost)`
|
|
11
|
+
: hasTimeTriggers
|
|
12
|
+
? ` min_machines_running = 1 # schedules/wake-ups need a running machine (no external wake-up for a cron instant)`
|
|
13
|
+
: !scaleToZero
|
|
14
|
+
? ` min_machines_running = 1 # kept running (--no-scale-to-zero)`
|
|
15
|
+
: ` min_machines_running = 0 # scale to zero`;
|
|
16
|
+
const stopLine = autostop === "stop"
|
|
17
|
+
? ` auto_stop_machines = "stop" # stop on idle (cold start on the next webhook)`
|
|
18
|
+
: ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
|
|
19
|
+
return `# Generated by \`fastagent deploy fly\`. Edit freely — it is not regenerated unless you pass --force.
|
|
20
|
+
app = "${appName}"
|
|
21
|
+
primary_region = "iad" # set your region (list: \`fly platform regions\`)
|
|
22
|
+
|
|
23
|
+
[build]
|
|
24
|
+
|
|
25
|
+
[env]
|
|
26
|
+
FASTAGENT_STATE_DIR = "/data" # the ONE machine-state root — auth, sessions, channel state
|
|
27
|
+
PORT = "${port}"
|
|
28
|
+
|
|
29
|
+
[http_service]
|
|
30
|
+
internal_port = ${port}
|
|
31
|
+
force_https = true
|
|
32
|
+
${stopLine}
|
|
33
|
+
auto_start_machines = true
|
|
34
|
+
${min}
|
|
35
|
+
|
|
36
|
+
[mounts]
|
|
37
|
+
source = "data"
|
|
38
|
+
destination = "/data" # FASTAGENT_STATE_DIR — persists across stop/suspend/redeploy
|
|
39
|
+
|
|
40
|
+
[[vm]]
|
|
41
|
+
size = "shared-cpu-1x"
|
|
42
|
+
memory = "512mb" # suspend is not recommended above 2 GB
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
/** Compute the Fly deploy plan from the resolved definition. */
|
|
46
|
+
export function planFlyDeploy(input) {
|
|
47
|
+
const { appName, port, modelAuth, channels, kitDir } = input;
|
|
48
|
+
// Kit layout: every artifact is namespaced under the kit (agent/fly.toml, agent/Dockerfile) so the
|
|
49
|
+
// host repo's own deploy files are never touched; the runbook passes explicit -c/--dockerfile flags
|
|
50
|
+
// (unambiguous across flyctl versions — no reliance on config-relative path resolution).
|
|
51
|
+
const flyTomlPath = kitDir ? `${kitDir}/fly.toml` : "fly.toml";
|
|
52
|
+
const artifacts = [
|
|
53
|
+
{
|
|
54
|
+
path: flyTomlPath,
|
|
55
|
+
content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers),
|
|
56
|
+
},
|
|
57
|
+
...containerArtifacts(input),
|
|
58
|
+
];
|
|
59
|
+
// The exact secret list the deployed machine needs, computed from the definition (host-neutral): the
|
|
60
|
+
// model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
|
|
61
|
+
// COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
|
|
62
|
+
// `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
|
|
63
|
+
const secrets = requiredSecrets(modelAuth, channels, input.extraSecrets);
|
|
64
|
+
const deployCmd = kitDir
|
|
65
|
+
? `fly deploy . --config ${kitDir}/fly.toml --dockerfile ${kitDir}/Dockerfile --app ${appName}`
|
|
66
|
+
: `fly deploy --app ${appName}`;
|
|
67
|
+
const runbook = [
|
|
68
|
+
`# Deploy "${appName}" to Fly.io. ${flyTomlPath} / Dockerfile(.dockerignore) are generated above.`,
|
|
69
|
+
`# Prereqs: flyctl installed (https://fly.io/docs/flyctl/install) and \`fly auth login\`.`,
|
|
70
|
+
``,
|
|
71
|
+
`# One-time setup (skip on a redeploy — a second run makes a SECOND app/volume, splitting state).`,
|
|
72
|
+
`# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
|
|
73
|
+
`# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
|
|
74
|
+
`fly apps create ${appName}`,
|
|
75
|
+
`# volume persists FASTAGENT_STATE_DIR=/data (sessions, auth, channel state) across stop/suspend/redeploy.`,
|
|
76
|
+
`# <region> MUST equal primary_region in fly.toml (a volume in another region can't mount) — fly.toml`,
|
|
77
|
+
`# is the single source for the region; skip this if the volume exists (fly volumes list --app ${appName}):`,
|
|
78
|
+
`fly volumes create data --app ${appName} --region <region> --size 1`,
|
|
79
|
+
];
|
|
80
|
+
if (secrets.length > 0) {
|
|
81
|
+
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
|
+
}
|
|
83
|
+
if (kitDir) {
|
|
84
|
+
runbook.push(``, `# Repo-as-workspace: the build context is the REPO ROOT (the whole repo is the agent's cwd); the`, `# config/Dockerfile live under ${kitDir}/ so they never collide with the repo's own deploy files.`, `# Run this from the repo root:`);
|
|
85
|
+
}
|
|
86
|
+
runbook.push(deployCmd);
|
|
87
|
+
if (kitDir) {
|
|
88
|
+
runbook.push(``, `# Write-back mechanics: git ships in the image and GH_TOKEN-style creds ride config.deploy.secrets;`, `# the POLICY (push vs PR, identity, remote) lives in persona.md. CAVEAT — whether .git survives is`, `# host-CLI-dependent (some context packers strip it from the upload): verify \`git status\` on the`, `# box after the first deploy; if it is missing, have the agent \`git clone\` its repo in the`, `# workspace instead (same token). Un-pushed changes never survive a redeploy — the image is a`, `# snapshot; durability lives in git.`);
|
|
89
|
+
}
|
|
90
|
+
// Model-auth guidance: an env key becomes a secret above. Otherwise the plan can't read the local
|
|
91
|
+
// credential's VALUE to set as a secret — true for OAuth AND a stored API key (both are
|
|
92
|
+
// `AuthResult.source` non-env labels), so the wording doesn't prejudge whether it's migratable.
|
|
93
|
+
if (!isEnvKey(modelAuth)) {
|
|
94
|
+
runbook.push(``, modelAuth === undefined
|
|
95
|
+
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/auth.json), or \`--run\` carries it automatically.`
|
|
96
|
+
: `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json on the /data volume.`);
|
|
97
|
+
}
|
|
98
|
+
// The fastagent-only post-step: point each channel's webhook at the live URL. Only fastagent knows the routes.
|
|
99
|
+
// The URLs below assume each channel's DEFAULT route key (POST /telegram, POST /webhook). Reading the
|
|
100
|
+
// real key would mean executing the channel factory (getMe, state-dir creation) — wrong at plan time
|
|
101
|
+
// — so the runbook states the assumption instead of silently printing a stale path for remapped glue.
|
|
102
|
+
const post = [];
|
|
103
|
+
if (channels.includes("telegram")) {
|
|
104
|
+
post.push(`# After deploy — register the Telegram webhook. The path assumes the default route (POST /telegram);`, `# if you remapped it in channels/telegram.ts, use your path. secret_token MUST equal TELEGRAM_SECRET_TOKEN:`, `curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \\`, ` -d url=https://${appName}.fly.dev/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`);
|
|
105
|
+
}
|
|
106
|
+
if (channels.includes("github")) {
|
|
107
|
+
post.push(`# After deploy — set the GitHub webhook (repo Settings → Webhooks). Path assumes the default route`, `# (POST /webhook); if you remapped it in channels/github.ts, use your path:`, `# Payload URL = https://${appName}.fly.dev/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
|
|
108
|
+
}
|
|
109
|
+
if (post.length > 0)
|
|
110
|
+
runbook.push(``, ...post);
|
|
111
|
+
// Single-machine tier: state lives on ONE volume tied to ONE machine. Scaling to multiple machines
|
|
112
|
+
// splits state (each gets its own volume) — that needs a shared/external backend, not this recipe.
|
|
113
|
+
runbook.push(``, `# Keep this a SINGLE machine: the /data volume (and all state on it) is tied to one machine.`, `# Multiple machines would each get their own volume and split sessions/turns — don't scale past 1.`);
|
|
114
|
+
return { artifacts, runbook };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent — the KEEP-mode
|
|
118
|
+
* single source (a user who renamed the app is not overridden by the basename guess). Accepts TOML's
|
|
119
|
+
* double OR single quotes; anything else (or no `app`) is undefined → the caller falls back to basename.
|
|
120
|
+
*/
|
|
121
|
+
export function parseFlyAppName(toml) {
|
|
122
|
+
return toml.match(/^\s*app\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
123
|
+
}
|
|
124
|
+
/** The `primary_region` from a fly.toml, or undefined — `--run` passes it to `fly volumes create` so the
|
|
125
|
+
* volume lands in the machine's region (fly.toml is the single source; see {@link parseFlyAppName}). */
|
|
126
|
+
export function parseFlyRegion(toml) {
|
|
127
|
+
return toml.match(/^\s*primary_region\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
128
|
+
}
|
|
129
|
+
/** The `min_machines_running` from a fly.toml, or undefined — the KEEP-mode check reads it so a kept
|
|
130
|
+
* file that still scales to zero can be warned about when time triggers (schedules/wake) exist. */
|
|
131
|
+
export function parseFlyMinMachines(toml) {
|
|
132
|
+
const m = toml.match(/^\s*min_machines_running\s*=\s*(\d+)/m)?.[1];
|
|
133
|
+
return m === undefined ? undefined : Number(m);
|
|
134
|
+
}
|
|
135
|
+
/** Sanitize a directory basename into a Fly app name: lowercase, [a-z0-9-], must start with a letter. */
|
|
136
|
+
export function toFlyAppName(basename) {
|
|
137
|
+
const slug = basename
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
140
|
+
.replace(/^-+|-+$/g, "");
|
|
141
|
+
return /^[a-z]/.test(slug) ? slug : `app-${slug || "agent"}`;
|
|
142
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent deploy fly --run` — drive flyctl to completion. The middle of the deploy (app / volume /
|
|
3
|
+
* secrets / deploy) that the plain runbook hands to the operator; `--run` executes it instead, so a
|
|
4
|
+
* coding agent runs ONE command. Idempotent (app/volume check-then-act; channel secrets come from the
|
|
5
|
+
* local env — NOT minted — so a re-run sets the same values) and resumable: it STOPS at a human gate
|
|
6
|
+
* (not logged in, a missing secret value, a taken app name) with one actionable line and a non-zero
|
|
7
|
+
* exit, so the agent clears the gate and re-runs. A `generate` channel secret absent from `.env` is a
|
|
8
|
+
* gate too (`missingSecrets`), not a silent mint — fill it in `.env` (use the random string that
|
|
9
|
+
* `add <channel>` prints).
|
|
10
|
+
*
|
|
11
|
+
* flyctl is behind the shared {@link CliRunner} seam — production spawns `fly`, tests inject a fake that
|
|
12
|
+
* records the command sequence and scripts outputs. That seam is the benchmark: the agent's journey
|
|
13
|
+
* encoded as an asserted command sequence + gate behavior, validated without a real Fly account.
|
|
14
|
+
*
|
|
15
|
+
* Non-interactive incantations the agent would otherwise get wrong: `--remote-only` (build on Fly's
|
|
16
|
+
* builders — no local Docker in a sandbox), `--yes` (no prompts), `--ha=false` + the mounted volume
|
|
17
|
+
* (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
|
|
18
|
+
* never land in argv/process listings.
|
|
19
|
+
*/
|
|
20
|
+
import type { ChannelKind } from "../../scaffold/add-channel.ts";
|
|
21
|
+
import type { CliRunner } from "../runner.ts";
|
|
22
|
+
/**
|
|
23
|
+
* The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
|
|
24
|
+
* FASTAGENT_AUTH_SEED materialization. ABSENT-ONLY by design: a present file (a refreshed volume copy)
|
|
25
|
+
* is never overwritten by the stale seed, so a box that ran its own OAuth refresh is not rolled back.
|
|
26
|
+
*/
|
|
27
|
+
export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
|
|
28
|
+
export interface FlyRunPlan {
|
|
29
|
+
appName: string;
|
|
30
|
+
region: string;
|
|
31
|
+
/** `KEY=value` secrets to set on Fly: model key (env auth) or `FASTAGENT_AUTH_SEED` (file auth) +
|
|
32
|
+
* channel secrets. Set via stdin, never argv. */
|
|
33
|
+
secrets: Record<string, string>;
|
|
34
|
+
/** Required secret names with NO local value — the run gates on these before any side effect. */
|
|
35
|
+
missingSecrets: string[];
|
|
36
|
+
channels: ChannelKind[];
|
|
37
|
+
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace dir). */
|
|
38
|
+
flyConfig: string;
|
|
39
|
+
}
|
|
40
|
+
/** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
|
|
41
|
+
export type FlyRunOutcome = {
|
|
42
|
+
ok: true;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
gate: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)` performs the
|
|
49
|
+
* post-deploy webhook step (the CLI passes its telegram registrar). Every gate is fail-visible.
|
|
50
|
+
*/
|
|
51
|
+
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<void>): Promise<FlyRunOutcome>;
|