Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/production-source-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Production Source Guard

on:
pull_request:
branches:
- production

jobs:
require-main-source:
name: Require main as PR source
runs-on: ubuntu-latest
steps:
- name: Validate source branch
run: |
if [ "${{ github.head_ref }}" != "main" ]; then
echo "Production promotions must come from main; received '${{ github.head_ref }}'." >&2
exit 1
fi
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
- All of `bun fmt`, `bun lint`, and `bun typecheck` must pass before considering tasks completed.
- NEVER run `bun test`. Always use `bun run test` (runs Vitest).

## Bun Gotcha

- In this environment, `bun` may not be on `PATH` even though Bun is installed at `/home/claude/.bun/bin/bun`.
- If plain `bun ...` fails with `bun: command not found`, use the absolute binary path instead.
- For `bun typecheck`, also prepend Bun to `PATH` so Turbo can find the package manager binary:
`env PATH="/home/claude/.bun/bin:$PATH" /home/claude/.bun/bin/bun typecheck`
- `bun fmt` and `bun lint` can be run directly with `/home/claude/.bun/bin/bun fmt` and `/home/claude/.bun/bin/bun lint`.

## Project Snapshot

T3 Code is a minimal web GUI for using code agents like Codex and Claude Code (coming soon).
Expand Down
2 changes: 2 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"effect": "catalog:",
"node-pty": "^1.1.0",
"open": "^10.1.0",
"web-push": "^3.6.7",
"ws": "^8.18.0"
},
"devDependencies": {
Expand All @@ -38,6 +39,7 @@
"@t3tools/web": "workspace:*",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@types/web-push": "^3.6.4",
"@types/ws": "^8.5.13",
"tsdown": "catalog:",
"typescript": "catalog:",
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export interface ServerConfigShape {
readonly authToken: string | undefined;
readonly autoBootstrapProjectFromCwd: boolean;
readonly logWebSocketEvents: boolean;
readonly webPushVapidPublicKey: string | undefined;
readonly webPushVapidPrivateKey: string | undefined;
readonly webPushSubject: string | undefined;
}

/**
Expand All @@ -54,6 +57,9 @@ export class ServerConfig extends ServiceMap.Service<ServerConfig, ServerConfigS
staticDir: undefined,
devUrl: undefined,
noBrowser: false,
webPushVapidPublicKey: undefined,
webPushVapidPrivateKey: undefined,
webPushSubject: undefined,
};
}),
);
Expand Down
19 changes: 9 additions & 10 deletions apps/server/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,16 @@ const runCli = (
env: Record<string, string> = { T3CODE_NO_BROWSER: "true" },
) => {
const uniqueStateDir = `/tmp/t3-cli-state-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const envLayer = ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
T3CODE_STATE_DIR: uniqueStateDir,
...env,
},
}),
);
return Command.runWith(t3Cli, { version: "0.0.0-test" })(args).pipe(
Effect.provide(
ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
T3CODE_STATE_DIR: uniqueStateDir,
...env,
},
}),
),
),
Effect.provide(Layer.mergeAll(testLayer, envLayer)),
);
};

Expand Down
64 changes: 63 additions & 1 deletion apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ interface CliInput {
readonly devUrl: Option.Option<URL>;
readonly noBrowser: Option.Option<boolean>;
readonly authToken: Option.Option<string>;
readonly webPushVapidPublicKey: Option.Option<string>;
readonly webPushVapidPrivateKey: Option.Option<string>;
readonly webPushSubject: Option.Option<string>;
readonly autoBootstrapProjectFromCwd: Option.Option<boolean>;
readonly logWebSocketEvents: Option.Option<boolean>;
}
Expand Down Expand Up @@ -112,6 +115,18 @@ const CliEnvConfig = Config.all({
Config.option,
Config.map(Option.getOrUndefined),
),
webPushVapidPublicKey: Config.string("T3CODE_WEB_PUSH_VAPID_PUBLIC_KEY").pipe(
Config.option,
Config.map(Option.getOrUndefined),
),
webPushVapidPrivateKey: Config.string("T3CODE_WEB_PUSH_VAPID_PRIVATE_KEY").pipe(
Config.option,
Config.map(Option.getOrUndefined),
),
webPushSubject: Config.string("T3CODE_WEB_PUSH_SUBJECT").pipe(
Config.option,
Config.map(Option.getOrUndefined),
),
autoBootstrapProjectFromCwd: Config.boolean("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe(
Config.option,
Config.map(Option.getOrUndefined),
Expand Down Expand Up @@ -158,6 +173,11 @@ const ServerConfigLive = (input: CliInput) =>
const devUrl = Option.getOrElse(input.devUrl, () => env.devUrl);
const noBrowser = resolveBooleanFlag(input.noBrowser, env.noBrowser ?? mode === "desktop");
const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken;
const webPushVapidPublicKey =
Option.getOrUndefined(input.webPushVapidPublicKey) ?? env.webPushVapidPublicKey;
const webPushVapidPrivateKey =
Option.getOrUndefined(input.webPushVapidPrivateKey) ?? env.webPushVapidPrivateKey;
const webPushSubject = Option.getOrUndefined(input.webPushSubject) ?? env.webPushSubject;
const autoBootstrapProjectFromCwd = resolveBooleanFlag(
input.autoBootstrapProjectFromCwd,
env.autoBootstrapProjectFromCwd ?? mode === "web",
Expand Down Expand Up @@ -185,6 +205,9 @@ const ServerConfigLive = (input: CliInput) =>
devUrl,
noBrowser,
authToken,
webPushVapidPublicKey,
webPushVapidPrivateKey,
webPushSubject,
autoBootstrapProjectFromCwd,
logWebSocketEvents,
} satisfies ServerConfigShape;
Expand Down Expand Up @@ -243,6 +266,22 @@ const makeServerProgram = (input: CliInput) =>
yield* cliConfig.fixPath;

const config = yield* ServerConfig;
const configuredWebPushFieldCount = [
config.webPushVapidPublicKey,
config.webPushVapidPrivateKey,
config.webPushSubject,
].filter((value) => typeof value === "string" && value.length > 0).length;

if (configuredWebPushFieldCount > 0 && configuredWebPushFieldCount < 3) {
yield* Effect.logWarning(
"web push configuration is incomplete; push notifications disabled",
{
hasPublicKey: Boolean(config.webPushVapidPublicKey),
hasPrivateKey: Boolean(config.webPushVapidPrivateKey),
hasSubject: Boolean(config.webPushSubject),
},
);
}

if (!config.devUrl && !config.staticDir) {
yield* Effect.logWarning(
Expand All @@ -261,11 +300,19 @@ const makeServerProgram = (input: CliInput) =>
config.host && !isWildcardHost(config.host)
? `http://${formatHostForUrl(config.host)}:${config.port}`
: localUrl;
const { authToken, devUrl, ...safeConfig } = config;
const {
authToken,
devUrl,
webPushVapidPublicKey: _webPushVapidPublicKey,
webPushVapidPrivateKey: _webPushVapidPrivateKey,
webPushSubject: _webPushSubject,
...safeConfig
} = config;
yield* Effect.logInfo("T3 Code running", {
...safeConfig,
devUrl: devUrl?.toString(),
authEnabled: Boolean(authToken),
webPushEnabled: configuredWebPushFieldCount === 3,
});

if (!config.noBrowser) {
Expand Down Expand Up @@ -317,6 +364,18 @@ const authTokenFlag = Flag.string("auth-token").pipe(
Flag.withAlias("token"),
Flag.optional,
);
const webPushVapidPublicKeyFlag = Flag.string("web-push-vapid-public-key").pipe(
Flag.withDescription("VAPID public key used for Web Push."),
Flag.optional,
);
const webPushVapidPrivateKeyFlag = Flag.string("web-push-vapid-private-key").pipe(
Flag.withDescription("VAPID private key used for Web Push."),
Flag.optional,
);
const webPushSubjectFlag = Flag.string("web-push-subject").pipe(
Flag.withDescription("VAPID subject used for Web Push."),
Flag.optional,
);
const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe(
Flag.withDescription(
"Create a project for the current working directory on startup when missing.",
Expand All @@ -339,6 +398,9 @@ export const t3Cli = Command.make("t3", {
devUrl: devUrlFlag,
noBrowser: noBrowserFlag,
authToken: authTokenFlag,
webPushVapidPublicKey: webPushVapidPublicKeyFlag,
webPushVapidPrivateKey: webPushVapidPrivateKeyFlag,
webPushSubject: webPushSubjectFlag,
autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag,
logWebSocketEvents: logWebSocketEventsFlag,
}).pipe(
Expand Down
Loading