Files
project-work/worker-toolkit-stocks-in-the-future/explore/.devcontainer/initialize.js
Eric Bell 2854619bc9 chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
2026-08-11 14:44:09 -04:00

163 lines
6.8 KiB
JavaScript

#!/usr/bin/env node
// Runs on the HOST before the container starts.
// Validates prerequisites and sets up files that the container needs
// without using ../ bind mounts (which break on newer Docker runtimes).
//
// This is Node.js (not bash) so it works on Windows without WSL.
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
// The devcontainer CLI runs initializeCommand from the workspace folder (explore/).
// Use CWD, not __dirname, so this works both in production and in tests.
if (!fs.existsSync('../.env')) {
console.error(`
❌ Missing .env file. Create it first:
Create a file named .env in the toolkit root with:
ANTHROPIC_API_KEY=your-key-here
ANTHROPIC_BASE_URL=https://app-llmproxy.dataannotation.tech/api/llm_proxy/raccoon
`);
process.exit(1);
}
// Copy small files from toolkit root into explore/ so the container
// can access them without ../ bind mounts.
fs.copyFileSync('../.env', '.env');
try {
fs.copyFileSync('../toolkit.json', 'toolkit.json');
} catch {}
// Link repo so the bind mount source stays within explore/.
// Use a junction on Windows (Docker Desktop can't follow symlinks,
// but it can follow junctions). On macOS/Linux, 'junction' is ignored
// and creates a regular symlink.
// Single-repo toolkits have ../repo; polyglot toolkits have ../repos (the member
// clones) instead. Link whichever exists so the matching bind mount resolves.
if (fs.existsSync('../repo') && !fs.existsSync('repo')) {
fs.symlinkSync(path.resolve('../repo'), 'repo', 'junction');
}
if (fs.existsSync('../repos') && !fs.existsSync('repos')) {
fs.symlinkSync(path.resolve('../repos'), 'repos', 'junction');
}
// Reference-data corpus lives at the toolkit root (../data, zeta toolkits only).
// Link it under explore/ so the corpus bind mount's source stays within explore/
// (../ bind mounts break on newer Docker runtimes; a resolved symlink/junction works,
// exactly as for repo/repos above). Only present when this toolkit ships a corpus.
if (fs.existsSync('../data') && !fs.existsSync('data')) {
fs.symlinkSync(path.resolve('../data'), 'data', 'junction');
}
// A named extra instance (EXPLORE_INSTANCE set, normally by instance.js) gets
// its OWN repo working tree, mounted at /workspace/repo in that container, so a
// `git checkout` in one instance doesn't disturb another. A `git clone --local`
// hardlinks the object store, so this is cheap and fully self-contained — unlike
// a git worktree, whose gitdir lives inside the source repo and so wouldn't
// bind-mount into the container. The devcontainer.json mount derives the dir
// name from EXPLORE_INSTANCE (repo<instance>); create it before that mount binds.
// post-create.sh then checks out the default commit + runs setup in the new
// container, exactly as it does for the primary repo.
const instance = process.env.EXPLORE_INSTANCE || '';
if (instance) {
try {
if (fs.existsSync('../repos')) {
// Polyglot toolkit: give the instance its OWN copy of every member repo at
// repos<instance>/<member>, mounted at /workspace/repos. A git clone --local
// hardlinks each member's object store, so this is cheap and fully isolated —
// a member checkout in one instance never disturbs another.
const dir = `repos${instance}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
for (const member of fs.readdirSync(path.resolve('../repos'))) {
const src = path.resolve('../repos', member);
if (!fs.statSync(src).isDirectory()) continue;
execSync(
`git clone --local ${JSON.stringify(src)} ${JSON.stringify(path.join(dir, member))}`,
{
stdio: 'inherit',
}
);
}
}
} else {
// Single-repo toolkit: clone repo → repo<instance>, mounted at /workspace/repo.
const dir = `repo${instance}`;
if (!fs.existsSync(dir)) {
execSync(
`git clone --local ${JSON.stringify(path.resolve('../repo'))} ${JSON.stringify(dir)}`,
{
stdio: 'inherit',
}
);
}
}
} catch {
console.error(
`\n❌ Couldn't create the repo working tree for instance "${instance}".\n` +
` This needs git on your PATH. Install git, then retry.\n`
);
process.exit(1);
}
}
// Best-effort: warn if an existing container for this folder doesn't publish
// the app ports. Docker fixes -p mappings when a container is CREATED, so a
// container built by an older toolkit (before/with different appPort) keeps its
// old mappings even when you re-run `up`. The only way to pick up new ports is
// to recreate the container — so we point that out here rather than letting the
// worker stare at a dead localhost. Wrapped so it can never block startup: any
// failure (docker missing, odd output) is swallowed and the check is skipped.
//
// Skipped for named instances: they're managed by instance.js (their own ports,
// and they carry an id-label instead of this folder's local_folder label), so
// this folder-scoped check would only ever inspect the primary container.
if (!instance)
try {
// Container ports we expect published. The browsable port is 3000 for every
// repo; Palolo also serves its API on 3001; zeta toolkits serve the corpus
// viewer on 3002. Read from toolkit.json when available, else assume the base pair.
let expected = [3000, 3001];
try {
const tk = JSON.parse(fs.readFileSync('toolkit.json', 'utf-8'));
expected = tk.explorePorts && tk.explorePorts.serverHost ? [3000, 3001] : [3000];
if (tk.explorePorts && tk.explorePorts.corpusHost) expected.push(3002);
} catch {}
const folder = process.cwd();
const ids = execSync(`docker ps -aq --filter "label=devcontainer.local_folder=${folder}"`, {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
})
.trim()
.split('\n')
.filter(Boolean);
for (const id of ids) {
const bindings = execSync(
`docker inspect --format "{{json .HostConfig.PortBindings}}" ${id}`,
{
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
}
).trim();
const missing = expected.filter((p) => !bindings.includes(`${p}/tcp`));
if (missing.length > 0) {
console.error(`
⚠️ An existing container for this folder doesn't publish port(s) ${missing.join(', ')}.
Docker fixes port mappings when a container is created, so re-running 'up'
alone won't add them. To expose the app, recreate the container:
npx @devcontainers/cli up --remove-existing-container
Note: recreating wipes the container's Claude history — run /create-snapshot
first if there's a conversation you want to keep.
`);
break;
}
}
} catch {
// docker unavailable or unexpected output — skip the check.
}