@cccarv82/freya 1.0.5 → 1.0.6

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 (3) hide show
  1. package/cli/index.js +4 -2
  2. package/cli/web.js +587 -111
  3. package/package.json +1 -1
package/cli/index.js CHANGED
@@ -11,7 +11,7 @@ freya - F.R.E.Y.A. CLI
11
11
 
12
12
  Usage:
13
13
  freya init [dir] [--force] [--here|--in-place] [--force-data] [--force-logs]
14
- freya web [--port <n>] [--dir <path>] [--no-open]
14
+ freya web [--port <n>] [--dir <path>] [--no-open] [--dev]
15
15
 
16
16
  Defaults:
17
17
  - If no [dir] is provided, creates ./freya
@@ -27,6 +27,7 @@ Examples:
27
27
 
28
28
  freya web
29
29
  freya web --dir ./freya --port 3872
30
+ freya web --dev # seeds demo data/logs for quick testing
30
31
  `;
31
32
  }
32
33
 
@@ -81,6 +82,7 @@ async function run(argv) {
81
82
  const port = Number(kv['--port'] || 3872);
82
83
  const dir = kv['--dir'] ? path.resolve(process.cwd(), kv['--dir']) : null;
83
84
  const open = !flags.has('--no-open');
85
+ const dev = flags.has('--dev');
84
86
 
85
87
  if (!Number.isFinite(port) || port <= 0) {
86
88
  process.stderr.write('Invalid --port\n');
@@ -88,7 +90,7 @@ async function run(argv) {
88
90
  return;
89
91
  }
90
92
 
91
- await cmdWeb({ port, dir, open });
93
+ await cmdWeb({ port, dir, open, dev });
92
94
  return;
93
95
  }
94
96
 
package/cli/web.js CHANGED
@@ -17,19 +17,33 @@ function guessOpenCmd() {
17
17
  }
18
18
 
19
19
  function exists(p) {
20
- try { fs.accessSync(p); return true; } catch { return false; }
20
+ try {
21
+ fs.accessSync(p);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
21
26
  }
22
27
 
23
28
  function newestFile(dir, prefix) {
24
29
  if (!exists(dir)) return null;
25
- const files = fs.readdirSync(dir)
30
+ const files = fs
31
+ .readdirSync(dir)
26
32
  .filter((f) => f.startsWith(prefix) && f.endsWith('.md'))
27
33
  .map((f) => ({ f, p: path.join(dir, f) }))
28
34
  .filter((x) => {
29
- try { return fs.statSync(x.p).isFile(); } catch { return false; }
35
+ try {
36
+ return fs.statSync(x.p).isFile();
37
+ } catch {
38
+ return false;
39
+ }
30
40
  })
31
41
  .sort((a, b) => {
32
- try { return fs.statSync(b.p).mtimeMs - fs.statSync(a.p).mtimeMs; } catch { return 0; }
42
+ try {
43
+ return fs.statSync(b.p).mtimeMs - fs.statSync(a.p).mtimeMs;
44
+ } catch {
45
+ return 0;
46
+ }
33
47
  });
34
48
  return files[0]?.p || null;
35
49
  }
@@ -58,8 +72,12 @@ function run(cmd, args, cwd) {
58
72
  const child = spawn(cmd, args, { cwd, shell: false, env: process.env });
59
73
  let stdout = '';
60
74
  let stderr = '';
61
- child.stdout.on('data', (d) => { stdout += d.toString(); });
62
- child.stderr.on('data', (d) => { stderr += d.toString(); });
75
+ child.stdout.on('data', (d) => {
76
+ stdout += d.toString();
77
+ });
78
+ child.stderr.on('data', (d) => {
79
+ stderr += d.toString();
80
+ });
63
81
  child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
64
82
  });
65
83
  }
@@ -73,109 +91,410 @@ function openBrowser(url) {
73
91
  }
74
92
  }
75
93
 
94
+ async function pickDirectoryNative() {
95
+ if (process.platform === 'win32') {
96
+ // PowerShell FolderBrowserDialog
97
+ const ps = [
98
+ '-NoProfile',
99
+ '-Command',
100
+ [
101
+ 'Add-Type -AssemblyName System.Windows.Forms;',
102
+ '$f = New-Object System.Windows.Forms.FolderBrowserDialog;',
103
+ "$f.Description = 'Select your FREYA workspace folder';",
104
+ '$f.ShowNewFolderButton = $true;',
105
+ 'if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.SelectedPath }'
106
+ ].join(' ')
107
+ ];
108
+ const r = await run('powershell', ps, process.cwd());
109
+ if (r.code !== 0) throw new Error((r.stderr || r.stdout || '').trim() || 'Failed to open folder picker');
110
+ const out = (r.stdout || '').trim();
111
+ return out || null;
112
+ }
113
+
114
+ if (process.platform === 'darwin') {
115
+ const r = await run('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select your FREYA workspace folder")'], process.cwd());
116
+ if (r.code !== 0) throw new Error((r.stderr || r.stdout || '').trim() || 'Failed to open folder picker');
117
+ const out = (r.stdout || '').trim();
118
+ return out || null;
119
+ }
120
+
121
+ // Linux: prefer zenity, then kdialog
122
+ if (exists('/usr/bin/zenity') || exists('/bin/zenity') || exists('/usr/local/bin/zenity')) {
123
+ const r = await run('zenity', ['--file-selection', '--directory', '--title=Select your FREYA workspace folder'], process.cwd());
124
+ if (r.code !== 0) return null;
125
+ return (r.stdout || '').trim() || null;
126
+ }
127
+ if (exists('/usr/bin/kdialog') || exists('/bin/kdialog') || exists('/usr/local/bin/kdialog')) {
128
+ const r = await run('kdialog', ['--getexistingdirectory', '.', 'Select your FREYA workspace folder'], process.cwd());
129
+ if (r.code !== 0) return null;
130
+ return (r.stdout || '').trim() || null;
131
+ }
132
+
133
+ return null;
134
+ }
135
+
76
136
  function html() {
137
+ // Aesthetic: “Noir control room” — dark glass, crisp typography, intentional hierarchy.
77
138
  return `<!doctype html>
