@bakapiano/ccsm 0.17.0 → 0.17.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/package.json +1 -1
- package/public/css/feedback.css +7 -69
- package/public/js/components/App.js +0 -2
- package/public/js/components/EntityFormModal.js +22 -10
- package/public/js/components/HealthOverlay.js +35 -25
- package/public/js/pages/ConfigurePage.js +34 -13
- package/scripts/dev.js +46 -0
- package/scripts/install.js +14 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
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",
|
package/public/css/feedback.css
CHANGED
|
@@ -302,82 +302,20 @@
|
|
|
302
302
|
50% { box-shadow: 0 0 0 4px rgba(26, 24, 21, 0); }
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
/*
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
inset: 0;
|
|
309
|
-
z-index: 9999;
|
|
310
|
-
background: rgba(26, 24, 21, 0.48);
|
|
311
|
-
backdrop-filter: blur(4px);
|
|
312
|
-
-webkit-backdrop-filter: blur(4px);
|
|
313
|
-
display: flex;
|
|
314
|
-
align-items: center;
|
|
315
|
-
justify-content: center;
|
|
316
|
-
animation: health-fade-in 0.2s ease-out;
|
|
317
|
-
}
|
|
318
|
-
@keyframes health-fade-in {
|
|
319
|
-
from { opacity: 0; }
|
|
320
|
-
to { opacity: 1; }
|
|
321
|
-
}
|
|
322
|
-
.health-card {
|
|
323
|
-
background: var(--bg-elev);
|
|
324
|
-
border: 1px solid var(--border);
|
|
325
|
-
border-radius: 14px;
|
|
326
|
-
padding: 36px 44px;
|
|
327
|
-
max-width: 420px;
|
|
328
|
-
text-align: center;
|
|
329
|
-
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
|
|
330
|
-
font-family: var(--body);
|
|
331
|
-
}
|
|
305
|
+
/* HealthOverlay reuses .offline-overlay / .offline-card above. The
|
|
306
|
+
only extra is the spinner shown during the "Checking…" phase — sized
|
|
307
|
+
to fill the .offline-brand slot in place of the BrandMark logo. */
|
|
332
308
|
.health-spinner {
|
|
333
|
-
width:
|
|
334
|
-
height:
|
|
335
|
-
border:
|
|
336
|
-
border-top-color: var(--
|
|
309
|
+
width: 36px;
|
|
310
|
+
height: 36px;
|
|
311
|
+
border: 3px solid var(--border);
|
|
312
|
+
border-top-color: var(--ink);
|
|
337
313
|
border-radius: 50%;
|
|
338
|
-
margin: 0 auto 14px;
|
|
339
314
|
animation: health-spin 0.8s linear infinite;
|
|
340
315
|
}
|
|
341
316
|
@keyframes health-spin {
|
|
342
317
|
to { transform: rotate(360deg); }
|
|
343
318
|
}
|
|
344
|
-
.health-dot {
|
|
345
|
-
width: 12px;
|
|
346
|
-
height: 12px;
|
|
347
|
-
border-radius: 50%;
|
|
348
|
-
background: var(--red, #b73f3f);
|
|
349
|
-
margin: 0 auto 14px;
|
|
350
|
-
}
|
|
351
|
-
.health-title {
|
|
352
|
-
font-size: 15px;
|
|
353
|
-
font-weight: 500;
|
|
354
|
-
letter-spacing: -0.005em;
|
|
355
|
-
color: var(--ink);
|
|
356
|
-
margin-bottom: 6px;
|
|
357
|
-
}
|
|
358
|
-
.health-meta {
|
|
359
|
-
font-size: 12.5px;
|
|
360
|
-
color: var(--ink-mid);
|
|
361
|
-
line-height: 1.4;
|
|
362
|
-
margin-bottom: 14px;
|
|
363
|
-
}
|
|
364
|
-
.health-start {
|
|
365
|
-
display: inline-block;
|
|
366
|
-
margin-top: 4px;
|
|
367
|
-
text-decoration: none;
|
|
368
|
-
}
|
|
369
|
-
.health-hint {
|
|
370
|
-
font-size: 11.5px;
|
|
371
|
-
color: var(--ink-muted);
|
|
372
|
-
margin-top: 14px;
|
|
373
|
-
}
|
|
374
|
-
.health-hint code {
|
|
375
|
-
font-family: var(--mono);
|
|
376
|
-
background: var(--bg);
|
|
377
|
-
padding: 1px 5px;
|
|
378
|
-
border-radius: 3px;
|
|
379
|
-
border: 1px solid var(--border);
|
|
380
|
-
}
|
|
381
319
|
|
|
382
320
|
/* ── Keybinding recorder modal ───────────────────────────────────── */
|
|
383
321
|
.kbd-recorder-overlay {
|
|
@@ -3,7 +3,6 @@ import { activeTab } from '../state.js';
|
|
|
3
3
|
import { Sidebar } from './Sidebar.js';
|
|
4
4
|
import { Toast } from './Toast.js';
|
|
5
5
|
import { DialogHost } from './DialogHost.js';
|
|
6
|
-
import { OfflineBanner } from './OfflineBanner.js';
|
|
7
6
|
import { HealthOverlay } from './HealthOverlay.js';
|
|
8
7
|
import { SessionsPage } from '../pages/SessionsPage.js';
|
|
9
8
|
import { LaunchPage } from '../pages/LaunchPage.js';
|
|
@@ -29,7 +28,6 @@ export function App() {
|
|
|
29
28
|
${tab === 'about' ? html`<${Panel} name="about"><${AboutPage} /></${Panel}>` : null}
|
|
30
29
|
</div>
|
|
31
30
|
</main>
|
|
32
|
-
<${OfflineBanner} />
|
|
33
31
|
<${Toast} />
|
|
34
32
|
<${DialogHost} />
|
|
35
33
|
<${HealthOverlay} />
|
|
@@ -26,7 +26,19 @@ export function EntityFormModal({
|
|
|
26
26
|
const [testing, setTesting] = useState(false);
|
|
27
27
|
const [testResult, setTestResult] = useState(null);
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// A field is read-only if its key is in the static `readOnlyKeys`
|
|
30
|
+
// prop OR its own `readOnly` predicate (called with the current
|
|
31
|
+
// draft) returns true. The predicate lets a field react to other
|
|
32
|
+
// fields' values — e.g. lock newSessionIdArgs once a known `type`
|
|
33
|
+
// is picked, since those args are an integration contract with the
|
|
34
|
+
// upstream CLI, not a user knob.
|
|
35
|
+
const isReadOnly = (field) => {
|
|
36
|
+
if (readOnlyKeys.includes(field.key)) return true;
|
|
37
|
+
if (typeof field.readOnly === 'function') {
|
|
38
|
+
try { return !!field.readOnly(draft); } catch { return false; }
|
|
39
|
+
}
|
|
40
|
+
return !!field.readOnly;
|
|
41
|
+
};
|
|
30
42
|
|
|
31
43
|
const submit = async (ev) => {
|
|
32
44
|
ev?.preventDefault?.();
|
|
@@ -61,7 +73,7 @@ export function EntityFormModal({
|
|
|
61
73
|
<span class="entity-field-label">${f.label}</span>
|
|
62
74
|
${f.type === 'select' ? html`
|
|
63
75
|
<select class="input" value=${draft[f.key] || ''}
|
|
64
|
-
disabled=${isReadOnly(f
|
|
76
|
+
disabled=${isReadOnly(f)}
|
|
65
77
|
onChange=${(e) => {
|
|
66
78
|
const next = { ...draft, [f.key]: e.target.value };
|
|
67
79
|
const sideEffects = f.onChange?.(e.target.value, next);
|
|
@@ -71,13 +83,13 @@ export function EntityFormModal({
|
|
|
71
83
|
<option value=${opt.value}>${opt.label}</option>`)}
|
|
72
84
|
</select>
|
|
73
85
|
` : f.type === 'iconRadio' ? html`
|
|
74
|
-
<div class=${`icon-radio${isReadOnly(f
|
|
86
|
+
<div class=${`icon-radio${isReadOnly(f) ? ' is-disabled' : ''}`}>
|
|
75
87
|
${(f.options || []).map((opt) => html`
|
|
76
88
|
<button type="button" key=${opt.value}
|
|
77
89
|
class=${`icon-radio-opt${draft[f.key] === opt.value ? ' is-active' : ''}`}
|
|
78
|
-
disabled=${isReadOnly(f
|
|
90
|
+
disabled=${isReadOnly(f)}
|
|
79
91
|
onClick=${() => {
|
|
80
|
-
if (isReadOnly(f
|
|
92
|
+
if (isReadOnly(f)) return;
|
|
81
93
|
const next = { ...draft, [f.key]: opt.value };
|
|
82
94
|
const sideEffects = f.onChange?.(opt.value, next);
|
|
83
95
|
setDraft(sideEffects ? { ...next, ...sideEffects } : next);
|
|
@@ -89,20 +101,20 @@ export function EntityFormModal({
|
|
|
89
101
|
` : f.type === 'checkbox' ? html`
|
|
90
102
|
<span class="entity-checkbox-row">
|
|
91
103
|
<input type="checkbox" checked=${!!draft[f.key]}
|
|
92
|
-
disabled=${isReadOnly(f
|
|
104
|
+
disabled=${isReadOnly(f)}
|
|
93
105
|
onChange=${(e) => setDraft({ ...draft, [f.key]: e.target.checked })} />
|
|
94
|
-
${f.hint ? html`<span class="entity-field-hint">${f.hint}</span>` : null}
|
|
106
|
+
${f.hint ? html`<span class="entity-field-hint">${typeof f.hint === 'function' ? f.hint(draft) : f.hint}</span>` : null}
|
|
95
107
|
</span>
|
|
96
108
|
` : html`
|
|
97
109
|
<input type=${f.type || 'text'}
|
|
98
110
|
class=${`input${f.mono ? ' mono' : ''}`}
|
|
99
111
|
placeholder=${f.placeholder || ''}
|
|
100
112
|
value=${draft[f.key] || ''}
|
|
101
|
-
readonly=${isReadOnly(f
|
|
113
|
+
readonly=${isReadOnly(f)}
|
|
102
114
|
onInput=${(e) => setDraft({ ...draft, [f.key]: e.target.value })}
|
|
103
|
-
autoFocus=${f.autoFocus && !isReadOnly(f
|
|
115
|
+
autoFocus=${f.autoFocus && !isReadOnly(f)} />`}
|
|
104
116
|
${f.hint && f.type !== 'checkbox' ? html`
|
|
105
|
-
<span class="entity-field-hint">${f.hint}</span>` : null}
|
|
117
|
+
<span class="entity-field-hint">${typeof f.hint === 'function' ? f.hint(draft) : f.hint}</span>` : null}
|
|
106
118
|
</label>`)}
|
|
107
119
|
${testResult ? html`
|
|
108
120
|
<div class=${`entity-test-result ${testResult.ok ? 'is-ok' : 'is-fail'}`}>
|
|
@@ -20,6 +20,7 @@ import { html } from '../html.js';
|
|
|
20
20
|
import { useEffect } from 'preact/hooks';
|
|
21
21
|
import { serverHealth, hasBootedOnline } from '../state.js';
|
|
22
22
|
import { pollHealth, refreshAll } from '../api.js';
|
|
23
|
+
import { BrandMark } from '../icons.js';
|
|
23
24
|
|
|
24
25
|
const THRESHOLD = 3; // failures before we switch from "checking" to "not running"
|
|
25
26
|
const FAST_POLL_MS = 1500;
|
|
@@ -28,11 +29,6 @@ export function HealthOverlay() {
|
|
|
28
29
|
const h = serverHealth.value;
|
|
29
30
|
const offline = h.state === 'offline';
|
|
30
31
|
const count = h.failureCount || 0;
|
|
31
|
-
|
|
32
|
-
// Don't render the overlay during the very first connect attempt
|
|
33
|
-
// (before we've ever been online) — main.js shows nothing prominent
|
|
34
|
-
// there anyway, and the modal flashing on every page load is
|
|
35
|
-
// annoying. Only show after we've seen the backend at least once.
|
|
36
32
|
const everSeen = hasBootedOnline.value;
|
|
37
33
|
|
|
38
34
|
useEffect(() => {
|
|
@@ -41,9 +37,6 @@ export function HealthOverlay() {
|
|
|
41
37
|
return () => clearInterval(id);
|
|
42
38
|
}, [offline]);
|
|
43
39
|
|
|
44
|
-
// When the backend comes back online after we've shown the overlay,
|
|
45
|
-
// refresh all derived state once — sessions/folders/workspaces may
|
|
46
|
-
// have changed during the outage (post-restart, post-upgrade).
|
|
47
40
|
useEffect(() => {
|
|
48
41
|
if (!offline && everSeen) {
|
|
49
42
|
refreshAll().catch(() => {});
|
|
@@ -54,27 +47,44 @@ export function HealthOverlay() {
|
|
|
54
47
|
|
|
55
48
|
const showStart = count >= THRESHOLD;
|
|
56
49
|
|
|
50
|
+
// Reuses the .offline-overlay / .offline-card classes so the card
|
|
51
|
+
// layout (brand mark, big title, copy, primary action button,
|
|
52
|
+
// collapsible npm-install fallback) matches what the OfflineBanner
|
|
53
|
+
// used to render. HealthOverlay differs only in the two states:
|
|
54
|
+
// early polls show a spinner + "Checking…" instead of the static
|
|
55
|
+
// "Backend not running" card.
|
|
57
56
|
return html`
|
|
58
|
-
<div class="
|
|
59
|
-
<div class="
|
|
57
|
+
<div class="offline-overlay" role="dialog" aria-modal="true" aria-live="polite">
|
|
58
|
+
<div class="offline-card">
|
|
59
|
+
<div class="offline-brand">${
|
|
60
|
+
showStart
|
|
61
|
+
? html`<${BrandMark} />`
|
|
62
|
+
: html`<div class="health-spinner" aria-hidden="true"></div>`
|
|
63
|
+
}</div>
|
|
60
64
|
${!showStart ? html`
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</div>
|
|
65
|
+
<h1 class="offline-title">Checking backend health…</h1>
|
|
66
|
+
<p class="offline-copy">
|
|
67
|
+
${count === 0 ? 'Probing localhost:7777.' : `${count} attempt${count > 1 ? 's' : ''}. Hang tight.`}
|
|
68
|
+
</p>
|
|
66
69
|
` : html`
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<div class="health-hint">
|
|
76
|
-
Or run <code>ccsm</code> in a terminal.
|
|
70
|
+
<h1 class="offline-title">Backend not running</h1>
|
|
71
|
+
<p class="offline-copy">
|
|
72
|
+
ccsm's local backend isn't reachable. Wake it manually below — we won't
|
|
73
|
+
auto-restart. Windows may ask once for permission; tick <em>Always allow</em>
|
|
74
|
+
to silence future prompts.
|
|
75
|
+
</p>
|
|
76
|
+
<div class="offline-actions">
|
|
77
|
+
<a class="action primary big" href="ccsm://start">Start backend</a>
|
|
77
78
|
</div>
|
|
79
|
+
<details class="offline-fallback">
|
|
80
|
+
<summary>Don't have ccsm installed?</summary>
|
|
81
|
+
<div class="offline-fallback-body">
|
|
82
|
+
<p>Install once via npm, then come back here:</p>
|
|
83
|
+
<pre><code>npm i -g @bakapiano/ccsm</code></pre>
|
|
84
|
+
<p>Or run a one-shot trial without installing:</p>
|
|
85
|
+
<pre><code>npx @bakapiano/ccsm</code></pre>
|
|
86
|
+
</div>
|
|
87
|
+
</details>
|
|
78
88
|
`}
|
|
79
89
|
</div>
|
|
80
90
|
</div>`;
|
|
@@ -42,30 +42,51 @@ function cliFieldsFor({ creating } = {}) {
|
|
|
42
42
|
{ value: 'copilot', label: 'GitHub Copilot', icon: html`<${IconCopilotColor} />` },
|
|
43
43
|
{ value: 'other', label: 'Other', icon: html`<${IconTerminal} />` },
|
|
44
44
|
],
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
45
|
+
// Type-change side effects. For known types we force the
|
|
46
|
+
// integration args (newSessionIdArgs / resumeIdArgs) to the
|
|
47
|
+
// canonical template — those fields are locked anyway so
|
|
48
|
+
// there's no value in leaving stale strings around. For
|
|
49
|
+
// type='other' we leave existing args alone so the user can
|
|
50
|
+
// keep editing them. Name + command are only prefilled when
|
|
51
|
+
// creating (don't clobber a saved CLI's name on edit).
|
|
52
|
+
onChange: (v, next) => {
|
|
48
53
|
const d = CLI_TYPE_DEFAULTS[v];
|
|
49
54
|
if (!d) return null;
|
|
50
|
-
const patch = {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
patch.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const patch = {};
|
|
56
|
+
if (v !== 'other') {
|
|
57
|
+
patch.resumeIdArgs = d.resumeIdArgs;
|
|
58
|
+
patch.newSessionIdArgs = d.newSessionIdArgs;
|
|
59
|
+
}
|
|
60
|
+
if (creating) {
|
|
61
|
+
if (!next.command || !next.command.trim()) patch.command = d.command || '';
|
|
62
|
+
if (!next.name || !next.name.trim()) {
|
|
63
|
+
patch.name = v === 'claude' ? 'Claude Code'
|
|
64
|
+
: v === 'codex' ? 'OpenAI Codex'
|
|
65
|
+
: v === 'copilot' ? 'GitHub Copilot'
|
|
66
|
+
: '';
|
|
67
|
+
}
|
|
57
68
|
}
|
|
58
69
|
return patch;
|
|
59
|
-
}
|
|
70
|
+
},
|
|
60
71
|
},
|
|
61
72
|
{ key: 'name', label: 'Name', placeholder: 'My CLI', required: true },
|
|
62
73
|
{ key: 'command', label: 'Command', mono: true, placeholder: 'ccp / claude / ...', required: true },
|
|
63
74
|
{ key: 'args', label: 'Args (space-separated)', mono: true, placeholder: '',
|
|
64
75
|
hint: 'Used on every launch.' },
|
|
65
76
|
{ key: 'newSessionIdArgs', label: 'New session id args', mono: true, placeholder: '--session-id <id>',
|
|
66
|
-
|
|
77
|
+
// Lock for known types — those args are an integration contract
|
|
78
|
+
// with the upstream CLI, not a user knob. Only Type=Other allows
|
|
79
|
+
// a custom value (for hand-rolled CLIs ccsm doesn't ship a
|
|
80
|
+
// template for).
|
|
81
|
+
readOnly: (d) => d.type && d.type !== 'other',
|
|
82
|
+
hint: (d) => d.type && d.type !== 'other'
|
|
83
|
+
? `Locked to the canonical flags for ${d.type}. Change Type to "Other" to override.`
|
|
84
|
+
: 'ccsm pre-generates a UUID and substitutes it for <id> on first launch — the upstream CLI session id is known immediately.' },
|
|
67
85
|
{ key: 'resumeIdArgs', label: 'Resume by id args', mono: true, placeholder: '--resume <id>',
|
|
68
|
-
|
|
86
|
+
readOnly: (d) => d.type && d.type !== 'other',
|
|
87
|
+
hint: (d) => d.type && d.type !== 'other'
|
|
88
|
+
? `Locked to the canonical flags for ${d.type}. Change Type to "Other" to override.`
|
|
89
|
+
: 'Used on every resume. Substitutes <id> with the captured session UUID.' },
|
|
69
90
|
{ key: 'shell', label: 'Shell', type: 'select', default: 'direct', options: [
|
|
70
91
|
{ value: 'direct', label: 'direct (real .exe / .cmd)' },
|
|
71
92
|
{ value: 'pwsh', label: 'pwsh (PowerShell aliases & functions)' },
|
package/scripts/dev.js
CHANGED
|
@@ -38,6 +38,52 @@ if (!fs.existsSync(configPath)) {
|
|
|
38
38
|
}, null, 2));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Mirror pages-root assets into public/ so the dev server can serve
|
|
42
|
+
// them at the URL paths the deployed site uses (manifest at
|
|
43
|
+
// /manifest.webmanifest, setup page at /setup/). Both mirror files
|
|
44
|
+
// are .gitignored — they exist only for local preview.
|
|
45
|
+
//
|
|
46
|
+
// The manifest is rewritten with a "ccsm-dev" identity so the PWA
|
|
47
|
+
// installed from dev shows up separately in Chrome's installed-apps
|
|
48
|
+
// list and Start Menu, instead of conflicting with the prod CCSM
|
|
49
|
+
// install.
|
|
50
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
51
|
+
const PAGES_ROOT = path.join(REPO_ROOT, 'pages-root');
|
|
52
|
+
const PUBLIC_DIR = path.join(REPO_ROOT, 'public');
|
|
53
|
+
|
|
54
|
+
function mirrorSetup() {
|
|
55
|
+
try {
|
|
56
|
+
const src = path.join(PAGES_ROOT, 'setup');
|
|
57
|
+
const dst = path.join(PUBLIC_DIR, 'setup');
|
|
58
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
59
|
+
for (const f of fs.readdirSync(src)) {
|
|
60
|
+
fs.copyFileSync(path.join(src, f), path.join(dst, f));
|
|
61
|
+
}
|
|
62
|
+
} catch (e) { console.warn('[dev] setup mirror failed:', e.message); }
|
|
63
|
+
}
|
|
64
|
+
function writeDevManifest() {
|
|
65
|
+
try {
|
|
66
|
+
const src = path.join(PAGES_ROOT, 'manifest.webmanifest');
|
|
67
|
+
const m = JSON.parse(fs.readFileSync(src, 'utf8'));
|
|
68
|
+
m.id = '/?ccsm-dev';
|
|
69
|
+
m.name = 'CCSM dev';
|
|
70
|
+
m.short_name = 'CCSM dev';
|
|
71
|
+
// Dev runs at host root (localhost:7788/), so scope + start_url
|
|
72
|
+
// anchor at `/` not `./` (which would resolve relative to the
|
|
73
|
+
// manifest URL — same result here, but explicit is clearer).
|
|
74
|
+
m.scope = '/';
|
|
75
|
+
m.start_url = '/';
|
|
76
|
+
// Drop related_applications self-reference — its URL points at
|
|
77
|
+
// the prod GH Pages manifest, not this dev one. Leaving it in
|
|
78
|
+
// would let prod's getInstalledRelatedApps() detect dev installs
|
|
79
|
+
// as if they were prod, which is the opposite of what we want.
|
|
80
|
+
delete m.related_applications;
|
|
81
|
+
fs.writeFileSync(path.join(PUBLIC_DIR, 'manifest.webmanifest'), JSON.stringify(m, null, 2));
|
|
82
|
+
} catch (e) { console.warn('[dev] manifest mirror failed:', e.message); }
|
|
83
|
+
}
|
|
84
|
+
mirrorSetup();
|
|
85
|
+
writeDevManifest();
|
|
86
|
+
|
|
41
87
|
const env = {
|
|
42
88
|
...process.env,
|
|
43
89
|
CCSM_HOME: DEV_HOME,
|
package/scripts/install.js
CHANGED
|
@@ -130,29 +130,24 @@ try {
|
|
|
130
130
|
warn('the hosted frontend\'s "Start ccsm" button will not be able to launch the backend. You can still run `ccsm` manually in a terminal.');
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
133
|
+
// Open the hosted setup guide. The page walks the user through the
|
|
134
|
+
// remaining one-time setup (allow ccsm:// protocol, firewall, install
|
|
135
|
+
// as PWA) and Step 1's "Try ccsm://start" button doubles as ccsm
|
|
136
|
+
// auto-launch — so we don't need a separate spawn here. Set
|
|
137
|
+
// CCSM_NO_AUTOLAUNCH=1 to skip (CI, headless setups).
|
|
137
138
|
if (process.env.CCSM_NO_AUTOLAUNCH !== '1') {
|
|
138
139
|
try {
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
detached: true,
|
|
148
|
-
stdio: 'ignore',
|
|
149
|
-
windowsHide: true,
|
|
150
|
-
});
|
|
151
|
-
child.unref();
|
|
152
|
-
log('launching ccsm now · check for the chromeless window');
|
|
140
|
+
// `start` on Windows opens the default browser without attaching a
|
|
141
|
+
// console. Run via cmd.exe /c since `start` is a cmd builtin.
|
|
142
|
+
require('node:child_process').spawn(
|
|
143
|
+
'cmd.exe',
|
|
144
|
+
['/d', '/s', '/c', 'start', '', 'https://bakapiano.github.io/ccsm/setup/'],
|
|
145
|
+
{ detached: true, stdio: 'ignore', windowsHide: true }
|
|
146
|
+
).unref();
|
|
147
|
+
log('opened setup guide · https://bakapiano.github.io/ccsm/setup/');
|
|
153
148
|
log('(set CCSM_NO_AUTOLAUNCH=1 to skip this on future installs)');
|
|
154
149
|
} catch (e) {
|
|
155
|
-
warn(`
|
|
150
|
+
warn(`setup guide open failed · ${e.message}`);
|
|
156
151
|
warn('run `ccsm` manually to start.');
|
|
157
152
|
}
|
|
158
153
|
}
|