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
157 changes: 152 additions & 5 deletions bin/cli/commands/init-shell.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,155 @@
/**
* `soul init-shell` — stub.
* Real implementation belongs to agent A16 (shell alias generator).
* `soul init-shell` — interactive shell alias/bootstrap writer.
*/
export async function run(_args: string[]): Promise<number> {
process.stderr.write("[A16] not yet implemented (owned by agent A16)\n");
return 1;

import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import {
applyShellInitPlan,
buildShellInitPlan,
detectShell,
type SupportedShell,
} from "../lib/shell-init";

interface Args {
shell?: SupportedShell;
rcFile?: string;
repoRoot?: string;
yes: boolean;
printOnly: boolean;
includeAliases?: boolean;
includeCompletions?: boolean;
}

export async function run(args: string[]): Promise<number> {
if (args.includes("-h") || args.includes("--help")) {
printHelp();
return 0;
}

const parsed = parseArgs(args);
if (!parsed) return 1;

const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
const shell = parsed.shell ?? detectShell();
let includeCompletions = parsed.includeCompletions;
let includeAliases = parsed.includeAliases;

if (!parsed.yes && !parsed.printOnly && interactive) {
if (includeCompletions === undefined) {
includeCompletions = await askYesNo(
"Append shell completions line (`source <(soul shell-completions)`)?",
true,
);
}
if (includeAliases === undefined) {
includeAliases = await askYesNo("Append claude-/codex- profile launchers?", true);
}
}

const plan = await buildShellInitPlan({
shell,
rcFile: parsed.rcFile,
repoRoot: parsed.repoRoot,
includeAliases,
includeCompletions,
});

process.stdout.write(`shell: ${plan.shell}\n`);
process.stdout.write(`rc file: ${plan.rcFile}\n`);
process.stdout.write(`profiles: ${plan.profiles.length ? plan.profiles.join(", ") : "(none)"}\n`);
process.stdout.write("\n");
process.stdout.write(plan.diff);

if (parsed.printOnly) return 0;

if (!parsed.yes) {
if (!interactive) {
process.stderr.write("soul init-shell: refusing to edit rc file without interactive confirmation\n");
process.stderr.write("re-run from a TTY or pass --yes after reviewing the diff\n");
return 1;
}
const ok = await askYesNo(`Write this block to ${plan.rcFile}?`, false);
if (!ok) {
process.stderr.write("soul init-shell: no changes written\n");
return 1;
}
}

const result = await applyShellInitPlan(plan);
process.stdout.write(`updated: ${result.rcFile}\n`);
if (result.backupPath) process.stdout.write(`backup: ${result.backupPath}\n`);
return 0;
}

function parseArgs(args: string[]): Args | null {
const parsed: Args = { yes: false, printOnly: false };

for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
if (arg === "--shell") {
const shell = args[++i] as SupportedShell | undefined;
if (!shell || !["zsh", "bash", "pwsh"].includes(shell)) {
process.stderr.write("soul init-shell: --shell must be zsh, bash, or pwsh\n");
return null;
}
parsed.shell = shell;
continue;
}
if (arg === "--rc-file") {
parsed.rcFile = args[++i];
if (!parsed.rcFile) {
process.stderr.write("soul init-shell: --rc-file needs a path\n");
return null;
}
continue;
}
if (arg === "--repo-root") {
parsed.repoRoot = args[++i];
if (!parsed.repoRoot) {
process.stderr.write("soul init-shell: --repo-root needs a path\n");
return null;
}
continue;
}
if (arg === "--yes" || arg === "-y") {
parsed.yes = true;
continue;
}
if (arg === "--print" || arg === "--dry-run") {
parsed.printOnly = true;
continue;
}
if (arg === "--no-completions") {
parsed.includeCompletions = false;
continue;
}
if (arg === "--no-aliases") {
parsed.includeAliases = false;
continue;
}
process.stderr.write(`soul init-shell: unknown argument ${arg}\n`);
printHelp();
return null;
}

return parsed;
}

async function askYesNo(question: string, defaultYes: boolean): Promise<boolean> {
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
const rl = createInterface({ input, output });
try {
const answer = (await rl.question(question + suffix)).trim().toLowerCase();
if (!answer) return defaultYes;
return answer === "y" || answer === "yes";
} finally {
rl.close();
}
}

