Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .codex/local-environment.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version = 1
name = "SimDeck"

[setup]
script = '''
cd "$CODEX_WORKTREE_PATH"
npm run codex:setup
'''

[cleanup]
script = '''
cd "$CODEX_WORKTREE_PATH"
npm run codex:cache:save
'''

[[actions]]
name = "Build and Restart Daemon"
icon = "run"
command = '''
cd "$CODEX_WORKTREE_PATH"
npm run codex:run
'''
platform = "darwin"
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ npm run package:vscode

This now builds the Rust server in `server/` and copies the resulting binary to `build/simdeck`.

Codex worktrees can use the checked-in local environment config at
`.codex/local-environment.toml`. Its setup runs `npm run codex:setup`, which
hydrates root/client `node_modules` and `server/target` from the shared cache
under `~/.cache/simdeck/codex-worktree-cache` or from another SimDeck checkout
before falling back to `npm ci` for missing package installs and ensuring
Homebrew `pkgconf`/`x264` are available for native builds. Its Run action
executes `npm run codex:run`, which builds the CLI and client, saves fresh
caches, and restarts the workspace-local daemon.

Run the local daemon:

```sh
Expand Down
34 changes: 34 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,40 @@ npx skills add NativeScript/SimDeck --skill simdeck -a codex -g

The npm postinstall message also prints this command after a global install.

## Codex local worktrees

This repo includes a Codex local environment at
`.codex/local-environment.toml`. Use it when creating Codex worktrees for
SimDeck. The setup script runs:

```sh
npm run codex:setup
```

That hydrates the root `node_modules`, `client/node_modules`, and
`server/target` from `~/.cache/simdeck/codex-worktree-cache` or a matching
existing SimDeck checkout. If either `node_modules` directory is still missing,
it falls back to `npm ci` for that package so lockfiles stay unchanged. On
macOS it also ensures the Homebrew `pkgconf` and `x264` packages are available
for the native Rust build. Set
`SIMDECK_CODEX_SKIP_BREW=1` if you want setup to report missing Homebrew
packages instead of installing them.

The cleanup script saves fresh caches with:

```sh
npm run codex:cache:save
```

The environment also exposes a **Build and Restart Daemon** Run action:

```sh
npm run codex:run
```

It builds the Rust CLI and React client, saves the refreshed caches, and runs
`./build/simdeck daemon restart` for the current workspace.

## Releasing

Releases are published from the `Release` GitHub Actions workflow at
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
"test:studio-provider": "node --test scripts/studio-provider-bridge.test.mjs scripts/studio-host-provider.test.mjs",
"test:stress": "node scripts/stress/simdeck.mjs",
"bench:encoder:build": "scripts/bench/build-encoder-benchmark.sh",
"codex:setup": "node scripts/codex-setup.mjs",
"codex:cache:save": "node scripts/codex-worktree-cache.mjs save --best-effort",
"codex:run": "node scripts/codex-run.mjs",
"ci": "npm run lint && npm run build:all && npm run test && npm run package:vscode-extension",
"dev": "npm run build:cli && node scripts/dev.mjs",
"preview:swiftui": "node scripts/experimental/swiftui-preview.mjs",
Expand Down
50 changes: 50 additions & 0 deletions scripts/codex-run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");

run("node", ["scripts/codex-setup.mjs", "--skip-npm"]);
run("npm", ["run", "build:cli"]);
run("npm", ["run", "build:client"]);
run("node", ["scripts/codex-worktree-cache.mjs", "save", "--best-effort"]);

const daemonArgs = ["daemon", "restart"];
pushOptionalEnv(daemonArgs, "--port", "SIMDECK_DAEMON_PORT");
pushOptionalEnv(daemonArgs, "--bind", "SIMDECK_DAEMON_BIND");
pushOptionalEnv(daemonArgs, "--advertise-host", "SIMDECK_ADVERTISE_HOST");
pushOptionalEnv(daemonArgs, "--video-codec", "SIMDECK_VIDEO_CODEC");
pushOptionalEnv(daemonArgs, "--stream-quality", "SIMDECK_STREAM_QUALITY");
pushOptionalEnv(daemonArgs, "--local-stream-fps", "SIMDECK_LOCAL_STREAM_FPS");
if (truthy(process.env.SIMDECK_LOW_LATENCY)) {
daemonArgs.push("--low-latency");
}

run("./build/simdeck", daemonArgs);

function run(command, args) {
console.log(`\n$ ${[command, ...args].join(" ")}`);
const result = spawnSync(command, args, {
cwd: ROOT,
stdio: "inherit",
env: process.env,
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

function pushOptionalEnv(args, flag, envName) {
const value = process.env[envName]?.trim();
if (value) {
args.push(flag, value);
}
}

function truthy(value) {
return value === "1" || value === "true" || value === "yes";
}
105 changes: 105 additions & 0 deletions scripts/codex-setup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { readdirSync, statSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const args = process.argv.slice(2);
const skipNpm = args.includes("--skip-npm");
const skipCache = args.includes("--skip-cache");

if (!skipCache) {
run("node", ["scripts/codex-worktree-cache.mjs", "hydrate"]);
}

ensureNativeBuildDependencies();

if (!skipNpm) {
ensureNodeModules(".", "root");
ensureNodeModules("client", "client");
}

function ensureNativeBuildDependencies() {
if (process.platform !== "darwin") {
return;
}

if (!commandSucceeds("pkg-config", ["--version"])) {
installBrewPackage("pkgconf", "pkg-config");
}

if (!commandSucceeds("pkg-config", ["--exists", "x264"])) {
installBrewPackage("x264", "x264 pkg-config metadata");
}
}

function installBrewPackage(formula, label) {
if (truthy(process.env.SIMDECK_CODEX_SKIP_BREW)) {
throw new Error(
`Missing ${label}. Install it with \`brew install ${formula}\` or unset SIMDECK_CODEX_SKIP_BREW.`,
);
}
if (!commandSucceeds("brew", ["--version"])) {
throw new Error(
`Missing ${label}, and Homebrew is not available to install ${formula}.`,
);
}
run("brew", ["install", formula]);
}

function ensureNodeModules(prefix, label) {
const modulesPath =
prefix === "."
? resolve(ROOT, "node_modules")
: resolve(ROOT, prefix, "node_modules");
if (existsAndHasContent(modulesPath)) {
console.log(
`[setup] skip ${label} npm install; node_modules already exists`,
);
return;
}

const args = prefix === "." ? ["ci"] : ["ci", "--prefix", prefix];
run("npm", args);
}

function existsAndHasContent(path) {
try {
const stats = statSync(path);
if (stats.isDirectory()) {
return readdirSync(path).length > 0;
}
return stats.size > 0;
} catch {
return false;
}
}

function commandSucceeds(command, args) {
const result = spawnSync(command, args, {
cwd: ROOT,
stdio: "ignore",
env: process.env,
});
return result.status === 0;
}

function run(command, args) {
console.log(`\n$ ${[command, ...args].join(" ")}`);
const result = spawnSync(command, args, {
cwd: ROOT,
stdio: "inherit",
env: process.env,
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

function truthy(value) {
return value === "1" || value === "true" || value === "yes";
}
Loading
Loading