opencode-janus 0.1.0

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/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # janus
2
+
3
+ Directory-based OpenCode configuration switching tool. Automatically switch between different OpenCode configurations based on your current working directory.
4
+
5
+ Named after the Roman god of transitions and new beginnings—janus transforms your configuration seamlessly as you move between different projects.
6
+
7
+ ## Installation
8
+
9
+ ### Via NPM (Recommended)
10
+
11
+ ```bash
12
+ npm install -g janus
13
+ ```
14
+
15
+ ### Via Bun (Development)
16
+
17
+ ```bash
18
+ # Clone the repository
19
+ git clone https://github.com/kuitos/janus.git
20
+ cd janus
21
+
22
+ # Install dependencies
23
+ bun install
24
+
25
+ # Build the project
26
+ bun run build
27
+
28
+ # Install globally from source
29
+ npm install -g .
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ Create a configuration file at `~/.config/janus/config.json`:
35
+
36
+ ```json
37
+ {
38
+ "mappings": [
39
+ {
40
+ "match": ["/Users/yourname/work/company/**"],
41
+ "configDir": "/Users/yourname/.config/opencode-company"
42
+ },
43
+ {
44
+ "match": ["/Users/yourname/projects/oss/**"],
45
+ "configDir": "/Users/yourname/.config/opencode-oss"
46
+ }
47
+ ]
48
+ }
49
+ ```
50
+
51
+ ### Configuration Format
52
+
53
+ - `mappings`: Array of mapping rules
54
+ - `match`: Array of path patterns (supports glob patterns with `**`)
55
+ - `configDir`: The configuration directory to use for matching paths
56
+
57
+ Each configuration directory should contain:
58
+ - `opencode.json` - OpenCode configuration
59
+ - `oh-my-opencode.json` - oh-my-opencode configuration
60
+
61
+ ## Usage
62
+
63
+ ### First Time Setup
64
+
65
+ After installation, create your configuration file at `~/.config/janus/config.json` (see Configuration section).
66
+
67
+ ### Install Shell Hook
68
+
69
+ Install the janus hook to automatically set the correct configuration based on your current directory:
70
+
71
+ ```bash
72
+ janus install
73
+ ```
74
+
75
+ The command will automatically detect and install to `.zshrc` (if it exists) or `.bashrc`.
76
+
77
+ After installation, restart your shell or run:
78
+
79
+ ```bash
80
+ source ~/.zshrc # or source ~/.bashrc
81
+ ```
82
+
83
+ Now you can use `opencode` directly - it will automatically use the correct configuration based on your current directory.
84
+
85
+ ### Uninstall
86
+
87
+ To uninstall the hook:
88
+
89
+ ```bash
90
+ janus uninstall
91
+ ```
92
+
93
+
94
+ ## How It Works
95
+
96
+ 1. **Path Matching**: When you run a command, janus checks your current working directory against the configured patterns
97
+ 2. **Longest Prefix Priority**: If multiple patterns match, the longest (most specific) pattern wins
98
+ 3. **Environment Variable**: The tool sets `OPENCODE_CONFIG_DIR` to point to the matched configuration directory
99
+ 4. **Process Isolation**: Each opencode process gets its own configuration, preventing conflicts between different projects
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ # Run tests
105
+ bun test
106
+
107
+ # Run tests with coverage
108
+ bun test --coverage
109
+
110
+ # Build for production
111
+ bun run build
112
+
113
+ # Type check
114
+ bun run typecheck
115
+ ```
116
+
117
+ ## Project Structure
118
+
119
+ ```
120
+ src/
121
+ ├── types.ts # TypeScript type definitions
122
+ ├── config.ts # Configuration loading
123
+ ├── config.test.ts # Config tests
124
+ ├── resolver.ts # Path matching logic
125
+ ├── resolver.test.ts # Resolver tests
126
+ ├── install.ts # Shell hook installation/uninstallation
127
+ ├── install.test.ts # Install command tests
128
+ ├── shell-hook.ts # Shell hook generation
129
+ ├── shell-hook.test.ts # Shell hook tests
130
+ ├── cli.ts # CLI entry point
131
+ └── cli.test.ts # CLI tests
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
137
+
138
+ ## Contributing
139
+
140
+ Contributions are welcome! Please feel free to submit a Pull Request.
141
+
142
+ ## Acknowledgments
143
+
144
+ - Built with [Bun](https://bun.sh)
145
+ - Inspired by [direnv](https://direnv.net/) and [projj](https://github.com/popomore/projj)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export declare function showHelp(): void;
3
+ export declare function main(args: string[]): Promise<number>;
4
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAWA,wBAAgB,QAAQ,IAAI,IAAI,CAU/B;AA8CD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyC1D"}
package/dist/cli.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
14
+
15
+ // src/cli.ts
16
+ import { parseArgs } from "node:util";
17
+
18
+ // src/install.ts
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { join } from "node:path";
22
+
23
+ // src/shell-hook.ts
24
+ function generateShellHook(shell) {
25
+ return `opencode() {
26
+ janus exec -- "$@"
27
+ }`;
28
+ }
29
+
30
+ // src/install.ts
31
+ var HOOK_START_MARKER = "# >>> janus auto-initialization >>>";
32
+ var HOOK_END_MARKER = "# <<< janus auto-initialization <<<";
33
+ function detectShellRcFile() {
34
+ const home = homedir();
35
+ const zshrcPath = join(home, ".zshrc");
36
+ const bashrcPath = join(home, ".bashrc");
37
+ if (existsSync(zshrcPath)) {
38
+ return zshrcPath;
39
+ }
40
+ if (existsSync(bashrcPath)) {
41
+ return bashrcPath;
42
+ }
43
+ return zshrcPath;
44
+ }
45
+ function getShellTypeFromRcFile(rcFile) {
46
+ return rcFile.endsWith(".zshrc") ? "zsh" : "bash";
47
+ }
48
+ function isHookInstalled(rcFilePath) {
49
+ if (!existsSync(rcFilePath)) {
50
+ return false;
51
+ }
52
+ const content = readFileSync(rcFilePath, "utf-8");
53
+ return content.includes(HOOK_START_MARKER);
54
+ }
55
+ function installHook(rcFilePath, shellType) {
56
+ if (existsSync(rcFilePath)) {
57
+ const content = readFileSync(rcFilePath, "utf-8");
58
+ if (content.includes(HOOK_START_MARKER)) {
59
+ throw new Error("Hook already installed. Use uninstall first.");
60
+ }
61
+ }
62
+ const hook = generateShellHook(shellType);
63
+ const hookBlock = `${HOOK_START_MARKER}
64
+ ${hook}
65
+ ${HOOK_END_MARKER}
66
+ `;
67
+ if (existsSync(rcFilePath)) {
68
+ const content = readFileSync(rcFilePath, "utf-8");
69
+ const newContent = content.endsWith(`
70
+ `) ? content + hookBlock : content + `
71
+ ` + hookBlock;
72
+ writeFileSync(rcFilePath, newContent, "utf-8");
73
+ } else {
74
+ writeFileSync(rcFilePath, hookBlock, "utf-8");
75
+ }
76
+ }
77
+ function uninstallHook(rcFilePath) {
78
+ if (!existsSync(rcFilePath)) {
79
+ throw new Error(`RC file not found: ${rcFilePath}`);
80
+ }
81
+ const content = readFileSync(rcFilePath, "utf-8");
82
+ if (!content.includes(HOOK_START_MARKER)) {
83
+ throw new Error("Hook not installed.");
84
+ }
85
+ const hookRegex = new RegExp(`\\n?${escapeRegex(HOOK_START_MARKER)}[\\s\\S]*?${escapeRegex(HOOK_END_MARKER)}\\n?`, "g");
86
+ const newContent = content.replace(hookRegex, `
87
+ `).trim();
88
+ writeFileSync(rcFilePath, newContent ? newContent + `
89
+ ` : "", "utf-8");
90
+ }
91
+ function escapeRegex(str) {
92
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
93
+ }
94
+
95
+ // src/cli.ts
96
+ function showHelp() {
97
+ console.log(`Usage: janus <command> [options]
98
+
99
+ Commands:
100
+ install Install shell hook to .zshrc or .bashrc
101
+ uninstall Uninstall shell hook from shell RC file
102
+
103
+ Options:
104
+ --help, -h Show this help message
105
+ `);
106
+ }
107
+ async function handleInstall() {
108
+ try {
109
+ const rcFile = detectShellRcFile();
110
+ const shellType = getShellTypeFromRcFile(rcFile);
111
+ if (isHookInstalled(rcFile)) {
112
+ console.error("Error: Hook already installed.");
113
+ console.error(`To reinstall, run: janus uninstall && janus install`);
114
+ return 1;
115
+ }
116
+ installHook(rcFile, shellType);
117
+ console.log(`✓ Successfully installed janus hook to ${rcFile}`);
118
+ console.log(` Please restart your shell or run: source ${rcFile}`);
119
+ return 0;
120
+ } catch (error) {
121
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
122
+ return 1;
123
+ }
124
+ }
125
+ async function handleUninstall() {
126
+ try {
127
+ const rcFile = detectShellRcFile();
128
+ if (!isHookInstalled(rcFile)) {
129
+ console.error("Error: Hook not installed.");
130
+ return 1;
131
+ }
132
+ uninstallHook(rcFile);
133
+ console.log(`✓ Successfully uninstalled janus hook from ${rcFile}`);
134
+ console.log(` Please restart your shell or run: source ${rcFile}`);
135
+ return 0;
136
+ } catch (error) {
137
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
138
+ return 1;
139
+ }
140
+ }
141
+ async function main(args) {
142
+ try {
143
+ const options = {
144
+ help: {
145
+ type: "boolean",
146
+ short: "h"
147
+ }
148
+ };
149
+ const { values, positionals } = parseArgs({
150
+ args,
151
+ options,
152
+ allowPositionals: true
153
+ });
154
+ if (values.help) {
155
+ showHelp();
156
+ return 0;
157
+ }
158
+ if (positionals.length === 0) {
159
+ console.error("Error: No command specified");
160
+ console.error("Use --help for usage information");
161
+ return 1;
162
+ }
163
+ const command = positionals[0];
164
+ if (command === "install") {
165
+ return await handleInstall();
166
+ } else if (command === "uninstall") {
167
+ return await handleUninstall();
168
+ } else {
169
+ console.error(`Error: Unknown command: ${command}`);
170
+ console.error("Use --help for usage information");
171
+ return 1;
172
+ }
173
+ } catch (error) {
174
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
175
+ return 1;
176
+ }
177
+ }
178
+ if (__require.main == __require.module) {
179
+ const exitCode = await main(process.argv.slice(2));
180
+ process.exit(exitCode);
181
+ }
182
+ export {
183
+ showHelp,
184
+ main
185
+ };
@@ -0,0 +1,5 @@
1
+ import { type Config } from './types';
2
+ export declare function getDefaultConfigPath(): string;
3
+ export declare function loadConfig(configPath: string): Config;
4
+ export declare function loadDefaultConfig(): Config;
5
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAEpD,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAerD;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C"}
package/dist/exec.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Mapping } from './types';
2
+ export declare function execWithConfig(configDir: string, opencodeArgs: string[]): Promise<number>;
3
+ export declare function exec(cwd: string, mappings: Mapping[], opencodeArgs: string[]): Promise<number>;
4
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED,wBAAsB,IAAI,CACxB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,OAAO,EAAE,EACnB,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,MAAM,CAAC,CAQjB"}
@@ -0,0 +1,7 @@
1
+ export { loadConfig } from './config';
2
+ export { testPath, formatTestResult } from './test-command';
3
+ export { generateShellHook } from './shell-hook';
4
+ export { exec } from './exec';
5
+ export { resolvePath, matchesPattern, findLongestMatch } from './resolver';
6
+ export type { Config, Mapping } from './types';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC"}