78
139
  <html>
79
140
  <head>
80
141
  <meta charset="utf-8" />
81
142
  <meta name="viewport" content="width=device-width, initial-scale=1" />
82
- <title>FREYA</title>
143
+ <title>FREYA Web</title>
83
144
  <style>
84
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Helvetica, Arial; margin: 0; background: #0b0f14; color: #e6edf3; }
85
- header { padding: 16px 18px; border-bottom: 1px solid #1f2a37; display:flex; gap:12px; align-items:center; }
86
- header h1 { font-size: 14px; margin:0; letter-spacing: .08em; text-transform: uppercase; color:#9fb2c7; }
87
- .wrap { max-width: 980px; margin: 0 auto; padding: 18px; }
88
- .card { border: 1px solid #1f2a37; border-radius: 12px; background: #0f1620; padding: 14px; }
89
- .grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
90
- @media (min-width: 920px) { .grid { grid-template-columns: 1.2fr .8fr; } }
91
- label { font-size: 12px; color:#9fb2c7; display:block; margin-bottom: 6px; }
92
- input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid #223041; background:#0b1220; color:#e6edf3; }
93
- .btns { display:flex; flex-wrap:wrap; gap: 10px; margin-top: 10px; }
94
- button { padding: 10px 12px; border-radius: 10px; border: 1px solid #223041; background:#1f6feb; color:white; cursor:pointer; }
95
- button.secondary { background: transparent; color:#e6edf3; }
96
- button.danger { background: #d73a49; }
97
- .row { display:grid; grid-template-columns: 1fr; gap: 10px; }
98
- .small { font-size: 12px; color:#9fb2c7; }
99
- pre { white-space: pre-wrap; background:#0b1220; border: 1px solid #223041; border-radius: 12px; padding: 12px; overflow:auto; }
100
- a { color:#58a6ff; }
145
+ :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);
151
+ --text: #e9f0ff;
152
+ --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;
159
+ --shadow: 0 30px 70px rgba(0,0,0,.55);
160
+ --radius: 16px;
161
+ }
162
+
163
+ * { box-sizing: border-box; }
164
+ html, body { height: 100%; }
165
+ body {
166
+ margin: 0;
167
+ color: var(--text);
168
+ 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;
175
+ }
176
+
177
+ /* subtle noise */
178
+ body:before {
179
+ content: "";
180
+ position: fixed;
181
+ inset: 0;
182
+ pointer-events: none;
183
+ 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;
188
+ mix-blend-mode: overlay;
189
+ opacity: .12;
190
+ }
191
+
192
+ header {
193
+ 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);
199
+ }
200
+
201
+ .top {
202
+ max-width: 1140px;
203
+ margin: 0 auto;
204
+ padding: 14px 18px;
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: space-between;
208
+ gap: 16px;
209
+ }
210
+
211
+ .brand {
212
+ display: flex;
213
+ align-items: baseline;
214
+ gap: 12px;
215
+ letter-spacing: .16em;
216
+ text-transform: uppercase;
217
+ font-weight: 700;
218
+ font-size: 12px;
219
+ color: var(--muted);
220
+ }
221
+
222
+ .badge {
223
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
224
+ font-size: 12px;
225
+ padding: 6px 10px;
226
+ border-radius: 999px;
227
+ border: 1px solid var(--line);
228
+ background: rgba(255,255,255,.03);
229
+ color: var(--faint);
230
+ }
231
+
232
+ .wrap {
233
+ max-width: 1140px;
234
+ margin: 0 auto;
235
+ padding: 18px;
236
+ }
237
+
238
+ .hero {
239
+ display: grid;
240
+ grid-template-columns: 1.2fr .8fr;
241
+ gap: 16px;
242
+ align-items: start;
243
+ margin-bottom: 16px;
244
+ }
245
+
246
+ @media (max-width: 980px) {
247
+ .hero { grid-template-columns: 1fr; }
248
+ }
249
+
250
+ .card {
251
+ 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;
258
+ }
259
+
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;
269
+ }
270
+
271
+ .card > * { position: relative; }
272
+
273
+ h2 {
274
+ margin: 0 0 8px;
275
+ font-size: 14px;
276
+ letter-spacing: .08em;
277
+ text-transform: uppercase;
278
+ color: var(--muted);
279
+ }
280
+
281
+ .sub {
282
+ margin: 0 0 10px;
283
+ font-size: 12px;
284
+ color: var(--faint);
285
+ line-height: 1.35;
286
+ }
287
+
288
+ label {
289
+ display: block;
290
+ font-size: 12px;
291
+ color: var(--muted);
292
+ margin-bottom: 6px;
293
+ }
294
+
295
+ .field {
296
+ display: grid;
297
+ grid-template-columns: 1fr auto;
298
+ gap: 10px;
299
+ align-items: center;
300
+ }
301
+
302
+ input {
303
+ width: 100%;
304
+ padding: 12px 12px;
305
+ border-radius: 12px;
306
+ border: 1px solid rgba(180,210,255,.22);
307
+ background: rgba(7,10,16,.55);
308
+ color: var(--text);
309
+ outline: none;
310
+ }
311
+
312
+ input::placeholder { color: rgba(233,240,255,.38); }
313
+
314
+ .btns {
315
+ display: flex;
316
+ flex-wrap: wrap;
317
+ gap: 10px;
318
+ margin-top: 10px;
319
+ }
320
+
321
+ button {
322
+ border: 1px solid rgba(180,210,255,.22);
323
+ border-radius: 12px;
324
+ background: rgba(255,255,255,.04);
325
+ color: var(--text);
326
+ padding: 10px 12px;
327
+ cursor: pointer;
328
+ transition: transform .08s ease, background .16s ease, border-color .16s ease;
329
+ font-weight: 600;
330
+ letter-spacing: .01em;
331
+ }
332
+
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); }
335
+
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);
339
+ }
340
+
341
+ .ghost { background: rgba(255,255,255,.02); }
342
+
343
+ .danger { border-color: rgba(251,113,133,.45); background: rgba(251,113,133,.12); }
344
+
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);
355
+ }
356
+
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); }
360
+
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; } }
367
+
368
+ .hr { height: 1px; background: rgba(180,210,255,.14); margin: 12px 0; }
369
+
370
+ .log {
371
+ border-radius: 14px;
372
+ border: 1px solid rgba(180,210,255,.18);
373
+ background: rgba(7,10,16,.55);
374
+ 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;
379
+ font-size: 12px;
380
+ line-height: 1.35;
381
+ white-space: pre-wrap;
382
+ color: rgba(233,240,255,.84);
383
+ }
384
+
385
+ .hint {
386
+ font-size: 12px;
387
+ color: var(--faint);
388
+ margin-top: 6px;
389
+ line-height: 1.35;
390
+ }
391
+
392
+ .footer {
393
+ margin-top: 12px;
394
+ font-size: 12px;
395
+ color: rgba(233,240,255,.45);
396
+ }
397
+
398
+ a { color: var(--accent2); text-decoration: none; }
399
+ a:hover { text-decoration: underline; }
101
400
  </style>
