odoo-forge 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/bin/odoo-forge.js +8 -0
- package/package.json +21 -0
- package/src/claude.js +92 -0
- package/src/codex.js +80 -0
- package/src/config.js +70 -0
- package/src/index.js +295 -0
- package/src/paths.js +26 -0
- package/src/runtime.js +27 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "odoo-forge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI installer and updater for Odoo Forge internal 1.0.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"odoo-forge-bundle": "0.1.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"odoo-forge": "bin/odoo-forge.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/odoo-forge.js",
|
|
18
|
+
"doctor": "node bin/odoo-forge.js doctor",
|
|
19
|
+
"test": "node --test"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/claude.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const MARKETPLACE_NAME = "odoo-forge-marketplace";
|
|
5
|
+
const PLUGIN_NAME = "odoo-forge";
|
|
6
|
+
|
|
7
|
+
function writeJson(filePath, value) {
|
|
8
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
9
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderClaudeMarketplace({
|
|
13
|
+
bundleRoot,
|
|
14
|
+
destinationRoot,
|
|
15
|
+
version,
|
|
16
|
+
}) {
|
|
17
|
+
const marketplaceDir = path.join(destinationRoot, "claude-marketplace");
|
|
18
|
+
const pluginDir = path.join(marketplaceDir, "plugins", PLUGIN_NAME);
|
|
19
|
+
const skillsSource = path.join(bundleRoot, "skills");
|
|
20
|
+
|
|
21
|
+
fs.rmSync(marketplaceDir, { force: true, recursive: true });
|
|
22
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
writeJson(path.join(marketplaceDir, ".claude-plugin", "marketplace.json"), {
|
|
25
|
+
name: MARKETPLACE_NAME,
|
|
26
|
+
owner: {
|
|
27
|
+
name: "Odoo Forge Team",
|
|
28
|
+
},
|
|
29
|
+
metadata: {
|
|
30
|
+
description: "Internal marketplace for Odoo Forge.",
|
|
31
|
+
version,
|
|
32
|
+
pluginRoot: "./plugins",
|
|
33
|
+
},
|
|
34
|
+
plugins: [
|
|
35
|
+
{
|
|
36
|
+
name: PLUGIN_NAME,
|
|
37
|
+
description: "Internal Odoo Forge skill suite.",
|
|
38
|
+
source: "./plugins/odoo-forge",
|
|
39
|
+
category: "development",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
writeJson(path.join(pluginDir, ".claude-plugin", "plugin.json"), {
|
|
45
|
+
name: PLUGIN_NAME,
|
|
46
|
+
description: "Internal Odoo Forge plugin for Claude Code.",
|
|
47
|
+
version,
|
|
48
|
+
author: {
|
|
49
|
+
name: "Internal Team",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
writeJson(path.join(pluginDir, ".mcp.json"), {
|
|
54
|
+
mcpServers: {
|
|
55
|
+
flowus: {
|
|
56
|
+
type: "stdio",
|
|
57
|
+
command: "odoo-forge",
|
|
58
|
+
args: ["mcp", "flowus"],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
fs.cpSync(skillsSource, path.join(pluginDir, "skills"), { recursive: true });
|
|
64
|
+
|
|
65
|
+
return marketplaceDir;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildClaudeCommandPlan({ marketplaceDir, mode }) {
|
|
69
|
+
if (mode === "update") {
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
command: "claude",
|
|
73
|
+
args: ["plugin", "marketplace", "update", MARKETPLACE_NAME],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
command: "claude",
|
|
77
|
+
args: ["plugin", "update", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, "--scope", "user"],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
command: "claude",
|
|
85
|
+
args: ["plugin", "marketplace", "add", marketplaceDir],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
command: "claude",
|
|
89
|
+
args: ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`, "--scope", "user"],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
package/src/codex.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getCodexConfigPath, getCodexSkillsLinkPath } 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() {
|
|
10
|
+
return `${CODEX_START_MARKER}
|
|
11
|
+
[mcp_servers.flowus]
|
|
12
|
+
type = "stdio"
|
|
13
|
+
command = "odoo-forge"
|
|
14
|
+
args = ["mcp", "flowus"]
|
|
15
|
+
${CODEX_END_MARKER}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function upsertManagedBlock({
|
|
19
|
+
originalContent,
|
|
20
|
+
startMarker,
|
|
21
|
+
endMarker,
|
|
22
|
+
block,
|
|
23
|
+
}) {
|
|
24
|
+
const startIndex = originalContent.indexOf(startMarker);
|
|
25
|
+
const endIndex = originalContent.indexOf(endMarker);
|
|
26
|
+
|
|
27
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
28
|
+
const blockEnd = endIndex + endMarker.length;
|
|
29
|
+
return `${originalContent.slice(0, startIndex)}${block}${originalContent.slice(blockEnd)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = originalContent.trimEnd();
|
|
33
|
+
return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureDirectoryLink({ linkPath, targetPath, platform = process.platform }) {
|
|
37
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
38
|
+
|
|
39
|
+
if (fs.existsSync(linkPath) || fs.lstatSync(path.dirname(linkPath)).isDirectory()) {
|
|
40
|
+
try {
|
|
41
|
+
const existingTarget = fs.readlinkSync(linkPath);
|
|
42
|
+
if (path.resolve(path.dirname(linkPath), existingTarget) === targetPath) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
fs.rmSync(linkPath, { force: true, recursive: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fs.rmSync(linkPath, { force: true, recursive: true });
|
|
51
|
+
fs.symlinkSync(targetPath, linkPath, platform === "win32" ? "junction" : "dir");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function installCodexWiring({
|
|
55
|
+
homeDir,
|
|
56
|
+
installRoot,
|
|
57
|
+
platform = process.platform,
|
|
58
|
+
}) {
|
|
59
|
+
const configPath = getCodexConfigPath({ homeDir });
|
|
60
|
+
const linkPath = getCodexSkillsLinkPath({ homeDir });
|
|
61
|
+
const codexDir = path.dirname(configPath);
|
|
62
|
+
const skillsTarget = path.join(installRoot, "current", "skills");
|
|
63
|
+
|
|
64
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
65
|
+
const currentConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
66
|
+
const nextConfig = upsertManagedBlock({
|
|
67
|
+
originalContent: currentConfig,
|
|
68
|
+
startMarker: CODEX_START_MARKER,
|
|
69
|
+
endMarker: CODEX_END_MARKER,
|
|
70
|
+
block: buildCodexManagedBlock(),
|
|
71
|
+
});
|
|
72
|
+
fs.writeFileSync(configPath, nextConfig);
|
|
73
|
+
|
|
74
|
+
ensureDirectoryLink({ linkPath, targetPath: skillsTarget, platform });
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
configPath,
|
|
78
|
+
linkPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function readJsonFile(filePath) {
|
|
6
|
+
if (!fs.existsSync(filePath)) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function writeJsonFile(filePath, value) {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getConfigPath({
|
|
19
|
+
homeDir = os.homedir(),
|
|
20
|
+
platform = process.platform,
|
|
21
|
+
env = process.env,
|
|
22
|
+
} = {}) {
|
|
23
|
+
if (platform === "darwin") {
|
|
24
|
+
return path.join(homeDir, "Library", "Application Support", "Odoo Forge", "config.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (platform === "win32") {
|
|
28
|
+
const appData = env.APPDATA ?? path.join(homeDir, "AppData", "Roaming");
|
|
29
|
+
return path.win32.join(appData, "Odoo Forge", "config.json");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return path.join(homeDir, ".config", "Odoo Forge", "config.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readUserConfig({ configPath }) {
|
|
36
|
+
return readJsonFile(configPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function writeUserConfig({ configPath, config }) {
|
|
40
|
+
writeJsonFile(configPath, config);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeFlowusToken({ configPath, token }) {
|
|
44
|
+
const current = readUserConfig({ configPath });
|
|
45
|
+
const next = {
|
|
46
|
+
...current,
|
|
47
|
+
mcp: {
|
|
48
|
+
...(current.mcp ?? {}),
|
|
49
|
+
flowus: {
|
|
50
|
+
...((current.mcp ?? {}).flowus ?? {}),
|
|
51
|
+
FLOWUS_TOKEN: token,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
writeUserConfig({ configPath, config: next });
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getFlowusToken({ config }) {
|
|
61
|
+
return config?.mcp?.flowus?.FLOWUS_TOKEN;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readState({ statePath }) {
|
|
65
|
+
return readJsonFile(statePath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function writeState({ statePath, state }) {
|
|
69
|
+
writeJsonFile(statePath, state);
|
|
70
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getConfigPath,
|
|
10
|
+
getFlowusToken,
|
|
11
|
+
readUserConfig,
|
|
12
|
+
writeFlowusToken,
|
|
13
|
+
writeState,
|
|
14
|
+
} from "./config.js";
|
|
15
|
+
import { buildClaudeCommandPlan, renderClaudeMarketplace } from "./claude.js";
|
|
16
|
+
import { installCodexWiring } from "./codex.js";
|
|
17
|
+
import { getCurrentRoot, getInstallRoot, getStatePath } from "./paths.js";
|
|
18
|
+
import { installRuntime } from "./runtime.js";
|
|
19
|
+
|
|
20
|
+
function printHelp() {
|
|
21
|
+
console.log(`Odoo Forge CLI
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
odoo-forge install
|
|
25
|
+
odoo-forge update
|
|
26
|
+
odoo-forge doctor
|
|
27
|
+
odoo-forge login flowus
|
|
28
|
+
odoo-forge mcp flowus`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function resolveBundleRoot(bundleRootOverride) {
|
|
32
|
+
if (bundleRootOverride) {
|
|
33
|
+
return bundleRootOverride;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const bundle = await import("odoo-forge-bundle");
|
|
38
|
+
return bundle.getBundleRoot();
|
|
39
|
+
} catch {
|
|
40
|
+
return path.resolve(
|
|
41
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
42
|
+
"../../odoo-forge-bundle/payload",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function defaultPromptForSecret(message) {
|
|
48
|
+
const rl = readline.createInterface({
|
|
49
|
+
input: process.stdin,
|
|
50
|
+
output: process.stdout,
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
return await rl.question(message);
|
|
54
|
+
} finally {
|
|
55
|
+
rl.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function defaultRunCommand(command, args, options = {}) {
|
|
60
|
+
return await new Promise((resolve) => {
|
|
61
|
+
const child = spawn(command, args, {
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
...options,
|
|
64
|
+
});
|
|
65
|
+
child.on("close", (code) => resolve({ code: code ?? 0 }));
|
|
66
|
+
child.on("error", () => resolve({ code: 1 }));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function defaultSpawnProcess(command, args, options = {}) {
|
|
71
|
+
return await new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn(command, args, {
|
|
73
|
+
stdio: "inherit",
|
|
74
|
+
...options,
|
|
75
|
+
});
|
|
76
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
77
|
+
child.on("error", reject);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function defaultHasCommand(command) {
|
|
82
|
+
const result = spawnSync(command, ["--version"], { stdio: "ignore" });
|
|
83
|
+
return result.status === 0 || result.status === 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function readProduct(bundleRoot) {
|
|
87
|
+
return JSON.parse(fs.readFileSync(path.join(bundleRoot, "config", "product.json"), "utf8"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function ensureFlowusToken({ ctx, configPath }) {
|
|
91
|
+
const existingConfig = readUserConfig({ configPath });
|
|
92
|
+
const envToken = ctx.env.ODOO_FORGE_FLOWUS_TOKEN;
|
|
93
|
+
const currentToken = envToken || getFlowusToken({ config: existingConfig });
|
|
94
|
+
|
|
95
|
+
if (currentToken) {
|
|
96
|
+
writeFlowusToken({ configPath, token: currentToken });
|
|
97
|
+
return currentToken;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const prompted = await ctx.promptForSecret("FlowUS token is required.\nPaste your FlowUS token: ");
|
|
101
|
+
const token = prompted.trim();
|
|
102
|
+
if (!token) {
|
|
103
|
+
throw new Error("FlowUS token is required.");
|
|
104
|
+
}
|
|
105
|
+
writeFlowusToken({ configPath, token });
|
|
106
|
+
return token;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function installClaude({ ctx, currentRoot, version, mode }) {
|
|
110
|
+
if (!(await ctx.hasCommand("claude"))) {
|
|
111
|
+
ctx.output.log("Claude CLI not found. Skipping Claude installation.");
|
|
112
|
+
return { enabled: false };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const marketplaceDir = renderClaudeMarketplace({
|
|
116
|
+
bundleRoot: currentRoot,
|
|
117
|
+
destinationRoot: currentRoot,
|
|
118
|
+
version,
|
|
119
|
+
});
|
|
120
|
+
const commandPlan = buildClaudeCommandPlan({ marketplaceDir, mode });
|
|
121
|
+
|
|
122
|
+
for (const step of commandPlan) {
|
|
123
|
+
const result = await ctx.runCommand(step.command, step.args);
|
|
124
|
+
if (result.code !== 0) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Claude command failed: ${step.command} ${step.args.join(" ")}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
enabled: true,
|
|
133
|
+
marketplaceDir,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function runDoctor(ctx) {
|
|
138
|
+
const installRoot = getInstallRoot({ homeDir: ctx.homeDir });
|
|
139
|
+
const statePath = getStatePath({ homeDir: ctx.homeDir, installRoot });
|
|
140
|
+
const configPath = getConfigPath({
|
|
141
|
+
homeDir: ctx.homeDir,
|
|
142
|
+
platform: ctx.platform,
|
|
143
|
+
env: ctx.env,
|
|
144
|
+
});
|
|
145
|
+
const config = readUserConfig({ configPath });
|
|
146
|
+
const currentRoot = getCurrentRoot({ homeDir: ctx.homeDir, installRoot });
|
|
147
|
+
|
|
148
|
+
ctx.output.log("Odoo Forge doctor");
|
|
149
|
+
ctx.output.log(`Install root: ${installRoot}`);
|
|
150
|
+
ctx.output.log(`Install root exists: ${fs.existsSync(installRoot) ? "yes" : "no"}`);
|
|
151
|
+
ctx.output.log(`State file exists: ${fs.existsSync(statePath) ? "yes" : "no"}`);
|
|
152
|
+
ctx.output.log(`Config file exists: ${fs.existsSync(configPath) ? "yes" : "no"}`);
|
|
153
|
+
ctx.output.log(`FlowUS token exists: ${getFlowusToken({ config }) ? "yes" : "no"}`);
|
|
154
|
+
ctx.output.log(`Current runtime exists: ${fs.existsSync(currentRoot) ? "yes" : "no"}`);
|
|
155
|
+
ctx.output.log(
|
|
156
|
+
`Claude marketplace exists: ${fs.existsSync(path.join(currentRoot, "claude-marketplace")) ? "yes" : "no"}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function runInstallLike(ctx, mode) {
|
|
161
|
+
const installRoot = getInstallRoot({ homeDir: ctx.homeDir });
|
|
162
|
+
const configPath = getConfigPath({
|
|
163
|
+
homeDir: ctx.homeDir,
|
|
164
|
+
platform: ctx.platform,
|
|
165
|
+
env: ctx.env,
|
|
166
|
+
});
|
|
167
|
+
const bundleRoot = await resolveBundleRoot(ctx.bundleRoot);
|
|
168
|
+
const product = await readProduct(bundleRoot);
|
|
169
|
+
|
|
170
|
+
await ensureFlowusToken({ ctx, configPath });
|
|
171
|
+
const { currentRoot } = installRuntime({
|
|
172
|
+
bundleRoot,
|
|
173
|
+
installRoot,
|
|
174
|
+
version: product.version,
|
|
175
|
+
homeDir: ctx.homeDir,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
installCodexWiring({
|
|
179
|
+
homeDir: ctx.homeDir,
|
|
180
|
+
installRoot,
|
|
181
|
+
platform: ctx.platform,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const claudeResult = await installClaude({
|
|
185
|
+
ctx,
|
|
186
|
+
currentRoot,
|
|
187
|
+
version: product.version,
|
|
188
|
+
mode,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
writeState({
|
|
192
|
+
statePath: getStatePath({ homeDir: ctx.homeDir, installRoot }),
|
|
193
|
+
state: {
|
|
194
|
+
version: product.version,
|
|
195
|
+
installedAt: new Date().toISOString(),
|
|
196
|
+
claudeEnabled: claudeResult.enabled,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
ctx.output.log(`${mode} complete.`);
|
|
201
|
+
ctx.output.log(`Install root: ${installRoot}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function runLogin(ctx, provider) {
|
|
205
|
+
if (provider !== "flowus") {
|
|
206
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const configPath = getConfigPath({
|
|
210
|
+
homeDir: ctx.homeDir,
|
|
211
|
+
platform: ctx.platform,
|
|
212
|
+
env: ctx.env,
|
|
213
|
+
});
|
|
214
|
+
const token =
|
|
215
|
+
ctx.env.ODOO_FORGE_FLOWUS_TOKEN ??
|
|
216
|
+
(await ctx.promptForSecret("Paste your FlowUS token: ")).trim();
|
|
217
|
+
|
|
218
|
+
if (!token) {
|
|
219
|
+
throw new Error("FlowUS token is required.");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
writeFlowusToken({ configPath, token });
|
|
223
|
+
ctx.output.log(`Saved FlowUS token to ${configPath}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function runMcp(ctx, provider) {
|
|
227
|
+
if (provider !== "flowus") {
|
|
228
|
+
throw new Error(`Unsupported MCP provider: ${provider}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const configPath = getConfigPath({
|
|
232
|
+
homeDir: ctx.homeDir,
|
|
233
|
+
platform: ctx.platform,
|
|
234
|
+
env: ctx.env,
|
|
235
|
+
});
|
|
236
|
+
const config = readUserConfig({ configPath });
|
|
237
|
+
const token = getFlowusToken({ config });
|
|
238
|
+
|
|
239
|
+
if (!token) {
|
|
240
|
+
throw new Error("Missing FlowUS token. Run `odoo-forge login flowus` first.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return await ctx.spawnProcess("npx", ["-y", "flowus-mcp-server@latest"], {
|
|
244
|
+
env: {
|
|
245
|
+
...ctx.env,
|
|
246
|
+
FLOWUS_TOKEN: token,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeContext(overrides = {}) {
|
|
252
|
+
return {
|
|
253
|
+
homeDir: overrides.homeDir ?? os.homedir(),
|
|
254
|
+
platform: overrides.platform ?? process.platform,
|
|
255
|
+
env: overrides.env ?? process.env,
|
|
256
|
+
bundleRoot: overrides.bundleRoot,
|
|
257
|
+
output: overrides.output ?? console,
|
|
258
|
+
promptForSecret: overrides.promptForSecret ?? defaultPromptForSecret,
|
|
259
|
+
runCommand: overrides.runCommand ?? defaultRunCommand,
|
|
260
|
+
spawnProcess: overrides.spawnProcess ?? defaultSpawnProcess,
|
|
261
|
+
hasCommand: overrides.hasCommand ?? defaultHasCommand,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function main(argv, overrides = {}) {
|
|
266
|
+
const ctx = normalizeContext(overrides);
|
|
267
|
+
const command = argv[0] ?? "help";
|
|
268
|
+
|
|
269
|
+
switch (command) {
|
|
270
|
+
case "install":
|
|
271
|
+
await runInstallLike(ctx, "install");
|
|
272
|
+
break;
|
|
273
|
+
case "update":
|
|
274
|
+
await runInstallLike(ctx, "update");
|
|
275
|
+
break;
|
|
276
|
+
case "doctor":
|
|
277
|
+
runDoctor(ctx);
|
|
278
|
+
break;
|
|
279
|
+
case "login":
|
|
280
|
+
await runLogin(ctx, argv[1]);
|
|
281
|
+
break;
|
|
282
|
+
case "mcp":
|
|
283
|
+
await runMcp(ctx, argv[1]);
|
|
284
|
+
break;
|
|
285
|
+
case "help":
|
|
286
|
+
case "--help":
|
|
287
|
+
case "-h":
|
|
288
|
+
printHelp();
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
console.error(`Unknown command: ${command}`);
|
|
292
|
+
printHelp();
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function getInstallRoot({ homeDir = os.homedir() } = {}) {
|
|
5
|
+
return path.join(homeDir, ".odoo-forge");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getCurrentRoot({ homeDir = os.homedir(), installRoot } = {}) {
|
|
9
|
+
return path.join(installRoot ?? getInstallRoot({ homeDir }), "current");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getVersionsRoot({ homeDir = os.homedir(), installRoot } = {}) {
|
|
13
|
+
return path.join(installRoot ?? getInstallRoot({ homeDir }), "versions");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getStatePath({ homeDir = os.homedir(), installRoot } = {}) {
|
|
17
|
+
return path.join(installRoot ?? getInstallRoot({ homeDir }), "state.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getCodexConfigPath({ homeDir = os.homedir() } = {}) {
|
|
21
|
+
return path.join(homeDir, ".codex", "config.toml");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getCodexSkillsLinkPath({ homeDir = os.homedir() } = {}) {
|
|
25
|
+
return path.join(homeDir, ".agents", "skills", "odoo-forge");
|
|
26
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getCurrentRoot, getVersionsRoot } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
export function installRuntime({
|
|
7
|
+
bundleRoot,
|
|
8
|
+
installRoot,
|
|
9
|
+
version,
|
|
10
|
+
homeDir,
|
|
11
|
+
}) {
|
|
12
|
+
const versionsRoot = getVersionsRoot({ homeDir, installRoot });
|
|
13
|
+
const currentRoot = getCurrentRoot({ homeDir, installRoot });
|
|
14
|
+
const versionRoot = path.join(versionsRoot, version);
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(versionsRoot, { recursive: true });
|
|
17
|
+
fs.rmSync(versionRoot, { force: true, recursive: true });
|
|
18
|
+
fs.cpSync(bundleRoot, versionRoot, { recursive: true });
|
|
19
|
+
|
|
20
|
+
fs.rmSync(currentRoot, { force: true, recursive: true });
|
|
21
|
+
fs.cpSync(versionRoot, currentRoot, { recursive: true });
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
currentRoot,
|
|
25
|
+
versionRoot,
|
|
26
|
+
};
|
|
27
|
+
}
|