odoo-forge 0.1.9 → 0.1.10
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/package.json +2 -2
- package/src/index.js +10 -125
- package/src/paths.js +0 -8
- package/src/claude.js +0 -50
- package/src/codex.js +0 -158
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "odoo-forge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "CLI installer and updater for Odoo Forge internal 1.0.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"odoo-forge-bundle": "0.1.
|
|
7
|
+
"odoo-forge-bundle": "0.1.10"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"odoo-forge": "bin/odoo-forge.js"
|
package/src/index.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import readline from "node:readline/promises";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
4
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
import { installClaudeWiring, readClaudeManagedToken } from "./claude.js";
|
|
9
|
-
import { installCodexWiring, readCodexManagedToken } from "./codex.js";
|
|
10
5
|
import {
|
|
11
6
|
getAgentsSkillsRoot,
|
|
12
|
-
getClaudeConfigPath,
|
|
13
|
-
getCodexConfigPath,
|
|
14
7
|
getInstalledSkillsPath,
|
|
15
8
|
getLegacyInstalledSkillsPath,
|
|
16
9
|
} from "./paths.js";
|
|
@@ -21,9 +14,7 @@ function printHelp() {
|
|
|
21
14
|
Usage:
|
|
22
15
|
odoo-forge install
|
|
23
16
|
odoo-forge update
|
|
24
|
-
odoo-forge doctor
|
|
25
|
-
odoo-forge login flowus
|
|
26
|
-
odoo-forge mcp flowus`);
|
|
17
|
+
odoo-forge doctor`);
|
|
27
18
|
}
|
|
28
19
|
|
|
29
20
|
async function resolveBundleRoot(bundleRootOverride) {
|
|
@@ -42,29 +33,6 @@ async function resolveBundleRoot(bundleRootOverride) {
|
|
|
42
33
|
}
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
async function defaultPromptForSecret(message) {
|
|
46
|
-
const rl = readline.createInterface({
|
|
47
|
-
input: process.stdin,
|
|
48
|
-
output: process.stdout,
|
|
49
|
-
});
|
|
50
|
-
try {
|
|
51
|
-
return await rl.question(message);
|
|
52
|
-
} finally {
|
|
53
|
-
rl.close();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function defaultSpawnProcess(command, args, options = {}) {
|
|
58
|
-
return await new Promise((resolve, reject) => {
|
|
59
|
-
const child = spawn(command, args, {
|
|
60
|
-
stdio: "inherit",
|
|
61
|
-
...options,
|
|
62
|
-
});
|
|
63
|
-
child.on("close", (code) => resolve(code ?? 0));
|
|
64
|
-
child.on("error", reject);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
36
|
function installSkills({ bundleRoot, homeDir }) {
|
|
69
37
|
const sourceSkillsDir = path.join(bundleRoot, "skills");
|
|
70
38
|
const targetSkillsDir = getInstalledSkillsPath({ homeDir });
|
|
@@ -84,120 +52,43 @@ function installSkills({ bundleRoot, homeDir }) {
|
|
|
84
52
|
return targetSkillsDir;
|
|
85
53
|
}
|
|
86
54
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
envToken ??
|
|
91
|
-
readCodexManagedToken({ homeDir: ctx.homeDir }) ??
|
|
92
|
-
readClaudeManagedToken({ homeDir: ctx.homeDir });
|
|
93
|
-
|
|
94
|
-
if (currentToken) {
|
|
95
|
-
return currentToken;
|
|
55
|
+
function countInstalledSkills(skillsPath) {
|
|
56
|
+
if (!fs.existsSync(skillsPath)) {
|
|
57
|
+
return 0;
|
|
96
58
|
}
|
|
97
59
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
return token;
|
|
60
|
+
return fs
|
|
61
|
+
.readdirSync(skillsPath, { withFileTypes: true })
|
|
62
|
+
.filter((entry) => entry.isDirectory())
|
|
63
|
+
.length;
|
|
104
64
|
}
|
|
105
65
|
|
|
106
66
|
function runDoctor(ctx) {
|
|
107
67
|
const skillsPath = getInstalledSkillsPath({ homeDir: ctx.homeDir });
|
|
108
|
-
const codexConfigPath = getCodexConfigPath({ homeDir: ctx.homeDir });
|
|
109
|
-
const claudeConfigPath = getClaudeConfigPath({ homeDir: ctx.homeDir });
|
|
110
|
-
const codexToken = readCodexManagedToken({ homeDir: ctx.homeDir });
|
|
111
|
-
const claudeToken = readClaudeManagedToken({ homeDir: ctx.homeDir });
|
|
112
68
|
|
|
113
69
|
ctx.output.log("Odoo Forge doctor");
|
|
114
70
|
ctx.output.log(`Skills root: ${skillsPath}`);
|
|
115
71
|
ctx.output.log(`Skills installed: ${fs.existsSync(skillsPath) ? "yes" : "no"}`);
|
|
116
|
-
ctx.output.log(`
|
|
117
|
-
ctx.output.log(`Codex FlowUS MCP exists: ${codexToken ? "yes" : "no"}`);
|
|
118
|
-
ctx.output.log(`Claude config exists: ${fs.existsSync(claudeConfigPath) ? "yes" : "no"}`);
|
|
119
|
-
ctx.output.log(`Claude FlowUS MCP exists: ${claudeToken ? "yes" : "no"}`);
|
|
120
|
-
ctx.output.log(`FlowUS token synchronized: ${codexToken && claudeToken && codexToken === claudeToken ? "yes" : "no"}`);
|
|
72
|
+
ctx.output.log(`Installed skills count: ${countInstalledSkills(skillsPath)}`);
|
|
121
73
|
}
|
|
122
74
|
|
|
123
75
|
async function runInstallLike(ctx, mode) {
|
|
124
76
|
const bundleRoot = await resolveBundleRoot(ctx.bundleRoot);
|
|
125
|
-
const token = await ensureFlowusToken({ ctx });
|
|
126
77
|
const skillsPath = installSkills({
|
|
127
78
|
bundleRoot,
|
|
128
79
|
homeDir: ctx.homeDir,
|
|
129
80
|
});
|
|
130
81
|
|
|
131
|
-
const codexResult = installCodexWiring({
|
|
132
|
-
homeDir: ctx.homeDir,
|
|
133
|
-
token,
|
|
134
|
-
});
|
|
135
|
-
const claudeResult = installClaudeWiring({
|
|
136
|
-
homeDir: ctx.homeDir,
|
|
137
|
-
token,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
82
|
ctx.output.log(`${mode} complete.`);
|
|
141
83
|
ctx.output.log(`Skills root: ${skillsPath}`);
|
|
142
|
-
ctx.output.log(`
|
|
143
|
-
ctx.output.log(`Claude config: ${claudeResult.configPath}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function runLogin(ctx, provider) {
|
|
147
|
-
if (provider !== "flowus") {
|
|
148
|
-
throw new Error(`Unsupported provider: ${provider}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const token =
|
|
152
|
-
ctx.env.ODOO_FORGE_FLOWUS_TOKEN ??
|
|
153
|
-
ctx.env.FLOWUS_TOKEN ??
|
|
154
|
-
(await ctx.promptForSecret("Paste your FlowUS token: ")).trim();
|
|
155
|
-
|
|
156
|
-
if (!token) {
|
|
157
|
-
throw new Error("FlowUS token is required.");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const codexResult = installCodexWiring({
|
|
161
|
-
homeDir: ctx.homeDir,
|
|
162
|
-
token,
|
|
163
|
-
});
|
|
164
|
-
const claudeResult = installClaudeWiring({
|
|
165
|
-
homeDir: ctx.homeDir,
|
|
166
|
-
token,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
ctx.output.log(`Saved FlowUS token to ${codexResult.configPath}`);
|
|
170
|
-
ctx.output.log(`Saved FlowUS token to ${claudeResult.configPath}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function runMcp(ctx, provider) {
|
|
174
|
-
if (provider !== "flowus") {
|
|
175
|
-
throw new Error(`Unsupported MCP provider: ${provider}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const token = ctx.env.FLOWUS_TOKEN ?? ctx.env.ODOO_FORGE_FLOWUS_TOKEN;
|
|
179
|
-
|
|
180
|
-
if (!token) {
|
|
181
|
-
throw new Error("Missing FlowUS token. Set FLOWUS_TOKEN before running `odoo-forge mcp flowus`.");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return await ctx.spawnProcess("npx", ["-y", "flowus-mcp-server@latest"], {
|
|
185
|
-
env: {
|
|
186
|
-
...ctx.env,
|
|
187
|
-
FLOWUS_TOKEN: token,
|
|
188
|
-
},
|
|
189
|
-
});
|
|
84
|
+
ctx.output.log(`Installed skills count: ${countInstalledSkills(skillsPath)}`);
|
|
190
85
|
}
|
|
191
86
|
|
|
192
87
|
function normalizeContext(overrides = {}) {
|
|
193
88
|
return {
|
|
194
89
|
homeDir: overrides.homeDir ?? os.homedir(),
|
|
195
|
-
platform: overrides.platform ?? process.platform,
|
|
196
|
-
env: overrides.env ?? process.env,
|
|
197
90
|
bundleRoot: overrides.bundleRoot,
|
|
198
91
|
output: overrides.output ?? console,
|
|
199
|
-
promptForSecret: overrides.promptForSecret ?? defaultPromptForSecret,
|
|
200
|
-
spawnProcess: overrides.spawnProcess ?? defaultSpawnProcess,
|
|
201
92
|
};
|
|
202
93
|
}
|
|
203
94
|
|
|
@@ -215,12 +106,6 @@ export async function main(argv, overrides = {}) {
|
|
|
215
106
|
case "doctor":
|
|
216
107
|
runDoctor(ctx);
|
|
217
108
|
break;
|
|
218
|
-
case "login":
|
|
219
|
-
await runLogin(ctx, argv[1]);
|
|
220
|
-
break;
|
|
221
|
-
case "mcp":
|
|
222
|
-
await runMcp(ctx, argv[1]);
|
|
223
|
-
break;
|
|
224
109
|
case "help":
|
|
225
110
|
case "--help":
|
|
226
111
|
case "-h":
|
package/src/paths.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
export function getCodexConfigPath({ homeDir = os.homedir() } = {}) {
|
|
5
|
-
return path.join(homeDir, ".codex", "config.toml");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function getClaudeConfigPath({ homeDir = os.homedir() } = {}) {
|
|
9
|
-
return path.join(homeDir, ".claude.json");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
4
|
export function getAgentsSkillsRoot({ homeDir = os.homedir() } = {}) {
|
|
13
5
|
return path.join(homeDir, ".agents", "skills");
|
|
14
6
|
}
|
package/src/claude.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
|
|
3
|
-
import { getClaudeConfigPath } from "./paths.js";
|
|
4
|
-
|
|
5
|
-
export function readClaudeConfig({ homeDir }) {
|
|
6
|
-
const configPath = getClaudeConfigPath({ homeDir });
|
|
7
|
-
if (!fs.existsSync(configPath)) {
|
|
8
|
-
return {};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function writeClaudeConfig({ homeDir, config }) {
|
|
15
|
-
const configPath = getClaudeConfigPath({ homeDir });
|
|
16
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
17
|
-
return configPath;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function buildClaudeFlowusServer({ token }) {
|
|
21
|
-
return {
|
|
22
|
-
type: "stdio",
|
|
23
|
-
command: "npx",
|
|
24
|
-
args: ["-y", "flowus-mcp-server@latest"],
|
|
25
|
-
env: {
|
|
26
|
-
FLOWUS_TOKEN: token,
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function installClaudeWiring({ homeDir, token }) {
|
|
32
|
-
const currentConfig = readClaudeConfig({ homeDir });
|
|
33
|
-
const nextConfig = {
|
|
34
|
-
...currentConfig,
|
|
35
|
-
mcpServers: {
|
|
36
|
-
...(currentConfig.mcpServers ?? {}),
|
|
37
|
-
flowus: buildClaudeFlowusServer({ token }),
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
const configPath = writeClaudeConfig({ homeDir, config: nextConfig });
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
configPath,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function readClaudeManagedToken({ homeDir }) {
|
|
48
|
-
const config = readClaudeConfig({ homeDir });
|
|
49
|
-
return config?.mcpServers?.flowus?.env?.FLOWUS_TOKEN ?? null;
|
|
50
|
-
}
|
package/src/codex.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { getCodexConfigPath } from "./paths.js";
|
|
5
|
-
|
|
6
|
-
export const CODEX_START_MARKER = "# BEGIN ODOO FORGE FLOWUS";
|
|
7
|
-
export const CODEX_END_MARKER = "# END ODOO FORGE FLOWUS";
|
|
8
|
-
|
|
9
|
-
export function buildCodexManagedBlock({ token }) {
|
|
10
|
-
return `${CODEX_START_MARKER}
|
|
11
|
-
[mcp_servers.flowus]
|
|
12
|
-
type = "stdio"
|
|
13
|
-
command = "npx"
|
|
14
|
-
args = ["-y", "flowus-mcp-server@latest"]
|
|
15
|
-
|
|
16
|
-
[mcp_servers.flowus.env]
|
|
17
|
-
FLOWUS_TOKEN = "${token}"
|
|
18
|
-
${CODEX_END_MARKER}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function stripManagedBlock({
|
|
22
|
-
originalContent,
|
|
23
|
-
startMarker,
|
|
24
|
-
endMarker,
|
|
25
|
-
}) {
|
|
26
|
-
const startIndex = originalContent.indexOf(startMarker);
|
|
27
|
-
const endIndex = originalContent.indexOf(endMarker);
|
|
28
|
-
|
|
29
|
-
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
30
|
-
return originalContent;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const blockEnd = endIndex + endMarker.length;
|
|
34
|
-
return `${originalContent.slice(0, startIndex)}${originalContent.slice(blockEnd)}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isFlowusSectionHeader(headerName) {
|
|
38
|
-
return headerName === "mcp_servers.flowus" || headerName.startsWith("mcp_servers.flowus.");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function removeFlowusSections(originalContent) {
|
|
42
|
-
const lines = originalContent.split("\n");
|
|
43
|
-
const keptLines = [];
|
|
44
|
-
let skipCurrentSection = false;
|
|
45
|
-
|
|
46
|
-
for (const line of lines) {
|
|
47
|
-
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
48
|
-
if (sectionMatch) {
|
|
49
|
-
skipCurrentSection = isFlowusSectionHeader(sectionMatch[1]);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!skipCurrentSection) {
|
|
53
|
-
keptLines.push(line);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return keptLines.join("\n");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function readSectionContent({ originalContent, sectionName }) {
|
|
61
|
-
const lines = originalContent.split("\n");
|
|
62
|
-
const collected = [];
|
|
63
|
-
let inSection = false;
|
|
64
|
-
|
|
65
|
-
for (const line of lines) {
|
|
66
|
-
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
67
|
-
if (sectionMatch) {
|
|
68
|
-
if (inSection) {
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
inSection = sectionMatch[1] === sectionName;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (inSection) {
|
|
76
|
-
collected.push(line);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return inSection ? collected.join("\n") : null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function extractTokenFromContent(originalContent) {
|
|
84
|
-
const flowusSection = readSectionContent({
|
|
85
|
-
originalContent,
|
|
86
|
-
sectionName: "mcp_servers.flowus",
|
|
87
|
-
});
|
|
88
|
-
const envSection = readSectionContent({
|
|
89
|
-
originalContent,
|
|
90
|
-
sectionName: "mcp_servers.flowus.env",
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const candidates = [envSection, flowusSection, originalContent];
|
|
94
|
-
for (const candidate of candidates) {
|
|
95
|
-
if (!candidate) {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const inlineMatch = candidate.match(/FLOWUS_TOKEN\s*=\s*"([^"]+)"/);
|
|
100
|
-
if (inlineMatch) {
|
|
101
|
-
return inlineMatch[1];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function upsertManagedBlock({
|
|
109
|
-
originalContent,
|
|
110
|
-
startMarker,
|
|
111
|
-
endMarker,
|
|
112
|
-
block,
|
|
113
|
-
}) {
|
|
114
|
-
const withoutManagedBlock = stripManagedBlock({
|
|
115
|
-
originalContent,
|
|
116
|
-
startMarker,
|
|
117
|
-
endMarker,
|
|
118
|
-
});
|
|
119
|
-
const withoutFlowusSections = removeFlowusSections(withoutManagedBlock);
|
|
120
|
-
const trimmed = withoutFlowusSections.trimEnd();
|
|
121
|
-
return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function readCodexManagedToken({ homeDir }) {
|
|
125
|
-
const configPath = getCodexConfigPath({ homeDir });
|
|
126
|
-
if (!fs.existsSync(configPath)) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const content = fs.readFileSync(configPath, "utf8");
|
|
131
|
-
const startIndex = content.indexOf(CODEX_START_MARKER);
|
|
132
|
-
const endIndex = content.indexOf(CODEX_END_MARKER);
|
|
133
|
-
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
134
|
-
const managedBlock = content.slice(startIndex, endIndex + CODEX_END_MARKER.length);
|
|
135
|
-
return extractTokenFromContent(managedBlock);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return extractTokenFromContent(content);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function installCodexWiring({ homeDir, token }) {
|
|
142
|
-
const configPath = getCodexConfigPath({ homeDir });
|
|
143
|
-
const codexDir = path.dirname(configPath);
|
|
144
|
-
|
|
145
|
-
fs.mkdirSync(codexDir, { recursive: true });
|
|
146
|
-
const currentConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
147
|
-
const nextConfig = upsertManagedBlock({
|
|
148
|
-
originalContent: currentConfig,
|
|
149
|
-
startMarker: CODEX_START_MARKER,
|
|
150
|
-
endMarker: CODEX_END_MARKER,
|
|
151
|
-
block: buildCodexManagedBlock({ token }),
|
|
152
|
-
});
|
|
153
|
-
fs.writeFileSync(configPath, nextConfig);
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
configPath,
|
|
157
|
-
};
|
|
158
|
-
}
|