@neobiotechlabs/neobiotech-dev-agent 0.1.41 → 0.1.43
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/doctor.js +17 -6
- package/dist/doctor.js.map +1 -1
- package/dist/index.js +18 -7
- package/dist/index.js.map +1 -1
- package/dist/setup-antigravity.js +66 -1
- package/dist/setup-antigravity.js.map +1 -1
- package/dist/setup-opencode.js +182 -0
- package/dist/setup-opencode.js.map +1 -0
- package/package.json +1 -1
|
@@ -8,17 +8,82 @@ const __dirname = __dn(__filename);
|
|
|
8
8
|
|
|
9
9
|
// src/cli/setup-antigravity.ts
|
|
10
10
|
import { spawnSync } from "child_process";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import os from "os";
|
|
11
13
|
import path from "path";
|
|
12
14
|
import { fileURLToPath } from "url";
|
|
15
|
+
var PLUGIN_NAME = "neobiotech-dev-agent";
|
|
16
|
+
function getAgyPluginDir() {
|
|
17
|
+
return path.join(os.homedir(), ".gemini", "config", "plugins", PLUGIN_NAME);
|
|
18
|
+
}
|
|
19
|
+
function stagePluginToTmpDir(pluginRoot) {
|
|
20
|
+
const stagingRoot = fs.mkdtempSync(
|
|
21
|
+
path.join(os.tmpdir(), `${PLUGIN_NAME}-antigravity-`)
|
|
22
|
+
);
|
|
23
|
+
const pluginDir = path.join(stagingRoot, PLUGIN_NAME);
|
|
24
|
+
fs.cpSync(pluginRoot, pluginDir, { recursive: true });
|
|
25
|
+
return { pluginDir, stagingRoot };
|
|
26
|
+
}
|
|
27
|
+
function cleanupStagingRoot(stagingRoot) {
|
|
28
|
+
try {
|
|
29
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
process.stderr.write(
|
|
32
|
+
`Warning: failed to remove Antigravity staging directory ${stagingRoot}: ${error.message}
|
|
33
|
+
`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function patchMcpConfigCwd() {
|
|
38
|
+
const agyPluginDir = getAgyPluginDir();
|
|
39
|
+
const configPath = path.join(agyPluginDir, "mcp_config.json");
|
|
40
|
+
if (!fs.existsSync(configPath)) {
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
`Warning: mcp_config.json not found at ${configPath}; skipping cwd patch.
|
|
43
|
+
`
|
|
44
|
+
);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
49
|
+
const cfg = JSON.parse(raw);
|
|
50
|
+
for (const server of Object.values(cfg.mcpServers ?? {})) {
|
|
51
|
+
if (server.cwd === ".") server.cwd = agyPluginDir;
|
|
52
|
+
}
|
|
53
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
54
|
+
return true;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
`Warning: failed to patch mcp_config.json cwd: ${err.message}
|
|
58
|
+
`
|
|
59
|
+
);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
13
63
|
function runSetupAntigravityCli(installer = (command, args, options) => spawnSync(command, args, options)) {
|
|
14
64
|
const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
-
const
|
|
65
|
+
const stagedPlugin = stagePluginToTmpDir(pluginRoot);
|
|
66
|
+
let result;
|
|
67
|
+
try {
|
|
68
|
+
result = installer("agy", ["plugin", "install", stagedPlugin.pluginDir], {
|
|
69
|
+
stdio: "inherit"
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`Antigravity plugin install failed: ${error.message}
|
|
74
|
+
`
|
|
75
|
+
);
|
|
76
|
+
return 1;
|
|
77
|
+
} finally {
|
|
78
|
+
cleanupStagingRoot(stagedPlugin.stagingRoot);
|
|
79
|
+
}
|
|
16
80
|
if (result.error) {
|
|
17
81
|
process.stderr.write(`Antigravity plugin install failed: ${result.error.message}
|
|
18
82
|
`);
|
|
19
83
|
return 1;
|
|
20
84
|
}
|
|
21
85
|
if (result.status !== 0) return result.status ?? 1;
|
|
86
|
+
patchMcpConfigCwd();
|
|
22
87
|
process.stdout.write(
|
|
23
88
|
[
|
|
24
89
|
"Antigravity plugin installed: neobiotech-dev-agent",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/setup-antigravity.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\ntype InstallerResult = {\n error?: Error;\n status: number | null;\n};\n\ntype Installer = (\n command: string,\n args: string[],\n options: { stdio: 'inherit' },\n) => InstallerResult;\n\nexport function runSetupAntigravityCli(\n installer: Installer = (command, args, options) => spawnSync(command, args, options),\n) {\n const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');\n const result = installer('agy', ['plugin', 'install',
|
|
1
|
+
{"version":3,"sources":["../src/cli/setup-antigravity.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\ntype InstallerResult = {\n error?: Error;\n status: number | null;\n};\n\ntype Installer = (\n command: string,\n args: string[],\n options: { stdio: 'inherit' },\n) => InstallerResult;\n\nconst PLUGIN_NAME = 'neobiotech-dev-agent';\n\ntype StagedPlugin = {\n pluginDir: string;\n stagingRoot: string;\n};\n\nfunction getAgyPluginDir(): string {\n return path.join(os.homedir(), '.gemini', 'config', 'plugins', PLUGIN_NAME);\n}\n\n/**\n * Copy the plugin directory to a temp location without `@scope` in the path.\n *\n * Antigravity CLI ≤1.1.22 interprets `@scope/name` inside a path as\n * `plugin@marketplace` syntax and rejects the install. By staging the\n * plugin under `/tmp/neobiotech-dev-agent-antigravity/` we give `agy`\n * a clean path that passes its marketplace heuristic.\n */\nfunction stagePluginToTmpDir(pluginRoot: string): StagedPlugin {\n const stagingRoot = fs.mkdtempSync(\n path.join(os.tmpdir(), `${PLUGIN_NAME}-antigravity-`),\n );\n const pluginDir = path.join(stagingRoot, PLUGIN_NAME);\n fs.cpSync(pluginRoot, pluginDir, { recursive: true });\n return { pluginDir, stagingRoot };\n}\n\nfunction cleanupStagingRoot(stagingRoot: string): void {\n try {\n fs.rmSync(stagingRoot, { recursive: true, force: true });\n } catch (error) {\n process.stderr.write(\n `Warning: failed to remove Antigravity staging directory ${stagingRoot}: ${(error as Error).message}\\n`,\n );\n }\n}\n\n/**\n * After `agy plugin install` copies the plugin into Antigravity's plugin\n * directory, the bundled `mcp_config.json` still has `\"cwd\": \".\"` which\n * Antigravity resolves against the *user's* CWD — not the plugin dir.\n *\n * We patch the installed copy so that `cwd` points to the absolute plugin\n * directory, making `dist/index.js` resolvable regardless of where the\n * user launches Antigravity.\n */\nfunction patchMcpConfigCwd(): boolean {\n const agyPluginDir = getAgyPluginDir();\n const configPath = path.join(agyPluginDir, 'mcp_config.json');\n if (!fs.existsSync(configPath)) {\n process.stderr.write(\n `Warning: mcp_config.json not found at ${configPath}; skipping cwd patch.\\n`,\n );\n return false;\n }\n try {\n const raw = fs.readFileSync(configPath, 'utf-8');\n const cfg = JSON.parse(raw);\n for (const server of Object.values(cfg.mcpServers ?? {}) as Array<{ cwd?: string }>) {\n if (server.cwd === '.') server.cwd = agyPluginDir;\n }\n fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\\n', 'utf-8');\n return true;\n } catch (err) {\n process.stderr.write(\n `Warning: failed to patch mcp_config.json cwd: ${(err as Error).message}\\n`,\n );\n return false;\n }\n}\n\nexport function runSetupAntigravityCli(\n installer: Installer = (command, args, options) => spawnSync(command, args, options),\n) {\n const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');\n const stagedPlugin = stagePluginToTmpDir(pluginRoot);\n let result: InstallerResult;\n\n try {\n result = installer('agy', ['plugin', 'install', stagedPlugin.pluginDir], {\n stdio: 'inherit',\n });\n } catch (error) {\n process.stderr.write(\n `Antigravity plugin install failed: ${(error as Error).message}\\n`,\n );\n return 1;\n } finally {\n cleanupStagingRoot(stagedPlugin.stagingRoot);\n }\n\n if (result.error) {\n process.stderr.write(`Antigravity plugin install failed: ${result.error.message}\\n`);\n return 1;\n }\n if (result.status !== 0) return result.status ?? 1;\n\n patchMcpConfigCwd();\n\n process.stdout.write(\n [\n 'Antigravity plugin installed: neobiotech-dev-agent',\n 'Required environment variables: JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN',\n 'Restart Antigravity, then verify /mcp and /agents.',\n 'Run /gate-parallel-review for a full Gate review.',\n ].join('\\n') + '\\n',\n );\n return 0;\n}\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n process.exitCode = runSetupAntigravityCli();\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,iBAAiB;AAC1B,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAa9B,IAAM,cAAc;AAOpB,SAAS,kBAA0B;AACjC,SAAO,KAAK,KAAK,GAAG,QAAQ,GAAG,WAAW,UAAU,WAAW,WAAW;AAC5E;AAUA,SAAS,oBAAoB,YAAkC;AAC7D,QAAM,cAAc,GAAG;AAAA,IACrB,KAAK,KAAK,GAAG,OAAO,GAAG,GAAG,WAAW,eAAe;AAAA,EACtD;AACA,QAAM,YAAY,KAAK,KAAK,aAAa,WAAW;AACpD,KAAG,OAAO,YAAY,WAAW,EAAE,WAAW,KAAK,CAAC;AACpD,SAAO,EAAE,WAAW,YAAY;AAClC;AAEA,SAAS,mBAAmB,aAA2B;AACrD,MAAI;AACF,OAAG,OAAO,aAAa,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACzD,SAAS,OAAO;AACd,YAAQ,OAAO;AAAA,MACb,2DAA2D,WAAW,KAAM,MAAgB,OAAO;AAAA;AAAA,IACrG;AAAA,EACF;AACF;AAWA,SAAS,oBAA6B;AACpC,QAAM,eAAe,gBAAgB;AACrC,QAAM,aAAa,KAAK,KAAK,cAAc,iBAAiB;AAC5D,MAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAC9B,YAAQ,OAAO;AAAA,MACb,yCAAyC,UAAU;AAAA;AAAA,IACrD;AACA,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,MAAM,GAAG,aAAa,YAAY,OAAO;AAC/C,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,eAAW,UAAU,OAAO,OAAO,IAAI,cAAc,CAAC,CAAC,GAA8B;AACnF,UAAI,OAAO,QAAQ,IAAK,QAAO,MAAM;AAAA,IACvC;AACA,OAAG,cAAc,YAAY,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,MAAM,OAAO;AACzE,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,iDAAkD,IAAc,OAAO;AAAA;AAAA,IACzE;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,uBACd,YAAuB,CAAC,SAAS,MAAM,YAAY,UAAU,SAAS,MAAM,OAAO,GACnF;AACA,QAAM,aAAa,KAAK,QAAQ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,IAAI;AAClF,QAAM,eAAe,oBAAoB,UAAU;AACnD,MAAI;AAEJ,MAAI;AACF,aAAS,UAAU,OAAO,CAAC,UAAU,WAAW,aAAa,SAAS,GAAG;AAAA,MACvE,OAAO;AAAA,IACT,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,OAAO;AAAA,MACb,sCAAuC,MAAgB,OAAO;AAAA;AAAA,IAChE;AACA,WAAO;AAAA,EACT,UAAE;AACA,uBAAmB,aAAa,WAAW;AAAA,EAC7C;AAEA,MAAI,OAAO,OAAO;AAChB,YAAQ,OAAO,MAAM,sCAAsC,OAAO,MAAM,OAAO;AAAA,CAAI;AACnF,WAAO;AAAA,EACT;AACA,MAAI,OAAO,WAAW,EAAG,QAAO,OAAO,UAAU;AAEjD,oBAAkB;AAElB,UAAQ,OAAO;AAAA,IACb;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI,IAAI;AAAA,EACjB;AACA,SAAO;AACT;AAEA,IAAI,QAAQ,KAAK,CAAC,MAAM,cAAc,YAAY,GAAG,GAAG;AACtD,UAAQ,WAAW,uBAAuB;AAC5C;","names":[]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath as __f2p } from "node:url";
|
|
4
|
+
import { dirname as __dn } from "node:path";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const __filename = __f2p(import.meta.url);
|
|
7
|
+
const __dirname = __dn(__filename);
|
|
8
|
+
|
|
9
|
+
// src/cli/setup-opencode.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var PLUGIN_NAME = "neobiotech-dev-agent";
|
|
15
|
+
var MCP_SERVER_KEY = PLUGIN_NAME;
|
|
16
|
+
function getOpenCodeConfigDir() {
|
|
17
|
+
return path.join(os.homedir(), ".config", "opencode");
|
|
18
|
+
}
|
|
19
|
+
function findOpenCodeConfig(configDir) {
|
|
20
|
+
const jsoncPath = path.join(configDir, "opencode.jsonc");
|
|
21
|
+
const jsonPath = path.join(configDir, "opencode.json");
|
|
22
|
+
if (fs.existsSync(jsoncPath)) return jsoncPath;
|
|
23
|
+
if (fs.existsSync(jsonPath)) return jsonPath;
|
|
24
|
+
return jsoncPath;
|
|
25
|
+
}
|
|
26
|
+
function stripJsonComments(input) {
|
|
27
|
+
let insideString = false;
|
|
28
|
+
let isEscaped = false;
|
|
29
|
+
let result = "";
|
|
30
|
+
for (let i = 0; i < input.length; i++) {
|
|
31
|
+
const char = input[i];
|
|
32
|
+
const nextChar = input[i + 1];
|
|
33
|
+
if (insideString) {
|
|
34
|
+
result += char;
|
|
35
|
+
if (isEscaped) {
|
|
36
|
+
isEscaped = false;
|
|
37
|
+
} else if (char === "\\") {
|
|
38
|
+
isEscaped = true;
|
|
39
|
+
} else if (char === '"') {
|
|
40
|
+
insideString = false;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '"') {
|
|
45
|
+
insideString = true;
|
|
46
|
+
result += char;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === "/" && nextChar === "/") {
|
|
50
|
+
while (i < input.length && input[i] !== "\n" && input[i] !== "\r") {
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
if (i < input.length) result += input[i];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (char === "/" && nextChar === "*") {
|
|
57
|
+
i += 2;
|
|
58
|
+
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) {
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
result += char;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function mergeOpenCodeMcpConfig(pluginRoot, targetDir = getOpenCodeConfigDir()) {
|
|
69
|
+
const configPath = findOpenCodeConfig(targetDir);
|
|
70
|
+
const messages = [];
|
|
71
|
+
let snippetPath = path.join(pluginRoot, "opencode.mcp.json");
|
|
72
|
+
if (!fs.existsSync(snippetPath)) {
|
|
73
|
+
const devSnippetPath = path.join(
|
|
74
|
+
pluginRoot,
|
|
75
|
+
"plugin-adapters",
|
|
76
|
+
"opencode",
|
|
77
|
+
"mcp.template.json"
|
|
78
|
+
);
|
|
79
|
+
if (fs.existsSync(devSnippetPath)) {
|
|
80
|
+
snippetPath = devSnippetPath;
|
|
81
|
+
} else {
|
|
82
|
+
return {
|
|
83
|
+
status: "conflict",
|
|
84
|
+
configPath,
|
|
85
|
+
messages: [`opencode.mcp.json not found in plugin at ${snippetPath}`]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const snippet = JSON.parse(fs.readFileSync(snippetPath, "utf-8"));
|
|
90
|
+
const serverConfig = snippet.mcp[MCP_SERVER_KEY];
|
|
91
|
+
let config = { $schema: "https://opencode.ai/config.json" };
|
|
92
|
+
if (fs.existsSync(configPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
95
|
+
const stripped = stripJsonComments(raw);
|
|
96
|
+
config = JSON.parse(stripped);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
status: "conflict",
|
|
100
|
+
configPath,
|
|
101
|
+
messages: [
|
|
102
|
+
`Failed to parse existing config ${configPath}: ${err.message}`,
|
|
103
|
+
"Please add the MCP config manually \u2014 see the printed snippet below."
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const mcp = config.mcp ?? {};
|
|
109
|
+
const existing = mcp[MCP_SERVER_KEY];
|
|
110
|
+
if (existing) {
|
|
111
|
+
const existingArgs = existing.args ?? [];
|
|
112
|
+
if (existing.command === "node" && existingArgs[0] === "dist/index.js" && existing.type === "local") {
|
|
113
|
+
return { status: "noop", configPath, messages: ["MCP server already configured (noop)."] };
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
status: "conflict",
|
|
117
|
+
configPath,
|
|
118
|
+
messages: [
|
|
119
|
+
`Existing mcp["${MCP_SERVER_KEY}"] has different settings \u2014 not overwriting.`,
|
|
120
|
+
"Please merge manually:",
|
|
121
|
+
JSON.stringify({ mcp: { [MCP_SERVER_KEY]: serverConfig } }, null, 2)
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const isNew = !fs.existsSync(configPath);
|
|
126
|
+
config.mcp = { ...mcp, [MCP_SERVER_KEY]: serverConfig };
|
|
127
|
+
const configDir = path.dirname(configPath);
|
|
128
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
129
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
130
|
+
messages.push(`MCP server ${isNew ? "added to" : "updated in"} ${configPath}`);
|
|
131
|
+
return { status: isNew ? "added" : "updated", configPath, messages };
|
|
132
|
+
}
|
|
133
|
+
function runSetupOpenCodeCli(args = process.argv.slice(2)) {
|
|
134
|
+
const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
135
|
+
const projectIndex = args.indexOf("--project");
|
|
136
|
+
let targetDir;
|
|
137
|
+
if (projectIndex !== -1) {
|
|
138
|
+
if (!args[projectIndex + 1]) {
|
|
139
|
+
process.stderr.write("--project requires a path\n");
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
targetDir = path.join(path.resolve(args[projectIndex + 1]), ".opencode");
|
|
143
|
+
} else {
|
|
144
|
+
targetDir = getOpenCodeConfigDir();
|
|
145
|
+
}
|
|
146
|
+
const result = mergeOpenCodeMcpConfig(pluginRoot, targetDir);
|
|
147
|
+
for (const msg of result.messages) {
|
|
148
|
+
if (result.status === "conflict") {
|
|
149
|
+
process.stderr.write(`${msg}
|
|
150
|
+
`);
|
|
151
|
+
} else {
|
|
152
|
+
process.stdout.write(`${msg}
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (result.status === "conflict") {
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
[
|
|
161
|
+
`OpenCode MCP server configured: ${MCP_SERVER_KEY}`,
|
|
162
|
+
`Config: ${result.configPath}`,
|
|
163
|
+
"",
|
|
164
|
+
"Required environment variables (set in your shell or .env):",
|
|
165
|
+
" JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN",
|
|
166
|
+
"Optional environment variables:",
|
|
167
|
+
" XRAY_CLIENT_ID, XRAY_CLIENT_SECRET",
|
|
168
|
+
" RISK_PLUGIN_ENDPOINT, RISK_PLUGIN_AUTH_TOKEN, RISK_PLUGIN_AUTHOR",
|
|
169
|
+
"",
|
|
170
|
+
"Restart OpenCode to apply changes."
|
|
171
|
+
].join("\n") + "\n"
|
|
172
|
+
);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
176
|
+
process.exitCode = runSetupOpenCodeCli();
|
|
177
|
+
}
|
|
178
|
+
export {
|
|
179
|
+
mergeOpenCodeMcpConfig,
|
|
180
|
+
runSetupOpenCodeCli
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=setup-opencode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/setup-opencode.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst PLUGIN_NAME = 'neobiotech-dev-agent';\nconst MCP_SERVER_KEY = PLUGIN_NAME;\n\n/** 전역 OpenCode config 디렉터리 (~/.config/opencode) */\nfunction getOpenCodeConfigDir(): string {\n return path.join(os.homedir(), '.config', 'opencode');\n}\n\n/** opencode.json 또는 opencode.jsonc 경로 탐색 (jsonc 우선) */\nfunction findOpenCodeConfig(configDir: string): string {\n const jsoncPath = path.join(configDir, 'opencode.jsonc');\n const jsonPath = path.join(configDir, 'opencode.json');\n if (fs.existsSync(jsoncPath)) return jsoncPath;\n if (fs.existsSync(jsonPath)) return jsonPath;\n return jsoncPath; // 없으면 새로 생성할 경로로 jsoncPath 반환\n}\n\n/** JSONC 주석(// 및 /* * /)을 문자열 내부 따옴표를 보존하며 제거 */\nfunction stripJsonComments(input: string): string {\n let insideString = false;\n let isEscaped = false;\n let result = '';\n\n for (let i = 0; i < input.length; i++) {\n const char = input[i];\n const nextChar = input[i + 1];\n\n if (insideString) {\n result += char;\n if (isEscaped) {\n isEscaped = false;\n } else if (char === '\\\\') {\n isEscaped = true;\n } else if (char === '\"') {\n insideString = false;\n }\n continue;\n }\n\n if (char === '\"') {\n insideString = true;\n result += char;\n continue;\n }\n\n if (char === '/' && nextChar === '/') {\n while (i < input.length && input[i] !== '\\n' && input[i] !== '\\r') {\n i++;\n }\n if (i < input.length) result += input[i];\n continue;\n }\n\n if (char === '/' && nextChar === '*') {\n i += 2;\n while (i < input.length && !(input[i] === '*' && input[i + 1] === '/')) {\n i++;\n }\n i++;\n continue;\n }\n\n result += char;\n }\n\n return result;\n}\n\ntype MergeResult = {\n status: 'added' | 'updated' | 'noop' | 'conflict';\n configPath: string;\n messages: string[];\n};\n\n/**\n * opencode.json(c)에 neobiotech-dev-agent MCP 설정을 병합한다.\n * - 이미 동일 key가 있고 `command: \"node\"` + `args[0]: \"dist/index.js\"` 이면 noop\n * - 이미 있지만 다른 설정이면 conflict (덮어쓰지 않음)\n * - 없으면 추가\n */\nexport function mergeOpenCodeMcpConfig(\n pluginRoot: string,\n targetDir: string = getOpenCodeConfigDir(),\n): MergeResult {\n const configPath = findOpenCodeConfig(targetDir);\n const messages: string[] = [];\n\n // 설치된 패키지의 MCP 스니펫 읽기 (패키지 배포본 또는 dev repo)\n let snippetPath = path.join(pluginRoot, 'opencode.mcp.json');\n if (!fs.existsSync(snippetPath)) {\n const devSnippetPath = path.join(\n pluginRoot,\n 'plugin-adapters',\n 'opencode',\n 'mcp.template.json',\n );\n if (fs.existsSync(devSnippetPath)) {\n snippetPath = devSnippetPath;\n } else {\n return {\n status: 'conflict',\n configPath,\n messages: [`opencode.mcp.json not found in plugin at ${snippetPath}`],\n };\n }\n }\n const snippet = JSON.parse(fs.readFileSync(snippetPath, 'utf-8')) as {\n mcp: Record<string, unknown>;\n };\n const serverConfig = snippet.mcp[MCP_SERVER_KEY];\n\n // 기존 config 로드\n let config: Record<string, unknown> = { $schema: 'https://opencode.ai/config.json' };\n if (fs.existsSync(configPath)) {\n try {\n const raw = fs.readFileSync(configPath, 'utf-8');\n const stripped = stripJsonComments(raw);\n config = JSON.parse(stripped) as Record<string, unknown>;\n } catch (err) {\n return {\n status: 'conflict',\n configPath,\n messages: [\n `Failed to parse existing config ${configPath}: ${(err as Error).message}`,\n 'Please add the MCP config manually — see the printed snippet below.',\n ],\n };\n }\n }\n\n // mcp 섹션 확인\n const mcp = (config.mcp ?? {}) as Record<string, unknown>;\n const existing = mcp[MCP_SERVER_KEY] as Record<string, unknown> | undefined;\n\n if (existing) {\n // 이미 neobiotech-dev-agent가 있는 경우\n const existingArgs = (existing.args as string[] | undefined) ?? [];\n if (\n existing.command === 'node' &&\n existingArgs[0] === 'dist/index.js' &&\n existing.type === 'local'\n ) {\n return { status: 'noop', configPath, messages: ['MCP server already configured (noop).'] };\n }\n // 다른 설정 존재 → conflict\n return {\n status: 'conflict',\n configPath,\n messages: [\n `Existing mcp[\"${MCP_SERVER_KEY}\"] has different settings — not overwriting.`,\n 'Please merge manually:',\n JSON.stringify({ mcp: { [MCP_SERVER_KEY]: serverConfig } }, null, 2),\n ],\n };\n }\n\n // 새로 추가\n const isNew = !fs.existsSync(configPath);\n config.mcp = { ...mcp, [MCP_SERVER_KEY]: serverConfig };\n const configDir = path.dirname(configPath);\n fs.mkdirSync(configDir, { recursive: true });\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\\n', 'utf-8');\n messages.push(`MCP server ${isNew ? 'added to' : 'updated in'} ${configPath}`);\n return { status: isNew ? 'added' : 'updated', configPath, messages };\n}\n\nexport function runSetupOpenCodeCli(args: string[] = process.argv.slice(2)): number {\n const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');\n\n // --global 플래그: 전역 config에 추가 (기본값)\n // --project <dir>: 프로젝트 수준 .opencode/ 에 추가\n const projectIndex = args.indexOf('--project');\n let targetDir: string;\n if (projectIndex !== -1) {\n if (!args[projectIndex + 1]) {\n process.stderr.write('--project requires a path\\n');\n return 1;\n }\n targetDir = path.join(path.resolve(args[projectIndex + 1]), '.opencode');\n } else {\n targetDir = getOpenCodeConfigDir();\n }\n\n const result = mergeOpenCodeMcpConfig(pluginRoot, targetDir);\n\n for (const msg of result.messages) {\n if (result.status === 'conflict') {\n process.stderr.write(`${msg}\\n`);\n } else {\n process.stdout.write(`${msg}\\n`);\n }\n }\n\n if (result.status === 'conflict') {\n return 2;\n }\n\n process.stdout.write(\n [\n `OpenCode MCP server configured: ${MCP_SERVER_KEY}`,\n `Config: ${result.configPath}`,\n '',\n 'Required environment variables (set in your shell or .env):',\n ' JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN',\n 'Optional environment variables:',\n ' XRAY_CLIENT_ID, XRAY_CLIENT_SECRET',\n ' RISK_PLUGIN_ENDPOINT, RISK_PLUGIN_AUTH_TOKEN, RISK_PLUGIN_AUTHOR',\n '',\n 'Restart OpenCode to apply changes.',\n ].join('\\n') + '\\n',\n );\n return 0;\n}\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n process.exitCode = runSetupOpenCodeCli();\n}\n"],"mappings":"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","names":[]}
|