@bergetai/odoo-cli 0.9.2 → 0.12.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 +66 -4
- package/dist/cli.js +326 -5
- package/dist/commands/attachments.js +158 -0
- package/dist/commands/auth.js +48 -12
- package/dist/commands/footprint.js +843 -0
- package/dist/commands/marketing.js +184 -0
- package/dist/commands/messages.js +309 -0
- package/dist/commands/users.js +58 -0
- package/dist/commands/version.js +6 -2
- package/dist/config.js +123 -11
- package/dist/global-options.js +1 -0
- package/dist/resource.js +1 -0
- package/package.json +2 -1
package/dist/config.js
CHANGED
|
@@ -3,8 +3,15 @@ import { chmodSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
6
7
|
import { usageError } from "./errors.js";
|
|
7
8
|
export function configFilePath() {
|
|
9
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
10
|
+
const base = xdg && xdg.trim() !== "" ? xdg : join(homedir(), ".config");
|
|
11
|
+
return join(base, "odoo", "config.toml");
|
|
12
|
+
}
|
|
13
|
+
/** Pre-TOML config location; supported read-only for one-way migration. */
|
|
14
|
+
function legacyConfigFilePath() {
|
|
8
15
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
9
16
|
const base = xdg && xdg.trim() !== "" ? xdg : join(homedir(), ".config");
|
|
10
17
|
return join(base, "odoo", "config.json");
|
|
@@ -15,6 +22,26 @@ export async function readConfigFile() {
|
|
|
15
22
|
try {
|
|
16
23
|
raw = await readFile(path, "utf8");
|
|
17
24
|
}
|
|
25
|
+
catch (cause) {
|
|
26
|
+
// No config.toml — fall back to the legacy JSON config, if any.
|
|
27
|
+
if (cause.code === "ENOENT") {
|
|
28
|
+
return readLegacyJsonConfigFile();
|
|
29
|
+
}
|
|
30
|
+
throw usageError(`could not read config file ${path}: ${cause.message}`);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return parseToml(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw usageError(`config file ${path} is not valid TOML`, "fix the file or re-run `odoo auth login`");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function readLegacyJsonConfigFile() {
|
|
40
|
+
const path = legacyConfigFilePath();
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = await readFile(path, "utf8");
|
|
44
|
+
}
|
|
18
45
|
catch (cause) {
|
|
19
46
|
if (cause.code === "ENOENT")
|
|
20
47
|
return {};
|
|
@@ -27,14 +54,85 @@ export async function readConfigFile() {
|
|
|
27
54
|
throw usageError(`config file ${path} is not valid JSON`, "fix the file or re-run `odoo auth login`");
|
|
28
55
|
}
|
|
29
56
|
}
|
|
57
|
+
const PROFILE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
58
|
+
/**
|
|
59
|
+
* Profile selection: `--profile` flag > `ODOO_PROFILE` env. Returns undefined
|
|
60
|
+
* when neither is set (callers decide whether that is fatal).
|
|
61
|
+
*/
|
|
62
|
+
export function resolveProfileName(flags) {
|
|
63
|
+
const raw = flags?.profile ?? process.env.ODOO_PROFILE;
|
|
64
|
+
const name = raw?.trim();
|
|
65
|
+
if (!name)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
68
|
+
throw usageError(`invalid profile name ${JSON.stringify(name)}`, "use letters, digits, '.', '_' or '-' (must start with a letter or digit)");
|
|
69
|
+
}
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Effective profiles: explicit `profiles` entries, plus the legacy top-level
|
|
74
|
+
* keys, which read as the "default" profile when no profiles.default exists.
|
|
75
|
+
*/
|
|
76
|
+
export function effectiveProfiles(file) {
|
|
77
|
+
const profiles = new Map();
|
|
78
|
+
const hasExplicitDefault = Object.hasOwn(file.profiles ?? {}, "default");
|
|
79
|
+
if (!hasExplicitDefault) {
|
|
80
|
+
const legacy = {};
|
|
81
|
+
let hasLegacy = false;
|
|
82
|
+
for (const key of CONFIG_KEYS) {
|
|
83
|
+
const value = file[key];
|
|
84
|
+
if (value !== undefined) {
|
|
85
|
+
legacy[key] = value;
|
|
86
|
+
hasLegacy = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (hasLegacy)
|
|
90
|
+
profiles.set("default", legacy);
|
|
91
|
+
}
|
|
92
|
+
for (const [name, credentials] of Object.entries(file.profiles ?? {})) {
|
|
93
|
+
profiles.set(name, credentials);
|
|
94
|
+
}
|
|
95
|
+
return profiles;
|
|
96
|
+
}
|
|
97
|
+
/** Sorted names of the profiles a config file effectively provides. */
|
|
98
|
+
export async function availableProfileNames() {
|
|
99
|
+
const profiles = effectiveProfiles(await readConfigFile());
|
|
100
|
+
return [...profiles.keys()].sort();
|
|
101
|
+
}
|
|
102
|
+
export async function profileSelectionHint() {
|
|
103
|
+
const available = await availableProfileNames();
|
|
104
|
+
return available.length > 0
|
|
105
|
+
? `pass --profile <name> or set ODOO_PROFILE (configured profiles: ${available.join(", ")})`
|
|
106
|
+
: "pass --profile <name> or set ODOO_PROFILE (no profiles configured yet — run `odoo auth login --profile <name> ...`)";
|
|
107
|
+
}
|
|
30
108
|
const CONFIG_KEYS = ["url", "db", "username", "api_key"];
|
|
31
109
|
/**
|
|
32
|
-
* Resolve configuration with precedence flags > env >
|
|
110
|
+
* Resolve configuration with precedence flags > env > profile (SPEC §2.1).
|
|
111
|
+
* A profile must be selected explicitly (`--profile` or `ODOO_PROFILE`) —
|
|
112
|
+
* commands never fall back to "some" credentials, so a command can never hit
|
|
113
|
+
* the wrong environment by accident.
|
|
33
114
|
* `overrides.apiKey` represents a key that was already resolved by the caller
|
|
34
115
|
* (e.g. from stdin during `auth login`) and wins over every channel.
|
|
35
116
|
*/
|
|
36
|
-
export async function resolveConfig(flags, overrides = {}) {
|
|
117
|
+
export async function resolveConfig(flags, overrides = {}, options = {}) {
|
|
118
|
+
const profile = resolveProfileName(flags);
|
|
119
|
+
if (!profile) {
|
|
120
|
+
throw usageError("no profile selected", await profileSelectionHint());
|
|
121
|
+
}
|
|
37
122
|
const file = await readConfigFile();
|
|
123
|
+
const profiles = effectiveProfiles(file);
|
|
124
|
+
let credentials = profiles.get(profile);
|
|
125
|
+
if (!credentials) {
|
|
126
|
+
if (options.allowUnknownProfile) {
|
|
127
|
+
// Bootstrap: `auth login`/`version` never read profile values, so a
|
|
128
|
+
// not-yet-existing profile is fine (all needed values come from
|
|
129
|
+
// flags/env). Regular commands still fail below.
|
|
130
|
+
credentials = {};
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
throw usageError(`unknown profile ${JSON.stringify(profile)}`, await profileSelectionHint());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
38
136
|
const apiKeyFileValue = flags.apiKeyFile
|
|
39
137
|
? await readApiKeyFile(flags.apiKeyFile)
|
|
40
138
|
: undefined;
|
|
@@ -42,26 +140,27 @@ export async function resolveConfig(flags, overrides = {}) {
|
|
|
42
140
|
url: [
|
|
43
141
|
[flags.url, "flag"],
|
|
44
142
|
[process.env.ODOO_URL, "env"],
|
|
45
|
-
[
|
|
143
|
+
[credentials.url, "config"],
|
|
46
144
|
],
|
|
47
145
|
db: [
|
|
48
146
|
[flags.db, "flag"],
|
|
49
147
|
[process.env.ODOO_DB, "env"],
|
|
50
|
-
[
|
|
148
|
+
[credentials.db, "config"],
|
|
51
149
|
],
|
|
52
150
|
username: [
|
|
53
151
|
[flags.username, "flag"],
|
|
54
152
|
[process.env.ODOO_USERNAME, "env"],
|
|
55
|
-
[
|
|
153
|
+
[credentials.username, "config"],
|
|
56
154
|
],
|
|
57
155
|
api_key: [
|
|
58
156
|
[overrides.apiKey, "flag"],
|
|
59
157
|
[process.env.ODOO_API_KEY, "env"],
|
|
60
158
|
[apiKeyFileValue, "flag"],
|
|
61
|
-
[
|
|
159
|
+
[credentials.api_key, "config"],
|
|
62
160
|
],
|
|
63
161
|
};
|
|
64
162
|
const resolved = {
|
|
163
|
+
profile,
|
|
65
164
|
url: undefined,
|
|
66
165
|
db: undefined,
|
|
67
166
|
username: undefined,
|
|
@@ -106,15 +205,25 @@ async function readApiKeyFile(path) {
|
|
|
106
205
|
}
|
|
107
206
|
return key;
|
|
108
207
|
}
|
|
109
|
-
/**
|
|
110
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Atomically write the credentials for one profile with mode 0600 (SPEC §2.2),
|
|
210
|
+
* as TOML. Preserves every other profile; the legacy top-level keys and
|
|
211
|
+
* `config.json` are folded in, so the first write completes the migration.
|
|
212
|
+
*/
|
|
213
|
+
export async function writeConfigFile(config, profile) {
|
|
214
|
+
const profiles = effectiveProfiles(await readConfigFile());
|
|
215
|
+
profiles.set(profile, config);
|
|
216
|
+
const next = { profiles: {} };
|
|
217
|
+
for (const name of [...profiles.keys()].sort()) {
|
|
218
|
+
next.profiles[name] = profiles.get(name);
|
|
219
|
+
}
|
|
111
220
|
const path = configFilePath();
|
|
112
|
-
const tmp = join(dirname(path), `.config.
|
|
113
|
-
const
|
|
221
|
+
const tmp = join(dirname(path), `.config.toml.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
222
|
+
const toml = `${stringifyToml(next)}\n`;
|
|
114
223
|
mkdirSync(dirname(path), { recursive: true });
|
|
115
224
|
// mode 0o600 applies at creation; if the temp file somehow exists already,
|
|
116
225
|
// chmodSync below still enforces the final mode on the renamed file.
|
|
117
|
-
await writeFile(tmp,
|
|
226
|
+
await writeFile(tmp, toml, { encoding: "utf8", mode: 0o600 });
|
|
118
227
|
try {
|
|
119
228
|
renameSync(tmp, path);
|
|
120
229
|
}
|
|
@@ -125,6 +234,9 @@ export async function writeConfigFile(config) {
|
|
|
125
234
|
// renameSync preserves the temp file's mode on POSIX; enforce 0600 regardless
|
|
126
235
|
// (e.g. across filesystems where the rename falls back to copy).
|
|
127
236
|
chmodSync(path, 0o600);
|
|
237
|
+
// Migration complete: the TOML file now holds everything, so the legacy
|
|
238
|
+
// JSON config would only risk divergence (it is shadowed on reads).
|
|
239
|
+
rmSync(legacyConfigFilePath(), { force: true });
|
|
128
240
|
}
|
|
129
241
|
export function missingValues(resolved) {
|
|
130
242
|
const missing = [];
|
package/dist/global-options.js
CHANGED
|
@@ -4,6 +4,7 @@ export const DEFAULT_TIMEOUT = "30s";
|
|
|
4
4
|
export function applyGlobalOptions(cmd) {
|
|
5
5
|
cmd.option("--json", "emit a single JSON object on stdout");
|
|
6
6
|
cmd.option("--dry-run, -n", "preview the mutation without applying it");
|
|
7
|
+
cmd.option("--profile <name>", "configuration profile to use (env: ODOO_PROFILE)");
|
|
7
8
|
cmd.option("--url <url>", "Odoo server URL (env: ODOO_URL)");
|
|
8
9
|
cmd.option("--db <name>", "database name (env: ODOO_DB)");
|
|
9
10
|
cmd.option("--username <login>", "login (env: ODOO_USERNAME)");
|
package/dist/resource.js
CHANGED
|
@@ -189,6 +189,7 @@ function humanCellWithLabels(config, field, value) {
|
|
|
189
189
|
}
|
|
190
190
|
export async function authenticatedClient(globals) {
|
|
191
191
|
const resolved = await resolveConfig({
|
|
192
|
+
profile: globals.profile,
|
|
192
193
|
url: globals.url,
|
|
193
194
|
db: globals.db,
|
|
194
195
|
username: globals.username,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bergetai/odoo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Agent-first CLI for the Odoo External API",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"commander": "^15.0.0",
|
|
38
|
+
"smol-toml": "^1.8.0",
|
|
38
39
|
"xmlrpc": "^1.3.2"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|