@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.
Files changed (2) hide show
  1. package/cli/web.js +530 -227
  2. 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
- const child = spawn(cmd, args, { cwd, shell: false, env: process.env });
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
- child.stdout.on('data', (d) => {
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: “Noir control room” — dark glass, crisp typography, intentional hierarchy.
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
- --bg: #070a10;
147
- --bg2: #0a1020;
148
- --panel: rgba(255,255,255,.04);
149
- --panel2: rgba(255,255,255,.06);
150
- --line: rgba(180,210,255,.16);
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,.52);
154
- --accent: #5eead4;
155
- --accent2: #60a5fa;
156
- --danger: #fb7185;
157
- --ok: #34d399;
158
- --warn: #fbbf24;
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
- --radius: 16px;
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(900px 560px at 18% 12%, rgba(94,234,212,.14), transparent 60%),
170
- radial-gradient(820px 540px at 72% 6%, rgba(96,165,250,.14), transparent 60%),
171
- radial-gradient(900px 700px at 70% 78%, rgba(251,113,133,.08), transparent 60%),
172
- linear-gradient(180deg, var(--bg), var(--bg2));
173
- font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial;
174
- overflow-x: hidden;
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 noise */
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
- linear-gradient(transparent 0, transparent 2px, rgba(255,255,255,.02) 3px),
185
- radial-gradient(circle at 10% 10%, rgba(255,255,255,.06), transparent 35%),
186
- radial-gradient(circle at 90% 30%, rgba(255,255,255,.04), transparent 35%);
187
- background-size: 100% 6px, 900px 900px, 900px 900px;
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
- header {
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: 0;
195
- z-index: 10;
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
- .top {
202
- max-width: 1140px;
203
- margin: 0 auto;
204
- padding: 14px 18px;
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
- gap: 16px;
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: baseline;
214
- gap: 12px;
215
- letter-spacing: .16em;
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
- .badge {
223
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
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: 6px 10px;
347
+ padding: 7px 10px;
226
348
  border-radius: 999px;
227
349
  border: 1px solid var(--line);
228
- background: rgba(255,255,255,.03);
350
+ background: rgba(255,255,255,.55);
229
351
  color: var(--faint);
230
352
  }
231
353
 
232
- .wrap {
233
- max-width: 1140px;
234
- margin: 0 auto;
235
- padding: 18px;
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
- .hero {
239
- display: grid;
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
- @media (max-width: 980px) {
247
- .hero { grid-template-columns: 1fr; }
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: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02));
253
- border-radius: var(--radius);
254
- box-shadow: var(--shadow);
255
- padding: 14px;
256
- position: relative;
257
- overflow: hidden;
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:before {
261
- content: "";
262
- position: absolute;
263
- inset: -2px;
264
- background:
265
- radial-gradient(900px 220px at 25% 0%, rgba(94,234,212,.12), transparent 60%),
266
- radial-gradient(900px 220px at 90% 30%, rgba(96,165,250,.10), transparent 60%);
267
- opacity: .55;
268
- pointer-events: none;
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
- .card > * { position: relative; }
425
+ .icon.orange { background: var(--chip2); color: var(--accent); }
272
426
 
273
- h2 {
274
- margin: 0 0 8px;
275
- font-size: 14px;
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
- .sub {
282
- margin: 0 0 10px;
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
- label {
289
- display: block;
290
- font-size: 12px;
291
- color: var(--muted);
292
- margin-bottom: 6px;
438
+ .grid2 {
439
+ display: grid;
440
+ grid-template-columns: 1fr 1fr;
441
+ gap: 14px;
442
+ margin-top: 14px;
293
443
  }
294
444
 
295
- .field {
296
- display: grid;
297
- grid-template-columns: 1fr auto;
298
- gap: 10px;
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: 12px 12px;
471
+ padding: 11px 12px;
305
472
  border-radius: 12px;
306
- border: 1px solid rgba(180,210,255,.22);
307
- background: rgba(7,10,16,.55);
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::placeholder { color: rgba(233,240,255,.38); }
479
+ [data-theme="dark"] input { background: rgba(0,0,0,.16); }
313
480
 
314
- .btns {
315
- display: flex;
316
- flex-wrap: wrap;
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
- margin-top: 10px;
487
+ align-items: center;
319
488
  }
320
489
 
321
- button {
322
- border: 1px solid rgba(180,210,255,.22);
490
+ .btn {
491
+ border: 1px solid var(--line);
323
492
  border-radius: 12px;
324
- background: rgba(255,255,255,.04);
493
+ background: var(--paper2);
325
494
  color: var(--text);
326
495
  padding: 10px 12px;
327
496
  cursor: pointer;
328
- transition: transform .08s ease, background .16s ease, border-color .16s ease;
329
- font-weight: 600;
330
- letter-spacing: .01em;
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
- button:hover { transform: translateY(-1px); background: rgba(255,255,255,.06); border-color: rgba(180,210,255,.32); }
334
- button:active { transform: translateY(0); }
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(94,234,212,.18), rgba(96,165,250,.16));
338
- border-color: rgba(94,234,212,.28);
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
- .ghost { background: rgba(255,255,255,.02); }
342
-
343
- .danger { border-color: rgba(251,113,133,.45); background: rgba(251,113,133,.12); }
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
- .pill {
346
- display: inline-flex;
347
- align-items: center;
348
- gap: 8px;
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
- .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 5px rgba(251,191,36,.14); }
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
- .two {
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
- .hr { height: 1px; background: rgba(180,210,255,.14); margin: 12px 0; }
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
- min-height: 220px;
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
- color: rgba(233,240,255,.84);
542
+ max-height: 420px;
543
+ overflow: auto;
544
+ color: rgba(15,23,42,.92);
383
545
  }
384
546
 
385
- .hint {
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
- margin-top: 6px;
389
- line-height: 1.35;
573
+ font-family: var(--mono);
390
574
  }
391
575
 
392
- .footer {
393
- margin-top: 12px;
576
+ .sidebar h3 {
577
+ margin: 0;
394
578
  font-size: 12px;
395
- color: rgba(233,240,255,.45);
579
+ letter-spacing: .10em;
580
+ text-transform: uppercase;
581
+ color: var(--muted);
396
582
  }
397
583
 
398
- a { color: var(--accent2); text-decoration: none; }
399
- a:hover { text-decoration: underline; }
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
- <header>
404
- <div class="top">
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
- <div class="btns">
424
- <button class="primary" onclick="doInit()">Init</button>
425
- <button onclick="doUpdate()">Update</button>
426
- <button onclick="doHealth()">Health</button>
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="footer">Atalho: <code>freya web --dir ./freya</code> (porta padrão 3872).</div>
431
- </div>
432
-
433
- <div class="card">
434
- <h2>2) Publish</h2>
435
- <p class="sub">Configure webhooks (opcional) para publicar relatórios com 1 clique. Ideal para mandar status no Teams/Discord.</p>
436
-
437
- <label>Discord webhook URL</label>
438
- <input id="discord" placeholder="https://discord.com/api/webhooks/..." />
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="hr"></div>
445
- <div class="btns">
446
- <button onclick="publish('discord')">Publish last → Discord</button>
447
- <button onclick="publish('teams')">Publish last → Teams</button>
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
- <div class="two">
454
- <div class="card">
455
- <h2>3) Generate</h2>
456
- <p class="sub">Gere relatórios e use o preview/log abaixo para validar. Depois, publique ou copie.</p>
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="btns">
459
- <button class="primary" onclick="runReport('status')">Executive</button>
460
- <button class="primary" onclick="runReport('sm-weekly')">SM Weekly</button>
461
- <button class="primary" onclick="runReport('blockers')">Blockers</button>
462
- <button class="ghost" onclick="runReport('daily')">Daily</button>
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
- <div class="hint" id="last"></div>
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','opening picker…');
816
+ setPill('run','picker…');
531
817
  const r = await api('/api/pick-dir', {});
532
- if (r && r.dir) $('dir').value = 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 unavailable');
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('Generate a report first.');
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 the webhook URL first.');
623
- setPill('run','publishing…');
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 workspaceDir = path.resolve(process.cwd(), payload.dir || dir || './freya');
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('npx', [pkg, 'init', workspaceDir], process.cwd());
747
- return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
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('npx', [pkg, 'init', '--here'], workspaceDir);
754
- return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
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
- return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
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
- return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
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') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",