codexmate 0.0.5 → 0.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "codexmate",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Codex/Claude Code 配置与会话管理 CLI + Web 工具",
5
5
  "bin": {
6
6
  "codexmate": "./cli.js"
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@iarna/toml": "^2.2.5",
14
- "json5": "^2.2.3"
14
+ "json5": "^2.2.3",
15
+ "zip-lib": "^1.2.1"
15
16
  },
16
17
  "engines": {
17
18
  "node": ">=14"
@@ -1,135 +1,136 @@
1
- const http = require('http');
2
- const path = require('path');
3
- const os = require('os');
4
- const fs = require('fs');
5
- const { spawn } = require('child_process');
6
-
7
- const PORT = 3737;
8
- const API_URL = `http://localhost:${PORT}/api`;
9
-
10
- function delay(ms) {
11
- return new Promise(resolve => setTimeout(resolve, ms));
12
- }
13
-
14
- function request(action, params = {}) {
15
- const payload = JSON.stringify({ action, params });
16
- return new Promise((resolve, reject) => {
17
- const req = http.request(API_URL, {
18
- method: 'POST',
19
- headers: {
20
- 'Content-Type': 'application/json',
21
- 'Content-Length': Buffer.byteLength(payload)
22
- }
23
- }, (res) => {
24
- let data = '';
25
- res.on('data', chunk => data += chunk);
26
- res.on('end', () => {
27
- try {
28
- const json = JSON.parse(data || '{}');
29
- resolve(json);
30
- } catch (e) {
31
- reject(e);
32
- }
33
- });
34
- });
35
- req.on('error', reject);
36
- req.write(payload);
37
- req.end();
38
- });
39
- }
40
-
41
- async function waitForServer(timeoutMs = 12000) {
42
- const start = Date.now();
43
- while (Date.now() - start < timeoutMs) {
44
- try {
45
- await request('status');
46
- return;
47
- } catch (_) {
48
- await delay(300);
49
- }
50
- }
51
- throw new Error('server not ready');
52
- }
53
-
54
- function escapeToml(value) {
55
- return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
56
- }
57
-
58
- function buildTemplate(provider, model, baseUrl, apiKey) {
59
- return `model_provider = "${escapeToml(provider)}"
60
- model = "${escapeToml(model)}"
61
-
62
- [model_providers.${escapeToml(provider)}]
63
- name = "${escapeToml(provider)}"
64
- base_url = "${escapeToml(baseUrl)}"
65
- wire_api = "responses"
66
- preferred_auth_method = "${escapeToml(apiKey)}"
67
- `;
68
- }
69
-
70
- function assert(condition, message) {
71
- if (!condition) {
72
- throw new Error(`Assertion failed: ${message}`);
73
- }
74
- }
75
-
76
- async function run() {
77
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-e2e-'));
78
- const env = {
79
- ...process.env,
80
- USERPROFILE: tempHome,
81
- HOME: tempHome,
82
- CODEXMATE_NO_BROWSER: '1'
83
- };
84
-
85
- const cliPath = path.join(__dirname, '..', '..', 'cli.js');
86
- const child = spawn(process.execPath, [cliPath, 'start'], {
87
- env,
88
- stdio: ['ignore', 'pipe', 'pipe']
89
- });
90
-
91
- try {
92
- await waitForServer();
93
-
94
- await request('apply-config-template', { template: buildTemplate('alpha', 'm-alpha', 'https://example.com', 'sk-alpha') });
95
- await request('apply-config-template', { template: buildTemplate('beta', 'm-beta', 'https://example.com', 'sk-beta') });
96
- await request('apply-config-template', { template: buildTemplate('gamma', 'm-gamma', 'https://example.com', 'sk-gamma') });
97
- await request('apply-config-template', { template: buildTemplate('delta', 'm-delta', 'https://example.com', 'sk-delta') });
98
-
99
- const recent = await request('get-recent-configs');
100
- assert(Array.isArray(recent.items), 'recent list should be array');
101
- assert(recent.items.length === 3, 'recent list should keep 3 items');
102
- assert(recent.items[0].provider === 'delta' && recent.items[0].model === 'm-delta', 'recent[0] should be delta');
103
- assert(recent.items[1].provider === 'gamma' && recent.items[1].model === 'm-gamma', 'recent[1] should be gamma');
104
- assert(recent.items[2].provider === 'beta' && recent.items[2].model === 'm-beta', 'recent[2] should be beta');
105
-
106
- await request('apply-config-template', { template: buildTemplate('broken', 'm-broken', 'not-a-url', '') });
107
- await request('delete-model', { model: 'm-broken' });
108
-
109
- const health = await request('config-health-check');
110
- assert(health && Array.isArray(health.issues), 'health check issues should be array');
111
- const codes = new Set(health.issues.map(item => item.code));
112
- assert(codes.has('base-url-invalid'), 'health check should flag invalid URL');
113
- assert(codes.has('api-key-missing'), 'health check should flag missing key');
114
- assert(codes.has('model-unavailable'), 'health check should flag missing model');
115
-
116
- await request('apply-config-template', {
117
- template: buildTemplate('local', 'm-local', `http://127.0.0.1:${PORT}`, 'sk-local')
118
- });
119
- const remoteHealth = await request('config-health-check', { remote: true, timeoutMs: 2000 });
120
- assert(remoteHealth && Array.isArray(remoteHealth.issues), 'remote health issues should be array');
121
- const remoteCodes = new Set(remoteHealth.issues.map(item => item.code));
122
- assert(remoteCodes.has('remote-models-parse'), 'remote health should flag models parse error');
123
- } finally {
124
- if (child && !child.killed) {
125
- child.kill('SIGINT');
126
- }
127
- await delay(300);
128
- fs.rmSync(tempHome, { recursive: true, force: true });
129
- }
130
- }
131
-
132
- run().catch((err) => {
133
- console.error(err);
134
- process.exit(1);
135
- });
1
+ const http = require('http');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const { spawn } = require('child_process');
6
+
7
+ const PORT = 3737;
8
+ const API_URL = `http://localhost:${PORT}/api`;
9
+
10
+ function delay(ms) {
11
+ return new Promise(resolve => setTimeout(resolve, ms));
12
+ }
13
+
14
+ function request(action, params = {}) {
15
+ const payload = JSON.stringify({ action, params });
16
+ return new Promise((resolve, reject) => {
17
+ const req = http.request(API_URL, {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ 'Content-Length': Buffer.byteLength(payload)
22
+ }
23
+ }, (res) => {
24
+ let data = '';
25
+ res.on('data', chunk => data += chunk);
26
+ res.on('end', () => {
27
+ try {
28
+ const json = JSON.parse(data || '{}');
29
+ resolve(json);
30
+ } catch (e) {
31
+ reject(e);
32
+ }
33
+ });
34
+ });
35
+ req.on('error', reject);
36
+ req.write(payload);
37
+ req.end();
38
+ });
39
+ }
40
+
41
+ async function waitForServer(timeoutMs = 12000) {
42
+ const start = Date.now();
43
+ while (Date.now() - start < timeoutMs) {
44
+ try {
45
+ await request('status');
46
+ return;
47
+ } catch (_) {
48
+ await delay(300);
49
+ }
50
+ }
51
+ throw new Error('server not ready');
52
+ }
53
+
54
+ function escapeToml(value) {
55
+ return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
56
+ }
57
+
58
+ function buildTemplate(provider, model, baseUrl, apiKey) {
59
+ return `model_provider = "${escapeToml(provider)}"
60
+ model = "${escapeToml(model)}"
61
+
62
+ [model_providers.${escapeToml(provider)}]
63
+ name = "${escapeToml(provider)}"
64
+ base_url = "${escapeToml(baseUrl)}"
65
+ wire_api = "responses"
66
+ preferred_auth_method = "${escapeToml(apiKey)}"
67
+ `;
68
+ }
69
+
70
+ function assert(condition, message) {
71
+ if (!condition) {
72
+ throw new Error(`Assertion failed: ${message}`);
73
+ }
74
+ }
75
+
76
+ async function run() {
77
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-e2e-'));
78
+ const env = {
79
+ ...process.env,
80
+ USERPROFILE: tempHome,
81
+ HOME: tempHome,
82
+ CODEXMATE_NO_BROWSER: '1'
83
+ };
84
+
85
+ const cliPath = path.join(__dirname, '..', '..', 'cli.js');
86
+ const child = spawn(process.execPath, [cliPath, 'start'], {
87
+ env,
88
+ stdio: ['ignore', 'pipe', 'pipe']
89
+ });
90
+
91
+ try {
92
+ await waitForServer();
93
+
94
+ await request('apply-config-template', { template: buildTemplate('alpha', 'm-alpha', 'https://example.com', 'sk-alpha') });
95
+ await request('apply-config-template', { template: buildTemplate('beta', 'm-beta', 'https://example.com', 'sk-beta') });
96
+ await request('apply-config-template', { template: buildTemplate('gamma', 'm-gamma', 'https://example.com', 'sk-gamma') });
97
+ await request('apply-config-template', { template: buildTemplate('delta', 'm-delta', 'https://example.com', 'sk-delta') });
98
+
99
+ const recent = await request('get-recent-configs');
100
+ assert(Array.isArray(recent.items), 'recent list should be array');
101
+ assert(recent.items.length === 3, 'recent list should keep 3 items');
102
+ assert(recent.items[0].provider === 'delta' && recent.items[0].model === 'm-delta', 'recent[0] should be delta');
103
+ assert(recent.items[1].provider === 'gamma' && recent.items[1].model === 'm-gamma', 'recent[1] should be gamma');
104
+ assert(recent.items[2].provider === 'beta' && recent.items[2].model === 'm-beta', 'recent[2] should be beta');
105
+
106
+ await request('apply-config-template', { template: buildTemplate('broken', 'm-broken', 'not-a-url', '') });
107
+ await request('delete-model', { model: 'm-broken' });
108
+
109
+ const health = await request('config-health-check');
110
+ assert(health && Array.isArray(health.issues), 'health check issues should be array');
111
+ const codes = new Set(health.issues.map(item => item.code));
112
+ assert(codes.has('base-url-invalid'), 'health check should flag invalid URL');
113
+ assert(codes.has('api-key-missing'), 'health check should flag missing key');
114
+ assert(codes.has('model-unavailable'), 'health check should flag missing model');
115
+
116
+ await request('apply-config-template', {
117
+ template: buildTemplate('local', 'm-local', `http://127.0.0.1:${PORT}`, 'sk-local')
118
+ });
119
+ const remoteHealth = await request('config-health-check', { remote: true, timeoutMs: 2000 });
120
+ assert(remoteHealth && Array.isArray(remoteHealth.issues), 'remote health issues should be array');
121
+ assert(remoteHealth.ok === true, 'remote health should pass');
122
+ assert(remoteHealth.remote && remoteHealth.remote.type === 'speed-test', 'remote health should include speed-test info');
123
+ assert(typeof remoteHealth.remote.durationMs === 'number', 'remote health should include duration');
124
+ } finally {
125
+ if (child && !child.killed) {
126
+ child.kill('SIGINT');
127
+ }
128
+ await delay(300);
129
+ fs.rmSync(tempHome, { recursive: true, force: true });
130
+ }
131
+ }
132
+
133
+ run().catch((err) => {
134
+ console.error(err);
135
+ process.exit(1);
136
+ });
package/tests/e2e/run.js CHANGED
@@ -178,6 +178,29 @@ async function main() {
178
178
  const models = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
179
179
  assert(models.includes('e2e-model'), 'custom model not added');
180
180
 
181
+ const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
182
+ fs.mkdirSync(sessionsDir, { recursive: true });
183
+ const sessionId = 'e2e-session';
184
+ const sessionPath = path.join(sessionsDir, `${sessionId}.jsonl`);
185
+ const sessionRecords = [
186
+ {
187
+ type: 'session_meta',
188
+ payload: { id: sessionId, cwd: '/tmp/e2e' },
189
+ timestamp: '2025-01-01T00:00:00.000Z'
190
+ },
191
+ {
192
+ type: 'response_item',
193
+ payload: { type: 'message', role: 'user', content: 'hello' },
194
+ timestamp: '2025-01-01T00:00:01.000Z'
195
+ },
196
+ {
197
+ type: 'response_item',
198
+ payload: { type: 'message', role: 'assistant', content: 'world' },
199
+ timestamp: '2025-01-01T00:00:02.000Z'
200
+ }
201
+ ];
202
+ fs.writeFileSync(sessionPath, sessionRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
203
+
181
204
  const statusResult = runSync(node, [cliPath, 'status'], { env });
182
205
  assert(statusResult.status === 0, 'status failed');
183
206
  assert(statusResult.stdout.includes('提供商: e2e'), 'status provider not shown');
@@ -255,6 +278,46 @@ async function main() {
255
278
  });
256
279
  assert(apiModelsByUrlUnlimited.unlimited === true, 'api models-by-url unlimited missing');
257
280
 
281
+ const apiSessions = await postJson(port, {
282
+ action: 'list-sessions',
283
+ params: { source: 'codex', limit: 50, forceRefresh: true }
284
+ });
285
+ assert(Array.isArray(apiSessions.sessions), 'api sessions missing');
286
+ assert(apiSessions.sessions.some(item => item.sessionId === sessionId), 'api sessions missing codex entry');
287
+
288
+ const cloneResult = await postJson(port, {
289
+ action: 'clone-session',
290
+ params: { source: 'codex', sessionId }
291
+ });
292
+ assert(cloneResult.success === true, 'clone-session failed');
293
+ assert(cloneResult.sessionId && cloneResult.sessionId !== sessionId, 'clone-session id invalid');
294
+ assert(fs.existsSync(cloneResult.filePath), 'clone-session file missing');
295
+
296
+ const apiSessionsAfterClone = await postJson(port, {
297
+ action: 'list-sessions',
298
+ params: { source: 'codex', limit: 50, forceRefresh: true }
299
+ });
300
+ assert(Array.isArray(apiSessionsAfterClone.sessions), 'api sessions after clone missing');
301
+ assert(
302
+ apiSessionsAfterClone.sessions[0]
303
+ && apiSessionsAfterClone.sessions[0].sessionId === cloneResult.sessionId,
304
+ 'clone session not latest'
305
+ );
306
+
307
+ const deleteResult = await postJson(port, {
308
+ action: 'delete-session',
309
+ params: { source: 'codex', sessionId }
310
+ });
311
+ assert(deleteResult.success === true, 'delete-session failed');
312
+ assert(!fs.existsSync(sessionPath), 'delete-session file still exists');
313
+
314
+ const apiSessionsAfterDelete = await postJson(port, {
315
+ action: 'list-sessions',
316
+ params: { source: 'codex', limit: 50, forceRefresh: true }
317
+ });
318
+ assert(!apiSessionsAfterDelete.sessions.some(item => item.sessionId === sessionId), 'deleted session still listed');
319
+ assert(apiSessionsAfterDelete.sessions.some(item => item.sessionId === cloneResult.sessionId), 'clone session missing after delete');
320
+
258
321
  const speedResult = await postJson(port, { action: 'speed-test', params: { name: 'e2e2' } }, 4000);
259
322
  assert(speedResult.ok === true, 'speed-test failed');
260
323