@minniexcode/codex-switch 0.0.1 → 0.0.2

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 CHANGED
@@ -17,12 +17,9 @@ codexs
17
17
 
18
18
  ## Status
19
19
 
20
- This scoped package is currently being reserved and scaffolded for the first public release.
20
+ The repository now contains the first end-to-end modular CLI implementation for the MVP command set defined in `docs/`.
21
21
 
22
- The product scope is already defined, but the full CLI feature set is not implemented yet. The first published versions may be bootstrap releases used to reserve the npm package name and establish the command entrypoint.
23
-
24
- If you install the package now, expect a minimal CLI shell rather than the complete switching workflow.
25
- The project is scaffolded as a TypeScript CLI and publishes compiled output from `dist/`.
22
+ The project is implemented as a TypeScript CLI, builds into `dist/`, and is organized into `cli`, `app`, `domain`, and `infra` layers for maintainability.
26
23
 
27
24
  ## Why This Exists
28
25
 
@@ -55,15 +52,17 @@ Managing multiple Codex providers or profiles locally usually falls into two bad
55
52
 
56
53
  Core design principles:
57
54
 
58
- - `config.toml` remains the source of the active top-level `profile`
59
- - `providers.json` stores provider-to-profile and provider-to-key mappings
55
+ - `providers.json` is the management-state single source of truth for provider metadata and mappings
56
+ - `config.toml` and `auth.json` are runtime mirrors that codex-switch synchronizes safely
57
+ - `backups/latest.json` tracks rollback state for the latest managed mutation window
60
58
  - all writes should be backed up first
61
59
  - failures should trigger rollback
60
+ - write operations should execute under a lightweight single-process file lock
62
61
  - CLI output should stay stable and machine-readable
63
62
 
64
- ## Planned MVP
63
+ ## MVP Commands
65
64
 
66
- The planned MVP command surface is:
65
+ The current MVP command surface is:
67
66
 
68
67
  ```bash
69
68
  codexs list
@@ -78,7 +77,7 @@ codexs doctor
78
77
  codexs rollback
79
78
  ```
80
79
 
81
- Planned shared flags:
80
+ Shared flags:
82
81
 
83
82
  ```bash
84
83
  --json
@@ -119,7 +118,7 @@ One-off execution:
119
118
  npx @minniexcode/codex-switch
120
119
  ```
121
120
 
122
- Current bootstrap behavior:
121
+ Current CLI entry check:
123
122
 
124
123
  ```bash
125
124
  codexs --help
@@ -127,22 +126,46 @@ codexs --help
127
126
 
128
127
  ## Current Repository Contents
129
128
 
130
- This repository currently contains the product definition and PRD used to shape the first implementation:
129
+ This repository contains both the product documents and the CLI implementation:
131
130
 
132
131
  - [Product Overview](./docs/codex-switch-product-overview.md)
133
132
  - [Product Research](./docs/codex-switch-product-research.md)
134
133
  - [PRD](./docs/codex-switch-prd.md)
134
+ - [Technical Architecture](./docs/codex-switch-technical-architecture.md)
135
+ - [Command Design](./docs/codex-switch-command-design.md)
136
+
137
+ ## Implementation Notes
138
+
139
+ Current implementation characteristics:
140
+
141
+ - modular TypeScript architecture split into `app`, `domain`, `infra`, and `cli`
142
+ - repository-style infra modules for providers, config, backups, and write locks
143
+ - a shared mutation orchestration contract that wraps backup, rollback, and lock handling
144
+ - safe write flows with backup manifests under `backups/`
145
+ - rollback support for `config.toml` and optional `auth.json`
146
+ - `status` and `doctor` expose live-state drift so future backfill/edit/sync flows can reuse the same core model
147
+ - stable `--json` envelopes for automation
148
+ - test coverage in `tests/` using a custom serial runner (`tests/run-tests.js`) because the current environment hits `node --test` worker/spawn restrictions
149
+
150
+ ## Storage Model
135
151
 
136
- ## Roadmap
152
+ The current storage model is intentionally split:
137
153
 
138
- Near-term priorities:
154
+ - management state: `providers.json`
155
+ - runtime state: `config.toml` and `auth.json`
156
+ - rollback state: `backups/latest.json` and timestamped backup manifests
139
157
 
140
- - publish the package name and CLI entrypoint
141
- - implement provider storage and validation
142
- - implement config backup and rollback
143
- - implement safe switching flow around `config.toml`
144
- - support structured `--json` output for automation
145
- - add cross-platform tests for Windows, macOS, and Linux paths
158
+ That keeps the MVP file-based while preserving the same boundary a future database-backed registry would use.
159
+
160
+ ## Concurrency And Drift
161
+
162
+ Current write semantics are intentionally lightweight:
163
+
164
+ - every mutating command runs inside `~/.codex/.codex-switch.lock`
165
+ - each mutation creates a backup first and rolls back on failure
166
+ - `status` and `doctor` detect when the active runtime profile in `config.toml` is no longer mapped in `providers.json`
167
+
168
+ That drift signal is the contract for future `edit`, `sync`, and explicit backfill flows. The current version detects and reports drift, but does not silently write live runtime changes back into the management registry.
146
169
 
147
170
  ## Non-Goals for MVP
148
171
 
@@ -156,13 +179,12 @@ The first version is not trying to be:
156
179
 
157
180
  ## Development
158
181
 
159
- At this stage the package is a bootstrap CLI shell. The implementation will follow the product documents in `docs/`.
160
-
161
182
  Local development:
162
183
 
163
184
  ```bash
