@cccarv82/freya 1.0.4 → 1.0.5
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 +37 -6
- package/cli/web.js +381 -0
- package/package.json +2 -2
package/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
const { cmdInit } = require('./init');
|
|
6
|
+
const { cmdWeb } = require('./web');
|
|
6
7
|
|
|
7
8
|
function usage() {
|
|
8
9
|
return `
|
|
@@ -10,6 +11,7 @@ freya - F.R.E.Y.A. CLI
|
|
|
10
11
|
|
|
11
12
|
Usage:
|
|
12
13
|
freya init [dir] [--force] [--here|--in-place] [--force-data] [--force-logs]
|
|
14
|
+
freya web [--port <n>] [--dir <path>] [--no-open]
|
|
13
15
|
|
|
14
16
|
Defaults:
|
|
15
17
|
- If no [dir] is provided, creates ./freya
|
|
@@ -22,23 +24,37 @@ Examples:
|
|
|
22
24
|
freya init --here --force # update agents/scripts, keep data/logs
|
|
23
25
|
freya init --here --force-data # overwrite data/ too (danger)
|
|
24
26
|
npx @cccarv82/freya init
|
|
27
|
+
|
|
28
|
+
freya web
|
|
29
|
+
freya web --dir ./freya --port 3872
|
|
25
30
|
`;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
function parseArgs(argv) {
|
|
29
34
|
const args = [];
|
|
30
35
|
const flags = new Set();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
const kv = {};
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const a = argv[i];
|
|
40
|
+
if (a.startsWith('--')) {
|
|
41
|
+
const next = argv[i + 1];
|
|
42
|
+
if (next && !next.startsWith('--')) {
|
|
43
|
+
kv[a] = next;
|
|
44
|
+
i++;
|
|
45
|
+
} else {
|
|
46
|
+
flags.add(a);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
args.push(a);
|
|
50
|
+
}
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
return { args, flags };
|
|
53
|
+
return { args, flags, kv };
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
async function run(argv) {
|
|
41
|
-
const { args, flags } = parseArgs(argv);
|
|
57
|
+
const { args, flags, kv } = parseArgs(argv);
|
|
42
58
|
const command = args[0];
|
|
43
59
|
|
|
44
60
|
if (!command || command === 'help' || flags.has('--help') || flags.has('-h')) {
|
|
@@ -61,6 +77,21 @@ async function run(argv) {
|
|
|
61
77
|
return;
|
|
62
78
|
}
|
|
63
79
|
|
|
80
|
+
if (command === 'web') {
|
|
81
|
+
const port = Number(kv['--port'] || 3872);
|
|
82
|
+
const dir = kv['--dir'] ? path.resolve(process.cwd(), kv['--dir']) : null;
|
|
83
|
+
const open = !flags.has('--no-open');
|
|
84
|
+
|
|
85
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
86
|
+
process.stderr.write('Invalid --port\n');
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await cmdWeb({ port, dir, open });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
process.stderr.write(`Unknown command: ${command}\n`);
|
|
65
96
|
process.stdout.write(usage());
|
|
66
97
|
process.exitCode = 1;
|
package/cli/web.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function guessNpmCmd() {
|
|
9
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function guessOpenCmd() {
|
|
13
|
+
// Minimal cross-platform opener without extra deps
|
|
14
|
+
if (process.platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', ''] };
|
|
15
|
+
if (process.platform === 'darwin') return { cmd: 'open', args: [] };
|
|
16
|
+
return { cmd: 'xdg-open', args: [] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exists(p) {
|
|
20
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function newestFile(dir, prefix) {
|
|
24
|
+
if (!exists(dir)) return null;
|
|
25
|
+
const files = fs.readdirSync(dir)
|
|
26
|
+
.filter((f) => f.startsWith(prefix) && f.endsWith('.md'))
|
|
27
|
+
.map((f) => ({ f, p: path.join(dir, f) }))
|
|
28
|
+
.filter((x) => {
|
|
29
|
+
try { return fs.statSync(x.p).isFile(); } catch { return false; }
|
|
30
|
+
})
|
|
31
|
+
.sort((a, b) => {
|
|
32
|
+
try { return fs.statSync(b.p).mtimeMs - fs.statSync(a.p).mtimeMs; } catch { return 0; }
|
|
33
|
+
});
|
|
34
|
+
return files[0]?.p || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function safeJson(res, code, obj) {
|
|
38
|
+
const body = JSON.stringify(obj);
|
|
39
|
+
res.writeHead(code, {
|
|
40
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
41
|
+
'Cache-Control': 'no-store',
|
|
42
|
+
'Content-Length': Buffer.byteLength(body)
|
|
43
|
+
});
|
|
44
|
+
res.end(body);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readBody(req) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
req.on('data', (c) => chunks.push(c));
|
|
51
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
52
|
+
req.on('error', reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function run(cmd, args, cwd) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const child = spawn(cmd, args, { cwd, shell: false, env: process.env });
|
|
59
|
+
let stdout = '';
|
|
60
|
+
let stderr = '';
|
|
61
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
62
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
63
|
+
child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function openBrowser(url) {
|
|
68
|
+
const { cmd, args } = guessOpenCmd();
|
|
69
|
+
try {
|
|
70
|
+
spawn(cmd, [...args, url], { detached: true, stdio: 'ignore' }).unref();
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function html() {
|
|
77
|
+
return `<!doctype html>
|
|
78
|
+
<html>
|
|
79
|
+
<head>
|
|
80
|
+
<meta charset="utf-8" />
|
|
81
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
82
|
+
<title>FREYA</title>
|
|
83
|
+
<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; }
|
|
101
|
+
</style>
|
|
102
|
+
</head>
|
|
103
|
+
<body>
|
|
104
|
+
<header>
|
|
105
|
+
<h1>FREYA • Local-first Status Assistant</h1>
|
|
106
|
+
<span class="small" id="status"></span>
|
|
107
|
+
</header>
|
|
108
|
+
<div class="wrap">
|
|
109
|
+
<div class="grid">
|
|
110
|
+
<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>
|
|
124
|
+
|
|
125
|
+
<hr style="border:0;border-top:1px solid #1f2a37;margin:14px 0" />
|
|
126
|
+
|
|
127
|
+
<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>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div style="margin-top:12px">
|
|
135
|
+
<label>Output</label>
|
|
136
|
+
<pre id="out"></pre>
|
|
137
|
+
<div class="small" id="last"></div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<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>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<script>
|
|
165
|
+
const $ = (id) => document.getElementById(id);
|
|
166
|
+
const state = {
|
|
167
|
+
lastReportPath: null,
|
|
168
|
+
lastText: ''
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function setOut(text) {
|
|
172
|
+
state.lastText = text;
|
|
173
|
+
$('out').textContent = text || '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setLast(path) {
|
|
177
|
+
state.lastReportPath = path;
|
|
178
|
+
$('last').textContent = path ? ('Last report: ' + path) : '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function saveLocal() {
|
|
182
|
+
localStorage.setItem('freya.dir', $('dir').value);
|
|
183
|
+
localStorage.setItem('freya.discord', $('discord').value);
|
|
184
|
+
localStorage.setItem('freya.teams', $('teams').value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function loadLocal() {
|
|
188
|
+
$('dir').value = localStorage.getItem('freya.dir') || '';
|
|
189
|
+
$('discord').value = localStorage.getItem('freya.discord') || '';
|
|
190
|
+
$('teams').value = localStorage.getItem('freya.teams') || '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function api(path, body) {
|
|
194
|
+
const res = await fetch(path, {
|
|
195
|
+
method: body ? 'POST' : 'GET',
|
|
196
|
+
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
197
|
+
body: body ? JSON.stringify(body) : undefined
|
|
198
|
+
});
|
|
199
|
+
const json = await res.json();
|
|
200
|
+
if (!res.ok) throw new Error(json.error || 'Request failed');
|
|
201
|
+
return json;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dirOrDefault() {
|
|
205
|
+
const d = $('dir').value.trim();
|
|
206
|
+
return d || './freya';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
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);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
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);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
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);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
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);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
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);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
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.');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
loadLocal();
|
|
259
|
+
$('status').textContent = 'ready';
|
|
260
|
+
</script>
|
|
261
|
+
</body>
|
|
262
|
+
</html>`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function cmdWeb({ port, dir, open }) {
|
|
266
|
+
const host = '127.0.0.1';
|
|
267
|
+
|
|
268
|
+
const server = http.createServer(async (req, res) => {
|
|
269
|
+
try {
|
|
270
|
+
if (!req.url) return safeJson(res, 404, { error: 'Not found' });
|
|
271
|
+
|
|
272
|
+
if (req.method === 'GET' && req.url === '/') {
|
|
273
|
+
const body = html();
|
|
274
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
275
|
+
res.end(body);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (req.url.startsWith('/api/')) {
|
|
280
|
+
const raw = await readBody(req);
|
|
281
|
+
const payload = raw ? JSON.parse(raw) : {};
|
|
282
|
+
|
|
283
|
+
const workspaceDir = path.resolve(process.cwd(), payload.dir || dir || './freya');
|
|
284
|
+
|
|
285
|
+
if (req.url === '/api/init') {
|
|
286
|
+
const pkg = '@cccarv82/freya';
|
|
287
|
+
const r = await run('npx', [pkg, 'init', workspaceDir], process.cwd());
|
|
288
|
+
return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (req.url === '/api/update') {
|
|
292
|
+
const pkg = '@cccarv82/freya';
|
|
293
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
294
|
+
const r = await run('npx', [pkg, 'init', '--here'], workspaceDir);
|
|
295
|
+
return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const npmCmd = guessNpmCmd();
|
|
299
|
+
|
|
300
|
+
if (req.url === '/api/health') {
|
|
301
|
+
const r = await run(npmCmd, ['run', 'health'], workspaceDir);
|
|
302
|
+
return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (req.url === '/api/migrate') {
|
|
306
|
+
const r = await run(npmCmd, ['run', 'migrate'], workspaceDir);
|
|
307
|
+
return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (req.url === '/api/report') {
|
|
311
|
+
const script = payload.script;
|
|
312
|
+
if (!script) return safeJson(res, 400, { error: 'Missing script' });
|
|
313
|
+
|
|
314
|
+
const r = await run(npmCmd, ['run', script], workspaceDir);
|
|
315
|
+
const out = (r.stdout + r.stderr).trim();
|
|
316
|
+
|
|
317
|
+
const reportsDir = path.join(workspaceDir, 'docs', 'reports');
|
|
318
|
+
const prefixMap = {
|
|
319
|
+
blockers: 'blockers-',
|
|
320
|
+
'sm-weekly': 'sm-weekly-',
|
|
321
|
+
status: 'executive-',
|
|
322
|
+
daily: null
|
|
323
|
+
};
|
|
324
|
+
const prefix = prefixMap[script] || null;
|
|
325
|
+
const reportPath = prefix ? newestFile(reportsDir, prefix) : null;
|
|
326
|
+
|
|
327
|
+
return safeJson(res, r.code === 0 ? 200 : 400, { output: out, reportPath });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (req.url === '/api/publish') {
|
|
331
|
+
const webhookUrl = payload.webhookUrl;
|
|
332
|
+
const text = payload.text;
|
|
333
|
+
if (!webhookUrl) return safeJson(res, 400, { error: 'Missing webhookUrl' });
|
|
334
|
+
if (!text) return safeJson(res, 400, { error: 'Missing text' });
|
|
335
|
+
|
|
336
|
+
// Minimal webhook post: Discord expects {content}, Teams expects {text}
|
|
337
|
+
const u = new URL(webhookUrl);
|
|
338
|
+
const isDiscord = u.hostname.includes('discord.com') || u.hostname.includes('discordapp.com');
|
|
339
|
+
const body = JSON.stringify(isDiscord ? { content: text.slice(0, 1800) } : { text: text.slice(0, 1800) });
|
|
340
|
+
|
|
341
|
+
const options = {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
hostname: u.hostname,
|
|
344
|
+
path: u.pathname + u.search,
|
|
345
|
+
headers: {
|
|
346
|
+
'Content-Type': 'application/json',
|
|
347
|
+
'Content-Length': Buffer.byteLength(body)
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const req2 = require(u.protocol === 'https:' ? 'https' : 'http').request(options, (r2) => {
|
|
352
|
+
const chunks = [];
|
|
353
|
+
r2.on('data', (c) => chunks.push(c));
|
|
354
|
+
r2.on('end', () => {
|
|
355
|
+
if (r2.statusCode >= 200 && r2.statusCode < 300) return safeJson(res, 200, { ok: true });
|
|
356
|
+
return safeJson(res, 400, { error: `Webhook error ${r2.statusCode}: ${Buffer.concat(chunks).toString('utf8')}` });
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
req2.on('error', (e) => safeJson(res, 400, { error: e.message }));
|
|
360
|
+
req2.write(body);
|
|
361
|
+
req2.end();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return safeJson(res, 404, { error: 'Not found' });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
safeJson(res, 404, { error: 'Not found' });
|
|
369
|
+
} catch (e) {
|
|
370
|
+
safeJson(res, 500, { error: e.message || String(e) });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
375
|
+
|
|
376
|
+
const url = `http://${host}:${port}/`;
|
|
377
|
+
process.stdout.write(`FREYA web running at ${url}\n`);
|
|
378
|
+
if (open) openBrowser(url);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = { cmdWeb };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cccarv82/freya",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Personal AI Assistant with local-first persistence",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"health": "node scripts/validate-data.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"daily": "node scripts/generate-daily-summary.js",
|
|
11
11
|
"status": "node scripts/generate-executive-report.js",
|
|
12
12
|
"blockers": "node scripts/generate-blockers-report.js",
|
|
13
|
-
"test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-fs-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js"
|
|
13
|
+
"test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-fs-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [],
|
|
16
16
|
"author": "",
|