@bakapiano/ccsm 0.22.3 → 0.22.4
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/CLAUDE.md +538 -538
- package/README.md +189 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +139 -139
- package/lib/codexSeed.js +183 -183
- package/lib/config.js +274 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/localCliSessions.js +519 -519
- package/lib/persistedSessions.js +129 -129
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +225 -225
- package/lib/workspace.js +233 -233
- package/package.json +57 -57
- package/public/css/base.css +99 -99
- package/public/css/cards.css +183 -183
- package/public/css/feedback.css +504 -504
- package/public/css/forms.css +453 -453
- package/public/css/layout.css +176 -176
- package/public/css/modal.css +190 -190
- package/public/css/responsive.css +176 -176
- package/public/css/sidebar.css +707 -707
- package/public/css/terminals.css +592 -592
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2725 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +371 -371
- package/public/js/backend.js +149 -149
- package/public/js/components/App.js +73 -73
- package/public/js/components/DirectoryPicker.js +203 -203
- package/public/js/components/EntityFormModal.js +153 -153
- package/public/js/components/Modal.js +57 -57
- package/public/js/components/OfflineBanner.js +67 -67
- package/public/js/components/PageTitleBar.js +13 -13
- package/public/js/components/PendingApprovalOverlay.js +128 -128
- package/public/js/components/Picker.js +179 -179
- package/public/js/components/Popover.js +55 -55
- package/public/js/components/RestartOverlay.js +36 -36
- package/public/js/components/Sidebar.js +380 -380
- package/public/js/components/TerminalInstance.js +148 -22
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/XtermTerminal.js +62 -15
- package/public/js/components/useDragSort.js +67 -67
- package/public/js/dialog.js +67 -67
- package/public/js/icons.js +212 -212
- package/public/js/main.js +296 -296
- package/public/js/pages/AboutPage.js +90 -90
- package/public/js/pages/ConfigurePage.js +713 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +100 -100
- package/public/js/state.js +335 -335
- package/public/manifest.webmanifest +25 -0
- package/public/setup/index.html +567 -0
- package/scripts/dev.js +149 -149
- package/scripts/install.js +153 -153
- package/scripts/restart-helper.js +96 -96
- package/scripts/upgrade-helper.js +687 -687
- package/server.js +1807 -1807
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "/?ccsm-dev",
|
|
3
|
+
"name": "CCSM dev",
|
|
4
|
+
"short_name": "CCSM dev",
|
|
5
|
+
"version": "0.0.0-dev",
|
|
6
|
+
"description": "Single pane over every live claude session on this machine.",
|
|
7
|
+
"start_url": "/",
|
|
8
|
+
"scope": "/",
|
|
9
|
+
"display": "standalone",
|
|
10
|
+
"display_override": [
|
|
11
|
+
"window-controls-overlay",
|
|
12
|
+
"standalone"
|
|
13
|
+
],
|
|
14
|
+
"background_color": "#ffffff",
|
|
15
|
+
"theme_color": "#ffffff",
|
|
16
|
+
"icons": [
|
|
17
|
+
{
|
|
18
|
+
"src": "favicon.svg",
|
|
19
|
+
"type": "image/svg+xml",
|
|
20
|
+
"sizes": "any",
|
|
21
|
+
"purpose": "any"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"prefer_related_applications": false
|
|
25
|
+
}
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>ccsm · setup</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="../favicon.svg" />
|
|
8
|
+
<!-- Sits under the same /ccsm/ scope as the router so an installed PWA
|
|
9
|
+
navigation here doesn't leave scope and pop an address bar. -->
|
|
10
|
+
<link rel="manifest" href="../manifest.webmanifest" />
|
|
11
|
+
<!-- Capture `beforeinstallprompt` as early as possible — Chrome fires
|
|
12
|
+
it the moment install criteria are met, which can be BEFORE the
|
|
13
|
+
bottom-of-body script registers its listener if the page is slow
|
|
14
|
+
to paint. Stash it on `window` so the later script consumes it. -->
|
|
15
|
+
<script>
|
|
16
|
+
window.__deferredPrompt = null;
|
|
17
|
+
window.addEventListener('beforeinstallprompt', (ev) => {
|
|
18
|
+
ev.preventDefault();
|
|
19
|
+
window.__deferredPrompt = ev;
|
|
20
|
+
window.dispatchEvent(new Event('ccsm-bip-ready'));
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
24
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
25
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" />
|
|
26
|
+
<style>
|
|
27
|
+
:root {
|
|
28
|
+
--bg: #faf9f5;
|
|
29
|
+
--bg-elev: #ffffff;
|
|
30
|
+
--ink: #1a1815;
|
|
31
|
+
--ink-mid: #6b665d;
|
|
32
|
+
--ink-muted: #9a9489;
|
|
33
|
+
--border: #e8e3d5;
|
|
34
|
+
--border-soft: #efeadd;
|
|
35
|
+
--accent: #b3614a;
|
|
36
|
+
--accent-soft: rgba(179, 97, 74, 0.10);
|
|
37
|
+
--green: #4a8a4a;
|
|
38
|
+
--green-soft: rgba(74, 138, 74, 0.10);
|
|
39
|
+
--warn: #c79544;
|
|
40
|
+
}
|
|
41
|
+
* { box-sizing: border-box; }
|
|
42
|
+
html, body { margin: 0; padding: 0; }
|
|
43
|
+
body {
|
|
44
|
+
background: var(--bg);
|
|
45
|
+
color: var(--ink);
|
|
46
|
+
font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
line-height: 1.55;
|
|
49
|
+
min-height: 100vh;
|
|
50
|
+
}
|
|
51
|
+
a { color: var(--accent); text-decoration: none; }
|
|
52
|
+
a:hover { text-decoration: underline; }
|
|
53
|
+
code, pre {
|
|
54
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
55
|
+
}
|
|
56
|
+
pre {
|
|
57
|
+
background: var(--ink);
|
|
58
|
+
color: #e8e3d5;
|
|
59
|
+
padding: 12px 16px;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
overflow-x: auto;
|
|
62
|
+
font-size: 13px;
|
|
63
|
+
margin: 10px 0;
|
|
64
|
+
}
|
|
65
|
+
pre code { font-size: 13px; }
|
|
66
|
+
:not(pre) > code {
|
|
67
|
+
background: var(--bg-elev);
|
|
68
|
+
border: 1px solid var(--border);
|
|
69
|
+
border-radius: 3px;
|
|
70
|
+
padding: 1px 6px;
|
|
71
|
+
font-size: 12.5px;
|
|
72
|
+
}
|
|
73
|
+
.wrap {
|
|
74
|
+
max-width: 720px;
|
|
75
|
+
margin: 0 auto;
|
|
76
|
+
padding: 56px 24px 96px;
|
|
77
|
+
}
|
|
78
|
+
.brand {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 10px;
|
|
82
|
+
margin-bottom: 24px;
|
|
83
|
+
}
|
|
84
|
+
.brand-mark {
|
|
85
|
+
width: 28px;
|
|
86
|
+
height: 28px;
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
}
|
|
89
|
+
.brand-name {
|
|
90
|
+
font-size: 16px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
letter-spacing: -0.01em;
|
|
93
|
+
}
|
|
94
|
+
.brand-dot { color: var(--accent); }
|
|
95
|
+
h1 {
|
|
96
|
+
font-size: 28px;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
letter-spacing: -0.02em;
|
|
99
|
+
margin: 0 0 8px;
|
|
100
|
+
}
|
|
101
|
+
.subtitle {
|
|
102
|
+
color: var(--ink-mid);
|
|
103
|
+
margin: 0 0 32px;
|
|
104
|
+
font-size: 15px;
|
|
105
|
+
}
|
|
106
|
+
.step {
|
|
107
|
+
background: var(--bg-elev);
|
|
108
|
+
border: 1px solid var(--border);
|
|
109
|
+
border-radius: 12px;
|
|
110
|
+
padding: 22px 26px;
|
|
111
|
+
margin-bottom: 16px;
|
|
112
|
+
transition: border-color .15s, opacity .25s;
|
|
113
|
+
position: relative;
|
|
114
|
+
}
|
|
115
|
+
.step.is-done {
|
|
116
|
+
border-color: rgba(74, 138, 74, 0.30);
|
|
117
|
+
background: var(--green-soft);
|
|
118
|
+
}
|
|
119
|
+
.step-head {
|
|
120
|
+
display: flex;
|
|
121
|
+
gap: 18px;
|
|
122
|
+
align-items: flex-start;
|
|
123
|
+
}
|
|
124
|
+
.step-number {
|
|
125
|
+
flex: 0 0 28px;
|
|
126
|
+
width: 28px;
|
|
127
|
+
height: 28px;
|
|
128
|
+
border-radius: 50%;
|
|
129
|
+
background: var(--bg);
|
|
130
|
+
border: 1px solid var(--border);
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
font-weight: 500;
|
|
136
|
+
color: var(--ink-mid);
|
|
137
|
+
font-family: 'JetBrains Mono', monospace;
|
|
138
|
+
}
|
|
139
|
+
.step.is-done .step-number {
|
|
140
|
+
background: var(--green);
|
|
141
|
+
color: var(--bg-elev);
|
|
142
|
+
border-color: var(--green);
|
|
143
|
+
}
|
|
144
|
+
.step.is-done .step-number::before {
|
|
145
|
+
content: "✓";
|
|
146
|
+
}
|
|
147
|
+
.step.is-done .step-number span { display: none; }
|
|
148
|
+
.step-content { flex: 1; min-width: 0; }
|
|
149
|
+
.step h2 {
|
|
150
|
+
margin: 0 0 8px;
|
|
151
|
+
font-size: 17px;
|
|
152
|
+
font-weight: 500;
|
|
153
|
+
letter-spacing: -0.01em;
|
|
154
|
+
}
|
|
155
|
+
.step p {
|
|
156
|
+
margin: 0 0 10px;
|
|
157
|
+
color: var(--ink-mid);
|
|
158
|
+
}
|
|
159
|
+
.step p strong { color: var(--ink); font-weight: 500; }
|
|
160
|
+
.step .hint {
|
|
161
|
+
font-size: 12.5px;
|
|
162
|
+
color: var(--ink-muted);
|
|
163
|
+
}
|
|
164
|
+
.btn {
|
|
165
|
+
appearance: none;
|
|
166
|
+
border: 1px solid var(--ink);
|
|
167
|
+
background: var(--ink);
|
|
168
|
+
color: var(--bg-elev);
|
|
169
|
+
padding: 8px 16px;
|
|
170
|
+
border-radius: 6px;
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
font: inherit;
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
font-weight: 500;
|
|
175
|
+
transition: background .12s, color .12s;
|
|
176
|
+
margin: 6px 0 4px;
|
|
177
|
+
}
|
|
178
|
+
.btn:hover { background: #000; }
|
|
179
|
+
.btn.subtle {
|
|
180
|
+
background: var(--bg-elev);
|
|
181
|
+
color: var(--ink);
|
|
182
|
+
}
|
|
183
|
+
.btn.subtle:hover { background: var(--border-soft); }
|
|
184
|
+
.step-status {
|
|
185
|
+
display: inline-block;
|
|
186
|
+
margin-top: 10px;
|
|
187
|
+
padding: 4px 10px;
|
|
188
|
+
border-radius: 999px;
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
background: var(--bg);
|
|
191
|
+
color: var(--ink-muted);
|
|
192
|
+
border: 1px solid var(--border);
|
|
193
|
+
font-family: 'JetBrains Mono', monospace;
|
|
194
|
+
}
|
|
195
|
+
.step-status.is-ok {
|
|
196
|
+
background: var(--green-soft);
|
|
197
|
+
color: var(--green);
|
|
198
|
+
border-color: rgba(74, 138, 74, 0.3);
|
|
199
|
+
}
|
|
200
|
+
.step-status.is-warn {
|
|
201
|
+
background: rgba(199, 149, 68, 0.10);
|
|
202
|
+
color: var(--warn);
|
|
203
|
+
border-color: rgba(199, 149, 68, 0.35);
|
|
204
|
+
}
|
|
205
|
+
.copy-btn {
|
|
206
|
+
margin-left: 8px;
|
|
207
|
+
font-size: 11.5px;
|
|
208
|
+
background: transparent;
|
|
209
|
+
border: 1px solid var(--border-soft);
|
|
210
|
+
color: #e8e3d5;
|
|
211
|
+
border-radius: 4px;
|
|
212
|
+
padding: 2px 8px;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
}
|
|
215
|
+
.copy-btn:hover { background: rgba(255,255,255,0.08); }
|
|
216
|
+
.install-row {
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: 4px;
|
|
220
|
+
}
|
|
221
|
+
.install-row pre { flex: 1; margin: 10px 0; }
|
|
222
|
+
.footer {
|
|
223
|
+
margin-top: 36px;
|
|
224
|
+
padding-top: 24px;
|
|
225
|
+
border-top: 1px solid var(--border);
|
|
226
|
+
color: var(--ink-muted);
|
|
227
|
+
font-size: 12.5px;
|
|
228
|
+
text-align: center;
|
|
229
|
+
}
|
|
230
|
+
.footer a { color: var(--ink-mid); }
|
|
231
|
+
/* Mark all-steps-done banner */
|
|
232
|
+
.done-banner {
|
|
233
|
+
margin-top: 20px;
|
|
234
|
+
padding: 18px 22px;
|
|
235
|
+
background: var(--green);
|
|
236
|
+
color: var(--bg-elev);
|
|
237
|
+
border-radius: 10px;
|
|
238
|
+
display: none;
|
|
239
|
+
text-align: center;
|
|
240
|
+
font-weight: 500;
|
|
241
|
+
}
|
|
242
|
+
.done-banner.show { display: block; animation: pop 0.4s ease-out; }
|
|
243
|
+
@keyframes pop {
|
|
244
|
+
from { transform: scale(0.96); opacity: 0; }
|
|
245
|
+
to { transform: scale(1); opacity: 1; }
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
248
|
+
</head>
|
|
249
|
+
<body>
|
|
250
|
+
<div class="wrap">
|
|
251
|
+
<div class="brand">
|
|
252
|
+
<span class="brand-mark">
|
|
253
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
254
|
+
<rect width="32" height="32" rx="6" fill="#1a1815"/>
|
|
255
|
+
<text x="16" y="20" text-anchor="middle" fill="#e8e3d5" font-family="JetBrains Mono, monospace" font-size="10" font-weight="600">ccsm</text>
|
|
256
|
+
</svg>
|
|
257
|
+
</span>
|
|
258
|
+
<span class="brand-name">CCSM setup</span>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<h1>Set up ccsm</h1>
|
|
262
|
+
<p class="subtitle">Three quick steps. We'll auto-detect what's already done — just handle the un-checked ones.</p>
|
|
263
|
+
|
|
264
|
+
<!-- Step 1 · ccsm:// protocol ──────────────────────────────────── -->
|
|
265
|
+
<div class="step" id="step-protocol">
|
|
266
|
+
<div class="step-head">
|
|
267
|
+
<div class="step-number"><span>1</span></div>
|
|
268
|
+
<div class="step-content">
|
|
269
|
+
<h2>Allow the ccsm:// link handler</h2>
|
|
270
|
+
<p>ccsm registers a <code>ccsm://</code> URL protocol so the "Start backend" button can wake it from the browser. The first time it fires, Chrome shows a confirmation:</p>
|
|
271
|
+
<p class="hint">→ <strong>Always allow ccsm.exe to open links of this type</strong> · then click <strong>Open</strong>.</p>
|
|
272
|
+
<p>Click below to trigger the prompt. (If the backend is already running, this is a no-op.)</p>
|
|
273
|
+
<button class="btn" id="test-protocol">Try ccsm://start</button>
|
|
274
|
+
<span class="step-status" id="status-protocol">Not tested yet</span>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<!-- Step 2 · Localhost networking ──────────────────────────────── -->
|
|
280
|
+
<div class="step" id="step-firewall">
|
|
281
|
+
<div class="step-head">
|
|
282
|
+
<div class="step-number"><span>2</span></div>
|
|
283
|
+
<div class="step-content">
|
|
284
|
+
<h2>Allow localhost networking</h2>
|
|
285
|
+
<p>The backend listens on <code>localhost:7777</code> and this page (hosted on GitHub Pages) fetches from it. Two things to check:</p>
|
|
286
|
+
<ul>
|
|
287
|
+
<li><strong>Windows Firewall</strong> — first time <code>node.exe</code> binds the port, Windows pops "Allow this app to communicate". Tick <strong>Private networks</strong> + Allow access.</li>
|
|
288
|
+
<li><strong>Browser</strong> — Chrome 99+ treats <code>localhost</code> as a secure origin from HTTPS pages out of the box, so usually nothing to do here. If you see a "Mixed content" badge in the URL bar, click it → Allow.</li>
|
|
289
|
+
</ul>
|
|
290
|
+
<p>We can verify the browser side by trying a probe:</p>
|
|
291
|
+
<button class="btn subtle" id="test-localhost">Probe localhost:7777</button>
|
|
292
|
+
<span class="step-status" id="status-firewall">Not tested yet</span>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<!-- Step 3 · Install as app ────────────────────────────────────── -->
|
|
298
|
+
<div class="step" id="step-pwa">
|
|
299
|
+
<div class="step-head">
|
|
300
|
+
<div class="step-number"><span>3</span></div>
|
|
301
|
+
<div class="step-content">
|
|
302
|
+
<h2>Install as app</h2>
|
|
303
|
+
<p>For a chromeless window with no address bar:</p>
|
|
304
|
+
<ol>
|
|
305
|
+
<li>Open <a href="../" target="_blank" rel="noopener">bakapiano.github.io/ccsm</a> in Chrome.</li>
|
|
306
|
+
<li>Look for the install icon (<code>⊕</code>) on the right side of the URL bar.</li>
|
|
307
|
+
<li>Click <strong>Install</strong>. Chrome creates a Start Menu shortcut and opens ccsm as a standalone app.</li>
|
|
308
|
+
</ol>
|
|
309
|
+
<p class="hint">From then on, the <code>ccsm</code> command launches the installed PWA window (we auto-detect your install) — no address bar, OS title bar only.</p>
|
|
310
|
+
<button class="btn" id="install-pwa-btn">Install now</button>
|
|
311
|
+
<span class="step-status" id="status-pwa">Detecting…</span>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div class="done-banner" id="done-banner">
|
|
317
|
+
All set. Open ccsm and enjoy your sessions.
|
|
318
|
+
<div style="margin-top:8px"><a href="../" style="color:#fff; text-decoration:underline">→ Launch ccsm</a></div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="footer">
|
|
322
|
+
<a href="../" target="_blank">ccsm router</a> ·
|
|
323
|
+
<a href="https://github.com/bakapiano/ccsm" target="_blank">GitHub</a> ·
|
|
324
|
+
<a href="https://www.npmjs.com/package/@bakapiano/ccsm" target="_blank">npm</a>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<script>
|
|
329
|
+
(function () {
|
|
330
|
+
const $ = (sel) => document.querySelector(sel);
|
|
331
|
+
const protocolStatus = $('#status-protocol');
|
|
332
|
+
const firewallStatus = $('#status-firewall');
|
|
333
|
+
const pwaStatus = $('#status-pwa');
|
|
334
|
+
|
|
335
|
+
// ── Background health probe — reused by the protocol-test wait
|
|
336
|
+
// loop and the localhost probe button. Doesn't have its own visible
|
|
337
|
+
// step (the page is opened from npm postinstall so we assume the
|
|
338
|
+
// user just installed); the result just feeds into step 2's status.
|
|
339
|
+
async function checkInstalled() {
|
|
340
|
+
try {
|
|
341
|
+
const ctrl = new AbortController();
|
|
342
|
+
const t = setTimeout(() => ctrl.abort(), 2000);
|
|
343
|
+
const r = await fetch('http://localhost:7777/api/health', { signal: ctrl.signal, cache: 'no-store' });
|
|
344
|
+
clearTimeout(t);
|
|
345
|
+
if (!r.ok) throw new Error('http ' + r.status);
|
|
346
|
+
const j = await r.json();
|
|
347
|
+
if (j.name === '@bakapiano/ccsm') {
|
|
348
|
+
// Reached backend → networking is fine, mark step 2 done.
|
|
349
|
+
if (!firewallStatus.classList.contains('is-ok')) {
|
|
350
|
+
firewallStatus.className = 'step-status is-ok';
|
|
351
|
+
firewallStatus.textContent = '✓ Reached localhost:7777';
|
|
352
|
+
$('#step-firewall').classList.add('is-done');
|
|
353
|
+
}
|
|
354
|
+
updateDoneBanner();
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
} catch (e) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
checkInstalled();
|
|
363
|
+
setInterval(checkInstalled, 4000);
|
|
364
|
+
|
|
365
|
+
// ── Step 1 · ccsm:// trigger ────────────────────────────────────
|
|
366
|
+
$('#test-protocol').addEventListener('click', () => {
|
|
367
|
+
protocolStatus.className = 'step-status is-warn';
|
|
368
|
+
protocolStatus.textContent = 'Waiting for Chrome prompt…';
|
|
369
|
+
location.href = 'ccsm://start';
|
|
370
|
+
// We can't directly tell whether the user accepted the protocol
|
|
371
|
+
// prompt — the next /api/health success is the best signal.
|
|
372
|
+
let waited = 0;
|
|
373
|
+
const t = setInterval(async () => {
|
|
374
|
+
waited += 1500;
|
|
375
|
+
const ok = await checkInstalled();
|
|
376
|
+
if (ok) {
|
|
377
|
+
protocolStatus.className = 'step-status is-ok';
|
|
378
|
+
protocolStatus.textContent = '✓ Backend woke via ccsm://';
|
|
379
|
+
clearInterval(t);
|
|
380
|
+
} else if (waited > 15000) {
|
|
381
|
+
protocolStatus.className = 'step-status';
|
|
382
|
+
protocolStatus.textContent = '— Timed out. If a Chrome dialog appeared, click "Open ccsm.cmd" and retry.';
|
|
383
|
+
clearInterval(t);
|
|
384
|
+
}
|
|
385
|
+
}, 1500);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// ── Step 2 · manual probe ───────────────────────────────────────
|
|
389
|
+
$('#test-localhost').addEventListener('click', async () => {
|
|
390
|
+
firewallStatus.className = 'step-status is-warn';
|
|
391
|
+
firewallStatus.textContent = 'Probing…';
|
|
392
|
+
const ok = await checkInstalled();
|
|
393
|
+
if (ok) {
|
|
394
|
+
firewallStatus.className = 'step-status is-ok';
|
|
395
|
+
firewallStatus.textContent = '✓ Reached localhost:7777';
|
|
396
|
+
$('#step-firewall').classList.add('is-done');
|
|
397
|
+
} else {
|
|
398
|
+
firewallStatus.className = 'step-status';
|
|
399
|
+
firewallStatus.textContent = '— Backend not running. Start it first (step 1).';
|
|
400
|
+
}
|
|
401
|
+
updateDoneBanner();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// ── Step 3 · PWA detection + beforeinstallprompt ────────────────
|
|
405
|
+
// Three signals for "is the PWA installed in this browser":
|
|
406
|
+
// (a) display-mode standalone — we're INSIDE the PWA window right now
|
|
407
|
+
// (b) navigator.getInstalledRelatedApps — returns the manifest if it's
|
|
408
|
+
// been installed at any point (requires `related_applications`
|
|
409
|
+
// self-reference in the manifest, which we just added)
|
|
410
|
+
// (c) `appinstalled` event firing within this session
|
|
411
|
+
// Any of (a) (b) (c) flips the step to done.
|
|
412
|
+
function isStandalone() {
|
|
413
|
+
return window.matchMedia('(display-mode: standalone), (display-mode: window-controls-overlay)').matches;
|
|
414
|
+
}
|
|
415
|
+
let installedFlag = false;
|
|
416
|
+
function setInstalled(reason) {
|
|
417
|
+
installedFlag = true;
|
|
418
|
+
pwaStatus.className = 'step-status is-ok';
|
|
419
|
+
pwaStatus.textContent = '✓ Installed' + (reason ? ' · ' + reason : '');
|
|
420
|
+
$('#step-pwa').classList.add('is-done');
|
|
421
|
+
refreshInstallBtn();
|
|
422
|
+
updateDoneBanner();
|
|
423
|
+
}
|
|
424
|
+
async function updatePwaStatus() {
|
|
425
|
+
if (isStandalone()) { setInstalled('running as PWA'); return; }
|
|
426
|
+
if (typeof navigator.getInstalledRelatedApps === 'function') {
|
|
427
|
+
try {
|
|
428
|
+
const apps = await navigator.getInstalledRelatedApps();
|
|
429
|
+
if (apps.some((a) => a.platform === 'webapp')) {
|
|
430
|
+
setInstalled('detected via getInstalledRelatedApps');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
} catch {}
|
|
434
|
+
}
|
|
435
|
+
if (!installedFlag) {
|
|
436
|
+
pwaStatus.className = 'step-status';
|
|
437
|
+
pwaStatus.textContent = '— Not installed yet';
|
|
438
|
+
}
|
|
439
|
+
updateDoneBanner();
|
|
440
|
+
}
|
|
441
|
+
updatePwaStatus();
|
|
442
|
+
window.matchMedia('(display-mode: standalone)').addEventListener?.('change', updatePwaStatus);
|
|
443
|
+
|
|
444
|
+
// Install button is always visible; its click behavior depends on
|
|
445
|
+
// what state we're in:
|
|
446
|
+
// - deferredPrompt available → fire native Chrome install dialog
|
|
447
|
+
// - already installed (standalone display-mode) → open the router
|
|
448
|
+
// in a new tab, which Chrome opens AS the installed PWA window
|
|
449
|
+
// - no prompt available + not installed → open the router so the
|
|
450
|
+
// user can hit Chrome's URL-bar install icon there. Setup page
|
|
451
|
+
// is at /ccsm/setup/, but Chrome only shows the install icon on
|
|
452
|
+
// the manifest's `start_url` (/ccsm/), which is why we route
|
|
453
|
+
// them there.
|
|
454
|
+
const installBtn = $('#install-pwa-btn');
|
|
455
|
+
function refreshInstallBtn() {
|
|
456
|
+
if (isStandalone() || installedFlag) {
|
|
457
|
+
installBtn.textContent = 'Open installed app';
|
|
458
|
+
} else if (window.__deferredPrompt) {
|
|
459
|
+
installBtn.textContent = 'Install now';
|
|
460
|
+
} else {
|
|
461
|
+
installBtn.textContent = 'Open router to install';
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
refreshInstallBtn();
|
|
465
|
+
window.addEventListener('ccsm-bip-ready', refreshInstallBtn);
|
|
466
|
+
|
|
467
|
+
// After a successful install Chrome opens a new PWA standalone
|
|
468
|
+
// window at the install-trigger URL — which for us is /ccsm/setup/,
|
|
469
|
+
// NOT manifest.start_url. So the freshly-popped PWA shows the setup
|
|
470
|
+
// page instead of the actual app. We handle this two ways:
|
|
471
|
+
// • The PWA window's setup page (it's a fresh load) self-detects
|
|
472
|
+
// standalone display-mode at startup and bounces to ../ — see
|
|
473
|
+
// the bouncePwaWindow() block right after this.
|
|
474
|
+
// • THIS tab (the regular browser one that triggered the install)
|
|
475
|
+
// just closes itself so the user isn't left with a stale setup
|
|
476
|
+
// tab next to the PWA window. window.close() is allowed for
|
|
477
|
+
// script-opened tabs; for postinstall-spawned ones (Windows
|
|
478
|
+
// `start` from terminal) Chrome silently refuses and the page
|
|
479
|
+
// stays in the "✓ Installed" state, which is harmless.
|
|
480
|
+
function jumpToApp() {
|
|
481
|
+
setTimeout(() => {
|
|
482
|
+
try { window.close(); } catch {}
|
|
483
|
+
}, 600);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// The PWA window's own bounce-to-start_url logic. Triggers when:
|
|
487
|
+
// (a) the page loads INSIDE a standalone PWA window — Chrome
|
|
488
|
+
// just opened it as the post-install window
|
|
489
|
+
// (b) the display-mode transitions to standalone while this page
|
|
490
|
+
// is alive (rare — covers the case where Chrome upgrades a
|
|
491
|
+
// browser tab into a PWA window mid-session)
|
|
492
|
+
// Either path replaces the URL with ../ so the setup page never
|
|
493
|
+
// becomes the PWA's resting state. Uses location.replace so /setup/
|
|
494
|
+
// doesn't sit in the PWA window's history.
|
|
495
|
+
function bouncePwaWindow() {
|
|
496
|
+
if (location.pathname.endsWith('/setup/') || location.pathname.endsWith('/setup/index.html')) {
|
|
497
|
+
location.replace('../');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (window.matchMedia('(display-mode: standalone), (display-mode: window-controls-overlay)').matches) {
|
|
501
|
+
bouncePwaWindow();
|
|
502
|
+
}
|
|
503
|
+
window.matchMedia('(display-mode: standalone)').addEventListener?.('change', (ev) => {
|
|
504
|
+
if (ev.matches) bouncePwaWindow();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
installBtn.addEventListener('click', async () => {
|
|
508
|
+
if (window.__deferredPrompt) {
|
|
509
|
+
installBtn.disabled = true;
|
|
510
|
+
try {
|
|
511
|
+
const result = await window.__deferredPrompt.prompt();
|
|
512
|
+
if (result.outcome === 'accepted') {
|
|
513
|
+
pwaStatus.className = 'step-status is-ok';
|
|
514
|
+
pwaStatus.textContent = '✓ Installed · launching…';
|
|
515
|
+
$('#step-pwa').classList.add('is-done');
|
|
516
|
+
updateDoneBanner();
|
|
517
|
+
jumpToApp();
|
|
518
|
+
}
|
|
519
|
+
} finally {
|
|
520
|
+
window.__deferredPrompt = null;
|
|
521
|
+
installBtn.disabled = false;
|
|
522
|
+
refreshInstallBtn();
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// No native prompt — fall back to opening the router. Chrome
|
|
527
|
+
// detects an installed PWA at that origin and opens it as the
|
|
528
|
+
// standalone window automatically.
|
|
529
|
+
window.open('../', '_blank', 'noopener');
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// appinstalled fires AFTER the native dialog resolves "accepted".
|
|
533
|
+
// We've usually already started jumpToApp() from the prompt's then
|
|
534
|
+
// branch above, but this is the catch-all in case the user
|
|
535
|
+
// installed via the URL-bar icon (no deferredPrompt path).
|
|
536
|
+
window.addEventListener('appinstalled', () => {
|
|
537
|
+
pwaStatus.className = 'step-status is-ok';
|
|
538
|
+
pwaStatus.textContent = '✓ Installed · launching…';
|
|
539
|
+
$('#step-pwa').classList.add('is-done');
|
|
540
|
+
refreshInstallBtn();
|
|
541
|
+
updateDoneBanner();
|
|
542
|
+
jumpToApp();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ── Done banner when all steps are checked ──────────────────────
|
|
546
|
+
function updateDoneBanner() {
|
|
547
|
+
const steps = document.querySelectorAll('.step');
|
|
548
|
+
const allDone = Array.from(steps).every((s) => s.classList.contains('is-done'));
|
|
549
|
+
$('#done-banner').classList.toggle('show', allDone);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── Copy buttons ────────────────────────────────────────────────
|
|
553
|
+
document.querySelectorAll('.copy-btn').forEach((btn) => {
|
|
554
|
+
btn.addEventListener('click', () => {
|
|
555
|
+
const target = document.querySelector(btn.getAttribute('data-copy'));
|
|
556
|
+
if (!target) return;
|
|
557
|
+
navigator.clipboard.writeText(target.textContent).then(() => {
|
|
558
|
+
const orig = btn.textContent;
|
|
559
|
+
btn.textContent = 'Copied';
|
|
560
|
+
setTimeout(() => { btn.textContent = orig; }, 1200);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
})();
|
|
565
|
+
</script>
|
|
566
|
+
</body>
|
|
567
|
+
</html>
|