@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 +44 -22
- package/dist/app/add-provider.js +45 -0
- package/dist/app/export-providers.js +62 -0
- package/dist/app/get-current-profile.js +14 -0
- package/dist/app/get-status.js +75 -0
- package/dist/app/import-providers.js +74 -0
- package/dist/app/list-providers.js +23 -0
- package/dist/app/remove-provider.js +31 -0
- package/dist/app/rollback-latest.js +26 -0
- package/dist/app/run-doctor.js +130 -0
- package/dist/app/run-mutation.js +63 -0
- package/dist/app/switch-provider.js +43 -0
- package/dist/app/types.js +2 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli/output.js +156 -0
- package/dist/cli.js +182 -22
- package/dist/domain/backup.js +2 -0
- package/dist/domain/config.js +106 -0
- package/dist/domain/errors.js +36 -0
- package/dist/domain/providers.js +92 -0
- package/dist/domain/runtime-state.js +56 -0
- package/dist/infra/backup-repo.js +151 -0
- package/dist/infra/codex-cli.js +53 -0
- package/dist/infra/codex-paths.js +58 -0
- package/dist/infra/config-repo.js +56 -0
- package/dist/infra/fs-utils.js +97 -0
- package/dist/infra/lock-repo.js +99 -0
- package/dist/infra/providers-repo.js +69 -0
- package/docs/codex-switch-command-design.md +621 -0
- package/docs/codex-switch-prd.md +2 -0
- package/docs/codex-switch-product-overview.md +2 -0
- package/docs/codex-switch-technical-architecture.md +1001 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -17,12 +17,9 @@ codexs
|
|
|
17
17
|
|
|
18
18
|
## Status
|
|
19
19
|
|
|
20
|
-
|
|
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
|
|
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
|
-
- `
|
|
59
|
-
- `
|
|
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
|
-
##
|
|
63
|
+
## MVP Commands
|
|
65
64
|
|
|
66
|
-
The
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
152
|
+
The current storage model is intentionally split:
|
|
137
153
|
|
|
138
|
-
|
|
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
|
-
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|