@cccarv82/freya 1.0.6 → 1.0.8
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/cli/web.js +530 -227
- package/package.json +1 -1
package/cli/web.js
CHANGED
|
@@ -9,6 +9,15 @@ function guessNpmCmd() {
|
|
|
9
9
|
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function guessNpxCmd() {
|
|
13
|
+
return process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function guessNpxYesFlag() {
|
|
17
|
+
// npx supports --yes/-y on modern npm; use -y for broad compatibility
|
|
18
|
+
return '-y';
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
function guessOpenCmd() {
|
|
13
22
|
// Minimal cross-platform opener without extra deps
|
|
14
23
|
if (process.platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', ''] };
|
|
@@ -58,6 +67,26 @@ function safeJson(res, code, obj) {
|
|
|
58
67
|
res.end(body);
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
function looksLikeFreyaWorkspace(dir) {
|
|
71
|
+
// minimal check: has scripts/validate-data.js and data/
|
|
72
|
+
return (
|
|
73
|
+
exists(path.join(dir, 'package.json')) &&
|
|
74
|
+
exists(path.join(dir, 'scripts')) &&
|
|
75
|
+
exists(path.join(dir, 'data'))
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeWorkspaceDir(inputDir) {
|
|
80
|
+
const d = path.resolve(process.cwd(), inputDir);
|
|
81
|
+
if (looksLikeFreyaWorkspace(d)) return d;
|
|
82
|
+
|
|
83
|
+
// Common case: user picked parent folder that contains ./freya
|
|
84
|
+
const child = path.join(d, 'freya');
|
|
85
|
+
if (looksLikeFreyaWorkspace(child)) return child;
|
|
86
|
+
|
|
87
|
+
return d;
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
function readBody(req) {
|
|
62
91
|
return new Promise((resolve, reject) => {
|
|
63
92
|
const chunks = [];
|
|
@@ -69,15 +98,29 @@ function readBody(req) {
|
|
|
69
98
|
|
|
70
99
|
function run(cmd, args, cwd) {
|
|
71
100
|
return new Promise((resolve) => {
|
|
72
|
-
|
|
101
|
+
let child;
|
|
102
|
+
try {
|
|
103
|
+
child = spawn(cmd, args, { cwd, shell: false, env: process.env });
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
let stdout = '';
|
|
74
109
|
let stderr = '';
|
|
75
|
-
|
|
110
|
+
|
|
111
|
+
child.stdout && child.stdout.on('data', (d) => {
|
|
76
112
|
stdout += d.toString();
|
|
77
113
|
});
|
|
78
|
-
child.stderr.on('data', (d) => {
|
|
114
|
+
child.stderr && child.stderr.on('data', (d) => {
|
|
79
115
|
stderr += d.toString();
|
|
80
116
|
});
|
|
117
|
+
|
|
118
|
+
// Prevent unhandled error event (e.g., ENOENT on Windows when cmd not found)
|
|
119
|
+
child.on('error', (e) => {
|
|
120
|
+
stderr += `\n${e.message || String(e)}`;
|
|
121
|
+
resolve({ code: 1, stdout, stderr });
|
|
122
|
+
});
|
|
123
|
+
|
|
81
124
|
child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
82
125
|
});
|
|
83
126
|
}
|
|
@@ -134,7 +177,7 @@ async function pickDirectoryNative() {
|
|
|
134
177
|
}
|
|
135
178
|
|
|
136
179
|
function html() {
|
|
137
|
-
// Aesthetic: “
|
|
180
|
+
// Aesthetic: “clean workstation” — light-first UI inspired by modern productivity apps.
|
|
138
181
|
return `<!doctype html>
|
|
139
182
|
<html>
|
|
140
183
|
<head>
|
|
@@ -142,336 +185,551 @@ function html() {
|
|
|
142
185
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
143
186
|
<title>FREYA Web</title>
|
|
144
187
|
<style>
|
|
188
|
+
/*
|
|
189
|
+
Design goals:
|
|
190
|
+
- Light theme by default (inspired by your reference screenshots)
|
|
191
|
+
- Dark mode toggle
|
|
192
|
+
- App-like layout: sidebar + main surface
|
|
193
|
+
- Clear onboarding and affordances
|
|
194
|
+
*/
|
|
195
|
+
|
|
145
196
|
:root {
|
|
146
|
-
--
|
|
147
|
-
--
|
|
148
|
-
--
|
|
149
|
-
--
|
|
150
|
-
|
|
197
|
+
--radius: 14px;
|
|
198
|
+
--shadow: 0 18px 55px rgba(16, 24, 40, .10);
|
|
199
|
+
--shadow2: 0 10px 20px rgba(16, 24, 40, .08);
|
|
200
|
+
--ring: 0 0 0 4px rgba(59, 130, 246, .18);
|
|
201
|
+
|
|
202
|
+
/* Light */
|
|
203
|
+
--bg: #f6f7fb;
|
|
204
|
+
--paper: #ffffff;
|
|
205
|
+
--paper2: #fbfbfd;
|
|
206
|
+
--line: rgba(16, 24, 40, .10);
|
|
207
|
+
--line2: rgba(16, 24, 40, .14);
|
|
208
|
+
--text: #0f172a;
|
|
209
|
+
--muted: rgba(15, 23, 42, .68);
|
|
210
|
+
--faint: rgba(15, 23, 42, .50);
|
|
211
|
+
|
|
212
|
+
--primary: #2563eb;
|
|
213
|
+
--primary2: #0ea5e9;
|
|
214
|
+
--accent: #f97316;
|
|
215
|
+
--ok: #16a34a;
|
|
216
|
+
--warn: #f59e0b;
|
|
217
|
+
--danger: #e11d48;
|
|
218
|
+
|
|
219
|
+
--chip: rgba(37, 99, 235, .08);
|
|
220
|
+
--chip2: rgba(249, 115, 22, .10);
|
|
221
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
222
|
+
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
[data-theme="dark"] {
|
|
226
|
+
--bg: #0b1020;
|
|
227
|
+
--paper: rgba(255,255,255,.06);
|
|
228
|
+
--paper2: rgba(255,255,255,.04);
|
|
229
|
+
--line: rgba(255,255,255,.12);
|
|
230
|
+
--line2: rgba(255,255,255,.18);
|
|
151
231
|
--text: #e9f0ff;
|
|
152
232
|
--muted: rgba(233,240,255,.72);
|
|
153
|
-
--faint: rgba(233,240,255,.
|
|
154
|
-
|
|
155
|
-
--
|
|
156
|
-
--
|
|
157
|
-
--
|
|
158
|
-
--
|
|
233
|
+
--faint: rgba(233,240,255,.54);
|
|
234
|
+
|
|
235
|
+
--primary: #60a5fa;
|
|
236
|
+
--primary2: #22c55e;
|
|
237
|
+
--accent: #fb923c;
|
|
238
|
+
--chip: rgba(96, 165, 250, .14);
|
|
239
|
+
--chip2: rgba(251, 146, 60, .14);
|
|
240
|
+
|
|
159
241
|
--shadow: 0 30px 70px rgba(0,0,0,.55);
|
|
160
|
-
--
|
|
242
|
+
--shadow2: 0 18px 40px rgba(0,0,0,.35);
|
|
243
|
+
--ring: 0 0 0 4px rgba(96, 165, 250, .22);
|
|
161
244
|
}
|
|
162
245
|
|
|
163
246
|
* { box-sizing: border-box; }
|
|
164
247
|
html, body { height: 100%; }
|
|
165
248
|
body {
|
|
166
249
|
margin: 0;
|
|
167
|
-
color: var(--text);
|
|
168
250
|
background:
|
|
169
|
-
radial-gradient(
|
|
170
|
-
radial-gradient(
|
|
171
|
-
radial-gradient(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
251
|
+
radial-gradient(1200px 800px at 20% -10%, rgba(37,99,235,.12), transparent 55%),
|
|
252
|
+
radial-gradient(900px 600px at 92% 10%, rgba(249,115,22,.12), transparent 55%),
|
|
253
|
+
radial-gradient(1100px 700px at 70% 105%, rgba(14,165,233,.10), transparent 55%),
|
|
254
|
+
var(--bg);
|
|
255
|
+
color: var(--text);
|
|
256
|
+
font-family: var(--sans);
|
|
175
257
|
}
|
|
176
258
|
|
|
177
|
-
/* subtle
|
|
259
|
+
/* subtle grain */
|
|
178
260
|
body:before {
|
|
179
261
|
content: "";
|
|
180
262
|
position: fixed;
|
|
181
263
|
inset: 0;
|
|
182
264
|
pointer-events: none;
|
|
183
265
|
background-image:
|
|
184
|
-
|
|
185
|
-
radial-gradient(circle at
|
|
186
|
-
|
|
187
|
-
background-size:
|
|
266
|
+
radial-gradient(circle at 15% 20%, rgba(255,255,255,.38), transparent 32%),
|
|
267
|
+
radial-gradient(circle at 80% 10%, rgba(255,255,255,.26), transparent 38%),
|
|
268
|
+
linear-gradient(transparent 0, transparent 3px, rgba(0,0,0,.02) 4px);
|
|
269
|
+
background-size: 900px 900px, 900px 900px, 100% 7px;
|
|
270
|
+
opacity: .08;
|
|
188
271
|
mix-blend-mode: overlay;
|
|
189
|
-
opacity: .12;
|
|
190
272
|
}
|
|
191
273
|
|
|
192
|
-
|
|
274
|
+
.app {
|
|
275
|
+
max-width: 1260px;
|
|
276
|
+
margin: 18px auto;
|
|
277
|
+
padding: 0 18px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.frame {
|
|
281
|
+
display: grid;
|
|
282
|
+
grid-template-columns: 280px 1fr;
|
|
283
|
+
gap: 14px;
|
|
284
|
+
min-height: calc(100vh - 36px);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@media (max-width: 980px) {
|
|
288
|
+
.frame { grid-template-columns: 1fr; }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.sidebar {
|
|
292
|
+
background: var(--paper);
|
|
293
|
+
border: 1px solid var(--line);
|
|
294
|
+
border-radius: var(--radius);
|
|
295
|
+
box-shadow: var(--shadow2);
|
|
296
|
+
padding: 14px;
|
|
193
297
|
position: sticky;
|
|
194
|
-
top:
|
|
195
|
-
|
|
196
|
-
backdrop-filter: blur(14px);
|
|
197
|
-
background: rgba(7,10,16,.56);
|
|
198
|
-
border-bottom: 1px solid var(--line);
|
|
298
|
+
top: 18px;
|
|
299
|
+
height: fit-content;
|
|
199
300
|
}
|
|
200
301
|
|
|
201
|
-
.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
302
|
+
.main {
|
|
303
|
+
background: var(--paper);
|
|
304
|
+
border: 1px solid var(--line);
|
|
305
|
+
border-radius: var(--radius);
|
|
306
|
+
box-shadow: var(--shadow);
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.topbar {
|
|
205
311
|
display: flex;
|
|
206
312
|
align-items: center;
|
|
207
313
|
justify-content: space-between;
|
|
208
|
-
|
|
314
|
+
padding: 14px 16px;
|
|
315
|
+
border-bottom: 1px solid var(--line);
|
|
316
|
+
background: linear-gradient(180deg, var(--paper2), var(--paper));
|
|
209
317
|
}
|
|
210
318
|
|
|
211
319
|
.brand {
|
|
212
320
|
display: flex;
|
|
213
|
-
align-items:
|
|
214
|
-
gap:
|
|
215
|
-
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 10px;
|
|
323
|
+
font-weight: 800;
|
|
324
|
+
letter-spacing: .08em;
|
|
216
325
|
text-transform: uppercase;
|
|
217
|
-
font-weight: 700;
|
|
218
326
|
font-size: 12px;
|
|
219
327
|
color: var(--muted);
|
|
220
328
|
}
|
|
221
329
|
|
|
222
|
-
.
|
|
223
|
-
|
|
330
|
+
.spark {
|
|
331
|
+
width: 10px;
|
|
332
|
+
height: 10px;
|
|
333
|
+
border-radius: 4px;
|
|
334
|
+
background: linear-gradient(135deg, var(--accent), var(--primary));
|
|
335
|
+
box-shadow: 0 0 0 6px rgba(249,115,22,.12);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.actions {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: 10px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.chip {
|
|
345
|
+
font-family: var(--mono);
|
|
224
346
|
font-size: 12px;
|
|
225
|
-
padding:
|
|
347
|
+
padding: 7px 10px;
|
|
226
348
|
border-radius: 999px;
|
|
227
349
|
border: 1px solid var(--line);
|
|
228
|
-
background: rgba(255,255,255,.
|
|
350
|
+
background: rgba(255,255,255,.55);
|
|
229
351
|
color: var(--faint);
|
|
230
352
|
}
|
|
231
353
|
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
354
|
+
[data-theme="dark"] .chip { background: rgba(0,0,0,.20); }
|
|
355
|
+
|
|
356
|
+
.toggle {
|
|
357
|
+
border: 1px solid var(--line);
|
|
358
|
+
border-radius: 999px;
|
|
359
|
+
background: var(--paper2);
|
|
360
|
+
padding: 7px 10px;
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
color: var(--muted);
|
|
363
|
+
font-weight: 700;
|
|
364
|
+
font-size: 12px;
|
|
236
365
|
}
|
|
237
366
|
|
|
238
|
-
.
|
|
239
|
-
|
|
240
|
-
grid-template-columns: 1.2fr .8fr;
|
|
241
|
-
gap: 16px;
|
|
242
|
-
align-items: start;
|
|
243
|
-
margin-bottom: 16px;
|
|
367
|
+
.section {
|
|
368
|
+
padding: 16px;
|
|
244
369
|
}
|
|
245
370
|
|
|
246
|
-
|
|
247
|
-
|
|
371
|
+
h1 {
|
|
372
|
+
margin: 0;
|
|
373
|
+
font-size: 22px;
|
|
374
|
+
letter-spacing: -.02em;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.subtitle {
|
|
378
|
+
margin-top: 6px;
|
|
379
|
+
color: var(--muted);
|
|
380
|
+
font-size: 13px;
|
|
381
|
+
line-height: 1.4;
|
|
382
|
+
max-width: 860px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.cards {
|
|
386
|
+
display: grid;
|
|
387
|
+
grid-template-columns: repeat(4, 1fr);
|
|
388
|
+
gap: 12px;
|
|
389
|
+
margin-top: 14px;
|
|
248
390
|
}
|
|
249
391
|
|
|
392
|
+
@media (max-width: 1100px) { .cards { grid-template-columns: repeat(2, 1fr);} }
|
|
393
|
+
@media (max-width: 620px) { .cards { grid-template-columns: 1fr;} }
|
|
394
|
+
|
|
250
395
|
.card {
|
|
251
396
|
border: 1px solid var(--line);
|
|
252
|
-
background:
|
|
253
|
-
border-radius:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
397
|
+
background: var(--paper2);
|
|
398
|
+
border-radius: 14px;
|
|
399
|
+
padding: 12px;
|
|
400
|
+
display: grid;
|
|
401
|
+
gap: 6px;
|
|
402
|
+
cursor: pointer;
|
|
403
|
+
transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
|
|
404
|
+
box-shadow: 0 1px 0 rgba(16,24,40,.04);
|
|
258
405
|
}
|
|
259
406
|
|
|
260
|
-
.card:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
407
|
+
.card:hover {
|
|
408
|
+
transform: translateY(-1px);
|
|
409
|
+
border-color: var(--line2);
|
|
410
|
+
box-shadow: 0 10px 22px rgba(16,24,40,.10);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.icon {
|
|
414
|
+
width: 34px;
|
|
415
|
+
height: 34px;
|
|
416
|
+
border-radius: 12px;
|
|
417
|
+
display: grid;
|
|
418
|
+
place-items: center;
|
|
419
|
+
background: var(--chip);
|
|
420
|
+
border: 1px solid var(--line);
|
|
421
|
+
color: var(--primary);
|
|
422
|
+
font-weight: 900;
|
|
269
423
|
}
|
|
270
424
|
|
|
271
|
-
.
|
|
425
|
+
.icon.orange { background: var(--chip2); color: var(--accent); }
|
|
272
426
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
font-size:
|
|
276
|
-
letter-spacing: .08em;
|
|
277
|
-
text-transform: uppercase;
|
|
278
|
-
color: var(--muted);
|
|
427
|
+
.title {
|
|
428
|
+
font-weight: 800;
|
|
429
|
+
font-size: 13px;
|
|
279
430
|
}
|
|
280
431
|
|
|
281
|
-
.
|
|
282
|
-
|
|
432
|
+
.desc {
|
|
433
|
+
color: var(--muted);
|
|
283
434
|
font-size: 12px;
|
|
284
|
-
color: var(--faint);
|
|
285
435
|
line-height: 1.35;
|
|
286
436
|
}
|
|
287
437
|
|
|
288
|
-
|
|
289
|
-
display:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
margin-
|
|
438
|
+
.grid2 {
|
|
439
|
+
display: grid;
|
|
440
|
+
grid-template-columns: 1fr 1fr;
|
|
441
|
+
gap: 14px;
|
|
442
|
+
margin-top: 14px;
|
|
293
443
|
}
|
|
294
444
|
|
|
295
|
-
.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
445
|
+
@media (max-width: 980px) { .grid2 { grid-template-columns: 1fr; } }
|
|
446
|
+
|
|
447
|
+
.panel {
|
|
448
|
+
border: 1px solid var(--line);
|
|
449
|
+
background: var(--paper);
|
|
450
|
+
border-radius: 14px;
|
|
451
|
+
overflow: hidden;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.panelHead {
|
|
455
|
+
display: flex;
|
|
299
456
|
align-items: center;
|
|
457
|
+
justify-content: space-between;
|
|
458
|
+
padding: 12px 12px;
|
|
459
|
+
border-bottom: 1px solid var(--line);
|
|
460
|
+
background: linear-gradient(180deg, var(--paper2), var(--paper));
|
|
300
461
|
}
|
|
301
462
|
|
|
463
|
+
.panelHead b { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
464
|
+
|
|
465
|
+
.panelBody { padding: 12px; }
|
|
466
|
+
|
|
467
|
+
label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
|
468
|
+
|
|
302
469
|
input {
|
|
303
470
|
width: 100%;
|
|
304
|
-
padding:
|
|
471
|
+
padding: 11px 12px;
|
|
305
472
|
border-radius: 12px;
|
|
306
|
-
border: 1px solid
|
|
307
|
-
background: rgba(
|
|
473
|
+
border: 1px solid var(--line);
|
|
474
|
+
background: rgba(255,255,255,.72);
|
|
308
475
|
color: var(--text);
|
|
309
476
|
outline: none;
|
|
310
477
|
}
|
|
311
478
|
|
|
312
|
-
input
|
|
479
|
+
[data-theme="dark"] input { background: rgba(0,0,0,.16); }
|
|
313
480
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
481
|
+
input:focus { box-shadow: var(--ring); border-color: rgba(37,99,235,.35); }
|
|
482
|
+
|
|
483
|
+
.row {
|
|
484
|
+
display: grid;
|
|
485
|
+
grid-template-columns: 1fr auto;
|
|
317
486
|
gap: 10px;
|
|
318
|
-
|
|
487
|
+
align-items: center;
|
|
319
488
|
}
|
|
320
489
|
|
|
321
|
-
|
|
322
|
-
border: 1px solid
|
|
490
|
+
.btn {
|
|
491
|
+
border: 1px solid var(--line);
|
|
323
492
|
border-radius: 12px;
|
|
324
|
-
background:
|
|
493
|
+
background: var(--paper2);
|
|
325
494
|
color: var(--text);
|
|
326
495
|
padding: 10px 12px;
|
|
327
496
|
cursor: pointer;
|
|
328
|
-
|
|
329
|
-
font-
|
|
330
|
-
|
|
497
|
+
font-weight: 800;
|
|
498
|
+
font-size: 12px;
|
|
499
|
+
transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
|
|
331
500
|
}
|
|
332
501
|
|
|
333
|
-
|
|
334
|
-
|
|
502
|
+
.btn:hover { transform: translateY(-1px); border-color: var(--line2); box-shadow: 0 10px 22px rgba(16,24,40,.10); }
|
|
503
|
+
.btn:active { transform: translateY(0); }
|
|
335
504
|
|
|
336
|
-
.primary {
|
|
337
|
-
background: linear-gradient(135deg, rgba(
|
|
338
|
-
border-color: rgba(
|
|
505
|
+
.btn.primary {
|
|
506
|
+
background: linear-gradient(135deg, rgba(37,99,235,.14), rgba(14,165,233,.12));
|
|
507
|
+
border-color: rgba(37,99,235,.22);
|
|
508
|
+
color: var(--text);
|
|
339
509
|
}
|
|
340
510
|
|
|
341
|
-
.
|
|
342
|
-
|
|
343
|
-
|
|
511
|
+
.btn.orange {
|
|
512
|
+
background: linear-gradient(135deg, rgba(249,115,22,.16), rgba(37,99,235,.08));
|
|
513
|
+
border-color: rgba(249,115,22,.24);
|
|
514
|
+
}
|
|
344
515
|
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
padding: 8px 10px;
|
|
350
|
-
border-radius: 999px;
|
|
351
|
-
border: 1px solid rgba(180,210,255,.18);
|
|
352
|
-
background: rgba(0,0,0,.18);
|
|
353
|
-
font-size: 12px;
|
|
354
|
-
color: var(--faint);
|
|
516
|
+
.btn.danger {
|
|
517
|
+
background: rgba(225,29,72,.10);
|
|
518
|
+
border-color: rgba(225,29,72,.28);
|
|
519
|
+
color: var(--text);
|
|
355
520
|
}
|
|
356
521
|
|
|
357
|
-
.
|
|
358
|
-
.dot.ok { background: var(--ok); box-shadow: 0 0 0 5px rgba(52,211,153,.12); }
|
|
359
|
-
.dot.err { background: var(--danger); box-shadow: 0 0 0 5px rgba(251,113,133,.12); }
|
|
522
|
+
.btn.small { padding: 9px 10px; font-weight: 800; }
|
|
360
523
|
|
|
361
|
-
.
|
|
362
|
-
display: grid;
|
|
363
|
-
grid-template-columns: 1fr 1fr;
|
|
364
|
-
gap: 12px;
|
|
365
|
-
}
|
|
366
|
-
@media (max-width: 980px) { .two { grid-template-columns: 1fr; } }
|
|
524
|
+
.stack { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
367
525
|
|
|
368
|
-
.
|
|
526
|
+
.help {
|
|
527
|
+
margin-top: 8px;
|
|
528
|
+
color: var(--faint);
|
|
529
|
+
font-size: 12px;
|
|
530
|
+
line-height: 1.35;
|
|
531
|
+
}
|
|
369
532
|
|
|
370
533
|
.log {
|
|
534
|
+
border: 1px solid var(--line);
|
|
535
|
+
background: rgba(255,255,255,.65);
|
|
371
536
|
border-radius: 14px;
|
|
372
|
-
border: 1px solid rgba(180,210,255,.18);
|
|
373
|
-
background: rgba(7,10,16,.55);
|
|
374
537
|
padding: 12px;
|
|
375
|
-
|
|
376
|
-
max-height: 420px;
|
|
377
|
-
overflow: auto;
|
|
378
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
538
|
+
font-family: var(--mono);
|
|
379
539
|
font-size: 12px;
|
|
380
540
|
line-height: 1.35;
|
|
381
541
|
white-space: pre-wrap;
|
|
382
|
-
|
|
542
|
+
max-height: 420px;
|
|
543
|
+
overflow: auto;
|
|
544
|
+
color: rgba(15,23,42,.92);
|
|
383
545
|
}
|
|
384
546
|
|
|
385
|
-
.
|
|
547
|
+
[data-theme="dark"] .log { background: rgba(0,0,0,.20); color: rgba(233,240,255,.84); }
|
|
548
|
+
|
|
549
|
+
.statusRow { display:flex; align-items:center; justify-content: space-between; gap: 10px; }
|
|
550
|
+
|
|
551
|
+
.pill {
|
|
552
|
+
display: inline-flex;
|
|
553
|
+
align-items: center;
|
|
554
|
+
gap: 8px;
|
|
555
|
+
padding: 7px 10px;
|
|
556
|
+
border-radius: 999px;
|
|
557
|
+
border: 1px solid var(--line);
|
|
558
|
+
background: rgba(255,255,255,.55);
|
|
559
|
+
font-size: 12px;
|
|
560
|
+
color: var(--muted);
|
|
561
|
+
font-family: var(--mono);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
[data-theme="dark"] .pill { background: rgba(0,0,0,.18); }
|
|
565
|
+
|
|
566
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 5px rgba(245,158,11,.15); }
|
|
567
|
+
.dot.ok { background: var(--ok); box-shadow: 0 0 0 5px rgba(22,163,74,.12); }
|
|
568
|
+
.dot.err { background: var(--danger); box-shadow: 0 0 0 5px rgba(225,29,72,.14); }
|
|
569
|
+
|
|
570
|
+
.small {
|
|
386
571
|
font-size: 12px;
|
|
387
572
|
color: var(--faint);
|
|
388
|
-
|
|
389
|
-
line-height: 1.35;
|
|
573
|
+
font-family: var(--mono);
|
|
390
574
|
}
|
|
391
575
|
|
|
392
|
-
.
|
|
393
|
-
margin
|
|
576
|
+
.sidebar h3 {
|
|
577
|
+
margin: 0;
|
|
394
578
|
font-size: 12px;
|
|
395
|
-
|
|
579
|
+
letter-spacing: .10em;
|
|
580
|
+
text-transform: uppercase;
|
|
581
|
+
color: var(--muted);
|
|
396
582
|
}
|
|
397
583
|
|
|
398
|
-
|
|
399
|
-
|
|
584
|
+
.sideBlock { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line); }
|
|
585
|
+
|
|
586
|
+
.sidePath {
|
|
587
|
+
margin-top: 8px;
|
|
588
|
+
border: 1px solid var(--line);
|
|
589
|
+
background: var(--paper2);
|
|
590
|
+
border-radius: 12px;
|
|
591
|
+
padding: 10px;
|
|
592
|
+
font-family: var(--mono);
|
|
593
|
+
font-size: 12px;
|
|
594
|
+
color: var(--muted);
|
|
595
|
+
word-break: break-word;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.sideBtn { width: 100%; margin-top: 8px; }
|
|
599
|
+
|
|
600
|
+
.k { display: inline-block; padding: 2px 6px; border: 1px solid var(--line); border-radius: 8px; background: rgba(255,255,255,.65); font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
601
|
+
[data-theme="dark"] .k { background: rgba(0,0,0,.18); }
|
|
602
|
+
|
|
400
603
|
</style>
|
|
401
604
|
</head>
|
|
402
605
|
<body>
|
|
403
|
-
<
|
|
404
|
-
<div class="
|
|
405
|
-
<div class="brand">FREYA <span style="opacity:.55">•</span> web console</div>
|
|
406
|
-
<div class="badge" id="status">ready</div>
|
|
407
|
-
</div>
|
|
408
|
-
</header>
|
|
409
|
-
|
|
410
|
-
<div class="wrap">
|
|
411
|
-
<div class="hero">
|
|
412
|
-
<div class="card">
|
|
413
|
-
<h2>1) Workspace</h2>
|
|
414
|
-
<p class="sub">Escolha onde está (ou onde será criada) sua workspace da FREYA. Se você já tem uma workspace antiga, use <b>Update</b> — seus <b>data/logs</b> ficam preservados.</p>
|
|
415
|
-
|
|
416
|
-
<label>Workspace dir</label>
|
|
417
|
-
<div class="field">
|
|
418
|
-
<input id="dir" placeholder="./freya" />
|
|
419
|
-
<button class="ghost" onclick="pickDir()">Browse…</button>
|
|
420
|
-
</div>
|
|
421
|
-
<div class="hint">Dica: a workspace contém <code>data/</code>, <code>logs/</code> e <code>scripts/</code>.</div>
|
|
606
|
+
<div class="app">
|
|
607
|
+
<div class="frame">
|
|
422
608
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
<
|
|
426
|
-
<
|
|
427
|
-
<button onclick="doMigrate()">Migrate</button>
|
|
609
|
+
<aside class="sidebar">
|
|
610
|
+
<div style="display:flex; align-items:center; justify-content: space-between; gap:10px;">
|
|
611
|
+
<h3>FREYA</h3>
|
|
612
|
+
<span class="pill"><span class="dot" id="dot"></span><span id="pill">idle</span></span>
|
|
428
613
|
</div>
|
|
429
614
|
|
|
430
|
-
<div class="
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
<div style="height:10px"></div>
|
|
441
|
-
<label>Teams webhook URL</label>
|
|
442
|
-
<input id="teams" placeholder="https://..." />
|
|
615
|
+
<div class="sideBlock">
|
|
616
|
+
<h3>Workspace</h3>
|
|
617
|
+
<div class="sidePath" id="sidePath">./freya</div>
|
|
618
|
+
<button class="btn sideBtn" onclick="pickDir()">Select workspace…</button>
|
|
619
|
+
<button class="btn primary sideBtn" onclick="doInit()">Init workspace</button>
|
|
620
|
+
<button class="btn sideBtn" onclick="doUpdate()">Update (preserve data/logs)</button>
|
|
621
|
+
<button class="btn sideBtn" onclick="doHealth()">Health</button>
|
|
622
|
+
<button class="btn sideBtn" onclick="doMigrate()">Migrate</button>
|
|
623
|
+
<div class="help">Dica: se você já tem uma workspace antiga, use <b>Update</b>. Por padrão, <b>data/</b> e <b>logs/</b> não são sobrescritos.</div>
|
|
624
|
+
</div>
|
|
443
625
|
|
|
444
|
-
<div class="
|
|
445
|
-
|
|
446
|
-
<button onclick="publish('discord')">Publish
|
|
447
|
-
<button onclick="publish('teams')">Publish
|
|
626
|
+
<div class="sideBlock">
|
|
627
|
+
<h3>Publish</h3>
|
|
628
|
+
<button class="btn sideBtn" onclick="publish('discord')">Publish → Discord</button>
|
|
629
|
+
<button class="btn sideBtn" onclick="publish('teams')">Publish → Teams</button>
|
|
630
|
+
<div class="help">Configure os webhooks no painel principal. O publish envia o último relatório gerado.</div>
|
|
448
631
|
</div>
|
|
449
|
-
<div class="hint">O publish usa o texto do último relatório gerado. Para MVP, limitamos em ~1800 caracteres (evita limites de webhook). Depois a gente melhora para anexos/chunks.</div>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
632
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
633
|
+
<div class="sideBlock">
|
|
634
|
+
<h3>Atalhos</h3>
|
|
635
|
+
<div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
|
|
636
|
+
<div class="help"><span class="k">--port</span> muda a porta (default 3872).</div>
|
|
637
|
+
</div>
|
|
638
|
+
</aside>
|
|
639
|
+
|
|
640
|
+
<main class="main">
|
|
641
|
+
<div class="topbar">
|
|
642
|
+
<div class="brand"><span class="spark"></span> Local-first status assistant</div>
|
|
643
|
+
<div class="actions">
|
|
644
|
+
<span class="chip" id="chipPort">127.0.0.1:3872</span>
|
|
645
|
+
<button class="toggle" id="themeToggle" onclick="toggleTheme()">Theme</button>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
457
648
|
|
|
458
|
-
<div class="
|
|
459
|
-
<
|
|
460
|
-
<
|
|
461
|
-
|
|
462
|
-
<
|
|
649
|
+
<div class="section">
|
|
650
|
+
<h1>Morning, how can I help?</h1>
|
|
651
|
+
<div class="subtitle">Selecione uma workspace e gere relatórios (Executive / SM / Blockers / Daily). Você pode publicar no Discord/Teams com 1 clique.</div>
|
|
652
|
+
|
|
653
|
+
<div class="cards">
|
|
654
|
+
<div class="card" onclick="runReport('status')">
|
|
655
|
+
<div class="icon">E</div>
|
|
656
|
+
<div class="title">Executive report</div>
|
|
657
|
+
<div class="desc">Status pronto para stakeholders (entregas, projetos, blockers).</div>
|
|
658
|
+
</div>
|
|
659
|
+
<div class="card" onclick="runReport('sm-weekly')">
|
|
660
|
+
<div class="icon">S</div>
|
|
661
|
+
<div class="title">SM weekly</div>
|
|
662
|
+
<div class="desc">Resumo, wins, riscos e foco da próxima semana.</div>
|
|
663
|
+
</div>
|
|
664
|
+
<div class="card" onclick="runReport('blockers')">
|
|
665
|
+
<div class="icon orange">B</div>
|
|
666
|
+
<div class="title">Blockers</div>
|
|
667
|
+
<div class="desc">Lista priorizada por severidade + idade (pra destravar rápido).</div>
|
|
668
|
+
</div>
|
|
669
|
+
<div class="card" onclick="runReport('daily')">
|
|
670
|
+
<div class="icon">D</div>
|
|
671
|
+
<div class="title">Daily</div>
|
|
672
|
+
<div class="desc">Ontem / Hoje / Bloqueios — pronto pra standup.</div>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<div class="grid2">
|
|
677
|
+
<div class="panel">
|
|
678
|
+
<div class="panelHead"><b>Workspace & publish settings</b><span class="small" id="last"></span></div>
|
|
679
|
+
<div class="panelBody">
|
|
680
|
+
<label>Workspace dir</label>
|
|
681
|
+
<div class="row">
|
|
682
|
+
<input id="dir" placeholder="./freya" />
|
|
683
|
+
<button class="btn small" onclick="pickDir()">Browse</button>
|
|
684
|
+
</div>
|
|
685
|
+
<div class="help">Escolha a pasta que contém <code>data/</code>, <code>logs/</code> e <code>scripts/</code>.</div>
|
|
686
|
+
|
|
687
|
+
<div style="height:12px"></div>
|
|
688
|
+
|
|
689
|
+
<div class="stack">
|
|
690
|
+
<button class="btn primary" onclick="doInit()">Init</button>
|
|
691
|
+
<button class="btn" onclick="doUpdate()">Update</button>
|
|
692
|
+
<button class="btn" onclick="doHealth()">Health</button>
|
|
693
|
+
<button class="btn" onclick="doMigrate()">Migrate</button>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
<div style="height:16px"></div>
|
|
697
|
+
|
|
698
|
+
<label>Discord webhook URL</label>
|
|
699
|
+
<input id="discord" placeholder="https://discord.com/api/webhooks/..." />
|
|
700
|
+
<div style="height:10px"></div>
|
|
701
|
+
|
|
702
|
+
<label>Teams webhook URL</label>
|
|
703
|
+
<input id="teams" placeholder="https://..." />
|
|
704
|
+
<div class="help">O publish usa incoming webhooks. (Depois a gente evolui para anexos/chunks.)</div>
|
|
705
|
+
|
|
706
|
+
<div style="height:10px"></div>
|
|
707
|
+
<div class="stack">
|
|
708
|
+
<button class="btn" onclick="publish('discord')">Publish last → Discord</button>
|
|
709
|
+
<button class="btn" onclick="publish('teams')">Publish last → Teams</button>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<div class="panel">
|
|
715
|
+
<div class="panelHead">
|
|
716
|
+
<b>Output</b>
|
|
717
|
+
<div class="stack">
|
|
718
|
+
<button class="btn small" onclick="copyOut()">Copy</button>
|
|
719
|
+
<button class="btn small" onclick="clearOut()">Clear</button>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
<div class="panelBody">
|
|
723
|
+
<div class="log" id="out"></div>
|
|
724
|
+
<div class="help">Dica: quando um report gera arquivo, mostramos o conteúdo real do report aqui (melhor que stdout).</div>
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
</div>
|
|
463
729
|
</div>
|
|
464
730
|
|
|
465
|
-
|
|
466
|
-
</div>
|
|
731
|
+
</main>
|
|
467
732
|
|
|
468
|
-
<div class="card">
|
|
469
|
-
<h2>Output</h2>
|
|
470
|
-
<div class="pill"><span class="dot" id="dot"></span><span id="pill">idle</span></div>
|
|
471
|
-
<div style="height:10px"></div>
|
|
472
|
-
<div class="log" id="out"></div>
|
|
473
|
-
<div class="footer">Dica: se o report foi salvo em arquivo, ele aparece em “Last report”.</div>
|
|
474
|
-
</div>
|
|
475
733
|
</div>
|
|
476
734
|
</div>
|
|
477
735
|
|
|
@@ -479,12 +737,24 @@ function html() {
|
|
|
479
737
|
const $ = (id) => document.getElementById(id);
|
|
480
738
|
const state = { lastReportPath: null, lastText: '' };
|
|
481
739
|
|
|
740
|
+
function applyTheme(theme) {
|
|
741
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
742
|
+
localStorage.setItem('freya.theme', theme);
|
|
743
|
+
$('themeToggle').textContent = theme === 'dark' ? 'Light' : 'Dark';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function toggleTheme() {
|
|
747
|
+
const t = localStorage.getItem('freya.theme') || 'light';
|
|
748
|
+
applyTheme(t === 'dark' ? 'light' : 'dark');
|
|
749
|
+
}
|
|
750
|
+
|
|
482
751
|
function setPill(kind, text) {
|
|
483
752
|
const dot = $('dot');
|
|
484
753
|
dot.classList.remove('ok','err');
|
|
485
754
|
if (kind === 'ok') dot.classList.add('ok');
|
|
486
755
|
if (kind === 'err') dot.classList.add('err');
|
|
487
756
|
$('pill').textContent = text;
|
|
757
|
+
$('status') && ($('status').textContent = text);
|
|
488
758
|
}
|
|
489
759
|
|
|
490
760
|
function setOut(text) {
|
|
@@ -492,6 +762,21 @@ function html() {
|
|
|
492
762
|
$('out').textContent = text || '';
|
|
493
763
|
}
|
|
494
764
|
|
|
765
|
+
function clearOut() {
|
|
766
|
+
setOut('');
|
|
767
|
+
setPill('ok', 'idle');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function copyOut() {
|
|
771
|
+
try {
|
|
772
|
+
await navigator.clipboard.writeText(state.lastText || '');
|
|
773
|
+
setPill('ok','copied');
|
|
774
|
+
setTimeout(() => setPill('ok','idle'), 800);
|
|
775
|
+
} catch (e) {
|
|
776
|
+
setPill('err','copy failed');
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
495
780
|
function setLast(p) {
|
|
496
781
|
state.lastReportPath = p;
|
|
497
782
|
$('last').textContent = p ? ('Last report: ' + p) : '';
|
|
@@ -507,6 +792,7 @@ function html() {
|
|
|
507
792
|
$('dir').value = localStorage.getItem('freya.dir') || './freya';
|
|
508
793
|
$('discord').value = localStorage.getItem('freya.discord') || '';
|
|
509
794
|
$('teams').value = localStorage.getItem('freya.teams') || '';
|
|
795
|
+
$('sidePath').textContent = $('dir').value || './freya';
|
|
510
796
|
}
|
|
511
797
|
|
|
512
798
|
async function api(p, body) {
|
|
@@ -527,13 +813,16 @@ function html() {
|
|
|
527
813
|
|
|
528
814
|
async function pickDir() {
|
|
529
815
|
try {
|
|
530
|
-
setPill('run','
|
|
816
|
+
setPill('run','picker…');
|
|
531
817
|
const r = await api('/api/pick-dir', {});
|
|
532
|
-
if (r && r.dir)
|
|
818
|
+
if (r && r.dir) {
|
|
819
|
+
$('dir').value = r.dir;
|
|
820
|
+
$('sidePath').textContent = r.dir;
|
|
821
|
+
}
|
|
533
822
|
saveLocal();
|
|
534
823
|
setPill('ok','ready');
|
|
535
824
|
} catch (e) {
|
|
536
|
-
setPill('err','picker
|
|
825
|
+
setPill('err','picker failed');
|
|
537
826
|
setOut(String(e && e.message ? e.message : e));
|
|
538
827
|
}
|
|
539
828
|
}
|
|
@@ -541,6 +830,7 @@ function html() {
|
|
|
541
830
|
async function doInit() {
|
|
542
831
|
try {
|
|
543
832
|
saveLocal();
|
|
833
|
+
$('sidePath').textContent = dirOrDefault();
|
|
544
834
|
setPill('run','init…');
|
|
545
835
|
setOut('');
|
|
546
836
|
const r = await api('/api/init', { dir: dirOrDefault() });
|
|
@@ -556,6 +846,7 @@ function html() {
|
|
|
556
846
|
async function doUpdate() {
|
|
557
847
|
try {
|
|
558
848
|
saveLocal();
|
|
849
|
+
$('sidePath').textContent = dirOrDefault();
|
|
559
850
|
setPill('run','update…');
|
|
560
851
|
setOut('');
|
|
561
852
|
const r = await api('/api/update', { dir: dirOrDefault() });
|
|
@@ -571,6 +862,7 @@ function html() {
|
|
|
571
862
|
async function doHealth() {
|
|
572
863
|
try {
|
|
573
864
|
saveLocal();
|
|
865
|
+
$('sidePath').textContent = dirOrDefault();
|
|
574
866
|
setPill('run','health…');
|
|
575
867
|
setOut('');
|
|
576
868
|
const r = await api('/api/health', { dir: dirOrDefault() });
|
|
@@ -586,6 +878,7 @@ function html() {
|
|
|
586
878
|
async function doMigrate() {
|
|
587
879
|
try {
|
|
588
880
|
saveLocal();
|
|
881
|
+
$('sidePath').textContent = dirOrDefault();
|
|
589
882
|
setPill('run','migrate…');
|
|
590
883
|
setOut('');
|
|
591
884
|
const r = await api('/api/migrate', { dir: dirOrDefault() });
|
|
@@ -601,6 +894,7 @@ function html() {
|
|
|
601
894
|
async function runReport(name) {
|
|
602
895
|
try {
|
|
603
896
|
saveLocal();
|
|
897
|
+
$('sidePath').textContent = dirOrDefault();
|
|
604
898
|
setPill('run', name + '…');
|
|
605
899
|
setOut('');
|
|
606
900
|
const r = await api('/api/report', { dir: dirOrDefault(), script: name });
|
|
@@ -617,10 +911,10 @@ function html() {
|
|
|
617
911
|
async function publish(target) {
|
|
618
912
|
try {
|
|
619
913
|
saveLocal();
|
|
620
|
-
if (!state.lastText) throw new Error('
|
|
914
|
+
if (!state.lastText) throw new Error('Gere um relatório primeiro.');
|
|
621
915
|
const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
|
|
622
|
-
if (!webhookUrl) throw new Error('Configure
|
|
623
|
-
setPill('run','
|
|
916
|
+
if (!webhookUrl) throw new Error('Configure o webhook antes.');
|
|
917
|
+
setPill('run','publish…');
|
|
624
918
|
await api('/api/publish', { webhookUrl, text: state.lastText });
|
|
625
919
|
setPill('ok','published');
|
|
626
920
|
} catch (e) {
|
|
@@ -629,7 +923,11 @@ function html() {
|
|
|
629
923
|
}
|
|
630
924
|
}
|
|
631
925
|
|
|
926
|
+
// init
|
|
927
|
+
applyTheme(localStorage.getItem('freya.theme') || 'light');
|
|
928
|
+
$('chipPort').textContent = location.host;
|
|
632
929
|
loadLocal();
|
|
930
|
+
setPill('ok','idle');
|
|
633
931
|
</script>
|
|
634
932
|
</body>
|
|
635
933
|
</html>`;
|
|
@@ -734,7 +1032,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
734
1032
|
const raw = await readBody(req);
|
|
735
1033
|
const payload = raw ? JSON.parse(raw) : {};
|
|
736
1034
|
|
|
737
|
-
const
|
|
1035
|
+
const requestedDir = payload.dir || dir || './freya';
|
|
1036
|
+
const workspaceDir = normalizeWorkspaceDir(requestedDir);
|
|
738
1037
|
|
|
739
1038
|
if (req.url === '/api/pick-dir') {
|
|
740
1039
|
const picked = await pickDirectoryNative();
|
|
@@ -743,27 +1042,31 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
743
1042
|
|
|
744
1043
|
if (req.url === '/api/init') {
|
|
745
1044
|
const pkg = '@cccarv82/freya';
|
|
746
|
-
const r = await run(
|
|
747
|
-
|
|
1045
|
+
const r = await run(guessNpxCmd(), [guessNpxYesFlag(), pkg, 'init', workspaceDir], process.cwd());
|
|
1046
|
+
const output = (r.stdout + r.stderr).trim();
|
|
1047
|
+
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'init failed', output });
|
|
748
1048
|
}
|
|
749
1049
|
|
|
750
1050
|
if (req.url === '/api/update') {
|
|
751
1051
|
const pkg = '@cccarv82/freya';
|
|
752
1052
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
753
|
-
const r = await run(
|
|
754
|
-
|
|
1053
|
+
const r = await run(guessNpxCmd(), [guessNpxYesFlag(), pkg, 'init', '--here'], workspaceDir);
|
|
1054
|
+
const output = (r.stdout + r.stderr).trim();
|
|
1055
|
+
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'update failed', output });
|
|
755
1056
|
}
|
|
756
1057
|
|
|
757
1058
|
const npmCmd = guessNpmCmd();
|
|
758
1059
|
|
|
759
1060
|
if (req.url === '/api/health') {
|
|
760
1061
|
const r = await run(npmCmd, ['run', 'health'], workspaceDir);
|
|
761
|
-
|
|
1062
|
+
const output = (r.stdout + r.stderr).trim();
|
|
1063
|
+
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'health failed', output });
|
|
762
1064
|
}
|
|
763
1065
|
|
|
764
1066
|
if (req.url === '/api/migrate') {
|
|
765
1067
|
const r = await run(npmCmd, ['run', 'migrate'], workspaceDir);
|
|
766
|
-
|
|
1068
|
+
const output = (r.stdout + r.stderr).trim();
|
|
1069
|
+
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'migrate failed', output });
|
|
767
1070
|
}
|
|
768
1071
|
|
|
769
1072
|
if (req.url === '/api/report') {
|
|
@@ -787,7 +1090,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
787
1090
|
// Prefer showing the actual report content when available.
|
|
788
1091
|
const output = reportText ? reportText : out;
|
|
789
1092
|
|
|
790
|
-
return safeJson(res, r.code === 0 ? 200 : 400, { output, reportPath, reportText });
|
|
1093
|
+
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output, reportPath, reportText } : { error: output || 'report failed', output, reportPath, reportText });
|
|
791
1094
|
}
|
|
792
1095
|
|
|
793
1096
|
if (req.url === '/api/publish') {
|