@bergetai/odoo-cli 0.11.0 → 0.12.1
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 +34 -4
- package/dist/cli.js +7 -5
- package/dist/commands/auth.js +48 -12
- 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/README.md
CHANGED
|
@@ -16,10 +16,17 @@ Requires Node.js 20 or later.
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
# Authenticate (API key via stdin — never as a flag)
|
|
19
|
-
pass odoo/key | odoo auth login --url https://acme.odoo.com --db acme --username automation
|
|
19
|
+
pass odoo/key | odoo auth login --profile production --url https://acme.odoo.com --db acme --username automation
|
|
20
|
+
|
|
21
|
+
# Authenticate a second environment (kept side by side in one config)
|
|
22
|
+
pass odoo/stage-key | odoo auth login --profile stage --url https://odoo.stage.berget.ai --db berget-stage --username automation
|
|
23
|
+
|
|
24
|
+
# Always select the profile explicitly — commands never guess credentials
|
|
25
|
+
odoo --profile production partners list --company --json
|
|
26
|
+
ODOO_PROFILE=stage odoo partners list --company --json
|
|
20
27
|
|
|
21
28
|
# Check credentials and their configuration sources
|
|
22
|
-
odoo auth status --json
|
|
29
|
+
odoo auth status --profile production --json
|
|
23
30
|
|
|
24
31
|
# Unauthenticated connectivity check
|
|
25
32
|
odoo version
|
|
@@ -108,7 +115,30 @@ odoo crm records message-delete 253924 258332 --dry-run
|
|
|
108
115
|
odoo crm records message-delete 253924 258332
|
|
109
116
|
```
|
|
110
117
|
|
|
111
|
-
Configuration precedence: flags > environment (`ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_API_KEY`) > `$XDG_CONFIG_HOME/odoo/config.
|
|
118
|
+
Configuration precedence: flags > environment (`ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_API_KEY`) > the selected profile in `$XDG_CONFIG_HOME/odoo/config.toml`. The API key is never accepted as a flag value.
|
|
119
|
+
|
|
120
|
+
### Profiles
|
|
121
|
+
|
|
122
|
+
Credentials are stored per **profile** — one TOML table per environment:
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[profiles.production]
|
|
126
|
+
url = "https://odoo.berget.ai"
|
|
127
|
+
db = "berget"
|
|
128
|
+
username = "automation"
|
|
129
|
+
api_key = "…"
|
|
130
|
+
server_version = "18.0+e"
|
|
131
|
+
|
|
132
|
+
[profiles.stage]
|
|
133
|
+
url = "https://odoo.stage.berget.ai"
|
|
134
|
+
db = "berget-stage"
|
|
135
|
+
username = "automation"
|
|
136
|
+
api_key = "…"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Every command requires an explicit profile: pass `--profile <name>` or set `ODOO_PROFILE`. Without one the command exits 2 — credentials are never guessed, so a command can never hit the wrong environment by accident. `auth login` and the flags-only `odoo version` check tolerate a profile that does not exist yet (bootstrap), while any other command reports an unknown profile.
|
|
140
|
+
|
|
141
|
+
Older configs (`config.json`, including top-level credential keys) keep working: they read as the `default` profile and migrate to `config.toml` on the next `auth login` — the legacy file is removed once the TOML config holds everything.
|
|
112
142
|
|
|
113
143
|
Note: `odoo --version` prints the CLI's own version; `odoo version` queries the server.
|
|
114
144
|
|
|
@@ -127,7 +157,7 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
|
|
|
127
157
|
- `--dry-run` for all mutations — full validation, resolution, and before/after diffs without mutating
|
|
128
158
|
- Internal notes (`add-note`) — creates internal Log Notes only (subtype_id=2, message_type=comment), **NEVER sends email to customer**, by design prevents accidental customer communication. Optional `--author <email|name|id>` attributes the note to the partner who actually sent the logged message (resolved via `res.partner`; ambiguous matches list candidates and never guess)
|
|
129
159
|
- Log note correction (`message-update`, `message-delete`) — repair formatting or remove duplicate log notes; strictly limited to comment-type messages (emails/notifications refused by design), full `--dry-run` support, unknown delete ids skipped
|
|
130
|
-
- Config
|
|
160
|
+
- Config profiles (named environments in one `0600` config file, explicit `--profile`/`ODOO_PROFILE` selection), atomic config writes with legacy migration, XML-RPC transport with timeouts and HTTPS enforcement, and an auth failure taxonomy (db missing / bad credentials / unreachable)
|
|
131
161
|
|
|
132
162
|
## Compatibility
|
|
133
163
|
|
package/dist/cli.js
CHANGED
|
@@ -60,9 +60,10 @@ CLI's own version, while \`odoo version\` queries the server's version.`)
|
|
|
60
60
|
.configureHelp({ sortSubcommands: true });
|
|
61
61
|
program.addHelpText("after", `
|
|
62
62
|
Examples:
|
|
63
|
-
$ pass odoo/key | odoo auth login --url https://acme.odoo.com --db acme --username automation
|
|
64
|
-
$ odoo auth
|
|
65
|
-
$ odoo
|
|
63
|
+
$ pass odoo/key | odoo auth login --profile production --url https://acme.odoo.com --db acme --username automation
|
|
64
|
+
$ odoo auth login --profile stage --url https://stage.odoo.com --db stage --username automation
|
|
65
|
+
$ odoo auth status --profile production --json
|
|
66
|
+
$ odoo --profile production crm leads list
|
|
66
67
|
|
|
67
68
|
Docs: https://www.odoo.com/documentation/18.0/developer/reference/external_api.html`);
|
|
68
69
|
applyGlobalOptions(program);
|
|
@@ -78,8 +79,8 @@ Docs: https://www.odoo.com/documentation/18.0/developer/reference/external_api.h
|
|
|
78
79
|
.description("verify credentials and write them to the config file (key via stdin/env/file)")
|
|
79
80
|
.addHelpText("after", `
|
|
80
81
|
Examples:
|
|
81
|
-
$ pass odoo/key | odoo auth login --url https://acme.odoo.com --db acme --username automation
|
|
82
|
-
$ ODOO_API_KEY=... odoo auth login --url https://
|
|
82
|
+
$ pass odoo/key | odoo auth login --profile production --url https://acme.odoo.com --db acme --username automation
|
|
83
|
+
$ ODOO_API_KEY=... odoo auth login --profile stage --url https://stage.odoo.com --db stage --username automation`)
|
|
83
84
|
.action(async () => {
|
|
84
85
|
await run(authLoginCommand, program);
|
|
85
86
|
});
|
|
@@ -1088,6 +1089,7 @@ function collectGlobalOptions(program) {
|
|
|
1088
1089
|
return {
|
|
1089
1090
|
json: Boolean(o.json),
|
|
1090
1091
|
dryRun: Boolean(o.dryRun),
|
|
1092
|
+
profile: o.profile,
|
|
1091
1093
|
url: o.url,
|
|
1092
1094
|
db: o.db,
|
|
1093
1095
|
username: o.username,
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OdooClient } from "../client.js";
|
|
2
|
-
import { missingValueHint, missingValues, resolveConfig, writeConfigFile, } from "../config.js";
|
|
2
|
+
import { missingValueHint, missingValues, profileSelectionHint, resolveConfig, resolveProfileName, effectiveProfiles, readConfigFile, writeConfigFile, } from "../config.js";
|
|
3
3
|
import { parseDurationMs } from "../duration.js";
|
|
4
4
|
import { usageError } from "../errors.js";
|
|
5
5
|
import { printJson } from "../output.js";
|
|
@@ -12,6 +12,7 @@ async function readStdinKey() {
|
|
|
12
12
|
}
|
|
13
13
|
function authFlags(globals) {
|
|
14
14
|
return {
|
|
15
|
+
profile: globals.profile,
|
|
15
16
|
url: globals.url,
|
|
16
17
|
db: globals.db,
|
|
17
18
|
username: globals.username,
|
|
@@ -34,9 +35,9 @@ export async function authLoginCommand(globals) {
|
|
|
34
35
|
else if (!process.env.ODOO_API_KEY && !globals.apiKeyFile) {
|
|
35
36
|
throw usageError("no API key provided", "pipe the key via stdin, set ODOO_API_KEY, or pass --api-key-file — login never prompts");
|
|
36
37
|
}
|
|
37
|
-
const resolved = await resolveConfig(authFlags(globals), {
|
|
38
|
-
|
|
39
|
-
});
|
|
38
|
+
const resolved = await resolveConfig(authFlags(globals), { apiKey: stdinKey },
|
|
39
|
+
// Bootstrap: the profile may not exist yet — login creates it.
|
|
40
|
+
{ allowUnknownProfile: true });
|
|
40
41
|
const missing = missingValues(resolved);
|
|
41
42
|
if (missing.length > 0) {
|
|
42
43
|
const details = missing
|
|
@@ -61,9 +62,10 @@ export async function authLoginCommand(globals) {
|
|
|
61
62
|
username: resolved.username,
|
|
62
63
|
api_key: resolved.apiKey,
|
|
63
64
|
server_version: version.server_version,
|
|
64
|
-
});
|
|
65
|
+
}, resolved.profile);
|
|
65
66
|
if (globals.json) {
|
|
66
67
|
printJson({
|
|
68
|
+
profile: resolved.profile,
|
|
67
69
|
url: resolved.url,
|
|
68
70
|
db: resolved.db,
|
|
69
71
|
username: resolved.username,
|
|
@@ -73,7 +75,7 @@ export async function authLoginCommand(globals) {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
else {
|
|
76
|
-
process.stdout.write(`Logged in as '${resolved.username}' (uid ${uid}) on ${resolved.url} (Odoo ${version.server_version})\n`);
|
|
78
|
+
process.stdout.write(`Logged in as '${resolved.username}' (uid ${uid}) on ${resolved.url} (Odoo ${version.server_version}) [profile: ${resolved.profile}]\n`);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
/**
|
|
@@ -82,17 +84,50 @@ export async function authLoginCommand(globals) {
|
|
|
82
84
|
* stdout, even when exiting non-zero (SPEC §7.3 exception).
|
|
83
85
|
*/
|
|
84
86
|
export async function authStatusCommand(globals) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
// Profile resolution is handled softly so the §7.3 envelope (with the
|
|
88
|
+
// missing "profile" key) still reaches stdout under --json, both for a
|
|
89
|
+
// missing selection and for a selected profile with no stored credentials.
|
|
90
|
+
const profileName = resolveProfileName(globals);
|
|
87
91
|
const envelope = {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
profile: profileName ?? null,
|
|
93
|
+
url: null,
|
|
94
|
+
db: null,
|
|
95
|
+
username: null,
|
|
91
96
|
server_version: null,
|
|
92
97
|
uid: null,
|
|
93
98
|
authenticated: false,
|
|
94
|
-
config_source:
|
|
99
|
+
config_source: null,
|
|
95
100
|
};
|
|
101
|
+
if (!profileName) {
|
|
102
|
+
const hint = await profileSelectionHint();
|
|
103
|
+
if (globals.json) {
|
|
104
|
+
envelope.missing = ["profile"];
|
|
105
|
+
printJson(envelope);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
process.stderr.write(`Error: no profile selected — ${hint}\n`);
|
|
109
|
+
}
|
|
110
|
+
process.exitCode = 2;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const knownProfiles = effectiveProfiles(await readConfigFile());
|
|
114
|
+
if (!knownProfiles.has(profileName)) {
|
|
115
|
+
if (globals.json) {
|
|
116
|
+
envelope.missing = ["profile"];
|
|
117
|
+
printJson(envelope);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
process.stderr.write(`Error: no stored credentials for profile ${JSON.stringify(profileName)} — run \`odoo auth login --profile ${profileName} ...\`\n`);
|
|
121
|
+
}
|
|
122
|
+
process.exitCode = 2;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const resolved = await resolveConfig(authFlags(globals));
|
|
126
|
+
envelope.url = resolved.url ?? null;
|
|
127
|
+
envelope.db = resolved.db ?? null;
|
|
128
|
+
envelope.username = resolved.username ?? null;
|
|
129
|
+
envelope.config_source = resolved.sources;
|
|
130
|
+
const missing = missingValues(resolved);
|
|
96
131
|
if (missing.length > 0) {
|
|
97
132
|
envelope.missing = missing;
|
|
98
133
|
if (globals.json) {
|
|
@@ -123,6 +158,7 @@ export async function authStatusCommand(globals) {
|
|
|
123
158
|
printJson(envelope);
|
|
124
159
|
}
|
|
125
160
|
else {
|
|
161
|
+
process.stdout.write(`Profile: ${profileName}\n`);
|
|
126
162
|
process.stdout.write(`URL: ${envelope.url} (${resolved.sources.url})\n`);
|
|
127
163
|
process.stdout.write(`Database: ${envelope.db} (${resolved.sources.db})\n`);
|
|
128
164
|
process.stdout.write(`Username: ${envelope.username} (${resolved.sources.username})\n`);
|
package/dist/commands/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OdooClient } from "../client.js";
|
|
2
|
-
import { resolveConfig, missingValueHint
|
|
2
|
+
import { resolveConfig, missingValueHint } from "../config.js";
|
|
3
3
|
import { parseDurationMs } from "../duration.js";
|
|
4
4
|
import { usageError } from "../errors.js";
|
|
5
5
|
import { printJson } from "../output.js";
|
|
@@ -9,12 +9,16 @@ import { printJson } from "../output.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export async function versionCommand(globals) {
|
|
11
11
|
const flags = {
|
|
12
|
+
profile: globals.profile,
|
|
12
13
|
url: globals.url,
|
|
13
14
|
db: globals.db,
|
|
14
15
|
username: globals.username,
|
|
15
16
|
apiKeyFile: globals.apiKeyFile,
|
|
16
17
|
};
|
|
17
|
-
const resolved = await resolveConfig(flags
|
|
18
|
+
const resolved = await resolveConfig(flags, {},
|
|
19
|
+
// Flags-only connectivity check — profile values are unused, so a
|
|
20
|
+
// not-yet-existing profile is fine.
|
|
21
|
+
{ allowUnknownProfile: true });
|
|
18
22
|
if (!resolved.url) {
|
|
19
23
|
throw usageError("no Odoo URL configured", missingValueHint("url"));
|
|
20
24
|
}
|
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.1",
|
|
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": {
|