@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.
- package/cli/index.js +4 -2
- package/cli/web.js +587 -111
- 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 {
|
|
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
|
|
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 {
|
|
35
|
+
try {
|
|
36
|
+
return fs.statSync(x.p).isFile();
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
30
40
|
})
|
|
31
41
|
.sort((a, b) => {
|
|
32
|
-
try {
|
|
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) => {
|
|
62
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
<
|
|
106
|
-
|
|
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="
|
|
411
|
+
<div class="hero">
|
|
110
412
|
<div class="card">
|
|
111
|
-
<
|
|
112
|
-
|
|
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
|
-
<
|
|
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="
|
|
129
|
-
<button onclick="
|
|
130
|
-
<button onclick="
|
|
131
|
-
<button onclick="
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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(
|
|
177
|
-
state.lastReportPath =
|
|
178
|
-
$('last').textContent =
|
|
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(
|
|
194
|
-
const res = await fetch(
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|