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
25 changes: 21 additions & 4 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ export interface InitArgs {
const DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
const DEFAULT_FLAIR = "http://127.0.0.1:9926";

/**
* Resolve the effective home directory.
*
* Prefers $HOME (or %USERPROFILE% on Windows) over node:os's homedir(),
* which resolves via the OS user database (getpwuid) as a fallback.
* In a container running as root with no matching /etc/passwd entry for
* an image-injected HOME override (e.g. our Docker test image sets
* HOME=/home/tps without creating a real "tps" user), homedir() and a
* live $HOME override can disagree — and Bun's os.homedir() has been
* observed to cache its result independent of later env mutations,
* so two independent callers (this process and any subprocess it spawns)
* are not guaranteed to agree unless they both read the same live env var.
* Reading process.env.HOME directly sidesteps that class of mismatch.
*/
function resolveHome(): string {
return process.env.HOME || process.env.USERPROFILE || homedir();
}

function generateEd25519Key(keyPath: string, pubPath: string): void {
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
const privDer = privateKey.export({ type: "pkcs8", format: "der" }) as Buffer;
Expand All @@ -34,8 +52,7 @@ function generateEd25519Key(keyPath: string, pubPath: string): void {
writeFileSync(pubPath, pubDer.subarray(12).toString("hex") + "\n", { mode: 0o644 });
}

function agentYaml(agentId: string, model: string, flairUrl: string): string {
const home = homedir();
function agentYaml(agentId: string, model: string, flairUrl: string, home: string): string {
return `# TPS Agent config — generated by tps init
agentId: ${agentId}
name: ${agentId}
Expand Down Expand Up @@ -85,7 +102,7 @@ async function registerWithFlair(
}

export async function runInit(args: InitArgs): Promise<void> {
const home = homedir();
const home = resolveHome();
const agentId = args.agentId ?? "my-agent";
const name = args.name ?? agentId;
const flairUrl = args.flairUrl ?? process.env.FLAIR_URL ?? DEFAULT_FLAIR;
Expand Down Expand Up @@ -124,7 +141,7 @@ export async function runInit(args: InitArgs): Promise<void> {
}

// 3. agent.yaml
writeFileSync(configPath, agentYaml(agentId, model, flairUrl), { mode: 0o644 });
writeFileSync(configPath, agentYaml(agentId, model, flairUrl, home), { mode: 0o644 });
console.log(`✓ Agent config written: ~/.tps/agents/${agentId}/agent.yaml`);

// 4. Workspace README
Expand Down
20 changes: 15 additions & 5 deletions packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ import { join, resolve } from "node:path";
import { homedir } from "node:os";

const TPS_BIN = resolve(import.meta.dir, "../bin/tps.ts");
const HOME = homedir();
// Prefer the live $HOME over homedir(), which resolves via the OS user
// database as a fallback. In the Docker test image (root, no matching
// /etc/passwd entry for the injected HOME=/home/tps) that fallback can
// disagree with $HOME, and Bun's os.homedir() has been observed to cache
// its result independent of later env mutations performed by sibling test
// files in this same process — so relying on it here (and independently
// again inside the spawned subprocess) risks the test's expected path and
// the subprocess's actual write path silently diverging. Reading
// process.env.HOME directly, and threading that same string through to
// the subprocess's env below, keeps both sides in lockstep.
const HOME = process.env.HOME || homedir();
const TEST_ID = "tps-init-test-agent";

afterEach(() => {
Expand All @@ -32,15 +42,15 @@ describe("tps init", () => {
});

test("errors if agent already exists without --force", () => {
spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID], { encoding: "utf-8", env: process.env });
const result = spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID], { encoding: "utf-8", env: process.env });
spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID], { encoding: "utf-8", env: { ...process.env, HOME } });
const result = spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID], { encoding: "utf-8", env: { ...process.env, HOME } });
expect(result.status).not.toBe(0);
expect((result.stderr ?? "") + (result.stdout ?? "")).toContain("already exists");
});

test("--force overwrites existing", () => {
spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID], { encoding: "utf-8", env: process.env });
const result = spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID, "--force"], { encoding: "utf-8", env: process.env });
spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID], { encoding: "utf-8", env: { ...process.env, HOME } });
const result = spawnSync("bun", [TPS_BIN, "init", "--id", TEST_ID, "--force"], { encoding: "utf-8", env: { ...process.env, HOME } });
expect(result.status).toBe(0);
});
});
Loading