pinokiod 5.3.5 → 5.3.7

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": "pinokiod",
3
- "version": "5.3.5",
3
+ "version": "5.3.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -44,6 +44,10 @@ const DEFAULT_PORT = 42000
44
44
  const NOTIFICATION_SOUND_EXTENSIONS = new Set(['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.wav', '.webm'])
45
45
  const LOG_STREAM_INITIAL_BYTES = 512 * 1024
46
46
  const LOG_STREAM_KEEPALIVE_MS = 25000
47
+ const REGISTRY_PING_PNG = Buffer.from(
48
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMBAAObF+6bAAAAAElFTkSuQmCC',
49
+ 'base64'
50
+ )
47
51
 
48
52
  const ex = fn => (req, res, next) => {
49
53
  Promise.resolve(fn(req, res, next)).catch(next);
@@ -4836,6 +4840,88 @@ class Server {
4836
4840
  })
4837
4841
  }))
4838
4842
 
4843
+ this.app.get("/registry/ping.png", ex(async (_req, res) => {
4844
+ res.setHeader('Content-Type', 'image/png')
4845
+ res.setHeader('Cache-Control', 'no-store')
4846
+ res.setHeader('Content-Length', String(REGISTRY_PING_PNG.length))
4847
+ res.end(REGISTRY_PING_PNG)
4848
+ }))
4849
+
4850
+ this.app.get("/registry/checkin", ex(async (req, res) => {
4851
+ const repoUrl = typeof req.query.repo === 'string' ? req.query.repo.trim() : ''
4852
+ const returnRaw = typeof req.query.return === 'string' ? req.query.return.trim() : ''
4853
+ const successRaw = typeof req.query.success === 'string' ? req.query.success.trim() : ''
4854
+ const appSlug = typeof req.query.app === 'string' ? req.query.app.trim() : ''
4855
+ const registryEnabled = await this.isRegistryBetaEnabled().catch(() => false)
4856
+
4857
+ let returnUrl = null
4858
+ if (returnRaw) {
4859
+ try {
4860
+ const u = new URL(returnRaw)
4861
+ if (u.protocol === 'http:' || u.protocol === 'https:') returnUrl = u.toString()
4862
+ } catch (_) {}
4863
+ }
4864
+ let successUrl = null
4865
+ if (successRaw) {
4866
+ try {
4867
+ const u = new URL(successRaw)
4868
+ if (u.protocol === 'http:' || u.protocol === 'https:') successUrl = u.toString()
4869
+ } catch (_) {}
4870
+ }
4871
+
4872
+ let candidates = []
4873
+ if (repoUrl && this.kernel && this.kernel.git) {
4874
+ const apiRoot = this.kernel.path('api')
4875
+ const normalizeKey = (value) => {
4876
+ const key = this.kernel.git.normalizeRemote(value)
4877
+ return key ? String(key).toLowerCase() : ''
4878
+ }
4879
+ const targetKey = normalizeKey(repoUrl)
4880
+ const seen = new Set()
4881
+ if (targetKey) {
4882
+ let entries = []
4883
+ try {
4884
+ entries = await fs.promises.readdir(apiRoot, { withFileTypes: true })
4885
+ } catch (_) {
4886
+ entries = []
4887
+ }
4888
+ for (const entry of entries || []) {
4889
+ if (!entry || !entry.isDirectory()) continue
4890
+ const folder = entry.name
4891
+ if (!folder || folder.includes('/') || folder.includes('\\\\')) continue
4892
+ const workspace = path.join(apiRoot, folder)
4893
+ let remote = null
4894
+ try {
4895
+ remote = await git.getConfig({
4896
+ fs,
4897
+ http,
4898
+ dir: workspace,
4899
+ path: 'remote.origin.url'
4900
+ })
4901
+ } catch (_) {}
4902
+ if (!remote) continue
4903
+ const key = normalizeKey(remote)
4904
+ if (!key || key !== targetKey) continue
4905
+ if (seen.has(folder)) continue
4906
+ seen.add(folder)
4907
+ candidates.push({ folder })
4908
+ }
4909
+ }
4910
+ }
4911
+
4912
+ const data = {
4913
+ repoUrl,
4914
+ appSlug,
4915
+ returnUrl,
4916
+ successUrl,
4917
+ registryEnabled: !!registryEnabled,
4918
+ candidates,
4919
+ }
4920
+ const dataJson = JSON.stringify(data).replace(/</g, '\\u003c')
4921
+
4922
+ res.render("registry_checkin", { returnUrl, dataJson })
4923
+ }))
4924
+
4839
4925
  // Registry linking endpoint