102
401
  </head>
103
402
  <body>
104
403
  <header>
105
- <h1>FREYA • Local-first Status Assistant</h1>
106
- <span class="small" id="status"></span>
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>
107
408
  </header>
409
+
108
410
  <div class="wrap">
109
- <div class="grid">
411
+ <div class="hero">
110
412
  <div class="card">
111
- <div class="row">
112
- <div>
113
- <label>Workspace dir</label>
114
- <input id="dir" placeholder="/path/to/freya (or ./freya)" />
115
- <div class="small">Dica: a workspace é a pasta que contém <code>data/</code>, <code>logs/</code>, <code>scripts/</code>.</div>
116
- </div>
117
- <div class="btns">
118
- <button onclick="doInit()">Init (preserva data/logs)</button>
119
- <button class="secondary" onclick="doUpdate()">Update (init --here)</button>
120
- <button class="secondary" onclick="doHealth()">Health</button>
121
- <button class="secondary" onclick="doMigrate()">Migrate</button>
122
- </div>
123
- </div>
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>
124
415
 
125
- <hr style="border:0;border-top:1px solid #1f2a37;margin:14px 0" />
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>
126
422
 
127
423
  <div class="btns">
128
- <button onclick="runReport('status')">Generate Executive Report</button>
129
- <button onclick="runReport('sm-weekly')">Generate SM Weekly</button>
130
- <button onclick="runReport('blockers')">Generate Blockers</button>
131
- <button onclick="runReport('daily')">Generate Daily</button>
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>
132
428
  </div>