164
185
  npm install
165
186
  npm run build
187
+ npm test
166
188
  node dist/cli.js --help
167
189
  ```
168
190
 
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addProvider = addProvider;
4
+ const providers_1 = require("../domain/providers");
5
+ const errors_1 = require("../domain/errors");
6
+ const fs_utils_1 = require("../infra/fs-utils");
7
+ const providers_repo_1 = require("../infra/providers-repo");
8
+ const run_mutation_1 = require("./run-mutation");
9
+ /**
10
+ * Adds a new provider record to the managed providers registry.
11
+ */
12
+ function addProvider(args) {
13
+ (0, fs_utils_1.ensureDir)(args.codexDir);
14
+ const providers = (0, providers_repo_1.readProvidersFileIfExists)(args.providersPath);
15
+ if (providers.providers[args.providerName]) {
16
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Provider "${args.providerName}" already exists.`);
17
+ }
18
+ const next = {
19
+ providers: {
20
+ ...providers.providers,
21
+ [args.providerName]: (0, providers_1.cleanProviderRecord)({
22
+ profile: args.profile,
23
+ apiKey: args.apiKey,
24
+ baseUrl: args.baseUrl ?? undefined,
25
+ note: args.note ?? undefined,
26
+ tags: args.tags,
27
+ }),
28
+ },
29
+ };
30
+ return (0, run_mutation_1.runMutation)({
31
+ codexDir: args.codexDir,
32
+ backupsDir: args.backupsDir,
33
+ latestBackupPath: args.latestBackupPath,
34
+ operation: "add",
35
+ files: [{ absolutePath: args.providersPath, relativePath: "providers.json" }],
36
+ mutate: () => {
37
+ // Persist only the normalized provider payload so later reads are deterministic.
38
+ (0, providers_repo_1.writeProvidersFile)(args.providersPath, next);
39
+ return {
40
+ provider: args.providerName,
41
+ profile: args.profile,
42
+ };
43
+ },
44
+ });
45
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.exportProviders = exportProviders;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const errors_1 = require("../domain/errors");
40
+ const fs_utils_1 = require("../infra/fs-utils");
41
+ const providers_repo_1 = require("../infra/providers-repo");
42
+ /**
43
+ * Exports the current providers registry to a user-specified file.
44
+ */
45
+ function exportProviders(args) {
46
+ const absoluteTarget = path.resolve(args.targetFile);
47
+ if (fs.existsSync(absoluteTarget) && !args.force) {
48
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Export target already exists. Re-run with --force to overwrite.", {
49
+ file: absoluteTarget,
50
+ });
51
+ }
52
+ const providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
53
+ // Create the target directory first so exports work for nested paths.
54
+ (0, fs_utils_1.ensureDir)(path.dirname(absoluteTarget));
55
+ (0, providers_repo_1.writeProvidersFile)(absoluteTarget, providers);
56
+ return {
57
+ data: {
58
+ exportedTo: absoluteTarget,
59
+ count: Object.keys(providers.providers).length,
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCurrentProfile = getCurrentProfile;
4
+ const config_repo_1 = require("../infra/config-repo");
5
+ /**
6
+ * Returns the currently active top-level Codex profile.
7
+ */
8
+ function getCurrentProfile(configPath) {
9
+ return {
10
+ data: {
11
+ profile: (0, config_repo_1.readCurrentProfile)(configPath),
12
+ },
13
+ };
14
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getStatus = getStatus;
37
+ const fs = __importStar(require("node:fs"));
38
+ const config_1 = require("../domain/config");
39
+ const runtime_state_1 = require("../domain/runtime-state");
40
+ const providers_repo_1 = require("../infra/providers-repo");
41
+ /**
42
+ * Reports the current on-disk runtime state and how it maps back to managed providers.
43
+ */
44
+ function getStatus(codexDir, configPath, providersPath) {
45
+ const configExists = fs.existsSync(configPath);
46
+ const providersExists = fs.existsSync(providersPath);
47
+ let currentProfile = null;
48
+ const warnings = [];
49
+ const providers = providersExists ? (0, providers_repo_1.readProvidersFile)(providersPath) : null;
50
+ if (configExists) {
51
+ const configContent = fs.readFileSync(configPath, "utf8");
52
+ currentProfile = (0, config_1.parseTopLevelProfile)(configContent);
53
+ if (!currentProfile) {
54
+ warnings.push("config.toml exists but has no top-level profile.");
55
+ }
56
+ }
57
+ const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
58
+ if (liveState.canBackfillActiveProvider) {
59
+ // Surface unmanaged live state without mutating anything during a read-only status call.
60
+ warnings.push("Current config profile is not mapped in providers.json. Backfill would be required before treating live state as managed.");
61
+ }
62
+ return {
63
+ warnings,
64
+ data: {
65
+ codexDir,
66
+ storage: (0, runtime_state_1.getStorageRoles)(),
67
+ configExists,
68
+ providersExists,
69
+ currentProfile,
70
+ currentProfileMapped: liveState.profileMapped,
71
+ provider: liveState.mappedProvider,
72
+ liveState,
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.importProviders = importProviders;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const providers_1 = require("../domain/providers");
40
+ const errors_1 = require("../domain/errors");
41
+ const fs_utils_1 = require("../infra/fs-utils");
42
+ const providers_repo_1 = require("../infra/providers-repo");
43
+ const run_mutation_1 = require("./run-mutation");
44
+ /**
45
+ * Imports provider definitions from an external JSON file into the managed registry.
46
+ */
47
+ function importProviders(args) {
48
+ const absoluteSource = path.resolve(args.sourceFile);
49
+ let imported;
50
+ try {
51
+ // Validate before writing so malformed imports never touch the managed file.
52
+ imported = (0, providers_1.validateProvidersShape)(JSON.parse(fs.readFileSync(absoluteSource, "utf8")));
53
+ }
54
+ catch (error) {
55
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Import file is not valid providers.json data.", {
56
+ file: absoluteSource,
57
+ cause: (0, errors_1.normalizeError)(error).message,
58
+ });
59
+ }
60
+ (0, fs_utils_1.ensureDir)(args.codexDir);
61
+ return (0, run_mutation_1.runMutation)({
62
+ codexDir: args.codexDir,
63
+ backupsDir: args.backupsDir,
64
+ latestBackupPath: args.latestBackupPath,
65
+ operation: "import",
66
+ files: [{ absolutePath: args.providersPath, relativePath: "providers.json" }],
67
+ mutate: () => {
68
+ (0, providers_repo_1.writeProvidersFile)(args.providersPath, imported);
69
+ return {
70
+ importedProviders: Object.keys(imported.providers).sort(),
71
+ };
72
+ },
73
+ });
74
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listProviders = listProviders;
4
+ const providers_repo_1 = require("../infra/providers-repo");
5
+ /**
6
+ * Returns the sorted list of configured providers for display.
7
+ */
8
+ function listProviders(providersPath) {
9
+ const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
10
+ const names = Object.keys(providers.providers).sort();
11
+ const items = names.map((name) => ({
12
+ name,
13
+ profile: providers.providers[name].profile,
14
+ note: providers.providers[name].note ?? null,
15
+ tags: providers.providers[name].tags ?? [],
16
+ }));
17
+ return {
18
+ data: {
19
+ providers: items,
20
+ count: items.length,
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeProvider = removeProvider;
4
+ const errors_1 = require("../domain/errors");
5
+ const providers_repo_1 = require("../infra/providers-repo");
6
+ const run_mutation_1 = require("./run-mutation");
7
+ /**
8
+ * Removes a provider from the managed providers registry.
9
+ */
10
+ function removeProvider(args) {
11
+ const providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
12
+ if (!providers.providers[args.providerName]) {
13
+ throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", `Provider "${args.providerName}" was not found.`);
14
+ }
15
+ const nextProviders = { ...providers.providers };
16
+ // Delete against a copied object so the original parsed state stays untouched.
17
+ delete nextProviders[args.providerName];
18
+ return (0, run_mutation_1.runMutation)({
19
+ codexDir: args.codexDir,
20
+ backupsDir: args.backupsDir,
21
+ latestBackupPath: args.latestBackupPath,
22
+ operation: "remove",
23
+ files: [{ absolutePath: args.providersPath, relativePath: "providers.json" }],
24
+ mutate: () => {
25
+ (0, providers_repo_1.writeProvidersFile)(args.providersPath, { providers: nextProviders });
26
+ return {
27
+ provider: args.providerName,
28
+ };
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rollbackLatest = rollbackLatest;
4
+ const errors_1 = require("../domain/errors");
5
+ const backup_repo_1 = require("../infra/backup-repo");
6
+ /**
7
+ * Restores the most recent mutation backup recorded by codex-switch.
8
+ */
9
+ function rollbackLatest(latestBackupPath) {
10
+ const manifest = (0, backup_repo_1.loadLatestManifest)(latestBackupPath);
11
+ try {
12
+ (0, backup_repo_1.restoreManifest)(manifest);
13
+ return {
14
+ data: {
15
+ restoredFiles: manifest.files.map((file) => file.relativePath),
16
+ backupPath: manifest.backupDir,
17
+ },
18
+ };
19
+ }
20
+ catch (error) {
21
+ throw (0, errors_1.cliError)("ROLLBACK_FAILED", "Rollback failed.", {
22
+ cause: (0, errors_1.normalizeError)(error).message,
23
+ backupPath: manifest.backupDir,
24
+ });
25
+ }
26
+ }
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runDoctor = runDoctor;
37
+ const fs = __importStar(require("node:fs"));
38
+ const config_1 = require("../domain/config");
39
+ const runtime_state_1 = require("../domain/runtime-state");
40
+ const codex_cli_1 = require("../infra/codex-cli");
41
+ const providers_repo_1 = require("../infra/providers-repo");
42
+ const errors_1 = require("../domain/errors");
43
+ /**
44
+ * Performs consistency checks across config.toml, providers.json, and the local Codex CLI.
45
+ */
46
+ function runDoctor(args) {
47
+ const issues = [];
48
+ let configProfiles = new Set();
49
+ let currentProfile = null;
50
+ let providers = null;
51
+ if (!fs.existsSync(args.configPath)) {
52
+ issues.push({
53
+ code: "CONFIG_NOT_FOUND",
54
+ message: "config.toml does not exist.",
55
+ file: args.configPath,
56
+ });
57
+ }
58
+ else {
59
+ const configContent = fs.readFileSync(args.configPath, "utf8");
60
+ configProfiles = (0, config_1.parseProfileNames)(configContent);
61
+ currentProfile = (0, config_1.parseTopLevelProfile)(configContent);
62
+ if (!currentProfile) {
63
+ issues.push({
64
+ code: "PROFILE_NOT_FOUND",
65
+ message: "config.toml has no top-level profile.",
66
+ file: args.configPath,
67
+ });
68
+ }
69
+ }
70
+ if (!fs.existsSync(args.providersPath)) {
71
+ issues.push({
72
+ code: "PROVIDERS_NOT_FOUND",
73
+ message: "providers.json does not exist.",
74
+ file: args.providersPath,
75
+ });
76
+ }
77
+ else {
78
+ try {
79
+ providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
80
+ // Every managed provider must map to a profile that still exists in config.toml.
81
+ for (const [name, provider] of Object.entries(providers.providers)) {
82
+ if (!configProfiles.has(provider.profile)) {
83
+ issues.push({
84
+ code: "PROFILE_NOT_FOUND",
85
+ message: `Provider "${name}" maps to missing profile "${provider.profile}".`,
86
+ provider: name,
87
+ profile: provider.profile,
88
+ });
89
+ }
90
+ }
91
+ }
92
+ catch (error) {
93
+ const normalized = (0, errors_1.normalizeError)(error);
94
+ issues.push({
95
+ code: normalized.code,
96
+ message: normalized.message,
97
+ ...(normalized.details ?? {}),
98
+ });
99
+ }
100
+ }
101
+ const drift = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
102
+ if (drift.canBackfillActiveProvider) {
103
+ // Distinguish unmanaged live state from hard parse/configuration errors.
104
+ issues.push({
105
+ code: "LIVE_STATE_DRIFT",
106
+ message: `Active profile "${drift.currentProfile}" is present in config.toml but not mapped by providers.json.`,
107
+ currentProfile: drift.currentProfile,
108
+ suggestedAction: "backfill-active-provider",
109
+ storage: (0, runtime_state_1.getStorageRoles)(),
110
+ });
111
+ }
112
+ const codexCheck = (0, codex_cli_1.checkCodexAvailable)();
113
+ if (!codexCheck.ok) {
114
+ issues.push({
115
+ code: "CODEX_LOGIN_FAILED",
116
+ message: "codex CLI is not available.",
117
+ cause: codexCheck.cause,
118
+ });
119
+ }
120
+ return {
121
+ data: {
122
+ healthy: issues.length === 0,
123
+ issues,
124
+ codexDir: args.codexDir,
125
+ storage: (0, runtime_state_1.getStorageRoles)(),
126
+ liveState: drift,
127
+ },
128
+ warnings: issues.length === 0 ? [] : [`doctor found ${issues.length} issue(s)`],
129
+ };
130
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runMutation = runMutation;
4
+ const errors_1 = require("../domain/errors");
5
+ const backup_repo_1 = require("../infra/backup-repo");
6
+ const lock_repo_1 = require("../infra/lock-repo");
7
+ /**
8
+ * Runs a write operation under a lock with automatic backup and rollback handling.
9
+ */
10
+ function runMutation(args) {
11
+ return (0, lock_repo_1.withCodexLock)(args.codexDir, args.operation, () => {
12
+ const backup = (0, backup_repo_1.createBackup)(args.codexDir, args.backupsDir, args.operation, args.files);
13
+ try {
14
+ const data = args.mutate({ backup });
15
+ // Record the successful backup only after the mutation completes.
16
+ (0, backup_repo_1.saveLatestManifest)(args.latestBackupPath, backup);
17
+ return {
18
+ data: {
19
+ ...data,
20
+ backupPath: backup.backupDir,
21
+ managedState: {
22
+ transaction: "single-process-file-lock",
23
+ backupFiles: listBackedUpFiles(backup.files),
24
+ },
25
+ },
26
+ };
27
+ }
28
+ catch (error) {
29
+ try {
30
+ // Roll back the managed files to their pre-mutation state on any failure.
31
+ (0, backup_repo_1.restoreManifest)(backup);
32
+ }
33
+ catch (rollbackError) {
34
+ throw (0, errors_1.cliError)("ROLLBACK_FAILED", `${capitalize(args.operation)} failed and rollback was not successful.`, {
35
+ cause: (0, errors_1.normalizeError)(error).message,
36
+ rollbackReason: (0, errors_1.normalizeError)(rollbackError).message,
37
+ backupPath: backup.backupDir,
38
+ });
39
+ }
40
+ const baseError = (0, errors_1.normalizeError)(error);
41
+ throw (0, errors_1.cliError)(baseError.code, baseError.message, {
42
+ ...(baseError.details ?? {}),
43
+ rollbackApplied: true,
44
+ backupPath: backup.backupDir,
45
+ });
46
+ }
47
+ });
48
+ }
49
+ /**
50
+ * Lists the files that existed before the mutation and were captured in the backup.
51
+ */
52
+ function listBackedUpFiles(files) {
53
+ return files.filter((entry) => entry.existed).map((entry) => entry.relativePath);
54
+ }
55
+ /**
56
+ * Uppercases the first character for human-readable operation names.
57
+ */
58
+ function capitalize(value) {
59
+ if (value.length === 0) {
60
+ return value;
61
+ }
62
+ return `${value[0].toUpperCase()}${value.slice(1)}`;
63
+ }