@bakapiano/ccsm 0.17.3 → 0.17.5
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/bin/ccsm.js +22 -14
- package/package.json +1 -1
- package/public/js/pages/AboutPage.js +4 -111
- package/public/js/pages/ConfigurePage.js +66 -1
package/bin/ccsm.js
CHANGED
|
@@ -122,21 +122,29 @@ function isSameVersion(running) {
|
|
|
122
122
|
// (b) bind port 7777 before the helper's own respawn does. Either
|
|
123
123
|
// way the upgrade derails. Bail out instead — the helper's UI on
|
|
124
124
|
// 7779 is already showing the user what's happening.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
//
|
|
126
|
+
// Exception: the helper itself spawns ccsm.cmd at the END of the
|
|
127
|
+
// upgrade (after npm install completes) to bring the new backend up.
|
|
128
|
+
// It sets CCSM_FROM_UPGRADE=1 in that child's env. We MUST skip the
|
|
129
|
+
// lock check in that case, otherwise we'd refuse our own respawn and
|
|
130
|
+
// the user would be stuck staring at "Backend not running".
|
|
131
|
+
if (process.env.CCSM_FROM_UPGRADE !== '1') {
|
|
132
|
+
const lockPath = path.join(HOME, '.upgrade.lock');
|
|
133
|
+
try {
|
|
134
|
+
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
135
|
+
const lock = JSON.parse(raw);
|
|
136
|
+
const ageMs = Date.now() - (lock.startedAt || 0);
|
|
137
|
+
const ownerAlive = lock.pid ? pidAlive(lock.pid) : false;
|
|
138
|
+
if (ownerAlive && ageMs < 10 * 60_000) {
|
|
139
|
+
console.log(`ccsm: upgrade in progress (helper pid=${lock.pid}, ${Math.round(ageMs/1000)}s ago, target=${lock.target || '?'})`);
|
|
140
|
+
console.log(` see http://localhost:${lock.helperPort || 7779}/ for live progress`);
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
// Stale lock (pid dead OR > 10min) — clean up and continue.
|
|
144
|
+
try { fs.unlinkSync(lockPath); } catch {}
|
|
145
|
+
} catch {
|
|
146
|
+
// ENOENT or parse error → no lock, proceed.
|
|
135
147
|
}
|
|
136
|
-
// Stale lock (pid dead OR > 10min) — clean up and continue.
|
|
137
|
-
try { fs.unlinkSync(lockPath); } catch {}
|
|
138
|
-
} catch {
|
|
139
|
-
// ENOENT or parse error → no lock, proceed.
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
// Case 1: existing instance on the preferred port
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.5",
|
|
4
4
|
"description": "Claude Code Session Manager — Windows web UI to manage many concurrent claude sessions: live list, snapshot/restore, focus existing window, new session in an isolated workspace with repo clones",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { html } from '../html.js';
|
|
2
|
-
import { useEffect, useState } from 'preact/hooks';
|
|
3
2
|
import { serverHealth, installPrompt, isInstalledPwa } from '../state.js';
|
|
4
3
|
import { setToast } from '../toast.js';
|
|
5
|
-
import { api } from '../api.js';
|
|
6
4
|
import { Card } from '../components/Card.js';
|
|
7
5
|
import { PageTitleBar } from '../components/PageTitleBar.js';
|
|
8
6
|
import { BrandMark, IconGithub, IconExternal } from '../icons.js';
|
|
@@ -43,119 +41,11 @@ function InstallCard() {
|
|
|
43
41
|
</${Card}>`;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
function UpgradeCard() {
|
|
47
|
-
const [info, setInfo] = useState(null); // { current, latest, updateAvailable, fetchedAt, error? }
|
|
48
|
-
const [checking, setChecking] = useState(true);
|
|
49
|
-
const [upgrading, setUpgrading] = useState(false);
|
|
50
|
-
|
|
51
|
-
const refresh = async (force = false) => {
|
|
52
|
-
setChecking(true);
|
|
53
|
-
try {
|
|
54
|
-
const r = await api('GET', '/api/version' + (force ? '?refresh=1' : ''));
|
|
55
|
-
setInfo(r);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
setInfo({ error: e.message });
|
|
58
|
-
} finally {
|
|
59
|
-
setChecking(false);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
useEffect(() => { refresh(false); }, []);
|
|
63
|
-
|
|
64
|
-
const onUpgrade = async () => {
|
|
65
|
-
if (!info?.updateAvailable) return;
|
|
66
|
-
setUpgrading(true);
|
|
67
|
-
try {
|
|
68
|
-
const r = await api('POST', '/api/upgrade', { target: 'latest' });
|
|
69
|
-
setToast(`upgrading to v${info.latest} · backend will restart`);
|
|
70
|
-
if (r?.helperUrl) {
|
|
71
|
-
setTimeout(() => { location.href = r.helperUrl; }, 300);
|
|
72
|
-
} else if (r?.closeFrontend) {
|
|
73
|
-
setTimeout(() => { try { window.close(); } catch {} }, 400);
|
|
74
|
-
}
|
|
75
|
-
} catch (e) {
|
|
76
|
-
setUpgrading(false);
|
|
77
|
-
setToast(e.message, 'error');
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Dev-only sandbox test path: reinstall the SAME version into a
|
|
82
|
-
// throwaway prefix under ~/.ccsm-dev/test-install. Exercises the
|
|
83
|
-
// whole helper UI + SSE + lockfile flow without touching the user's
|
|
84
|
-
// global install. respawn=false keeps the helper showing "done"
|
|
85
|
-
// until it self-exits.
|
|
86
|
-
const onTestUpgrade = async () => {
|
|
87
|
-
if (!info?.current) return;
|
|
88
|
-
setUpgrading(true);
|
|
89
|
-
try {
|
|
90
|
-
const r = await api('POST', '/api/upgrade', {
|
|
91
|
-
target: info.current,
|
|
92
|
-
installPrefix: 'C:\\Users\\jiannanli\\.ccsm-dev\\test-install',
|
|
93
|
-
respawn: false,
|
|
94
|
-
});
|
|
95
|
-
setToast(`test upgrade · reinstalling v${info.current} to sandbox`);
|
|
96
|
-
if (r?.helperUrl) {
|
|
97
|
-
setTimeout(() => { location.href = r.helperUrl; }, 300);
|
|
98
|
-
}
|
|
99
|
-
} catch (e) {
|
|
100
|
-
setUpgrading(false);
|
|
101
|
-
setToast(e.message, 'error');
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const current = info?.current || serverHealth.value.version || '';
|
|
106
|
-
const latest = info?.latest;
|
|
107
|
-
const updateAvailable = !!info?.updateAvailable;
|
|
108
|
-
|
|
109
|
-
return html`
|
|
110
|
-
<${Card} title="Version">
|
|
111
|
-
<div class="about-version-row">
|
|
112
|
-
<div>
|
|
113
|
-
<div class="about-version-line">
|
|
114
|
-
Installed · <span class="mono">v${current || '?'}</span>
|
|
115
|
-
</div>
|
|
116
|
-
${latest && !updateAvailable ? html`
|
|
117
|
-
<div class="muted-text" style="margin-top:4px">You're on the latest release.</div>
|
|
118
|
-
` : null}
|
|
119
|
-
${updateAvailable ? html`
|
|
120
|
-
<div class="about-update-line">
|
|
121
|
-
Update available · <span class="mono">v${latest}</span>
|
|
122
|
-
</div>
|
|
123
|
-
` : null}
|
|
124
|
-
${info?.error ? html`
|
|
125
|
-
<div class="muted-text" style="margin-top:4px">Couldn't reach npm registry.</div>
|
|
126
|
-
` : null}
|
|
127
|
-
</div>
|
|
128
|
-
<div class="about-version-actions">
|
|
129
|
-
<button class="action subtle" onClick=${() => refresh(true)} disabled=${checking || upgrading}>
|
|
130
|
-
${checking ? 'Checking…' : 'Check'}
|
|
131
|
-
</button>
|
|
132
|
-
${updateAvailable ? html`
|
|
133
|
-
<button class="action primary" onClick=${onUpgrade} disabled=${upgrading}>
|
|
134
|
-
${upgrading ? 'Upgrading…' : `Upgrade to v${latest}`}
|
|
135
|
-
</button>
|
|
136
|
-
` : null}
|
|
137
|
-
${info?.devMode && !updateAvailable ? html`
|
|
138
|
-
<button class="action subtle" onClick=${onTestUpgrade} disabled=${upgrading}
|
|
139
|
-
title="Reinstall to a sandbox prefix to exercise the updater UI without touching prod">
|
|
140
|
-
${upgrading ? 'Testing…' : 'Test upgrade flow'}
|
|
141
|
-
</button>
|
|
142
|
-
` : null}
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
${upgrading ? html`
|
|
146
|
-
<p class="muted-text" style="margin-top:var(--s-3)">
|
|
147
|
-
Running <code>npm i -g @bakapiano/ccsm@latest</code>. The backend will restart automatically — you'll see the "Backend not running" screen briefly.
|
|
148
|
-
</p>
|
|
149
|
-
` : null}
|
|
150
|
-
</${Card}>`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
44
|
export function AboutPage() {
|
|
154
45
|
const version = serverHealth.value.version;
|
|
155
46
|
|
|
156
47
|
return html`
|
|
157
48
|
<${PageTitleBar} title="About" />
|
|
158
|
-
<${UpgradeCard} />
|
|
159
49
|
<${InstallCard} />
|
|
160
50
|
<${Card} title="ccsm">
|
|
161
51
|
<div class="about-block">
|
|
@@ -193,5 +83,8 @@ export function AboutPage() {
|
|
|
193
83
|
<dd>MIT</dd>
|
|
194
84
|
</dl>
|
|
195
85
|
</div>
|
|
196
|
-
</${Card}
|
|
86
|
+
</${Card}>
|
|
87
|
+
<p class="muted-text" style="margin-top: var(--s-3); text-align:center;">
|
|
88
|
+
Looking for upgrade controls? They moved to <strong>Settings → General → Version</strong>.
|
|
89
|
+
</p>`;
|
|
197
90
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { html } from '../html.js';
|
|
6
6
|
import { useEffect, useState } from 'preact/hooks';
|
|
7
7
|
import {
|
|
8
|
-
config, configDirty, accentColor, folders, workspaces,
|
|
8
|
+
config, configDirty, accentColor, folders, workspaces, serverHealth,
|
|
9
9
|
setAccentColor, ACCENT_DEFAULT,
|
|
10
10
|
} from '../state.js';
|
|
11
11
|
import {
|
|
@@ -169,6 +169,10 @@ export function ConfigurePage() {
|
|
|
169
169
|
<span class="label">Theme accent</span>
|
|
170
170
|
<${AccentPicker} />
|
|
171
171
|
</div>
|
|
172
|
+
<div class="field">
|
|
173
|
+
<span class="label">Version</span>
|
|
174
|
+
<${VersionField} />
|
|
175
|
+
</div>
|
|
172
176
|
<div class="field">
|
|
173
177
|
<span class="label">Backend</span>
|
|
174
178
|
<${RestartButton} />
|
|
@@ -450,6 +454,67 @@ const PRESETS = [
|
|
|
450
454
|
{ name: 'Crimson', hex: '#b73f3f' },
|
|
451
455
|
];
|
|
452
456
|
|
|
457
|
+
function VersionField() {
|
|
458
|
+
const [info, setInfo] = useState(null);
|
|
459
|
+
const [checking, setChecking] = useState(true);
|
|
460
|
+
const [upgrading, setUpgrading] = useState(false);
|
|
461
|
+
|
|
462
|
+
const refresh = async (force = false) => {
|
|
463
|
+
setChecking(true);
|
|
464
|
+
try {
|
|
465
|
+
const r = await api('GET', '/api/version' + (force ? '?refresh=1' : ''));
|
|
466
|
+
setInfo(r);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
setInfo({ error: e.message });
|
|
469
|
+
} finally {
|
|
470
|
+
setChecking(false);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
useEffect(() => { refresh(false); }, []);
|
|
474
|
+
|
|
475
|
+
const onUpgrade = async () => {
|
|
476
|
+
if (!info?.updateAvailable) return;
|
|
477
|
+
setUpgrading(true);
|
|
478
|
+
try {
|
|
479
|
+
const r = await api('POST', '/api/upgrade', { target: 'latest' });
|
|
480
|
+
setToast(`upgrading to v${info.latest} · backend will restart`);
|
|
481
|
+
if (r?.helperUrl) {
|
|
482
|
+
setTimeout(() => { location.href = r.helperUrl; }, 300);
|
|
483
|
+
} else if (r?.closeFrontend) {
|
|
484
|
+
setTimeout(() => { try { window.close(); } catch {} }, 400);
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
setUpgrading(false);
|
|
488
|
+
setToast(e.message, 'error');
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const current = info?.current || serverHealth.value.version || '';
|
|
493
|
+
const latest = info?.latest;
|
|
494
|
+
const updateAvailable = !!info?.updateAvailable;
|
|
495
|
+
|
|
496
|
+
const hint = info?.error
|
|
497
|
+
? "Couldn't reach npm registry."
|
|
498
|
+
: updateAvailable ? `Update available · v${latest}`
|
|
499
|
+
: latest ? "You're on the latest release."
|
|
500
|
+
: 'Checks npm registry (cached 30 min).';
|
|
501
|
+
|
|
502
|
+
return html`
|
|
503
|
+
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
|
504
|
+
<span class="mono">v${current || '?'}</span>
|
|
505
|
+
${updateAvailable ? html`
|
|
506
|
+
<button class="action primary" disabled=${upgrading} onClick=${onUpgrade}>
|
|
507
|
+
${upgrading ? 'Upgrading…' : `Upgrade to v${latest}`}
|
|
508
|
+
</button>
|
|
509
|
+
` : null}
|
|
510
|
+
<button class="action" disabled=${checking || upgrading} onClick=${() => refresh(true)}>
|
|
511
|
+
${checking ? 'Checking…' : 'Check for updates'}
|
|
512
|
+
</button>
|
|
513
|
+
<span class="hint">${hint}</span>
|
|
514
|
+
</div>
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
|
|
453
518
|
function RestartButton() {
|
|
454
519
|
const [busy, setBusy] = useState(false);
|
|
455
520
|
const onClick = async () => {
|