function printHelp(): void {
process.stdout.write(`Usage: soul init-shell [--shell zsh|bash|pwsh] [--rc-file PATH] [--yes] [--print]\n\n`);
process.stdout.write("Prints the rc-file diff before writing. Without --yes, requires TTY confirmation.\n");
}
129 changes: 124 additions & 5 deletions bin/cli/commands/use.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,127 @@
/**
* `soul use <profile>` — stub.
* Real implementation belongs to agent A14 (materializer driver).
* `soul use <profile>` — A14 materializer stub plus A16 direnv bridge.
*
* The full materializer remains owned by A14. This file only wires the
* A16-owned `--direnv` path so users can point a directory at an already
* materialized profile workspace.
*/
export async function run(_args: string[]): Promise<number> {
process.stderr.write("[A14] not yet implemented (owned by agent A14)\n");
return 1;

import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { buildDirenvPlan, writeDirenvFile } from "../lib/shell-init";

interface Args {
profile?: string;
repoRoot?: string;
direnv: boolean;
yes: boolean;
printOnly: boolean;
}

export async function run(args: string[]): Promise<number> {
if (args.includes("-h") || args.includes("--help")) {
printHelp();
return 0;
}

const parsed = parseArgs(args);
if (!parsed) return 1;

if (!parsed.direnv) {
process.stderr.write("[A14] soul use materialization is not yet implemented on this branch\n");
process.stderr.write("A16 supports only: soul use --direnv <profile>\n");
return 1;
}

if (!parsed.profile) {
process.stderr.write("soul use --direnv: missing profile name\n");
return 1;
}

const plan = await buildDirenvPlan(parsed.profile, { repoRoot: parsed.repoRoot });
process.stdout.write(`profile: ${plan.profileName}\n`);
process.stdout.write(`workspace: ${plan.workspacePath}\n`);
process.stdout.write(`claude config: ${plan.claudeConfigDir}\n`);
process.stdout.write("\n");
process.stdout.write(plan.diff);

if (parsed.printOnly) return 0;

if (!parsed.yes) {
if (!process.stdin.isTTY || !process.stdout.isTTY) {
process.stderr.write("soul use --direnv: refusing to write .envrc without interactive confirmation\n");
process.stderr.write("re-run from a TTY or pass --yes after reviewing the diff\n");
return 1;
}
const ok = await askYesNo(`Write ${plan.envrcPath}?`, false);
if (!ok) {
process.stderr.write("soul use --direnv: no changes written\n");
return 1;
}
}

await writeDirenvFile(plan);
process.stdout.write(`wrote: ${plan.envrcPath}\n`);
process.stdout.write("next: direnv allow\n");
return 0;
}

function parseArgs(args: string[]): Args | null {
const parsed: Args = { direnv: false, yes: false, printOnly: false };

for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
if (arg === "--direnv") {
parsed.direnv = true;
const next = args[i + 1];
if (next && !next.startsWith("-") && !parsed.profile) {
parsed.profile = next;
i++;
}
continue;
}
if (arg === "--yes" || arg === "-y") {
parsed.yes = true;
continue;
}
if (arg === "--print" || arg === "--dry-run") {
parsed.printOnly = true;
continue;
}
if (arg === "--repo-root") {
parsed.repoRoot = args[++i];
if (!parsed.repoRoot) {
process.stderr.write("soul use --direnv: --repo-root needs a path\n");
return null;
}
continue;
}
if (!arg.startsWith("-") && !parsed.profile) {
parsed.profile = arg;
continue;
}

process.stderr.write(`soul use: unknown argument ${arg}\n`);
printHelp();
return null;
}

return parsed;
}

async function askYesNo(question: string, defaultYes: boolean): Promise<boolean> {
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
const rl = createInterface({ input, output });
try {
const answer = (await rl.question(question + suffix)).trim().toLowerCase();
if (!answer) return defaultYes;
return answer === "y" || answer === "yes";
} finally {
rl.close();
}
}

function printHelp(): void {
process.stdout.write("Usage: soul use --direnv <profile> [--yes] [--print]\n");
}
Loading
Loading