@bramblex/codex-workbench 0.1.16 → 0.1.18
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 +2 -0
- package/package.json +2 -2
- package/src/providers/codex.js +8 -0
- package/src/providers/pi.js +12 -4
- package/src/services/session-sources.js +5 -8
- package/src/services/update-checker.js +72 -0
- package/src/ui/workbench.js +9 -1
package/README.md
CHANGED
|
@@ -140,6 +140,8 @@ codex-workbench auto-detects installed backends by checking their session direct
|
|
|
140
140
|
|
|
141
141
|
Session metadata such as custom names, notes, and archive state is stored in workbench's own metadata file, not inside backend session files.
|
|
142
142
|
|
|
143
|
+
Every provider owns the full workbench command surface it advertises: new, resume, fork, archive, unarchive, and delete. A provider can implement an operation through its native CLI, workbench metadata, or file operations, but callers should not need provider-specific fallback logic.
|
|
144
|
+
|
|
143
145
|
---
|
|
144
146
|
|
|
145
147
|
## CLI commands
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bramblex/codex-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Terminal workbench for browsing and managing local and SSH Codex sessions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"LICENSE"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"test": "node -e \"const fs=require('fs'),{spawnSync}=require('child_process');function files(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?files(d+'/'+e.name):e.name.endsWith('.js')?[d+'/'+e.name]:[])}for(const f of files('src')){const r=spawnSync(process.execPath,['--check',f],{stdio:'inherit'});if(r.status)process.exit(r.status)}\" && node --check bin/codex-workbench && node --check scripts/pty-codex.js && node --check scripts/tui-pty-codex.js && node --check scripts/blessed-xterm-codex.js && node test/codex-bin.test.js && node test/blessed-compat.test.js && node test/config-paths.test.js && node test/session-sources.test.js && node test/workbench-config.test.js && node test/smoke.js",
|
|
36
|
+
"test": "node -e \"const fs=require('fs'),{spawnSync}=require('child_process');function files(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?files(d+'/'+e.name):e.name.endsWith('.js')?[d+'/'+e.name]:[])}for(const f of files('src')){const r=spawnSync(process.execPath,['--check',f],{stdio:'inherit'});if(r.status)process.exit(r.status)}\" && node --check bin/codex-workbench && node --check scripts/pty-codex.js && node --check scripts/tui-pty-codex.js && node --check scripts/blessed-xterm-codex.js && node test/codex-bin.test.js && node test/blessed-compat.test.js && node test/config-paths.test.js && node test/session-sources.test.js && node test/update-checker.test.js && node test/workbench-config.test.js && node test/smoke.js",
|
|
37
37
|
"pty:codex": "node scripts/pty-codex.js",
|
|
38
38
|
"tui:codex": "node scripts/tui-pty-codex.js",
|
|
39
39
|
"xterm:codex": "node scripts/blessed-xterm-codex.js"
|
package/src/providers/codex.js
CHANGED
|
@@ -249,6 +249,14 @@ function runSessionCommand(command, session, args, inherit) {
|
|
|
249
249
|
module.exports = {
|
|
250
250
|
id: 'codex',
|
|
251
251
|
label: 'Codex',
|
|
252
|
+
capabilities: {
|
|
253
|
+
new: true,
|
|
254
|
+
resume: true,
|
|
255
|
+
fork: true,
|
|
256
|
+
archive: true,
|
|
257
|
+
unarchive: true,
|
|
258
|
+
delete: true,
|
|
259
|
+
},
|
|
252
260
|
isAvailable,
|
|
253
261
|
getSessionFiles,
|
|
254
262
|
parseSession,
|
package/src/providers/pi.js
CHANGED
|
@@ -8,7 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { spawn, spawnSync } = require('child_process');
|
|
10
10
|
const { HOME, PI_CODING_AGENT_DIR } = require('../config');
|
|
11
|
-
const { updateMetadata } = require('../model/metadata');
|
|
11
|
+
const { removeMetadata, updateMetadata } = require('../model/metadata');
|
|
12
12
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Paths
|
|
@@ -272,9 +272,9 @@ function runSessionCommand(command, session, args, inherit) {
|
|
|
272
272
|
return runArgv(argv, cwd, inherit);
|
|
273
273
|
}
|
|
274
274
|
case 'delete': {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return
|
|
275
|
+
fs.unlinkSync(session.file);
|
|
276
|
+
removeMetadata(session);
|
|
277
|
+
return 0;
|
|
278
278
|
}
|
|
279
279
|
case 'archive':
|
|
280
280
|
case 'unarchive': {
|
|
@@ -312,6 +312,14 @@ function resolveBin() {
|
|
|
312
312
|
module.exports = {
|
|
313
313
|
id: 'pi',
|
|
314
314
|
label: 'pi',
|
|
315
|
+
capabilities: {
|
|
316
|
+
new: true,
|
|
317
|
+
resume: true,
|
|
318
|
+
fork: true,
|
|
319
|
+
archive: true,
|
|
320
|
+
unarchive: true,
|
|
321
|
+
delete: true,
|
|
322
|
+
},
|
|
315
323
|
isAvailable,
|
|
316
324
|
getSessionFiles,
|
|
317
325
|
parseSession,
|
|
@@ -105,6 +105,7 @@ function providerSummary(provider) {
|
|
|
105
105
|
return {
|
|
106
106
|
id: provider.id,
|
|
107
107
|
label: provider.label || provider.id,
|
|
108
|
+
capabilities: provider.capabilities || {},
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -121,19 +122,15 @@ function listSourceBackends(source) {
|
|
|
121
122
|
.map((backend) => ({
|
|
122
123
|
id: String(backend.id),
|
|
123
124
|
label: backend.label ? String(backend.label) : String(backend.id),
|
|
125
|
+
capabilities: backend.capabilities && typeof backend.capabilities === 'object'
|
|
126
|
+
? backend.capabilities
|
|
127
|
+
: {},
|
|
124
128
|
}));
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
function runSourceSessionCommand(session, command, args) {
|
|
128
132
|
if (!session.sourceRemote) {
|
|
129
|
-
|
|
130
|
-
// If the provider returns -1 (e.g. pi delete = file-based), fall through to file deletion
|
|
131
|
-
if (status === -1) {
|
|
132
|
-
const { deleteSessionFile } = require('../model/session-store');
|
|
133
|
-
deleteSessionFile(session);
|
|
134
|
-
return 0;
|
|
135
|
-
}
|
|
136
|
-
return status;
|
|
133
|
+
return runCodexCommand(command, session, args);
|
|
137
134
|
}
|
|
138
135
|
const source = configuredSourceOrThrow(session.sourceId);
|
|
139
136
|
const tty = command === 'resume' || command === 'fork';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const pkg = require('../../package.json');
|
|
5
|
+
|
|
6
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(pkg.name).replace(/^%40/, '@')}/latest`;
|
|
7
|
+
|
|
8
|
+
function parseVersion(version) {
|
|
9
|
+
return String(version || '')
|
|
10
|
+
.replace(/^v/, '')
|
|
11
|
+
.split('.')
|
|
12
|
+
.map((part) => Number.parseInt(part, 10))
|
|
13
|
+
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function compareVersions(a, b) {
|
|
17
|
+
const left = parseVersion(a);
|
|
18
|
+
const right = parseVersion(b);
|
|
19
|
+
const length = Math.max(left.length, right.length);
|
|
20
|
+
for (let i = 0; i < length; i += 1) {
|
|
21
|
+
const delta = (left[i] || 0) - (right[i] || 0);
|
|
22
|
+
if (delta !== 0) return delta;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fetchLatestVersion(timeoutMs = 1500) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const req = https.get(REGISTRY_URL, {
|
|
30
|
+
headers: { accept: 'application/json', 'user-agent': `${pkg.name}/${pkg.version}` },
|
|
31
|
+
timeout: timeoutMs,
|
|
32
|
+
}, (res) => {
|
|
33
|
+
if (res.statusCode !== 200) {
|
|
34
|
+
res.resume();
|
|
35
|
+
resolve(null);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let body = '';
|
|
39
|
+
res.setEncoding('utf8');
|
|
40
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
try {
|
|
43
|
+
const payload = JSON.parse(body);
|
|
44
|
+
resolve(payload && payload.version ? String(payload.version) : null);
|
|
45
|
+
} catch {
|
|
46
|
+
resolve(null);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
req.on('timeout', () => {
|
|
52
|
+
req.destroy();
|
|
53
|
+
resolve(null);
|
|
54
|
+
});
|
|
55
|
+
req.on('error', () => resolve(null));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function checkForUpdate(currentVersion = pkg.version) {
|
|
60
|
+
const latestVersion = await fetchLatestVersion();
|
|
61
|
+
if (!latestVersion || compareVersions(latestVersion, currentVersion) <= 0) return null;
|
|
62
|
+
return {
|
|
63
|
+
currentVersion,
|
|
64
|
+
latestVersion,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
checkForUpdate,
|
|
70
|
+
compareVersions,
|
|
71
|
+
fetchLatestVersion,
|
|
72
|
+
};
|
package/src/ui/workbench.js
CHANGED
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
updateSourceMetadata,
|
|
22
22
|
} = require('../services/session-sources');
|
|
23
23
|
const { usableCwd } = require('../services/codex-runner');
|
|
24
|
+
const { checkForUpdate } = require('../services/update-checker');
|
|
24
25
|
const { createDirectoryPicker } = require('./directory-picker');
|
|
25
26
|
|
|
26
27
|
async function runWorkbench() {
|
|
@@ -42,6 +43,7 @@ async function runWorkbench() {
|
|
|
42
43
|
let activePanel = 'projects';
|
|
43
44
|
let remoteLoadId = 0;
|
|
44
45
|
let remoteLoading = false;
|
|
46
|
+
let updateInfo = null;
|
|
45
47
|
let closed = false;
|
|
46
48
|
|
|
47
49
|
const screen = blessed.screen({
|
|
@@ -466,7 +468,8 @@ async function runWorkbench() {
|
|
|
466
468
|
const render = () => {
|
|
467
469
|
applyLayout();
|
|
468
470
|
const visible = currentSessions();
|
|
469
|
-
|
|
471
|
+
const updateText = updateInfo ? ` Update available: v${updateInfo.latestVersion}` : '';
|
|
472
|
+
header.setContent(` ${appTitle}${updateText}\n ${visible.length}/${sessions.length} visible ${groupDisplayName(currentGroup())}`);
|
|
470
473
|
detailBox.setContent(detailContent(selectedSession()));
|
|
471
474
|
updateFocusStyles();
|
|
472
475
|
screen.render();
|
|
@@ -891,6 +894,11 @@ async function runWorkbench() {
|
|
|
891
894
|
projectsList.focus();
|
|
892
895
|
render();
|
|
893
896
|
startRemoteReload(true);
|
|
897
|
+
checkForUpdate(pkg.version).then((nextUpdateInfo) => {
|
|
898
|
+
if (closed || !nextUpdateInfo) return;
|
|
899
|
+
updateInfo = nextUpdateInfo;
|
|
900
|
+
render();
|
|
901
|
+
});
|
|
894
902
|
|
|
895
903
|
return new Promise(() => {});
|
|
896
904
|
}
|