@taranek/orche 0.0.23 → 0.0.24

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taranek/orche",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "orche": "./dist/cli.js"
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
+ }