4840
4926
  this.app.get("/registry/link", ex(async (req, res) => {
4841
4927
  const { token, registry, app: appOrigin, next: nextRaw } = req.query
@@ -0,0 +1,272 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <style>
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ min-height: 100vh;
13
+ margin: 0;
14
+ background: #f5f5f5;
15
+ color: #0f172a;
16
+ }
17
+ .card {
18
+ background: white;
19
+ padding: 32px;
20
+ border-radius: 12px;
21
+ box-shadow: 0 2px 10px rgba(15, 23, 42, 0.08);
22
+ max-width: 520px;
23
+ width: calc(100% - 32px);
24
+ }
25
+ h1 {
26
+ margin: 0 0 8px;
27
+ font-size: 22px;
28
+ }
29
+ .meta {
30
+ font-size: 13px;
31
+ color: #64748b;
32
+ margin-bottom: 16px;
33
+ word-break: break-all;
34
+ }
35
+ .status {
36
+ padding: 10px 12px;
37
+ border-radius: 8px;
38
+ background: #eef2ff;
39
+ color: #1e3a8a;
40
+ font-size: 14px;
41
+ margin-bottom: 16px;
42
+ }
43
+ .status.error {
44
+ background: #fee2e2;
45
+ color: #991b1b;
46
+ }
47
+ .status.ok {
48
+ background: #dcfce7;
49
+ color: #166534;
50
+ }
51
+ .picker {
52
+ display: none;
53
+ margin-bottom: 16px;
54
+ }
55
+ .picker.visible {
56
+ display: block;
57
+ }
58
+ .picker-list {
59
+ display: grid;
60
+ gap: 8px;
61
+ }
62
+ .picker-item {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 8px;
66
+ padding: 10px 12px;
67
+ border: 1px solid #e2e8f0;
68
+ border-radius: 8px;
69
+ background: #f8fafc;
70
+ cursor: pointer;
71
+ }
72
+ .picker-item input {
73
+ margin: 0;
74
+ }
75
+ .actions {
76
+ display: flex;
77
+ gap: 10px;
78
+ flex-wrap: wrap;
79
+ }
80
+ .btn {
81
+ border: none;
82
+ border-radius: 8px;
83
+ padding: 10px 16px;
84
+ font-size: 14px;
85
+ cursor: pointer;
86
+ }
87
+ .btn-primary {
88
+ background: #2563eb;
89
+ color: white;
90
+ }
91
+ .btn-secondary {
92
+ background: #e2e8f0;
93
+ color: #0f172a;
94
+ }
95
+ .link {
96
+ display: inline-block;
97
+ margin-top: 12px;
98
+ color: #2563eb;
99
+ text-decoration: none;
100
+ font-size: 14px;
101
+ }
102
+ .link:hover {
103
+ text-decoration: underline;
104
+ }
105
+ </style>
106
+ </head>
107
+ <body>
108
+ <div class="card">
109
+ <h1>Check-in to Registry</h1>
110
+ <div class="meta" id="repo">Loading...</div>
111
+ <div class="status" id="status">Preparing check-in...</div>
112
+
113
+ <div class="picker" id="picker">
114
+ <div style="font-weight: 600; margin-bottom: 8px;">Choose an install</div>
115
+ <div class="picker-list" id="picker-list"></div>
116
+ </div>
117
+
118
+ <div class="actions" id="actions">
119
+ <button class="btn btn-primary" id="picker-submit" type="button">Check in</button>
120
+ <button class="btn btn-secondary" id="picker-cancel" type="button">Cancel</button>
121
+ </div>
122
+
123
+ <% if (returnUrl) { %>
124
+ <a class="link" id="return-link" href="<%= returnUrl %>">Return to registry</a>
125
+ <% } else { %>
126
+ <a class="link" href="/">Go back to Pinokio</a>
127
+ <% } %>
128
+ </div>
129
+
130
+ <script>
131
+ const data = <%- dataJson %>;
132
+ const repoEl = document.getElementById('repo');
133
+ const statusEl = document.getElementById('status');
134
+ const pickerEl = document.getElementById('picker');
135
+ const listEl = document.getElementById('picker-list');
136
+ const actionsEl = document.getElementById('actions');
137
+ const submitBtn = document.getElementById('picker-submit');
138
+ const cancelBtn = document.getElementById('picker-cancel');
139
+ if (actionsEl) actionsEl.style.display = 'none';
140
+
141
+ const setStatus = (text, kind) => {
142
+ statusEl.textContent = text;
143
+ statusEl.className = 'status' + (kind ? ' ' + kind : '');
144
+ };
145
+
146
+ const redirectToReturn = (params) => {
147
+ if (!data.returnUrl) {
148
+ setStatus('Missing return URL. Please go back manually.', 'error');
149
+ return;
150
+ }
151
+ try {
152
+ const u = new URL(data.returnUrl);
153
+ Object.entries(params || {}).forEach(([key, value]) => {
154
+ if (value == null || value === '') return;
155
+ u.searchParams.set(key, String(value));
156
+ });
157
+ window.location.href = u.toString();
158
+ } catch (err) {
159
+ setStatus('Invalid return URL. Please go back manually.', 'error');
160
+ }
161
+ };
162
+
163
+ const buildSuccessUrl = (hash) => {
164
+ if (!data.successUrl || !hash) return null;
165
+ const replaced = data.successUrl.replace(/__HASH__/g, encodeURIComponent(hash));
166
+ try {
167
+ const u = new URL(replaced);
168
+ if (u.protocol === 'http:' || u.protocol === 'https:') return u.toString();
169
+ } catch (err) {}
170
+ return null;
171
+ };
172
+
173
+ const candidates = Array.isArray(data.candidates) ? data.candidates : [];
174
+ const repoLabel = data.repoUrl ? `Repo: ${data.repoUrl}` : 'Missing repo URL';
175
+ if (repoEl) repoEl.textContent = repoLabel;
176
+
177
+ if (!data.repoUrl) {
178
+ setStatus('Missing repo URL. Return to the registry and try again.', 'error');
179
+ } else if (!data.returnUrl) {
180
+ setStatus('Missing return URL. Return to the registry and try again.', 'error');
181
+ } else if (!data.registryEnabled) {
182
+ redirectToReturn({ error: 'not_enabled' });
183
+ } else if (!candidates.length) {
184
+ redirectToReturn({ error: 'not_installed' });
185
+ } else if (candidates.length === 1) {
186
+ const entry = candidates[0];
187
+ runCheckin(entry.folder);
188
+ } else {
189
+ setStatus('Multiple installs found. Choose one to check in.', '');
190
+ pickerEl.classList.add('visible');
191
+ if (actionsEl) actionsEl.style.display = 'flex';
192
+ candidates.forEach((entry, idx) => {
193
+ const label = document.createElement('label');
194
+ label.className = 'picker-item';
195
+ const input = document.createElement('input');
196
+ input.type = 'radio';
197
+ input.name = 'workspace';
198
+ input.value = entry.folder;
199
+ if (idx === 0) input.checked = true;
200
+ const span = document.createElement('span');
201
+ span.textContent = entry.folder;
202
+ label.appendChild(input);
203
+ label.appendChild(span);
204
+ listEl.appendChild(label);
205
+ });
206
+ }
207
+
208
+ if (cancelBtn) {
209
+ cancelBtn.addEventListener('click', () => {
210
+ redirectToReturn({ error: 'cancelled' });
211
+ });
212
+ }
213
+
214
+ if (submitBtn) {
215
+ submitBtn.addEventListener('click', () => {
216
+ const selected = document.querySelector('input[name="workspace"]:checked');
217
+ const folder = selected && selected.value ? selected.value : '';
218
+ if (!folder) {
219
+ setStatus('Select a folder to continue.', 'error');
220
+ return;
221
+ }
222
+ runCheckin(folder);
223
+ });
224
+ }
225
+
226
+ async function runCheckin(folder) {
227
+ if (!folder) {
228
+ setStatus('Missing folder selection.', 'error');
229
+ return;
230
+ }
231
+ setStatus('Creating checkpoint...', '');
232
+ if (submitBtn) submitBtn.disabled = true;
233
+ if (cancelBtn) cancelBtn.disabled = true;
234
+ try {
235
+ const qs = new URLSearchParams({ workspace: folder, publish: '1' });
236
+ const res = await fetch(`/checkpoints/snapshot?${qs.toString()}`, {
237
+ method: 'POST',
238
+ headers: { 'Accept': 'application/json' }
239
+ });
240
+ const payload = res.ok ? await res.json().catch(() => null) : null;
241
+ if (!payload || !payload.ok) {
242
+ redirectToReturn({ error: 'snapshot_failed' });
243
+ return;
244
+ }
245
+ const publish = payload.publish || null;
246
+ if (publish && publish.ok) {
247
+ const hash = publish.hash || (payload.created && payload.created.hash) || '';
248
+ const successUrl = buildSuccessUrl(hash);
249
+ if (successUrl) {
250
+ window.location.href = successUrl;
251
+ return;
252
+ }
253
+ redirectToReturn({ ok: '1', hash });
254
+ return;
255
+ }
256
+ if (publish && publish.code === 'not_linked') {
257
+ redirectToReturn({ error: 'not_linked', connectUrl: publish.connectUrl || '' });
258
+ return;
259
+ }
260
+ if (publish && publish.code === 'error') {
261
+ redirectToReturn({ error: 'publish_failed', message: publish.error || '' });
262
+ return;
263
+ }
264
+ redirectToReturn({ error: 'not_enabled' });
265
+ } catch (err) {
266
+ const msg = err && err.message ? err.message : 'Publish failed';
267
+ redirectToReturn({ error: 'publish_failed', message: msg });
268
+ }
269
+ }
270
+ </script>
271
+ </body>
272
+ </html>