@cccarv82/freya 1.0.3 → 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/README.md +17 -0
- package/cli/index.js +45 -7
- package/cli/init.js +26 -5
- package/cli/web.js +381 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -58,6 +58,23 @@ freya init meu-projeto # cria ./meu-projeto
|
|
|
58
58
|
freya init --here # instala no diretório atual
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Atualizar uma workspace existente (sem perder dados)
|
|
62
|
+
Por padrão, ao rodar `init` em uma pasta existente, o CLI **preserva**:
|
|
63
|
+
- `data/**`
|
|
64
|
+
- `logs/**`
|
|
65
|
+
|
|
66
|
+
E atualiza/instala normalmente:
|
|
67
|
+
- `.agent/**`
|
|
68
|
+
- `scripts/**`
|
|
69
|
+
- `README.md`, `USER_GUIDE.md`
|
|
70
|
+
- `package.json` (merge de scripts)
|
|
71
|
+
|
|
72
|
+
Flags (use com cuidado):
|
|
73
|
+
```bash
|
|
74
|
+
freya init --here --force-data # permite sobrescrever data/
|
|
75
|
+
freya init --here --force-logs # permite sobrescrever logs/
|
|
76
|
+
```
|
|
77
|
+
|
|
61
78
|
## 🚀 Como Usar
|
|
62
79
|
|
|
63
80
|
1. Abra a pasta da workspace gerada (ex.: `./freya`) na **sua IDE**.
|
package/cli/index.js
CHANGED
|
@@ -3,39 +3,58 @@
|
|
|
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 `
|
|
9
10
|
freya - F.R.E.Y.A. CLI
|
|
10
11
|
|
|
11
12
|
Usage:
|
|
12
|
-
freya init [dir] [--force] [--here|--in-place]
|
|
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
|
|
18
|
+
- Preserves existing data/ and logs/ by default
|
|
16
19
|
|
|
17
20
|
Examples:
|
|
18
21
|
freya init # creates ./freya
|
|
19
22
|
freya init my-workspace # creates ./my-workspace
|
|
20
23
|
freya init --here # installs into current directory
|
|
24
|
+
freya init --here --force # update agents/scripts, keep data/logs
|
|
25
|
+
freya init --here --force-data # overwrite data/ too (danger)
|
|
21
26
|
npx @cccarv82/freya init
|
|
27
|
+
|
|
28
|
+
freya web
|
|
29
|
+
freya web --dir ./freya --port 3872
|
|
22
30
|
`;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
function parseArgs(argv) {
|
|
26
34
|
const args = [];
|
|
27
35
|
const flags = new Set();
|
|
36
|
+
const kv = {};
|
|
28
37
|
|
|
29
|
-
for (
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
return { args, flags };
|
|
53
|
+
return { args, flags, kv };
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
async function run(argv) {
|
|
38
|
-
const { args, flags } = parseArgs(argv);
|
|
57
|
+
const { args, flags, kv } = parseArgs(argv);
|
|
39
58
|
const command = args[0];
|
|
40
59
|
|
|
41
60
|
if (!command || command === 'help' || flags.has('--help') || flags.has('-h')) {
|
|
@@ -49,8 +68,27 @@ async function run(argv) {
|
|
|
49
68
|
const targetDir = args[1]
|
|
50
69
|
? path.resolve(process.cwd(), args[1])
|
|
51
70
|
: (inPlace ? process.cwd() : defaultDir);
|
|
71
|
+
|
|
52
72
|
const force = flags.has('--force');
|
|
53
|
-
|
|
73
|
+
const forceData = flags.has('--force-data');
|
|
74
|
+
const forceLogs = flags.has('--force-logs');
|
|
75
|
+
|
|
76
|
+
await cmdInit({ targetDir, force, forceData, forceLogs });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
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 });
|
|
54
92
|
return;
|
|
55
93
|
}
|
|
56
94
|
|
package/cli/init.js
CHANGED
|
@@ -36,14 +36,35 @@ function copyFile(src, dest, force) {
|
|
|
36
36
|
return { copied: true };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function
|
|
39
|
+
function isNonEmptyDir(dir) {
|
|
40
|
+
try {
|
|
41
|
+
const entries = fs.readdirSync(dir);
|
|
42
|
+
// ignore common empty markers
|
|
43
|
+
const meaningful = entries.filter((e) => e !== '.DS_Store');
|
|
44
|
+
return meaningful.length > 0;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function copyDirRecursive(srcDir, destDir, force, summary, options = {}) {
|
|
40
51
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
41
52
|
for (const ent of entries) {
|
|
42
53
|
const src = path.join(srcDir, ent.name);
|
|
43
54
|
const dest = path.join(destDir, ent.name);
|
|
44
55
|
|
|
56
|
+
// Preserve user state by default
|
|
45
57
|
if (ent.isDirectory()) {
|
|
46
|
-
|
|
58
|
+
if (ent.name === 'data' && isNonEmptyDir(dest) && !options.forceData) {
|
|
59
|
+
summary.skipped.push('data/**');
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ent.name === 'logs' && isNonEmptyDir(dest) && !options.forceLogs) {
|
|
63
|
+
summary.skipped.push('logs/**');
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
copyDirRecursive(src, dest, force, summary, options);
|
|
47
68
|
continue;
|
|
48
69
|
}
|
|
49
70
|
|
|
@@ -97,7 +118,7 @@ function ensurePackageJson(targetDir, force, summary) {
|
|
|
97
118
|
summary.updated.push('package.json');
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
async function cmdInit({ targetDir, force }) {
|
|
121
|
+
async function cmdInit({ targetDir, force, forceData = false, forceLogs = false }) {
|
|
101
122
|
const templateDir = path.join(__dirname, '..', 'templates', 'base');
|
|
102
123
|
if (!exists(templateDir)) throw new Error(`Missing template directory: ${templateDir}`);
|
|
103
124
|
|
|
@@ -105,8 +126,8 @@ async function cmdInit({ targetDir, force }) {
|
|
|
105
126
|
|
|
106
127
|
const summary = { copied: [], created: [], updated: [], skipped: [] };
|
|
107
128
|
|
|
108
|
-
// Copy template files
|
|
109
|
-
copyDirRecursive(templateDir, targetDir, force, summary);
|
|
129
|
+
// Copy template files (preserve data/logs by default)
|
|
130
|
+
copyDirRecursive(templateDir, targetDir, force, summary, { forceData, forceLogs });
|
|
110
131
|
|
|
111
132
|
// Ensure package.json has scripts
|
|
112
133
|
ensurePackageJson(targetDir, force, summary);
|
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": "",
|