@taranek/orche 0.0.17 → 0.0.19

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
@@ -7,25 +7,12 @@ import { pruneCommand } from "./prune.js";
7
7
  import { getMultiplexer } from "./multiplexer.js";
8
8
  import { buildLayout } from "./layout.js";
9
9
  import { getReviewBinaryPath } from "./review-manager.js";
10
+ import { loadConfig, parsePresetFlag } from "./config.js";
10
11
  const CONFIG_NAME = ".orche.json";
11
- const CONFIG_LOCAL_NAME = ".orche.local.json";
12
12
  function die(msg) {
13
13
  console.error(`error: ${msg}`);
14
14
  process.exit(1);
15
15
  }
16
- function loadConfig(cwd) {
17
- const localPath = path.join(cwd, CONFIG_LOCAL_NAME);
18
- const configPath = path.join(cwd, CONFIG_NAME);
19
- if (existsSync(localPath)) {
20
- const raw = readFileSync(localPath, "utf-8");
21
- return JSON.parse(raw);
22
- }
23
- if (!existsSync(configPath)) {
24
- die(`no ${CONFIG_NAME} found in ${cwd}`);
25
- }
26
- const raw = readFileSync(configPath, "utf-8");
27
- return JSON.parse(raw);
28
- }
29
16
  function deliverReview(pendingPath) {
30
17
  try {
31
18
  const pending = JSON.parse(readFileSync(pendingPath, "utf-8"));
@@ -134,8 +121,10 @@ function getRepoRoot(cwd) {
134
121
  function startSession() {
135
122
  const cwd = getRepoRoot(process.cwd());
136
123
  const repoName = path.basename(cwd);
137
- const taskName = process.argv.slice(3).find((a) => !a.startsWith("--")) || "session";
138
- const config = loadConfig(cwd);
124
+ const args = process.argv.slice(3);
125
+ const preset = parsePresetFlag(args);
126
+ const taskName = args.find((a) => !a.startsWith("-") && a !== preset) || "session";
127
+ const config = loadConfig(cwd, preset);
139
128
  const muxFlag = process.argv.includes("--tmux") ? "tmux" :
140
129
  process.argv.includes("--cmux") ? "cmux" :
141
130
  undefined;
@@ -157,20 +146,26 @@ function printUsage() {
157
146
  orche — orchestrate agents across git worktrees
158
147
 
159
148
  Usage:
160
- orche start <task> Start a new session for <task>
161
- orche review [path] Open the review UI for a worktree
162
- orche prune [--all] [-f] Remove orche worktrees (interactive multiselect)
149
+ orche start <task> [-p <preset>] Start a new session for <task>
150
+ orche review [path] Open the review UI for a worktree
151
+ orche prune [--all] [-f] Remove orche worktrees (interactive multiselect)
152
+
153
+ Options:
154
+ -p, --preset=<name> Load .orche.<name>.json instead of .orche.json
163
155
 
164
156
  Examples:
165
- orche start fix-auth Create worktree + session for "fix-auth"
166
- orche review Review changes in current directory
167
- orche review ./worktree Review changes in a specific worktree
168
- orche prune Pick worktrees to remove
169
- orche prune --all Remove all orche worktrees
170
- orche prune --force Allow removing worktrees with uncommitted changes
157
+ orche start fix-auth Create worktree + session for "fix-auth"
158
+ orche start fix-auth -p mobile Use .orche.mobile.json preset
159
+ orche start fix-auth -p debug Use .orche.debug.json preset
160
+ orche review Review changes in current directory
161
+ orche review ./worktree Review changes in a specific worktree
162
+ orche prune Pick worktrees to remove
163
+ orche prune --all Remove all orche worktrees
164
+ orche prune --force Allow removing worktrees with uncommitted changes
171
165
 
172
166
  Requires a ${CONFIG_NAME} file in the current directory.
173
- Use ${CONFIG_LOCAL_NAME} for local overrides (not committed).
167
+ Use .orche.local.json for local overrides (not committed).
168
+ Use .orche.<preset>.json for named presets (e.g. .orche.mobile.json).
174
169
  See .orche.example.json for the config format.
175
170
  `);
176
171
  }
@@ -0,0 +1,7 @@
1
+ import type { AgentsConfig } from "./types.js";
2
+ export declare class ConfigError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function listPresets(cwd: string): string[];
6
+ export declare function loadConfig(cwd: string, preset?: string): AgentsConfig;
7
+ export declare function parsePresetFlag(args: string[]): string | undefined;
package/dist/config.js ADDED
@@ -0,0 +1,52 @@
1
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ const CONFIG_NAME = ".orche.json";
4
+ const CONFIG_LOCAL_NAME = ".orche.local.json";
5
+ const CONFIG_PRESET_PREFIX = ".orche.";
6
+ const CONFIG_PRESET_SUFFIX = ".json";
7
+ export class ConfigError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "ConfigError";
11
+ }
12
+ }
13
+ export function listPresets(cwd) {
14
+ const files = readdirSync(cwd);
15
+ return files
16
+ .filter(f => f.startsWith(CONFIG_PRESET_PREFIX) && f.endsWith(CONFIG_PRESET_SUFFIX) && f !== CONFIG_NAME && f !== CONFIG_LOCAL_NAME)
17
+ .map(f => f.slice(CONFIG_PRESET_PREFIX.length, -CONFIG_PRESET_SUFFIX.length));
18
+ }
19
+ export function loadConfig(cwd, preset) {
20
+ if (preset) {
21
+ const presetPath = path.join(cwd, `${CONFIG_PRESET_PREFIX}${preset}${CONFIG_PRESET_SUFFIX}`);
22
+ if (!existsSync(presetPath)) {
23
+ const available = listPresets(cwd);
24
+ const list = available.length > 0
25
+ ? `\navailable presets: ${available.join(", ")}`
26
+ : "\nno preset files found in current directory";
27
+ throw new ConfigError(`preset "${preset}" not found (looked for ${CONFIG_PRESET_PREFIX}${preset}${CONFIG_PRESET_SUFFIX})${list}`);
28
+ }
29
+ const raw = readFileSync(presetPath, "utf-8");
30
+ return JSON.parse(raw);
31
+ }
32
+ const localPath = path.join(cwd, CONFIG_LOCAL_NAME);
33
+ const configPath = path.join(cwd, CONFIG_NAME);
34
+ if (existsSync(localPath)) {
35
+ const raw = readFileSync(localPath, "utf-8");
36
+ return JSON.parse(raw);
37
+ }
38
+ if (!existsSync(configPath)) {
39
+ throw new ConfigError(`no ${CONFIG_NAME} found in ${cwd}`);
40
+ }
41
+ const raw = readFileSync(configPath, "utf-8");
42
+ return JSON.parse(raw);
43
+ }
44
+ export function parsePresetFlag(args) {
45
+ const idx = args.indexOf("-p");
46
+ if (idx !== -1 && idx + 1 < args.length)
47
+ return args[idx + 1];
48
+ const long = args.find(a => a.startsWith("--preset="));
49
+ if (long)
50
+ return long.split("=")[1];
51
+ return undefined;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { loadConfig, listPresets, parsePresetFlag, ConfigError } from "./config.js";
6
+ function makeTmpDir() {
7
+ return mkdtempSync(path.join(os.tmpdir(), "orche-test-"));
8
+ }
9
+ function writeJSON(dir, name, data) {
10
+ writeFileSync(path.join(dir, name), JSON.stringify(data));
11
+ }
12
+ const baseConfig = { layout: { name: "main", command: "echo hi" } };
13
+ const mobileConfig = { layout: { name: "mobile", command: "echo mobile" } };
14
+ const debugConfig = { layout: { name: "debug", command: "echo debug" } };
15
+ describe("parsePresetFlag", () => {
16
+ it("parses -p flag", () => {
17
+ expect(parsePresetFlag(["-p", "mobile"])).toBe("mobile");
18
+ });
19
+ it("parses --preset= flag", () => {
20
+ expect(parsePresetFlag(["--preset=debug"])).toBe("debug");
21
+ });
22
+ it("returns undefined when no preset flag", () => {
23
+ expect(parsePresetFlag(["fix-auth", "--tmux"])).toBeUndefined();
24
+ });
25
+ it("returns undefined when -p has no value", () => {
26
+ expect(parsePresetFlag(["-p"])).toBeUndefined();
27
+ });
28
+ it("ignores -p when it is the last arg", () => {
29
+ expect(parsePresetFlag(["start", "-p"])).toBeUndefined();
30
+ });
31
+ });
32
+ describe("loadConfig", () => {
33
+ let tmp;
34
+ beforeEach(() => {
35
+ tmp = makeTmpDir();
36
+ });
37
+ afterEach(() => {
38
+ rmSync(tmp, { recursive: true, force: true });
39
+ });
40
+ it("loads .orche.json", () => {
41
+ writeJSON(tmp, ".orche.json", baseConfig);
42
+ const config = loadConfig(tmp);
43
+ expect(config.layout).toEqual(baseConfig.layout);
44
+ });
45
+ it("prefers .orche.local.json over .orche.json", () => {
46
+ writeJSON(tmp, ".orche.json", baseConfig);
47
+ writeJSON(tmp, ".orche.local.json", mobileConfig);
48
+ const config = loadConfig(tmp);
49
+ expect(config.layout).toEqual(mobileConfig.layout);
50
+ });
51
+ it("throws ConfigError when no config found", () => {
52
+ expect(() => loadConfig(tmp)).toThrow(ConfigError);
53
+ expect(() => loadConfig(tmp)).toThrow(".orche.json");
54
+ });
55
+ it("loads a preset config", () => {
56
+ writeJSON(tmp, ".orche.mobile.json", mobileConfig);
57
+ const config = loadConfig(tmp, "mobile");
58
+ expect(config.layout).toEqual(mobileConfig.layout);
59
+ });
60
+ it("throws ConfigError for missing preset", () => {
61
+ expect(() => loadConfig(tmp, "nonexistent")).toThrow(ConfigError);
62
+ expect(() => loadConfig(tmp, "nonexistent")).toThrow('preset "nonexistent" not found');
63
+ });
64
+ it("lists available presets in error when preset not found", () => {
65
+ writeJSON(tmp, ".orche.mobile.json", mobileConfig);
66
+ writeJSON(tmp, ".orche.debug.json", debugConfig);
67
+ try {
68
+ loadConfig(tmp, "nonexistent");
69
+ expect.unreachable("should have thrown");
70
+ }
71
+ catch (err) {
72
+ const msg = err.message;
73
+ expect(msg).toContain("available presets:");
74
+ expect(msg).toContain("mobile");
75
+ expect(msg).toContain("debug");
76
+ }
77
+ });
78
+ it("shows 'no preset files found' when none exist", () => {
79
+ try {
80
+ loadConfig(tmp, "nonexistent");
81
+ expect.unreachable("should have thrown");
82
+ }
83
+ catch (err) {
84
+ expect(err.message).toContain("no preset files found");
85
+ }
86
+ });
87
+ it("preset flag ignores .orche.local.json", () => {
88
+ writeJSON(tmp, ".orche.local.json", baseConfig);
89
+ writeJSON(tmp, ".orche.mobile.json", mobileConfig);
90
+ const config = loadConfig(tmp, "mobile");
91
+ expect(config.layout).toEqual(mobileConfig.layout);
92
+ });
93
+ });
94
+ describe("listPresets", () => {
95
+ let tmp;
96
+ beforeEach(() => {
97
+ tmp = makeTmpDir();
98
+ });
99
+ afterEach(() => {
100
+ rmSync(tmp, { recursive: true, force: true });
101
+ });
102
+ it("returns empty array when no presets", () => {
103
+ expect(listPresets(tmp)).toEqual([]);
104
+ });
105
+ it("lists preset names without .orche. prefix and .json suffix", () => {
106
+ writeJSON(tmp, ".orche.mobile.json", {});
107
+ writeJSON(tmp, ".orche.debug.json", {});
108
+ writeJSON(tmp, ".orche.test.json", {});
109
+ const presets = listPresets(tmp).sort();
110
+ expect(presets).toEqual(["debug", "mobile", "test"]);
111
+ });
112
+ it("excludes .orche.json and .orche.local.json", () => {
113
+ writeJSON(tmp, ".orche.json", {});
114
+ writeJSON(tmp, ".orche.local.json", {});
115
+ writeJSON(tmp, ".orche.mobile.json", {});
116
+ expect(listPresets(tmp)).toEqual(["mobile"]);
117
+ });
118
+ it("ignores non-orche json files", () => {
119
+ writeJSON(tmp, "package.json", {});
120
+ writeJSON(tmp, "tsconfig.json", {});
121
+ writeJSON(tmp, ".orche.mobile.json", {});
122
+ expect(listPresets(tmp)).toEqual(["mobile"]);
123
+ });
124
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taranek/orche",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "orche": "./dist/cli.js"
@@ -16,11 +16,14 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build": "tsc",
19
- "dev": "tsc --watch"
19
+ "dev": "tsc --watch",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest"
20
22
  },
21
23
  "devDependencies": {
22
24
  "@types/node": "^25.2.0",
23
- "typescript": "^5.2.2"
25
+ "typescript": "^5.2.2",
26
+ "vitest": "^4.1.5"
24
27
  },
25
28
  "dependencies": {
26
29
  "@clack/prompts": "^1.2.0",
package/src/cli.ts CHANGED
@@ -8,33 +8,16 @@ import { pruneCommand } from "./prune.js";
8
8
  import { getMultiplexer } from "./multiplexer.js";
9
9
  import { buildLayout } from "./layout.js";
10
10
  import { getReviewBinaryPath } from "./review-manager.js";
11
- import type { AgentsConfig, MultiplexerType } from "./types.js";
11
+ import { loadConfig, parsePresetFlag } from "./config.js";
12
+ import type { MultiplexerType } from "./types.js";
12
13
 
13
14
  const CONFIG_NAME = ".orche.json";
14
- const CONFIG_LOCAL_NAME = ".orche.local.json";
15
15
 
16
16
  function die(msg: string): never {
17
17
  console.error(`error: ${msg}`);
18
18
  process.exit(1);
19
19
  }
20
20
 
21
-
22
- function loadConfig(cwd: string): AgentsConfig {
23
- const localPath = path.join(cwd, CONFIG_LOCAL_NAME);
24
- const configPath = path.join(cwd, CONFIG_NAME);
25
-
26
- if (existsSync(localPath)) {
27
- const raw = readFileSync(localPath, "utf-8");
28
- return JSON.parse(raw) as AgentsConfig;
29
- }
30
-
31
- if (!existsSync(configPath)) {
32
- die(`no ${CONFIG_NAME} found in ${cwd}`);
33
- }
34
- const raw = readFileSync(configPath, "utf-8");
35
- return JSON.parse(raw) as AgentsConfig;
36
- }
37
-
38
21
  function deliverReview(pendingPath: string): void {
39
22
  try {
40
23
  const pending = JSON.parse(readFileSync(pendingPath, "utf-8"));
@@ -142,9 +125,11 @@ function getRepoRoot(cwd: string): string {
142
125
  function startSession(): void {
143
126
  const cwd = getRepoRoot(process.cwd());
144
127
  const repoName = path.basename(cwd);
145
- const taskName = process.argv.slice(3).find((a) => !a.startsWith("--")) || "session";
128
+ const args = process.argv.slice(3);
129
+ const preset = parsePresetFlag(args);
130
+ const taskName = args.find((a) => !a.startsWith("-") && a !== preset) || "session";
146
131
 
147
- const config = loadConfig(cwd);
132
+ const config = loadConfig(cwd, preset);
148
133
  const muxFlag: MultiplexerType | undefined =
149
134
  process.argv.includes("--tmux") ? "tmux" :
150
135
  process.argv.includes("--cmux") ? "cmux" :
@@ -176,20 +161,26 @@ function printUsage(): void {
176
161
  orche — orchestrate agents across git worktrees
177
162
 
178
163
  Usage:
179
- orche start <task> Start a new session for <task>
180
- orche review [path] Open the review UI for a worktree
181
- orche prune [--all] [-f] Remove orche worktrees (interactive multiselect)
164
+ orche start <task> [-p <preset>] Start a new session for <task>
165
+ orche review [path] Open the review UI for a worktree
166
+ orche prune [--all] [-f] Remove orche worktrees (interactive multiselect)
167
+
168
+ Options:
169
+ -p, --preset=<name> Load .orche.<name>.json instead of .orche.json
182
170
 
183
171
  Examples:
184
- orche start fix-auth Create worktree + session for "fix-auth"
185
- orche review Review changes in current directory
186
- orche review ./worktree Review changes in a specific worktree
187
- orche prune Pick worktrees to remove
188
- orche prune --all Remove all orche worktrees
189
- orche prune --force Allow removing worktrees with uncommitted changes
172
+ orche start fix-auth Create worktree + session for "fix-auth"
173
+ orche start fix-auth -p mobile Use .orche.mobile.json preset
174
+ orche start fix-auth -p debug Use .orche.debug.json preset
175
+ orche review Review changes in current directory
176
+ orche review ./worktree Review changes in a specific worktree
177
+ orche prune Pick worktrees to remove
178
+ orche prune --all Remove all orche worktrees
179
+ orche prune --force Allow removing worktrees with uncommitted changes
190
180
 
191
181
  Requires a ${CONFIG_NAME} file in the current directory.
192
- Use ${CONFIG_LOCAL_NAME} for local overrides (not committed).
182
+ Use .orche.local.json for local overrides (not committed).
183
+ Use .orche.<preset>.json for named presets (e.g. .orche.mobile.json).
193
184
  See .orche.example.json for the config format.
194
185
  `);
195
186
  }
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { loadConfig, listPresets, parsePresetFlag, ConfigError } from "./config.js";
6
+
7
+ function makeTmpDir(): string {
8
+ return mkdtempSync(path.join(os.tmpdir(), "orche-test-"));
9
+ }
10
+
11
+ function writeJSON(dir: string, name: string, data: unknown): void {
12
+ writeFileSync(path.join(dir, name), JSON.stringify(data));
13
+ }
14
+
15
+ const baseConfig = { layout: { name: "main", command: "echo hi" } };
16
+ const mobileConfig = { layout: { name: "mobile", command: "echo mobile" } };
17
+ const debugConfig = { layout: { name: "debug", command: "echo debug" } };
18
+
19
+ describe("parsePresetFlag", () => {
20
+ it("parses -p flag", () => {
21
+ expect(parsePresetFlag(["-p", "mobile"])).toBe("mobile");
22
+ });
23
+
24
+ it("parses --preset= flag", () => {
25
+ expect(parsePresetFlag(["--preset=debug"])).toBe("debug");
26
+ });
27
+
28
+ it("returns undefined when no preset flag", () => {
29
+ expect(parsePresetFlag(["fix-auth", "--tmux"])).toBeUndefined();
30
+ });
31
+
32
+ it("returns undefined when -p has no value", () => {
33
+ expect(parsePresetFlag(["-p"])).toBeUndefined();
34
+ });
35
+
36
+ it("ignores -p when it is the last arg", () => {
37
+ expect(parsePresetFlag(["start", "-p"])).toBeUndefined();
38
+ });
39
+ });
40
+
41
+ describe("loadConfig", () => {
42
+ let tmp: string;
43
+
44
+ beforeEach(() => {
45
+ tmp = makeTmpDir();
46
+ });
47
+
48
+ afterEach(() => {
49
+ rmSync(tmp, { recursive: true, force: true });
50
+ });
51
+
52
+ it("loads .orche.json", () => {
53
+ writeJSON(tmp, ".orche.json", baseConfig);
54
+ const config = loadConfig(tmp);
55
+ expect(config.layout).toEqual(baseConfig.layout);
56
+ });
57
+
58
+ it("prefers .orche.local.json over .orche.json", () => {
59
+ writeJSON(tmp, ".orche.json", baseConfig);
60
+ writeJSON(tmp, ".orche.local.json", mobileConfig);
61
+ const config = loadConfig(tmp);
62
+ expect(config.layout).toEqual(mobileConfig.layout);
63
+ });
64
+
65
+ it("throws ConfigError when no config found", () => {
66
+ expect(() => loadConfig(tmp)).toThrow(ConfigError);
67
+ expect(() => loadConfig(tmp)).toThrow(".orche.json");
68
+ });
69
+
70
+ it("loads a preset config", () => {
71
+ writeJSON(tmp, ".orche.mobile.json", mobileConfig);
72
+ const config = loadConfig(tmp, "mobile");
73
+ expect(config.layout).toEqual(mobileConfig.layout);
74
+ });
75
+
76
+ it("throws ConfigError for missing preset", () => {
77
+ expect(() => loadConfig(tmp, "nonexistent")).toThrow(ConfigError);
78
+ expect(() => loadConfig(tmp, "nonexistent")).toThrow('preset "nonexistent" not found');
79
+ });
80
+
81
+ it("lists available presets in error when preset not found", () => {
82
+ writeJSON(tmp, ".orche.mobile.json", mobileConfig);
83
+ writeJSON(tmp, ".orche.debug.json", debugConfig);
84
+ try {
85
+ loadConfig(tmp, "nonexistent");
86
+ expect.unreachable("should have thrown");
87
+ } catch (err) {
88
+ const msg = (err as Error).message;
89
+ expect(msg).toContain("available presets:");
90
+ expect(msg).toContain("mobile");
91
+ expect(msg).toContain("debug");
92
+ }
93
+ });
94
+
95
+ it("shows 'no preset files found' when none exist", () => {
96
+ try {
97
+ loadConfig(tmp, "nonexistent");
98
+ expect.unreachable("should have thrown");
99
+ } catch (err) {
100
+ expect((err as Error).message).toContain("no preset files found");
101
+ }
102
+ });
103
+
104
+ it("preset flag ignores .orche.local.json", () => {
105
+ writeJSON(tmp, ".orche.local.json", baseConfig);
106
+ writeJSON(tmp, ".orche.mobile.json", mobileConfig);
107
+ const config = loadConfig(tmp, "mobile");
108
+ expect(config.layout).toEqual(mobileConfig.layout);
109
+ });
110
+ });
111
+
112
+ describe("listPresets", () => {
113
+ let tmp: string;
114
+
115
+ beforeEach(() => {
116
+ tmp = makeTmpDir();
117
+ });
118
+
119
+ afterEach(() => {
120
+ rmSync(tmp, { recursive: true, force: true });
121
+ });
122
+
123
+ it("returns empty array when no presets", () => {
124
+ expect(listPresets(tmp)).toEqual([]);
125
+ });
126
+
127
+ it("lists preset names without .orche. prefix and .json suffix", () => {
128
+ writeJSON(tmp, ".orche.mobile.json", {});
129
+ writeJSON(tmp, ".orche.debug.json", {});
130
+ writeJSON(tmp, ".orche.test.json", {});
131
+ const presets = listPresets(tmp).sort();
132
+ expect(presets).toEqual(["debug", "mobile", "test"]);
133
+ });
134
+
135
+ it("excludes .orche.json and .orche.local.json", () => {
136
+ writeJSON(tmp, ".orche.json", {});
137
+ writeJSON(tmp, ".orche.local.json", {});
138
+ writeJSON(tmp, ".orche.mobile.json", {});
139
+ expect(listPresets(tmp)).toEqual(["mobile"]);
140
+ });
141
+
142
+ it("ignores non-orche json files", () => {
143
+ writeJSON(tmp, "package.json", {});
144
+ writeJSON(tmp, "tsconfig.json", {});
145
+ writeJSON(tmp, ".orche.mobile.json", {});
146
+ expect(listPresets(tmp)).toEqual(["mobile"]);
147
+ });
148
+ });
package/src/config.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { AgentsConfig } from "./types.js";
4
+
5
+ const CONFIG_NAME = ".orche.json";
6
+ const CONFIG_LOCAL_NAME = ".orche.local.json";
7
+ const CONFIG_PRESET_PREFIX = ".orche.";
8
+ const CONFIG_PRESET_SUFFIX = ".json";
9
+
10
+ export class ConfigError extends Error {
11
+ constructor(message: string) {
12
+ super(message);
13
+ this.name = "ConfigError";
14
+ }
15
+ }
16
+
17
+ export function listPresets(cwd: string): string[] {
18
+ const files = readdirSync(cwd);
19
+ return files
20
+ .filter(f => f.startsWith(CONFIG_PRESET_PREFIX) && f.endsWith(CONFIG_PRESET_SUFFIX) && f !== CONFIG_NAME && f !== CONFIG_LOCAL_NAME)
21
+ .map(f => f.slice(CONFIG_PRESET_PREFIX.length, -CONFIG_PRESET_SUFFIX.length));
22
+ }
23
+
24
+ export function loadConfig(cwd: string, preset?: string): AgentsConfig {
25
+ if (preset) {
26
+ const presetPath = path.join(cwd, `${CONFIG_PRESET_PREFIX}${preset}${CONFIG_PRESET_SUFFIX}`);
27
+ if (!existsSync(presetPath)) {
28
+ const available = listPresets(cwd);
29
+ const list = available.length > 0
30
+ ? `\navailable presets: ${available.join(", ")}`
31
+ : "\nno preset files found in current directory";
32
+ throw new ConfigError(`preset "${preset}" not found (looked for ${CONFIG_PRESET_PREFIX}${preset}${CONFIG_PRESET_SUFFIX})${list}`);
33
+ }
34
+ const raw = readFileSync(presetPath, "utf-8");
35
+ return JSON.parse(raw) as AgentsConfig;
36
+ }
37
+
38
+ const localPath = path.join(cwd, CONFIG_LOCAL_NAME);
39
+ const configPath = path.join(cwd, CONFIG_NAME);
40
+
41
+ if (existsSync(localPath)) {
42
+ const raw = readFileSync(localPath, "utf-8");
43
+ return JSON.parse(raw) as AgentsConfig;
44
+ }
45
+
46
+ if (!existsSync(configPath)) {
47
+ throw new ConfigError(`no ${CONFIG_NAME} found in ${cwd}`);
48
+ }
49
+ const raw = readFileSync(configPath, "utf-8");
50
+ return JSON.parse(raw) as AgentsConfig;
51
+ }
52
+
53
+ export function parsePresetFlag(args: string[]): string | undefined {
54
+ const idx = args.indexOf("-p");
55
+ if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
56
+ const long = args.find(a => a.startsWith("--preset="));
57
+ if (long) return long.split("=")[1];
58
+ return undefined;
59
+ }