@cccarv82/freya 1.0.6 → 1.0.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.
Files changed (2) hide show
  1. package/cli/web.js +494 -221
  2. package/package.json +1 -1
package/cli/web.js CHANGED
@@ -9,6 +9,10 @@ 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
+
12
16
  function guessOpenCmd() {
13
17
  // Minimal cross-platform opener without extra deps
14
18
  if (process.platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', ''] };
@@ -69,15 +73,29 @@ function readBody(req) {
69
73
 
70
74
  function run(cmd, args, cwd) {
71
75
  return new Promise((resolve) => {
72
- const child = spawn(cmd, args, { cwd, shell: false, env: process.env });
76
+ let child;
77
+ try {
78
+ child = spawn(cmd, args, { cwd, shell: false, env: process.env });
79
+ } catch (e) {
80
+ return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
81
+ }
82
+
73
83
  let stdout = '';
74
84
  let stderr = '';
75
- child.stdout.on('data', (d) => {
85
+
86
+ child.stdout && child.stdout.on('data', (d) => {
76
87
  stdout += d.toString();
77
88
  });
78
- child.stderr.on('data', (d) => {
89
+ child.stderr && child.stderr.on('data', (d) => {
79
90
  stderr += d.toString();
80
91
  });
92
+
93
+ // Prevent unhandled error event (e.g., ENOENT on Windows when cmd not found)
94
+ child.on('error', (e) => {
95
+ stderr += `\n${e.message || String(e)}`;
96
+ resolve({ code: 1, stdout, stderr });
97
+ });
98
+
81
99
  child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
82
100
  });
83
101
  }
@@ -134,7 +152,7 @@ async function pickDirectoryNative() {
134
152
  }
135
153
 
136
154
  function html() {
137
- // Aesthetic: “Noir control room” — dark glass, crisp typography, intentional hierarchy.
155
+ // Aesthetic: “clean workstation” — light-first UI inspired by modern productivity apps.
138
156
  return `<!doctype html>
139
157
  <html>
140
158
  <head>
@@ -142,336 +160,551 @@ function html() {
142
160
  <meta name="viewport" content="width=device-width, initial-scale=1" />
143
161
  <title>FREYA Web</title>
144
162
  <style>
163
+ /*
164
+ Design goals:
165
+ - Light theme by default (inspired by your reference screenshots)
166
+ - Dark mode toggle
167
+ - App-like layout: sidebar + main surface
168
+ - Clear onboarding and affordances
169
+ */
170
+
145
171
  :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);
172
+ --radius: 14px;
173
+ --shadow: 0 18px 55px rgba(16, 24, 40, .10);
174
+ --shadow2: 0 10px 20px rgba(16, 24, 40, .08);
175
+ --ring: 0 0 0 4px rgba(59, 130, 246, .18);
176
+
177
+ /* Light */
178
+ --bg: #f6f7fb;
179
+ --paper: #ffffff;
180
+ --paper2: #fbfbfd;
181
+ --line: rgba(16, 24, 40, .10);
182
+ --line2: rgba(16, 24, 40, .14);
183
+ --text: #0f172a;
184
+ --muted: rgba(15, 23, 42, .68);
185
+ --faint: rgba(15, 23, 42, .50);
186
+
187
+ --primary: #2563eb;
188
+ --primary2: #0ea5e9;
189
+ --accent: #f97316;
190
+ --ok: #16a34a;
191
+ --warn: #f59e0b;
192
+ --danger: #e11d48;
193
+
194
+ --chip: rgba(37, 99, 235, .08);
195
+ --chip2: rgba(249, 115, 22, .10);
196
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
197
+ --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
198
+ }
199
+
200
+ [data-theme="dark"] {
201
+ --bg: #0b1020;
202
+ --paper: rgba(255,255,255,.06);
203
+ --paper2: rgba(255,255,255,.04);
204
+ --line: rgba(255,255,255,.12);
205
+ --line2: rgba(255,255,255,.18);
151
206
  --text: #e9f0ff;
152
207
  --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;
208
+ --faint: rgba(233,240,255,.54);
209
+
210
+ --primary: #60a5fa;
211
+ --primary2: #22c55e;
212
+ --accent: #fb923c;
213
+ --chip: rgba(96, 165, 250, .14);
214
+ --chip2: rgba(251, 146, 60, .14);
215
+
159
216
  --shadow: 0 30px 70px rgba(0,0,0,.55);
160
- --radius: 16px;
217
+ --shadow2: 0 18px 40px rgba(0,0,0,.35);
218
+ --ring: 0 0 0 4px rgba(96, 165, 250, .22);
161
219
  }
162
220
 
163
221
  * { box-sizing: border-box; }
164
222
  html, body { height: 100%; }
165
223
  body {
166
224
  margin: 0;
167
- color: var(--text);
168
225
  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;
226
+ radial-gradient(1200px 800px at 20% -10%, rgba(37,99,235,.12), transparent 55%),
227
+ radial-gradient(900px 600px at 92% 10%, rgba(249,115,22,.12), transparent 55%),
228
+ radial-gradient(1100px 700px at 70% 105%, rgba(14,165,233,.10), transparent 55%),
229
+ var(--bg);
230
+ color: var(--text);
231
+ font-family: var(--sans);
175
232
  }
176
233
 
177
- /* subtle noise */
234
+ /* subtle grain */
178
235
  body:before {
179
236
  content: "";
180
237
  position: fixed;
181
238
  inset: 0;
182
239
  pointer-events: none;
183
240
  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;
241
+ radial-gradient(circle at 15% 20%, rgba(255,255,255,.38), transparent 32%),
242
+ radial-gradient(circle at 80% 10%, rgba(255,255,255,.26), transparent 38%),
243
+ linear-gradient(transparent 0, transparent 3px, rgba(0,0,0,.02) 4px);
244
+ background-size: 900px 900px, 900px 900px, 100% 7px;
245
+ opacity: .08;
188
246
  mix-blend-mode: overlay;
189
- opacity: .12;
190
247
  }
191
248
 
192
- header {
249
+ .app {
250
+ max-width: 1260px;
251
+ margin: 18px auto;
252
+ padding: 0 18px;
253
+ }
254
+
255
+ .frame {
256
+ display: grid;
257
+ grid-template-columns: 280px 1fr;
258
+ gap: 14px;
259
+ min-height: calc(100vh - 36px);
260
+ }
261
+
262
+ @media (max-width: 980px) {
263
+ .frame { grid-template-columns: 1fr; }
264
+ }
265
+
266
+ .sidebar {
267
+ background: var(--paper);
268
+ border: 1px solid var(--line);
269
+ border-radius: var(--radius);
270
+ box-shadow: var(--shadow2);
271
+ padding: 14px;
193
272
  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);
273
+ top: 18px;
274
+ height: fit-content;
275
+ }
276
+
277
+ .main {
278
+ background: var(--paper);
279
+ border: 1px solid var(--line);
280
+ border-radius: var(--radius);
281
+ box-shadow: var(--shadow);
282
+ overflow: hidden;
199
283
  }
200
284
 
201
- .top {
202
- max-width: 1140px;
203
- margin: 0 auto;
204
- padding: 14px 18px;
285
+ .topbar {
205
286
  display: flex;
206
287
  align-items: center;
207
288
  justify-content: space-between;
208
- gap: 16px;
289
+ padding: 14px 16px;
290
+ border-bottom: 1px solid var(--line);
291
+ background: linear-gradient(180deg, var(--paper2), var(--paper));
209
292
  }
210
293
 
211
294
  .brand {
212
295
  display: flex;
213
- align-items: baseline;
214
- gap: 12px;
215
- letter-spacing: .16em;
296
+ align-items: center;
297
+ gap: 10px;
298
+ font-weight: 800;
299
+ letter-spacing: .08em;
216
300
  text-transform: uppercase;
217
- font-weight: 700;
218
301
  font-size: 12px;
219
302
  color: var(--muted);
220
303
  }
221
304
 
222
- .badge {
223
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
305
+ .spark {
306
+ width: 10px;
307
+ height: 10px;
308
+ border-radius: 4px;
309
+ background: linear-gradient(135deg, var(--accent), var(--primary));
310
+ box-shadow: 0 0 0 6px rgba(249,115,22,.12);
311
+ }
312
+
313
+ .actions {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 10px;
317
+ }
318
+
319
+ .chip {
320
+ font-family: var(--mono);
224
321
  font-size: 12px;
225
- padding: 6px 10px;
322
+ padding: 7px 10px;
226
323
  border-radius: 999px;
227
324
  border: 1px solid var(--line);
228
- background: rgba(255,255,255,.03);
325
+ background: rgba(255,255,255,.55);
229
326
  color: var(--faint);
230
327
  }
231
328
 
232
- .wrap {
233
- max-width: 1140px;
234
- margin: 0 auto;
235
- padding: 18px;
329
+ [data-theme="dark"] .chip { background: rgba(0,0,0,.20); }
330
+
331
+ .toggle {
332
+ border: 1px solid var(--line);
333
+ border-radius: 999px;
334
+ background: var(--paper2);
335
+ padding: 7px 10px;
336
+ cursor: pointer;
337
+ color: var(--muted);
338
+ font-weight: 700;
339
+ font-size: 12px;
236
340
  }
237
341
 
238
- .hero {
239
- display: grid;
240
- grid-template-columns: 1.2fr .8fr;
241
- gap: 16px;
242
- align-items: start;
243
- margin-bottom: 16px;
342
+ .section {
343
+ padding: 16px;
244
344
  }
245
345
 
246
- @media (max-width: 980px) {
247
- .hero { grid-template-columns: 1fr; }
346
+ h1 {
347
+ margin: 0;
348
+ font-size: 22px;
349
+ letter-spacing: -.02em;
350
+ }
351
+
352
+ .subtitle {
353
+ margin-top: 6px;
354
+ color: var(--muted);
355
+ font-size: 13px;
356
+ line-height: 1.4;
357
+ max-width: 860px;
358
+ }
359
+
360
+ .cards {
361
+ display: grid;
362
+ grid-template-columns: repeat(4, 1fr);
363
+ gap: 12px;
364
+ margin-top: 14px;
248
365
  }
249
366
 
367
+ @media (max-width: 1100px) { .cards { grid-template-columns: repeat(2, 1fr);} }
368
+ @media (max-width: 620px) { .cards { grid-template-columns: 1fr;} }
369
+
250
370
  .card {
251
371
  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;
372
+ background: var(--paper2);
373
+ border-radius: 14px;
374
+ padding: 12px;
375
+ display: grid;
376
+ gap: 6px;
377
+ cursor: pointer;
378
+ transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
379
+ box-shadow: 0 1px 0 rgba(16,24,40,.04);
258
380
  }
259
381
 
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;
382
+ .card:hover {
383
+ transform: translateY(-1px);
384
+ border-color: var(--line2);
385
+ box-shadow: 0 10px 22px rgba(16,24,40,.10);
269
386
  }
270
387
 
271
- .card > * { position: relative; }
388
+ .icon {
389
+ width: 34px;
390
+ height: 34px;
391
+ border-radius: 12px;
392
+ display: grid;
393
+ place-items: center;
394
+ background: var(--chip);
395
+ border: 1px solid var(--line);
396
+ color: var(--primary);
397
+ font-weight: 900;
398
+ }
272
399
 
273
- h2 {
274
- margin: 0 0 8px;
275
- font-size: 14px;
276
- letter-spacing: .08em;
277
- text-transform: uppercase;
278
- color: var(--muted);
400
+ .icon.orange { background: var(--chip2); color: var(--accent); }
401
+
402
+ .title {
403
+ font-weight: 800;
404
+ font-size: 13px;
279
405
  }
280
406
 
281
- .sub {
282
- margin: 0 0 10px;
407
+ .desc {
408
+ color: var(--muted);
283
409
  font-size: 12px;
284
- color: var(--faint);
285
410
  line-height: 1.35;
286
411
  }
287
412
 
288
- label {
289
- display: block;
290
- font-size: 12px;
291
- color: var(--muted);
292
- margin-bottom: 6px;
413
+ .grid2 {
414
+ display: grid;
415
+ grid-template-columns: 1fr 1fr;
416
+ gap: 14px;
417
+ margin-top: 14px;
293
418
  }
294
419
 
295
- .field {
296
- display: grid;
297
- grid-template-columns: 1fr auto;
298
- gap: 10px;
420
+ @media (max-width: 980px) { .grid2 { grid-template-columns: 1fr; } }
421
+
422
+ .panel {
423
+ border: 1px solid var(--line);
424
+ background: var(--paper);
425
+ border-radius: 14px;
426
+ overflow: hidden;
427
+ }
428
+
429
+ .panelHead {
430
+ display: flex;
299
431
  align-items: center;
432
+ justify-content: space-between;
433
+ padding: 12px 12px;
434
+ border-bottom: 1px solid var(--line);
435
+ background: linear-gradient(180deg, var(--paper2), var(--paper));
300
436
  }
301
437
 
438
+ .panelHead b { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
439
+
440
+ .panelBody { padding: 12px; }
441
+
442
+ label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
443
+
302
444
  input {
303
445
  width: 100%;
304
- padding: 12px 12px;
446
+ padding: 11px 12px;
305
447
  border-radius: 12px;
306
- border: 1px solid rgba(180,210,255,.22);
307
- background: rgba(7,10,16,.55);
448
+ border: 1px solid var(--line);
449
+ background: rgba(255,255,255,.72);
308
450
  color: var(--text);
309
451
  outline: none;
310
452
  }
311
453
 
312
- input::placeholder { color: rgba(233,240,255,.38); }
454
+ [data-theme="dark"] input { background: rgba(0,0,0,.16); }
313
455
 
314
- .btns {
315
- display: flex;
316
- flex-wrap: wrap;
456
+ input:focus { box-shadow: var(--ring); border-color: rgba(37,99,235,.35); }
457
+
458
+ .row {
459
+ display: grid;
460
+ grid-template-columns: 1fr auto;
317
461
  gap: 10px;
318
- margin-top: 10px;
462
+ align-items: center;
319
463
  }
320
464
 
321
- button {
322
- border: 1px solid rgba(180,210,255,.22);
465
+ .btn {
466
+ border: 1px solid var(--line);
323
467
  border-radius: 12px;
324
- background: rgba(255,255,255,.04);
468
+ background: var(--paper2);
325
469
  color: var(--text);
326
470
  padding: 10px 12px;
327
471
  cursor: pointer;
328
- transition: transform .08s ease, background .16s ease, border-color .16s ease;
329
- font-weight: 600;
330
- letter-spacing: .01em;
472
+ font-weight: 800;
473
+ font-size: 12px;
474
+ transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
331
475
  }
332
476
 
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); }
477
+ .btn:hover { transform: translateY(-1px); border-color: var(--line2); box-shadow: 0 10px 22px rgba(16,24,40,.10); }
478
+ .btn:active { transform: translateY(0); }
335
479
 
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);
480
+ .btn.primary {
481
+ background: linear-gradient(135deg, rgba(37,99,235,.14), rgba(14,165,233,.12));
482
+ border-color: rgba(37,99,235,.22);
483
+ color: var(--text);
339
484
  }
340
485
 
341
- .ghost { background: rgba(255,255,255,.02); }
342
-
343
- .danger { border-color: rgba(251,113,133,.45); background: rgba(251,113,133,.12); }
486
+ .btn.orange {
487
+ background: linear-gradient(135deg, rgba(249,115,22,.16), rgba(37,99,235,.08));
488
+ border-color: rgba(249,115,22,.24);
489
+ }
344
490
 
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);
491
+ .btn.danger {
492
+ background: rgba(225,29,72,.10);
493
+ border-color: rgba(225,29,72,.28);
494
+ color: var(--text);
355
495
  }
356
496
 
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); }
497
+ .btn.small { padding: 9px 10px; font-weight: 800; }
360
498
 
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; } }
499
+ .stack { display: flex; flex-wrap: wrap; gap: 10px; }
367
500
 
368
- .hr { height: 1px; background: rgba(180,210,255,.14); margin: 12px 0; }
501
+ .help {
502
+ margin-top: 8px;
503
+ color: var(--faint);
504
+ font-size: 12px;
505
+ line-height: 1.35;
506
+ }
369
507
 
370
508
  .log {
509
+ border: 1px solid var(--line);
510
+ background: rgba(255,255,255,.65);
371
511
  border-radius: 14px;
372
- border: 1px solid rgba(180,210,255,.18);
373
- background: rgba(7,10,16,.55);
374
512
  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;
513
+ font-family: var(--mono);
379
514
  font-size: 12px;
380
515
  line-height: 1.35;
381
516
  white-space: pre-wrap;
382
- color: rgba(233,240,255,.84);
517
+ max-height: 420px;
518
+ overflow: auto;
519
+ color: rgba(15,23,42,.92);
383
520
  }
384
521
 
385
- .hint {
522
+ [data-theme="dark"] .log { background: rgba(0,0,0,.20); color: rgba(233,240,255,.84); }
523
+
524
+ .statusRow { display:flex; align-items:center; justify-content: space-between; gap: 10px; }
525
+
526
+ .pill {
527
+ display: inline-flex;
528
+ align-items: center;
529
+ gap: 8px;
530
+ padding: 7px 10px;
531
+ border-radius: 999px;
532
+ border: 1px solid var(--line);
533
+ background: rgba(255,255,255,.55);
534
+ font-size: 12px;
535
+ color: var(--muted);
536
+ font-family: var(--mono);
537
+ }
538
+
539
+ [data-theme="dark"] .pill { background: rgba(0,0,0,.18); }
540
+
541
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 5px rgba(245,158,11,.15); }
542
+ .dot.ok { background: var(--ok); box-shadow: 0 0 0 5px rgba(22,163,74,.12); }
543
+ .dot.err { background: var(--danger); box-shadow: 0 0 0 5px rgba(225,29,72,.14); }
544
+
545
+ .small {
386
546
  font-size: 12px;
387
547
  color: var(--faint);
388
- margin-top: 6px;
389
- line-height: 1.35;
548
+ font-family: var(--mono);
549
+ }
550
+
551
+ .sidebar h3 {
552
+ margin: 0;
553
+ font-size: 12px;
554
+ letter-spacing: .10em;
555
+ text-transform: uppercase;
556
+ color: var(--muted);
390
557
  }
391
558
 
392
- .footer {
393
- margin-top: 12px;
559
+ .sideBlock { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line); }
560
+
561
+ .sidePath {
562
+ margin-top: 8px;
563
+ border: 1px solid var(--line);
564
+ background: var(--paper2);
565
+ border-radius: 12px;
566
+ padding: 10px;
567
+ font-family: var(--mono);
394
568
  font-size: 12px;
395
- color: rgba(233,240,255,.45);
569
+ color: var(--muted);
570
+ word-break: break-word;
396
571
  }
397
572
 
398
- a { color: var(--accent2); text-decoration: none; }
399
- a:hover { text-decoration: underline; }
573
+ .sideBtn { width: 100%; margin-top: 8px; }
574
+
575
+ .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); }
576
+ [data-theme="dark"] .k { background: rgba(0,0,0,.18); }
577
+
400
578
  </style>
401
579
  </head>
402
580
  <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>
581
+ <div class="app">
582
+ <div class="frame">
422
583
 
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>
584
+ <aside class="sidebar">
585
+ <div style="display:flex; align-items:center; justify-content: space-between; gap:10px;">
586
+ <h3>FREYA</h3>
587
+ <span class="pill"><span class="dot" id="dot"></span><span id="pill">idle</span></span>
428
588
  </div>
429
589
 
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://..." />
590
+ <div class="sideBlock">
591
+ <h3>Workspace</h3>
592
+ <div class="sidePath" id="sidePath">./freya</div>
593
+ <button class="btn sideBtn" onclick="pickDir()">Select workspace…</button>
594
+ <button class="btn primary sideBtn" onclick="doInit()">Init workspace</button>
595
+ <button class="btn sideBtn" onclick="doUpdate()">Update (preserve data/logs)</button>
596
+ <button class="btn sideBtn" onclick="doHealth()">Health</button>
597
+ <button class="btn sideBtn" onclick="doMigrate()">Migrate</button>
598
+ <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>
599
+ </div>
443
600
 
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>
601
+ <div class="sideBlock">
602
+ <h3>Publish</h3>
603
+ <button class="btn sideBtn" onclick="publish('discord')">Publish → Discord</button>
604
+ <button class="btn sideBtn" onclick="publish('teams')">Publish → Teams</button>
605
+ <div class="help">Configure os webhooks no painel principal. O publish envia o último relatório gerado.</div>
448
606
  </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
607
 
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>
608
+ <div class="sideBlock">
609
+ <h3>Atalhos</h3>
610
+ <div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
611
+ <div class="help"><span class="k">--port</span> muda a porta (default 3872).</div>
612
+ </div>
613
+ </aside>
614
+
615
+ <main class="main">
616
+ <div class="topbar">
617
+ <div class="brand"><span class="spark"></span> Local-first status assistant</div>
618
+ <div class="actions">
619
+ <span class="chip" id="chipPort">127.0.0.1:3872</span>
620
+ <button class="toggle" id="themeToggle" onclick="toggleTheme()">Theme</button>
621
+ </div>
622
+ </div>
457
623
 
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>
624
+ <div class="section">
625
+ <h1>Morning, how can I help?</h1>
626
+ <div class="subtitle">Selecione uma workspace e gere relatórios (Executive / SM / Blockers / Daily). Você pode publicar no Discord/Teams com 1 clique.</div>
627
+
628
+ <div class="cards">
629
+ <div class="card" onclick="runReport('status')">
630
+ <div class="icon">E</div>
631
+ <div class="title">Executive report</div>
632
+ <div class="desc">Status pronto para stakeholders (entregas, projetos, blockers).</div>
633
+ </div>
634
+ <div class="card" onclick="runReport('sm-weekly')">
635
+ <div class="icon">S</div>
636
+ <div class="title">SM weekly</div>
637
+ <div class="desc">Resumo, wins, riscos e foco da próxima semana.</div>
638
+ </div>
639
+ <div class="card" onclick="runReport('blockers')">
640
+ <div class="icon orange">B</div>
641
+ <div class="title">Blockers</div>
642
+ <div class="desc">Lista priorizada por severidade + idade (pra destravar rápido).</div>
643
+ </div>
644
+ <div class="card" onclick="runReport('daily')">
645
+ <div class="icon">D</div>
646
+ <div class="title">Daily</div>
647
+ <div class="desc">Ontem / Hoje / Bloqueios — pronto pra standup.</div>
648
+ </div>
649
+ </div>
650
+
651
+ <div class="grid2">
652
+ <div class="panel">
653
+ <div class="panelHead"><b>Workspace & publish settings</b><span class="small" id="last"></span></div>
654
+ <div class="panelBody">
655
+ <label>Workspace dir</label>
656
+ <div class="row">
657
+ <input id="dir" placeholder="./freya" />
658
+ <button class="btn small" onclick="pickDir()">Browse</button>
659
+ </div>
660
+ <div class="help">Escolha a pasta que contém <code>data/</code>, <code>logs/</code> e <code>scripts/</code>.</div>
661
+
662
+ <div style="height:12px"></div>
663
+
664
+ <div class="stack">
665
+ <button class="btn primary" onclick="doInit()">Init</button>
666
+ <button class="btn" onclick="doUpdate()">Update</button>
667
+ <button class="btn" onclick="doHealth()">Health</button>
668
+ <button class="btn" onclick="doMigrate()">Migrate</button>
669
+ </div>
670
+
671
+ <div style="height:16px"></div>
672
+
673
+ <label>Discord webhook URL</label>
674
+ <input id="discord" placeholder="https://discord.com/api/webhooks/..." />
675
+ <div style="height:10px"></div>
676
+
677
+ <label>Teams webhook URL</label>
678
+ <input id="teams" placeholder="https://..." />
679
+ <div class="help">O publish usa incoming webhooks. (Depois a gente evolui para anexos/chunks.)</div>
680
+
681
+ <div style="height:10px"></div>
682
+ <div class="stack">
683
+ <button class="btn" onclick="publish('discord')">Publish last → Discord</button>
684
+ <button class="btn" onclick="publish('teams')">Publish last → Teams</button>
685
+ </div>
686
+ </div>
687
+ </div>
688
+
689
+ <div class="panel">
690
+ <div class="panelHead">
691
+ <b>Output</b>
692
+ <div class="stack">
693
+ <button class="btn small" onclick="copyOut()">Copy</button>
694
+ <button class="btn small" onclick="clearOut()">Clear</button>
695
+ </div>
696
+ </div>
697
+ <div class="panelBody">
698
+ <div class="log" id="out"></div>
699
+ <div class="help">Dica: quando um report gera arquivo, mostramos o conteúdo real do report aqui (melhor que stdout).</div>
700
+ </div>
701
+ </div>
702
+
703
+ </div>
463
704
  </div>
464
705
 
465
- <div class="hint" id="last"></div>
466
- </div>
706
+ </main>
467
707
 
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
708
  </div>
476
709
  </div>
477
710
 
@@ -479,12 +712,24 @@ function html() {
479
712
  const $ = (id) => document.getElementById(id);
480
713
  const state = { lastReportPath: null, lastText: '' };
481
714
 
715
+ function applyTheme(theme) {
716
+ document.documentElement.setAttribute('data-theme', theme);
717
+ localStorage.setItem('freya.theme', theme);
718
+ $('themeToggle').textContent = theme === 'dark' ? 'Light' : 'Dark';
719
+ }
720
+
721
+ function toggleTheme() {
722
+ const t = localStorage.getItem('freya.theme') || 'light';
723
+ applyTheme(t === 'dark' ? 'light' : 'dark');
724
+ }
725
+
482
726
  function setPill(kind, text) {
483
727
  const dot = $('dot');
484
728
  dot.classList.remove('ok','err');
485
729
  if (kind === 'ok') dot.classList.add('ok');
486
730
  if (kind === 'err') dot.classList.add('err');
487
731
  $('pill').textContent = text;
732
+ $('status') && ($('status').textContent = text);
488
733
  }
489
734
 
490
735
  function setOut(text) {
@@ -492,6 +737,21 @@ function html() {
492
737
  $('out').textContent = text || '';
493
738
  }
494
739
 
740
+ function clearOut() {
741
+ setOut('');
742
+ setPill('ok', 'idle');
743
+ }
744
+
745
+ async function copyOut() {
746
+ try {
747
+ await navigator.clipboard.writeText(state.lastText || '');
748
+ setPill('ok','copied');
749
+ setTimeout(() => setPill('ok','idle'), 800);
750
+ } catch (e) {
751
+ setPill('err','copy failed');
752
+ }
753
+ }
754
+
495
755
  function setLast(p) {
496
756
  state.lastReportPath = p;
497
757
  $('last').textContent = p ? ('Last report: ' + p) : '';
@@ -507,6 +767,7 @@ function html() {
507
767
  $('dir').value = localStorage.getItem('freya.dir') || './freya';
508
768
  $('discord').value = localStorage.getItem('freya.discord') || '';
509
769
  $('teams').value = localStorage.getItem('freya.teams') || '';
770
+ $('sidePath').textContent = $('dir').value || './freya';
510
771
  }
511
772
 
512
773
  async function api(p, body) {
@@ -527,13 +788,16 @@ function html() {
527
788
 
528
789
  async function pickDir() {
529
790
  try {
530
- setPill('run','opening picker…');
791
+ setPill('run','picker…');
531
792
  const r = await api('/api/pick-dir', {});
532
- if (r && r.dir) $('dir').value = r.dir;
793
+ if (r && r.dir) {
794
+ $('dir').value = r.dir;
795
+ $('sidePath').textContent = r.dir;
796
+ }
533
797
  saveLocal();
534
798
  setPill('ok','ready');
535
799
  } catch (e) {
536
- setPill('err','picker unavailable');
800
+ setPill('err','picker failed');
537
801
  setOut(String(e && e.message ? e.message : e));
538
802
  }
539
803
  }
@@ -541,6 +805,7 @@ function html() {
541
805
  async function doInit() {
542
806
  try {
543
807
  saveLocal();
808
+ $('sidePath').textContent = dirOrDefault();
544
809
  setPill('run','init…');
545
810
  setOut('');
546
811
  const r = await api('/api/init', { dir: dirOrDefault() });
@@ -556,6 +821,7 @@ function html() {
556
821
  async function doUpdate() {
557
822
  try {
558
823
  saveLocal();
824
+ $('sidePath').textContent = dirOrDefault();
559
825
  setPill('run','update…');
560
826
  setOut('');
561
827
  const r = await api('/api/update', { dir: dirOrDefault() });
@@ -571,6 +837,7 @@ function html() {
571
837
  async function doHealth() {
572
838
  try {
573
839
  saveLocal();
840
+ $('sidePath').textContent = dirOrDefault();
574
841
  setPill('run','health…');
575
842
  setOut('');
576
843
  const r = await api('/api/health', { dir: dirOrDefault() });
@@ -586,6 +853,7 @@ function html() {
586
853
  async function doMigrate() {
587
854
  try {
588
855
  saveLocal();
856
+ $('sidePath').textContent = dirOrDefault();
589
857
  setPill('run','migrate…');
590
858
  setOut('');
591
859
  const r = await api('/api/migrate', { dir: dirOrDefault() });
@@ -601,6 +869,7 @@ function html() {
601
869
  async function runReport(name) {
602
870
  try {
603
871
  saveLocal();
872
+ $('sidePath').textContent = dirOrDefault();
604
873
  setPill('run', name + '…');
605
874
  setOut('');
606
875
  const r = await api('/api/report', { dir: dirOrDefault(), script: name });
@@ -617,10 +886,10 @@ function html() {
617
886
  async function publish(target) {
618
887
  try {
619
888
  saveLocal();
620
- if (!state.lastText) throw new Error('Generate a report first.');
889
+ if (!state.lastText) throw new Error('Gere um relatório primeiro.');
621
890
  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…');
891
+ if (!webhookUrl) throw new Error('Configure o webhook antes.');
892
+ setPill('run','publish…');
624
893
  await api('/api/publish', { webhookUrl, text: state.lastText });
625
894
  setPill('ok','published');
626
895
  } catch (e) {
@@ -629,7 +898,11 @@ function html() {
629
898
  }
630
899
  }
631
900
 
901
+ // init
902
+ applyTheme(localStorage.getItem('freya.theme') || 'light');
903
+ $('chipPort').textContent = location.host;
632
904
  loadLocal();
905
+ setPill('ok','idle');
633
906
  </script>
634
907
  </body>
635
908
  </html>`;
@@ -743,14 +1016,14 @@ async function cmdWeb({ port, dir, open, dev }) {
743
1016
 
744
1017
  if (req.url === '/api/init') {
745
1018
  const pkg = '@cccarv82/freya';
746
- const r = await run('npx', [pkg, 'init', workspaceDir], process.cwd());
1019
+ const r = await run(guessNpxCmd(), [pkg, 'init', workspaceDir], process.cwd());
747
1020
  return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
748
1021
  }
749
1022
 
750
1023
  if (req.url === '/api/update') {
751
1024
  const pkg = '@cccarv82/freya';
752
1025
  fs.mkdirSync(workspaceDir, { recursive: true });
753
- const r = await run('npx', [pkg, 'init', '--here'], workspaceDir);
1026
+ const r = await run(guessNpxCmd(), [pkg, 'init', '--here'], workspaceDir);
754
1027
  return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
755
1028
  }
756
1029
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",