133
429
 
134
- <div style="margin-top:12px">
135
- <label>Output</label>
136
- <pre id="out"></pre>
137
- <div class="small" id="last"></div>
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://..." />
443
+
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>
138
448
  </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>
139
450
  </div>
451
+ </div>
140
452
 
453
+ <div class="two">
141
454
  <div class="card">
142
- <div class="row">
143
- <div>
144
- <label>Discord Webhook URL (optional)</label>
145
- <input id="discord" placeholder="https://discord.com/api/webhooks/..." />
146
- </div>
147
- <div>
148
- <label>Teams Webhook URL (optional)</label>
149
- <input id="teams" placeholder="https://..." />
150
- </div>
151
- <div class="btns">
152
- <button class="secondary" onclick="publish('discord')">Publish last → Discord</button>
153
- <button class="secondary" onclick="publish('teams')">Publish last → Teams</button>
154
- </div>
155
- <div class="small">
156
- Publica o último relatório gerado (cache local). Limite: ~1800 chars (pra evitar limites de webhook).
157
- </div>
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>
457
+
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>
158
463
  </div>
464
+
465
+ <div class="hint" id="last"></div>
159
466
  </div>
160
467
 
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>
161
475
  </div>
162
476
  </div>
163
477
 
164
478
  <script>
