@taranek/orche 0.0.23 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +8 -2
- package/dist/init.d.ts +1 -0
- package/dist/init.js +108 -0
- package/package.json +1 -1
- package/src/cli.ts +7 -2
- package/src/init.ts +121 -0
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createWorktree } from "./worktree.js";
|
|
6
6
|
import { pruneCommand } from "./prune.js";
|
|
7
|
+
import { initCommand } from "./init.js";
|
|
7
8
|
import { getMultiplexer } from "./multiplexer.js";
|
|
8
9
|
import { buildLayout } from "./layout.js";
|
|
9
10
|
import { getReviewBinaryPath } from "./review-manager.js";
|
|
@@ -146,14 +147,17 @@ function printUsage() {
|
|
|
146
147
|
orche — orchestrate agents across git worktrees
|
|
147
148
|
|
|
148
149
|
Usage:
|
|
150
|
+
orche init [-f] Bootstrap a repo: write .orche.json + .gitignore entry
|
|
149
151
|
orche start <task> [-p <preset>] Start a new session for <task>
|
|
150
152
|
orche review [path] Open the review UI for a worktree
|
|
151
153
|
orche prune [--all] [-f] Remove orche worktrees (interactive multiselect)
|
|
152
154
|
|
|
153
155
|
Options:
|
|
154
156
|
-p, --preset=<name> Load .orche.<name>.json instead of .orche.json
|
|
157
|
+
-f, --force (init/prune) overwrite/skip safety checks
|
|
155
158
|
|
|
156
159
|
Examples:
|
|
160
|
+
orche init Create .orche.json + add .orche/ to .gitignore
|
|
157
161
|
orche start fix-auth Create worktree + session for "fix-auth"
|
|
158
162
|
orche start fix-auth -p mobile Use .orche.mobile.json preset
|
|
159
163
|
orche start fix-auth -p debug Use .orche.debug.json preset
|
|
@@ -163,10 +167,9 @@ Examples:
|
|
|
163
167
|
orche prune --all Remove all orche worktrees
|
|
164
168
|
orche prune --force Allow removing worktrees with uncommitted changes
|
|
165
169
|
|
|
166
|
-
Requires a ${CONFIG_NAME} file in the current directory.
|
|
170
|
+
Requires a ${CONFIG_NAME} file in the current directory (run \`orche init\`).
|
|
167
171
|
Use .orche.local.json for local overrides (not committed).
|
|
168
172
|
Use .orche.<preset>.json for named presets (e.g. .orche.mobile.json).
|
|
169
|
-
See .orche.example.json for the config format.
|
|
170
173
|
`);
|
|
171
174
|
}
|
|
172
175
|
async function main() {
|
|
@@ -178,6 +181,9 @@ async function main() {
|
|
|
178
181
|
if (subcommand === "start") {
|
|
179
182
|
startSession();
|
|
180
183
|
}
|
|
184
|
+
else if (subcommand === "init") {
|
|
185
|
+
initCommand(process.argv.slice(3));
|
|
186
|
+
}
|
|
181
187
|
else if (subcommand === "prune") {
|
|
182
188
|
const repoRoot = getRepoRoot(process.cwd());
|
|
183
189
|
await pruneCommand(repoRoot, process.argv.slice(3));
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCommand(argv: string[]): void;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
const CONFIG_NAME = ".orche.json";
|
|
6
|
+
const GITIGNORE_NAME = ".gitignore";
|
|
7
|
+
const GITIGNORE_ENTRY = ".orche/";
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
multiplexer: "auto",
|
|
10
|
+
layout: {
|
|
11
|
+
direction: "horizontal",
|
|
12
|
+
panes: [
|
|
13
|
+
{ name: "agent", command: "echo 'replace me with your agent command (e.g. claude, codex, aider)'" },
|
|
14
|
+
{ name: "dev", command: "echo 'replace me with your dev server command (e.g. pnpm dev)'" },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
function parseOptions(argv) {
|
|
19
|
+
return {
|
|
20
|
+
force: argv.includes("--force") || argv.includes("-f"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isAvailable(bin) {
|
|
24
|
+
try {
|
|
25
|
+
execSync(`command -v ${bin}`, { stdio: "ignore" });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
// command -v exits nonzero when the binary is missing — expected, not an error.
|
|
30
|
+
console.log(`[init] ${bin} not found:`, err.message);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function ensureGitRepo(cwd) {
|
|
35
|
+
try {
|
|
36
|
+
execSync("git rev-parse --show-toplevel", { cwd, stdio: "ignore" });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(pc.red("error:"), "not inside a git repository");
|
|
40
|
+
console.error("run `git init` first, then `orche init`");
|
|
41
|
+
console.log(`[init] git rev-parse failed:`, err.message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function ensureMultiplexer() {
|
|
46
|
+
const tmux = isAvailable("tmux");
|
|
47
|
+
const cmux = isAvailable("cmux");
|
|
48
|
+
if (tmux && cmux)
|
|
49
|
+
return "both";
|
|
50
|
+
if (cmux)
|
|
51
|
+
return "cmux";
|
|
52
|
+
if (tmux)
|
|
53
|
+
return "tmux";
|
|
54
|
+
console.error(pc.red("error:"), "neither tmux nor cmux is installed");
|
|
55
|
+
console.error("orche needs a terminal multiplexer to drive multiple panes. install one:");
|
|
56
|
+
console.error(` ${pc.cyan("tmux")}: brew install tmux (or your package manager)`);
|
|
57
|
+
console.error(` ${pc.cyan("cmux")}: https://cmux.com`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
/** Write .orche.json. Refuses to overwrite unless force=true. */
|
|
61
|
+
function writeConfig(cwd, force) {
|
|
62
|
+
const configPath = path.join(cwd, CONFIG_NAME);
|
|
63
|
+
if (existsSync(configPath) && !force) {
|
|
64
|
+
console.log(pc.dim(`skipped ${CONFIG_NAME} (already exists — pass --force to overwrite)`));
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
|
|
68
|
+
console.log(`${pc.green("✓")} wrote ${pc.bold(CONFIG_NAME)}`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** Append .orche/ to .gitignore if not present. Creates the file if missing. */
|
|
72
|
+
function ensureGitignore(cwd) {
|
|
73
|
+
const gitignorePath = path.join(cwd, GITIGNORE_NAME);
|
|
74
|
+
let contents = "";
|
|
75
|
+
if (existsSync(gitignorePath)) {
|
|
76
|
+
contents = readFileSync(gitignorePath, "utf-8");
|
|
77
|
+
const lines = contents.split(/\r?\n/).map((l) => l.trim());
|
|
78
|
+
// Match either ".orche/" or ".orche" (no trailing slash) — both ignore the dir.
|
|
79
|
+
if (lines.includes(GITIGNORE_ENTRY) || lines.includes(".orche")) {
|
|
80
|
+
console.log(pc.dim(`skipped ${GITIGNORE_NAME} (already ignores .orche)`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const prefix = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
|
|
85
|
+
writeFileSync(gitignorePath, contents + prefix + GITIGNORE_ENTRY + "\n");
|
|
86
|
+
console.log(`${pc.green("✓")} added ${pc.bold(GITIGNORE_ENTRY)} to ${pc.bold(GITIGNORE_NAME)}`);
|
|
87
|
+
}
|
|
88
|
+
function printNextSteps(mux) {
|
|
89
|
+
const muxNote = mux === "both" ? pc.dim("(tmux + cmux both detected — using cmux first via 'auto')") :
|
|
90
|
+
mux === "cmux" ? pc.dim("(cmux detected)") :
|
|
91
|
+
pc.dim("(tmux detected)");
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(pc.bold("Next:"));
|
|
94
|
+
console.log(` 1. Edit ${pc.cyan(CONFIG_NAME)} — set the panes you want (agent, dev server, etc.)`);
|
|
95
|
+
console.log(` 2. Run ${pc.cyan("orche start <task-name>")} to spin up a worktree + session`);
|
|
96
|
+
console.log(` 3. ${pc.cyan("orche review")} inside a worktree to open the diff UI`);
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log(` ${muxNote}`);
|
|
99
|
+
}
|
|
100
|
+
export function initCommand(argv) {
|
|
101
|
+
const opts = parseOptions(argv);
|
|
102
|
+
const cwd = process.cwd();
|
|
103
|
+
ensureGitRepo(cwd);
|
|
104
|
+
const mux = ensureMultiplexer();
|
|
105
|
+
writeConfig(cwd, opts.force);
|
|
106
|
+
ensureGitignore(cwd);
|
|
107
|
+
printNextSteps(mux);
|
|
108
|
+
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createWorktree } from "./worktree.js";
|
|
7
7
|
import { pruneCommand } from "./prune.js";
|
|
8
|
+
import { initCommand } from "./init.js";
|
|
8
9
|
import { getMultiplexer } from "./multiplexer.js";
|
|
9
10
|
import { buildLayout } from "./layout.js";
|
|
10
11
|
import { getReviewBinaryPath } from "./review-manager.js";
|
|
@@ -161,14 +162,17 @@ function printUsage(): void {
|
|
|
161
162
|
orche — orchestrate agents across git worktrees
|
|
162
163
|
|
|
163
164
|
Usage:
|
|
165
|
+
orche init [-f] Bootstrap a repo: write .orche.json + .gitignore entry
|
|
164
166
|
orche start <task> [-p <preset>] Start a new session for <task>
|
|
165
167
|
orche review [path] Open the review UI for a worktree
|
|
166
168
|
orche prune [--all] [-f] Remove orche worktrees (interactive multiselect)
|
|
167
169
|
|
|
168
170
|
Options:
|
|
169
171
|
-p, --preset=<name> Load .orche.<name>.json instead of .orche.json
|
|
172
|
+
-f, --force (init/prune) overwrite/skip safety checks
|
|
170
173
|
|
|
171
174
|
Examples:
|
|
175
|
+
orche init Create .orche.json + add .orche/ to .gitignore
|
|
172
176
|
orche start fix-auth Create worktree + session for "fix-auth"
|
|
173
177
|
orche start fix-auth -p mobile Use .orche.mobile.json preset
|
|
174
178
|
orche start fix-auth -p debug Use .orche.debug.json preset
|
|
@@ -178,10 +182,9 @@ Examples:
|
|
|
178
182
|
orche prune --all Remove all orche worktrees
|
|
179
183
|
orche prune --force Allow removing worktrees with uncommitted changes
|
|
180
184
|
|
|
181
|
-
Requires a ${CONFIG_NAME} file in the current directory.
|
|
185
|
+
Requires a ${CONFIG_NAME} file in the current directory (run \`orche init\`).
|
|
182
186
|
Use .orche.local.json for local overrides (not committed).
|
|
183
187
|
Use .orche.<preset>.json for named presets (e.g. .orche.mobile.json).
|
|
184
|
-
See .orche.example.json for the config format.
|
|
185
188
|
`);
|
|
186
189
|
}
|
|
187
190
|
|
|
@@ -195,6 +198,8 @@ async function main(): Promise<void> {
|
|
|
195
198
|
|
|
196
199
|
if (subcommand === "start") {
|
|
197
200
|
startSession();
|
|
201
|
+
} else if (subcommand === "init") {
|
|
202
|
+
initCommand(process.argv.slice(3));
|
|
198
203
|
} else if (subcommand === "prune") {
|
|
199
204
|
const repoRoot = getRepoRoot(process.cwd());
|
|
200
205
|
await pruneCommand(repoRoot, process.argv.slice(3));
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
|
|
6
|
+
const CONFIG_NAME = ".orche.json";
|
|
7
|
+
const GITIGNORE_NAME = ".gitignore";
|
|
8
|
+
const GITIGNORE_ENTRY = ".orche/";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
multiplexer: "auto",
|
|
12
|
+
layout: {
|
|
13
|
+
direction: "horizontal",
|
|
14
|
+
panes: [
|
|
15
|
+
{ name: "agent", command: "echo 'replace me with your agent command (e.g. claude, codex, aider)'" },
|
|
16
|
+
{ name: "dev", command: "echo 'replace me with your dev server command (e.g. pnpm dev)'" },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface InitOptions {
|
|
22
|
+
force: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseOptions(argv: string[]): InitOptions {
|
|
26
|
+
return {
|
|
27
|
+
force: argv.includes("--force") || argv.includes("-f"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isAvailable(bin: string): boolean {
|
|
32
|
+
try {
|
|
33
|
+
execSync(`command -v ${bin}`, { stdio: "ignore" });
|
|
34
|
+
return true;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// command -v exits nonzero when the binary is missing — expected, not an error.
|
|
37
|
+
console.log(`[init] ${bin} not found:`, (err as Error).message);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureGitRepo(cwd: string): void {
|
|
43
|
+
try {
|
|
44
|
+
execSync("git rev-parse --show-toplevel", { cwd, stdio: "ignore" });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(pc.red("error:"), "not inside a git repository");
|
|
47
|
+
console.error("run `git init` first, then `orche init`");
|
|
48
|
+
console.log(`[init] git rev-parse failed:`, (err as Error).message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureMultiplexer(): "tmux" | "cmux" | "both" {
|
|
54
|
+
const tmux = isAvailable("tmux");
|
|
55
|
+
const cmux = isAvailable("cmux");
|
|
56
|
+
if (tmux && cmux) return "both";
|
|
57
|
+
if (cmux) return "cmux";
|
|
58
|
+
if (tmux) return "tmux";
|
|
59
|
+
|
|
60
|
+
console.error(pc.red("error:"), "neither tmux nor cmux is installed");
|
|
61
|
+
console.error("orche needs a terminal multiplexer to drive multiple panes. install one:");
|
|
62
|
+
console.error(` ${pc.cyan("tmux")}: brew install tmux (or your package manager)`);
|
|
63
|
+
console.error(` ${pc.cyan("cmux")}: https://cmux.com`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Write .orche.json. Refuses to overwrite unless force=true. */
|
|
68
|
+
function writeConfig(cwd: string, force: boolean): boolean {
|
|
69
|
+
const configPath = path.join(cwd, CONFIG_NAME);
|
|
70
|
+
if (existsSync(configPath) && !force) {
|
|
71
|
+
console.log(pc.dim(`skipped ${CONFIG_NAME} (already exists — pass --force to overwrite)`));
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
|
|
75
|
+
console.log(`${pc.green("✓")} wrote ${pc.bold(CONFIG_NAME)}`);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Append .orche/ to .gitignore if not present. Creates the file if missing. */
|
|
80
|
+
function ensureGitignore(cwd: string): void {
|
|
81
|
+
const gitignorePath = path.join(cwd, GITIGNORE_NAME);
|
|
82
|
+
let contents = "";
|
|
83
|
+
if (existsSync(gitignorePath)) {
|
|
84
|
+
contents = readFileSync(gitignorePath, "utf-8");
|
|
85
|
+
const lines = contents.split(/\r?\n/).map((l) => l.trim());
|
|
86
|
+
// Match either ".orche/" or ".orche" (no trailing slash) — both ignore the dir.
|
|
87
|
+
if (lines.includes(GITIGNORE_ENTRY) || lines.includes(".orche")) {
|
|
88
|
+
console.log(pc.dim(`skipped ${GITIGNORE_NAME} (already ignores .orche)`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const prefix = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
|
|
93
|
+
writeFileSync(gitignorePath, contents + prefix + GITIGNORE_ENTRY + "\n");
|
|
94
|
+
console.log(`${pc.green("✓")} added ${pc.bold(GITIGNORE_ENTRY)} to ${pc.bold(GITIGNORE_NAME)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function printNextSteps(mux: "tmux" | "cmux" | "both"): void {
|
|
98
|
+
const muxNote =
|
|
99
|
+
mux === "both" ? pc.dim("(tmux + cmux both detected — using cmux first via 'auto')") :
|
|
100
|
+
mux === "cmux" ? pc.dim("(cmux detected)") :
|
|
101
|
+
pc.dim("(tmux detected)");
|
|
102
|
+
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(pc.bold("Next:"));
|
|
105
|
+
console.log(` 1. Edit ${pc.cyan(CONFIG_NAME)} — set the panes you want (agent, dev server, etc.)`);
|
|
106
|
+
console.log(` 2. Run ${pc.cyan("orche start <task-name>")} to spin up a worktree + session`);
|
|
107
|
+
console.log(` 3. ${pc.cyan("orche review")} inside a worktree to open the diff UI`);
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log(` ${muxNote}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function initCommand(argv: string[]): void {
|
|
113
|
+
const opts = parseOptions(argv);
|
|
114
|
+
const cwd = process.cwd();
|
|
115
|
+
|
|
116
|
+
ensureGitRepo(cwd);
|
|
117
|
+
const mux = ensureMultiplexer();
|
|
118
|
+
writeConfig(cwd, opts.force);
|
|
119
|
+
ensureGitignore(cwd);
|
|
120
|
+
printNextSteps(mux);
|
|
121
|
+
}
|