165
479
  const $ = (id) => document.getElementById(id);
166
- const state = {
167
- lastReportPath: null,
168
- lastText: ''
169
- };
480
+ const state = { lastReportPath: null, lastText: '' };
481
+
482
+ function setPill(kind, text) {
483
+ const dot = $('dot');
484
+ dot.classList.remove('ok','err');
485
+ if (kind === 'ok') dot.classList.add('ok');
486
+ if (kind === 'err') dot.classList.add('err');
487
+ $('pill').textContent = text;
488
+ }
170
489
 
171
490
  function setOut(text) {
172
- state.lastText = text;
491
+ state.lastText = text || '';
173
492
  $('out').textContent = text || '';
174
493
  }
175
494
 
176
- function setLast(path) {
177
- state.lastReportPath = path;
178
- $('last').textContent = path ? ('Last report: ' + path) : '';
495
+ function setLast(p) {
496
+ state.lastReportPath = p;
497
+ $('last').textContent = p ? ('Last report: ' + p) : '';
179
498
  }
180
499
 
181
500
  function saveLocal() {
@@ -185,13 +504,13 @@ function html() {
185
504
  }
186
505
 
187
506
  function loadLocal() {
188
- $('dir').value = localStorage.getItem('freya.dir') || '';
507
+ $('dir').value = localStorage.getItem('freya.dir') || './freya';
189
508
  $('discord').value = localStorage.getItem('freya.discord') || '';
190
509
  $('teams').value = localStorage.getItem('freya.teams') || '';
191
510
  }
192
511
 
193
- async function api(path, body) {
194
- const res = await fetch(path, {
512
+ async function api(p, body) {
513
+ const res = await fetch(p, {
195
514
  method: body ? 'POST' : 'GET',
196
515
  headers: body ? { 'Content-Type': 'application/json' } : {},
197
516
  body: body ? JSON.stringify(body) : undefined
@@ -206,63 +525,198 @@ function html() {
206
525
  return d || './freya';
207
526
  }
208
527
 
528
+ async function pickDir() {
529
+ try {
530
+ setPill('run','opening picker…');
531
+ const r = await api('/api/pick-dir', {});
532
+ if (r && r.dir) $('dir').value = r.dir;
533
+ saveLocal();
534
+ setPill('ok','ready');
535
+ } catch (e) {
536
+ setPill('err','picker unavailable');
537
+ setOut(String(e && e.message ? e.message : e));
538
+ }
539
+ }
540
+
209
541
  async function doInit() {
210
- saveLocal();
211
- setOut('Running init...');
212
- const r = await api('/api/init', { dir: dirOrDefault() });
213
- setOut(r.output);
214
- setLast(null);
542
+ try {
543
+ saveLocal();
544
+ setPill('run','init');
545
+ setOut('');
546
+ const r = await api('/api/init', { dir: dirOrDefault() });
547
+ setOut(r.output);
548
+ setLast(null);
549
+ setPill('ok','init ok');
550
+ } catch (e) {
551
+ setPill('err','init failed');
552
+ setOut(String(e && e.message ? e.message : e));
553
+ }
215
554
  }
216
555
 
217
556
  async function doUpdate() {
218
- saveLocal();
219
- setOut('Running update...');
220
- const r = await api('/api/update', { dir: dirOrDefault() });
221
- setOut(r.output);
222
- setLast(null);
557
+ try {
558
+ saveLocal();
559
+ setPill('run','update');
560
+ setOut('');
561
+ const r = await api('/api/update', { dir: dirOrDefault() });
562
+ setOut(r.output);
563
+ setLast(null);
564
+ setPill('ok','update ok');
565
+ } catch (e) {
566
+ setPill('err','update failed');
567
+ setOut(String(e && e.message ? e.message : e));
568
+ }
223
569
  }
224
570
 
225
571
  async function doHealth() {
226
- saveLocal();
227
- setOut('Running health...');
228
- const r = await api('/api/health', { dir: dirOrDefault() });
229
- setOut(r.output);
230
- setLast(null);
572
+ try {
573
+ saveLocal();
574
+ setPill('run','health');
575
+ setOut('');
576
+ const r = await api('/api/health', { dir: dirOrDefault() });
577
+ setOut(r.output);
578
+ setLast(null);
579
+ setPill('ok','health ok');
580
+ } catch (e) {
581
+ setPill('err','health failed');
582
+ setOut(String(e && e.message ? e.message : e));
583
+ }
231
584
  }
232
585
 
233
586
  async function doMigrate() {
234
- saveLocal();
235
- setOut('Running migrate...');
236
- const r = await api('/api/migrate', { dir: dirOrDefault() });
237
- setOut(r.output);
238
- setLast(null);
587
+ try {
588
+ saveLocal();
589
+ setPill('run','migrate');
590
+ setOut('');
591
+ const r = await api('/api/migrate', { dir: dirOrDefault() });
592
+ setOut(r.output);
593
+ setLast(null);
594
+ setPill('ok','migrate ok');
595
+ } catch (e) {
596
+ setPill('err','migrate failed');
597
+ setOut(String(e && e.message ? e.message : e));
598
+ }
239
599
  }
240
600
 
241
601
  async function runReport(name) {
242
- saveLocal();
243
- setOut('Running ' + name + '...');
244
- const r = await api('/api/report', { dir: dirOrDefault(), script: name });
245
- setOut(r.output);
246
- setLast(r.reportPath || null);
602
+ try {
603
+ saveLocal();
604
+ setPill('run', name + '…');
605
+ setOut('');
606
+ const r = await api('/api/report', { dir: dirOrDefault(), script: name });
607
+ setOut(r.output);
608
+ setLast(r.reportPath || null);
609
+ if (r.reportText) state.lastText = r.reportText;
610
+ setPill('ok', name + ' ok');
611
+ } catch (e) {
612
+ setPill('err', name + ' failed');
613
+ setOut(String(e && e.message ? e.message : e));
614
+ }
247
615
  }
248
616
 
249
617
  async function publish(target) {
250
- saveLocal();
251
- if (!state.lastText) throw new Error('No cached output. Generate a report first.');
252
- const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
253
- setOut('Publishing to ' + target + '...');
254
- const r = await api('/api/publish', { webhookUrl, text: state.lastText });
255
- setOut('Published.');
618
+ try {
619
+ saveLocal();
620
+ if (!state.lastText) throw new Error('Generate a report first.');
621
+ 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…');
624
+ await api('/api/publish', { webhookUrl, text: state.lastText });
625
+ setPill('ok','published');
626
+ } catch (e) {
627
+ setPill('err','publish failed');
628
+ setOut(String(e && e.message ? e.message : e));
629
+ }
256
630
  }
257
631
 
258
632
  loadLocal();
259
- $('status').textContent = 'ready';
260
633
  </script>
261
634
  </body>
262
635
  </html>`;
263
636
  }
264
637
 
265
- async function cmdWeb({ port, dir, open }) {
638
+ function ensureDir(p) {
639
+ fs.mkdirSync(p, { recursive: true });
640
+ }
641
+
642
+ function isoDate(d = new Date()) {
643
+ return d.toISOString().slice(0, 10);
644
+ }
645
+
646
+ function isoNow() {
647
+ return new Date().toISOString();
648
+ }
649
+
650
+ function seedDevWorkspace(workspaceDir) {
651
+ // Only create if missing; never overwrite user content.
652
+ ensureDir(path.join(workspaceDir, 'data', 'tasks'));
653
+ ensureDir(path.join(workspaceDir, 'data', 'career'));
654
+ ensureDir(path.join(workspaceDir, 'data', 'blockers'));
655
+ ensureDir(path.join(workspaceDir, 'data', 'Clients', 'acme', 'rocket'));
656
+ ensureDir(path.join(workspaceDir, 'logs', 'daily'));
657
+
658
+ const taskLog = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
659
+ if (!exists(taskLog)) {
660
+ fs.writeFileSync(taskLog, JSON.stringify({
661
+ schemaVersion: 1,
662
+ tasks: [
663
+ { id: 't-demo-1', description: 'Preparar update executivo', category: 'DO_NOW', status: 'PENDING', createdAt: isoNow(), priority: 'high' },
664
+ { id: 't-demo-2', description: 'Revisar PR de integração Teams', category: 'SCHEDULE', status: 'PENDING', createdAt: isoNow(), priority: 'medium' },
665
+ { id: 't-demo-3', description: 'Rodar retro e registrar aprendizados', category: 'DO_NOW', status: 'COMPLETED', createdAt: isoNow(), completedAt: isoNow(), priority: 'low' }
666
+ ]
667
+ }, null, 2) + '\n', 'utf8');
668
+ }
669
+
670
+ const careerLog = path.join(workspaceDir, 'data', 'career', 'career-log.json');
671
+ if (!exists(careerLog)) {
672
+ fs.writeFileSync(careerLog, JSON.stringify({
673
+ schemaVersion: 1,
674
+ entries: [
675
+ { id: 'c-demo-1', date: isoDate(), type: 'Achievement', description: 'Publicou o CLI @cccarv82/freya com init/web', tags: ['shipping', 'tooling'], source: 'dev-seed' },
676
+ { id: 'c-demo-2', date: isoDate(), type: 'Feedback', description: 'Feedback: UX do painel web está “muito promissor”', tags: ['product'], source: 'dev-seed' }
677
+ ]
678
+ }, null, 2) + '\n', 'utf8');
679
+ }
680
+
681
+ const blockerLog = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
682
+ if (!exists(blockerLog)) {
683
+ fs.writeFileSync(blockerLog, JSON.stringify({
684
+ schemaVersion: 1,
685
+ blockers: [
686
+ { id: 'b-demo-1', title: 'Webhook do Teams falhando em ambientes com 2FA', description: 'Ajustar token / payload', createdAt: isoNow(), status: 'OPEN', severity: 'HIGH', nextAction: 'Validar payload e limites' },
687
+ { id: 'b-demo-2', title: 'Definir template de status report por cliente', description: 'Padronizar headings', createdAt: isoNow(), status: 'MITIGATING', severity: 'MEDIUM' }
688
+ ]
689
+ }, null, 2) + '\n', 'utf8');
690
+ }
691
+
692
+ const projectStatus = path.join(workspaceDir, 'data', 'Clients', 'acme', 'rocket', 'status.json');
693
+ if (!exists(projectStatus)) {
694
+ fs.writeFileSync(projectStatus, JSON.stringify({
695
+ schemaVersion: 1,
696
+ client: 'Acme',
697
+ project: 'Rocket',
698
+ currentStatus: 'Green — progressing as planned',
699
+ lastUpdated: isoNow(),
700
+ active: true,
701
+ history: [
702
+ { date: isoNow(), type: 'Status', content: 'Launched stage 1', source: 'dev-seed' },
703
+ { date: isoNow(), type: 'Risk', content: 'Potential delay on vendor dependency', source: 'dev-seed' }
704
+ ]
705
+ }, null, 2) + '\n', 'utf8');
706
+ }
707
+
708
+ const dailyLog = path.join(workspaceDir, 'logs', 'daily', `${isoDate()}.md`);
709
+ if (!exists(dailyLog)) {
710
+ fs.writeFileSync(dailyLog, `# Daily Log ${isoDate()}\n\n## [09:15] Raw Input\nReunião com a Acme. Tudo verde, mas preciso alinhar com fornecedor.\n\n## [16:40] Raw Input\nTerminei o relatório SM e publiquei no Discord.\n`, 'utf8');
711
+ }
712
+
713
+ return {
714
+ seeded: true,
715
+ paths: { taskLog, careerLog, blockerLog, projectStatus, dailyLog }
716
+ };
717
+ }
718
+
719
+ async function cmdWeb({ port, dir, open, dev }) {
266
720
  const host = '127.0.0.1';
267
721
 
268
722
  const server = http.createServer(async (req, res) => {
@@ -282,6 +736,11 @@ async function cmdWeb({ port, dir, open }) {
282
736
 
283
737
  const workspaceDir = path.resolve(process.cwd(), payload.dir || dir || './freya');
284
738
 
739
+ if (req.url === '/api/pick-dir') {
740
+ const picked = await pickDirectoryNative();
741
+ return safeJson(res, 200, { dir: picked });
742
+ }
743
+
285
744
  if (req.url === '/api/init') {
286
745
  const pkg = '@cccarv82/freya';
287
746
  const r = await run('npx', [pkg, 'init', workspaceDir], process.cwd());
@@ -323,8 +782,12 @@ async function cmdWeb({ port, dir, open }) {
323
782
  };
324
783
  const prefix = prefixMap[script] || null;
325
784
  const reportPath = prefix ? newestFile(reportsDir, prefix) : null;
785
+ const reportText = reportPath && exists(reportPath) ? fs.readFileSync(reportPath, 'utf8') : null;
786
+
787
+ // Prefer showing the actual report content when available.
788
+ const output = reportText ? reportText : out;
326
789
 
327
- return safeJson(res, r.code === 0 ? 200 : 400, { output: out, reportPath });
790
+ return safeJson(res, r.code === 0 ? 200 : 400, { output, reportPath, reportText });
328
791
  }
329
792
 
330
793
  if (req.url === '/api/publish') {
@@ -348,7 +811,8 @@ async function cmdWeb({ port, dir, open }) {
348
811
  }
349
812
  };
350
813
 
351
- const req2 = require(u.protocol === 'https:' ? 'https' : 'http').request(options, (r2) => {
814
+ const proto = u.protocol === 'https:' ? require('https') : require('http');
815
+ const req2 = proto.request(options, (r2) => {
352
816
  const chunks = [];
353
817
  r2.on('data', (c) => chunks.push(c));
354
818
  r2.on('end', () => {
@@ -374,6 +838,18 @@ async function cmdWeb({ port, dir, open }) {
374
838
  await new Promise((resolve) => server.listen(port, host, resolve));
375
839
 
376
840
  const url = `http://${host}:${port}/`;
841
+
842
+ // Optional dev seed (safe: only creates files if missing)
843
+ if (dev) {
844
+ const target = dir ? path.resolve(process.cwd(), dir) : path.join(process.cwd(), 'freya');
845
+ try {
846
+ seedDevWorkspace(target);
847
+ process.stdout.write(`Dev seed: created demo files in ${target}\n`);
848
+ } catch (e) {
849
+ process.stdout.write(`Dev seed failed: ${e.message || String(e)}\n`);
850
+ }
851
+ }
852
+
377
853
  process.stdout.write(`FREYA web running at ${url}\n`);
378
854
  if (open) openBrowser(url);
379
855
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",