wendkeep 0.70.0 → 0.71.1
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/CHANGELOG.md +29 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/en/commands/observer.md +54 -28
- package/docs/pt-BR/commands/observer.md +56 -30
- package/package.json +3 -2
- package/src/observer-memory-publish.mjs +334 -0
- package/src/observer-memory.mjs +308 -0
- package/src/observer-publish.mjs +20 -8
- package/src/observer-server.mjs +145 -15
- package/src/observer.mjs +54 -16
- package/web/observer/app.mjs +611 -0
- package/web/observer/favicon.svg +5 -0
- package/web/observer/index.html +125 -0
- package/web/observer/styles.css +229 -0
package/src/observer-publish.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { buildProjectSnapshot } from './observer-snapshot.mjs';
|
|
4
|
+
import { publishObserverMemory } from './observer-memory-publish.mjs';
|
|
4
5
|
|
|
5
6
|
const OUTBOX_REL = join('.brain', 'observer-outbox');
|
|
6
7
|
const REQUEST_TIMEOUT_MS = 500;
|
|
@@ -45,14 +46,13 @@ function removeOutbox(vaultBase, eventId) {
|
|
|
45
46
|
if (existsSync(path)) unlinkSync(path);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
async function postSnapshot(url,
|
|
49
|
+
async function postSnapshot(url, event) {
|
|
49
50
|
const controller = new AbortController();
|
|
50
51
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
51
52
|
try {
|
|
52
53
|
const response = await fetch(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(event.project_id)}/snapshot`, {
|
|
53
54
|
method: 'POST',
|
|
54
55
|
headers: {
|
|
55
|
-
authorization: `Bearer ${token}`,
|
|
56
56
|
'content-type': 'application/json',
|
|
57
57
|
},
|
|
58
58
|
body: JSON.stringify(event),
|
|
@@ -70,14 +70,14 @@ async function postSnapshot(url, token, event) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export async function retryObserverOutbox({ vaultBase, url
|
|
74
|
-
if (!url
|
|
73
|
+
export async function retryObserverOutbox({ vaultBase, url } = {}) {
|
|
74
|
+
if (!url) return { attempted: 0, confirmed: 0, pending: listOutbox(vaultBase).length };
|
|
75
75
|
let attempted = 0;
|
|
76
76
|
let confirmed = 0;
|
|
77
77
|
for (const event of listOutbox(vaultBase)) {
|
|
78
78
|
attempted += 1;
|
|
79
79
|
try {
|
|
80
|
-
await postSnapshot(url,
|
|
80
|
+
await postSnapshot(url, event);
|
|
81
81
|
removeOutbox(vaultBase, event.event_id);
|
|
82
82
|
confirmed += 1;
|
|
83
83
|
} catch { /* preserve the event for a later retry */ }
|
|
@@ -89,22 +89,33 @@ export async function publishObserverSnapshot({
|
|
|
89
89
|
vaultBase,
|
|
90
90
|
projectRoot,
|
|
91
91
|
url = process.env.WENDKEEP_OBSERVER_URL || '',
|
|
92
|
-
token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
|
|
93
92
|
now = new Date(),
|
|
94
93
|
} = {}) {
|
|
95
94
|
try {
|
|
96
95
|
const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
|
|
97
96
|
if (!url) return { ok: true, skipped: true, queued: false, hookExitCode: 0, event_id: event.event_id };
|
|
98
97
|
|
|
99
|
-
await retryObserverOutbox({ vaultBase, url
|
|
98
|
+
await retryObserverOutbox({ vaultBase, url });
|
|
99
|
+
let memory;
|
|
100
100
|
try {
|
|
101
|
-
|
|
101
|
+
memory = await publishObserverMemory({
|
|
102
|
+
vaultBase,
|
|
103
|
+
projectId: event.project_id,
|
|
104
|
+
url,
|
|
105
|
+
now,
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
memory = { ok: false, queued: false, error: error.message };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const response = await postSnapshot(url, event);
|
|
102
112
|
return {
|
|
103
113
|
ok: true,
|
|
104
114
|
queued: false,
|
|
105
115
|
hookExitCode: 0,
|
|
106
116
|
event_id: event.event_id,
|
|
107
117
|
duplicate: response.duplicate === true,
|
|
118
|
+
memory,
|
|
108
119
|
};
|
|
109
120
|
} catch (error) {
|
|
110
121
|
queueOutbox(vaultBase, event);
|
|
@@ -114,6 +125,7 @@ export async function publishObserverSnapshot({
|
|
|
114
125
|
hookExitCode: 0,
|
|
115
126
|
event_id: event.event_id,
|
|
116
127
|
error: error.message,
|
|
128
|
+
memory,
|
|
117
129
|
};
|
|
118
130
|
}
|
|
119
131
|
} catch (error) {
|
package/src/observer-server.mjs
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
appendObserverEvent,
|
|
6
|
+
getObserverProject,
|
|
7
|
+
listRegisteredObserverProjects,
|
|
8
|
+
readObserverIndex,
|
|
9
|
+
registerObserverProject,
|
|
10
|
+
} from './observer-store.mjs';
|
|
3
11
|
import { MAX_SNAPSHOT_BYTES, validateObserverSnapshot } from './observer-snapshot.mjs';
|
|
12
|
+
import {
|
|
13
|
+
applyMemoryEvent,
|
|
14
|
+
exportMemoryBundle,
|
|
15
|
+
readMemoryDocument,
|
|
16
|
+
readMemorySync,
|
|
17
|
+
readMemoryTree,
|
|
18
|
+
setMemoryMode,
|
|
19
|
+
searchMemory,
|
|
20
|
+
validateMemoryEvent,
|
|
21
|
+
} from './observer-memory.mjs';
|
|
4
22
|
|
|
5
23
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
6
24
|
const MAX_BODY_BYTES = MAX_SNAPSHOT_BYTES + 4096;
|
|
25
|
+
const MAX_MEMORY_BODY_BYTES = 8 * 1024 * 1024;
|
|
26
|
+
const STATIC_ROOT = fileURLToPath(new URL('../web/observer/', import.meta.url));
|
|
27
|
+
const STATIC_ASSETS = new Map([
|
|
28
|
+
['/index.html', { file: 'index.html', type: 'text/html; charset=utf-8' }],
|
|
29
|
+
['/styles.css', { file: 'styles.css', type: 'text/css; charset=utf-8' }],
|
|
30
|
+
['/app.mjs', { file: 'app.mjs', type: 'text/javascript; charset=utf-8' }],
|
|
31
|
+
['/favicon.svg', { file: 'favicon.svg', type: 'image/svg+xml' }],
|
|
32
|
+
]);
|
|
7
33
|
|
|
8
34
|
function loopbackOnly(host) {
|
|
9
35
|
return LOOPBACK_HOSTS.has(String(host || '').toLowerCase());
|
|
@@ -23,7 +49,25 @@ function errorResponse(res, status, code, message) {
|
|
|
23
49
|
json(res, status, { error: { code, message } });
|
|
24
50
|
}
|
|
25
51
|
|
|
26
|
-
function
|
|
52
|
+
function serveStatic(res, pathname) {
|
|
53
|
+
const asset = STATIC_ASSETS.get(pathname === '/' ? '/index.html' : pathname);
|
|
54
|
+
if (!asset) return false;
|
|
55
|
+
try {
|
|
56
|
+
const body = readFileSync(new URL(asset.file, `file://${STATIC_ROOT.replace(/\\/g, '/')}/`));
|
|
57
|
+
res.writeHead(200, {
|
|
58
|
+
'content-type': asset.type,
|
|
59
|
+
'cache-control': 'no-store',
|
|
60
|
+
'x-content-type-options': 'nosniff',
|
|
61
|
+
'content-length': body.byteLength,
|
|
62
|
+
});
|
|
63
|
+
res.end(body);
|
|
64
|
+
} catch {
|
|
65
|
+
errorResponse(res, 500, 'dashboard_asset_error', 'asset do dashboard indisponível.');
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
27
71
|
return new Promise((resolve, reject) => {
|
|
28
72
|
let size = 0;
|
|
29
73
|
let tooLarge = false;
|
|
@@ -31,7 +75,7 @@ function readBody(req) {
|
|
|
31
75
|
req.on('data', (chunk) => {
|
|
32
76
|
if (tooLarge) return;
|
|
33
77
|
size += chunk.length;
|
|
34
|
-
if (size >
|
|
78
|
+
if (size > maxBytes) {
|
|
35
79
|
tooLarge = true;
|
|
36
80
|
const error = new Error('corpo acima do limite.');
|
|
37
81
|
error.code = 'payload_too_large';
|
|
@@ -56,13 +100,6 @@ function parseJson(text) {
|
|
|
56
100
|
}
|
|
57
101
|
}
|
|
58
102
|
|
|
59
|
-
function authorized(req, token) {
|
|
60
|
-
if (!token) return false;
|
|
61
|
-
const header = String(req.headers.authorization || '');
|
|
62
|
-
return header === `Bearer ${token}`
|
|
63
|
-
|| req.headers['x-wendkeep-observer-token'] === token;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
103
|
function pathParts(url) {
|
|
67
104
|
return new URL(url, 'http://127.0.0.1').pathname.split('/').filter(Boolean).map((part) => decodeURIComponent(part));
|
|
68
105
|
}
|
|
@@ -71,11 +108,17 @@ function projectIdFrom(parts) {
|
|
|
71
108
|
return parts[0] === 'v1' && parts[1] === 'projects' && parts[2] ? parts[2] : '';
|
|
72
109
|
}
|
|
73
110
|
|
|
111
|
+
function projectKnown(dataDir, projectId) {
|
|
112
|
+
return Boolean(
|
|
113
|
+
getObserverProject(dataDir, projectId)
|
|
114
|
+
|| listRegisteredObserverProjects(dataDir).some((item) => item.projectId === projectId),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
74
118
|
export async function startObserverServer({
|
|
75
119
|
host = '127.0.0.1',
|
|
76
120
|
port = 8787,
|
|
77
121
|
dataDir,
|
|
78
|
-
token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
|
|
79
122
|
allowNonLoopback = false,
|
|
80
123
|
} = {}) {
|
|
81
124
|
if (!loopbackOnly(host) && !allowNonLoopback) {
|
|
@@ -84,15 +127,20 @@ export async function startObserverServer({
|
|
|
84
127
|
if (!dataDir) throw new Error('dataDir é obrigatório.');
|
|
85
128
|
const server = createServer(async (req, res) => {
|
|
86
129
|
try {
|
|
87
|
-
const
|
|
88
|
-
if (req.method === 'GET' &&
|
|
130
|
+
const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
131
|
+
if (req.method === 'GET' && pathname === '/healthz') {
|
|
89
132
|
json(res, 200, { ok: true, service: 'wendkeep-observer', schema_version: 1 });
|
|
90
133
|
return;
|
|
91
134
|
}
|
|
92
|
-
if (
|
|
93
|
-
|
|
135
|
+
if (req.method === 'GET' && (pathname === '/' || STATIC_ASSETS.has(pathname))) {
|
|
136
|
+
serveStatic(res, pathname);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (req.method === 'GET' && !pathname.startsWith('/v1/')) {
|
|
140
|
+
errorResponse(res, 404, 'not_found', 'rota não encontrada.');
|
|
94
141
|
return;
|
|
95
142
|
}
|
|
143
|
+
const parts = pathParts(req.url || '/');
|
|
96
144
|
if (parts[0] !== 'v1' || parts[1] !== 'projects') {
|
|
97
145
|
errorResponse(res, 404, 'not_found', 'rota não encontrada.');
|
|
98
146
|
return;
|
|
@@ -113,6 +161,88 @@ export async function startObserverServer({
|
|
|
113
161
|
return;
|
|
114
162
|
}
|
|
115
163
|
|
|
164
|
+
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'GET') {
|
|
165
|
+
if (!projectKnown(dataDir, projectId)) {
|
|
166
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
json(res, 200, readMemorySync(dataDir, projectId));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (parts.length === 4 && parts[3] === 'sync' && req.method === 'PUT') {
|
|
174
|
+
if (!projectKnown(dataDir, projectId)) {
|
|
175
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const body = parseJson(await readBody(req));
|
|
179
|
+
try {
|
|
180
|
+
json(res, 200, setMemoryMode(dataDir, projectId, body.mode));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
errorResponse(res, 400, error?.code || 'invalid_memory_mode', error?.message || 'modo inválido.');
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (parts.length >= 4 && parts[3] === 'memory') {
|
|
188
|
+
if (!projectKnown(dataDir, projectId)) {
|
|
189
|
+
errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const memoryAction = parts[4] || '';
|
|
193
|
+
if (memoryAction === 'tree' && req.method === 'GET') {
|
|
194
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
195
|
+
json(res, 200, readMemoryTree(dataDir, projectId, query.get('prefix') || ''));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (memoryAction === 'document' && req.method === 'GET') {
|
|
199
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
200
|
+
try {
|
|
201
|
+
json(res, 200, readMemoryDocument(dataDir, projectId, query.get('path') || ''));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const status = error?.code === 'memory_not_found' ? 404 : 400;
|
|
204
|
+
errorResponse(res, status, error?.code || 'invalid_memory_path', error?.message || 'documento inválido.');
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (memoryAction === 'search' && req.method === 'GET') {
|
|
209
|
+
const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
|
|
210
|
+
json(res, 200, { project_id: projectId, query: query.get('q') || '', results: searchMemory(dataDir, projectId, query.get('q') || '') });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (memoryAction === 'export' && req.method === 'GET') {
|
|
214
|
+
json(res, 200, exportMemoryBundle(dataDir, projectId));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (memoryAction === 'events' && req.method === 'POST') {
|
|
218
|
+
const body = parseJson(await readBody(req, MAX_MEMORY_BODY_BYTES));
|
|
219
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
220
|
+
if (events.length === 0) {
|
|
221
|
+
errorResponse(res, 400, 'invalid_memory_batch', 'events deve conter pelo menos um evento.');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const results = [];
|
|
225
|
+
for (const event of events) {
|
|
226
|
+
if (event?.project_id !== projectId) {
|
|
227
|
+
results.push({ accepted: false, errors: ['project_id do evento não corresponde à rota.'] });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const validation = validateMemoryEvent(event);
|
|
231
|
+
results.push(validation.ok ? applyMemoryEvent(dataDir, event) : { accepted: false, errors: validation.errors });
|
|
232
|
+
}
|
|
233
|
+
const accepted = results.filter((item) => item.accepted).length;
|
|
234
|
+
const duplicates = results.filter((item) => item.duplicate).length;
|
|
235
|
+
const conflicts = results.filter((item) => item.conflict).length;
|
|
236
|
+
const rejected = results.length - accepted - duplicates - conflicts;
|
|
237
|
+
if (conflicts > 0 || rejected > 0) {
|
|
238
|
+
json(res, conflicts > 0 ? 409 : 400, { accepted, duplicates, conflicts, rejected, results });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
json(res, accepted > 0 ? 201 : 200, { accepted, duplicates, conflicts, rejected, results });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
116
246
|
if (parts.length === 3 && req.method === 'GET') {
|
|
117
247
|
const project = getObserverProject(dataDir, projectId);
|
|
118
248
|
if (!project) {
|
package/src/observer.mjs
CHANGED
|
@@ -2,20 +2,22 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { appendObserverEvent, readObserverIndex, registerObserverProject } from './observer-store.mjs';
|
|
4
4
|
import { buildProjectSnapshot } from './observer-snapshot.mjs';
|
|
5
|
+
import { compareMemoryParity, publishObserverMemory } from './observer-memory-publish.mjs';
|
|
5
6
|
import { startObserverServer } from './observer-server.mjs';
|
|
6
7
|
import { resolveProjectVault } from '../packages/vault/src/project-vault.mjs';
|
|
7
8
|
|
|
8
9
|
export const OBSERVER_HELP = `wendkeep observer — Observer local multi-projeto
|
|
9
10
|
|
|
10
11
|
Uso:
|
|
11
|
-
wendkeep observer serve [--data-dir P] [--host 127.0.0.1] [--port 8787]
|
|
12
|
+
wendkeep observer serve [--data-dir P] [--host 127.0.0.1] [--port 8787]
|
|
12
13
|
[--allow-non-loopback]
|
|
13
14
|
wendkeep observer register --project P [--vault V] [--data-dir D] [--json]
|
|
14
15
|
wendkeep observer publish --project P [--vault V] [--data-dir D] [--json]
|
|
16
|
+
wendkeep observer memory import --project P [--vault V] [--url U] [--json]
|
|
15
17
|
wendkeep observer status [--data-dir D] [--json]
|
|
16
18
|
|
|
17
|
-
O
|
|
18
|
-
|
|
19
|
+
O Observer local pode manter snapshots operacionais e uma cópia completa da memória em volume
|
|
20
|
+
Docker. O comando memory import faz a primeira migração de um vault para o container.
|
|
19
21
|
`;
|
|
20
22
|
|
|
21
23
|
function optionValue(argv, name) {
|
|
@@ -41,8 +43,8 @@ function vaultBase(argv, root) {
|
|
|
41
43
|
return resolveProjectVault({ startDir: root }).base;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
function print(value, asJson) {
|
|
45
|
-
|
|
46
|
+
function print(value, asJson, write = (chunk) => process.stdout.write(chunk)) {
|
|
47
|
+
write((asJson ? JSON.stringify(value, null, 2) : String(value)) + '\n');
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function summary(index) {
|
|
@@ -52,7 +54,7 @@ function summary(index) {
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
export async function runObserver(argv = []) {
|
|
57
|
+
export async function runObserver(argv = [], { write = (chunk) => process.stdout.write(chunk) } = {}) {
|
|
56
58
|
const [sub] = argv;
|
|
57
59
|
const asJson = argv.includes('--json');
|
|
58
60
|
if (!sub || sub === 'help') {
|
|
@@ -62,30 +64,66 @@ export async function runObserver(argv = []) {
|
|
|
62
64
|
const dir = dataDir(argv);
|
|
63
65
|
|
|
64
66
|
if (sub === 'status') {
|
|
65
|
-
print(summary(readObserverIndex(dir)), asJson);
|
|
67
|
+
print(summary(readObserverIndex(dir)), asJson, write);
|
|
66
68
|
return 0;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
if (sub === 'serve') {
|
|
70
|
-
const token = optionValue(argv, '--token') || process.env.WENDKEEP_OBSERVER_TOKEN || '';
|
|
71
72
|
const host = optionValue(argv, '--host') || '127.0.0.1';
|
|
72
73
|
const server = await startObserverServer({
|
|
73
74
|
dataDir: dir,
|
|
74
75
|
host,
|
|
75
76
|
port: Number(optionValue(argv, '--port') || 8787),
|
|
76
|
-
token,
|
|
77
77
|
allowNonLoopback: argv.includes('--allow-non-loopback'),
|
|
78
78
|
});
|
|
79
|
-
if (!token) {
|
|
80
|
-
await server.close();
|
|
81
|
-
throw new Error('Observer exige --token ou WENDKEEP_OBSERVER_TOKEN.');
|
|
82
|
-
}
|
|
83
79
|
const address = server.address();
|
|
84
80
|
process.stdout.write(`wendkeep observer listening: http://${address.address}:${address.port}\n`);
|
|
85
81
|
return 0;
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
if (
|
|
84
|
+
if (sub === 'memory') {
|
|
85
|
+
const action = argv[1] || '';
|
|
86
|
+
if (action !== 'import') throw new Error('observer memory: use memory import.');
|
|
87
|
+
const root = projectRoot(argv);
|
|
88
|
+
const vault = vaultBase(argv, root);
|
|
89
|
+
const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
|
|
90
|
+
const url = optionValue(argv, '--url') || process.env.WENDKEEP_OBSERVER_URL || '';
|
|
91
|
+
if (!url) throw new Error('observer memory import: --url ou WENDKEEP_OBSERVER_URL é obrigatório.');
|
|
92
|
+
const headers = { 'content-type': 'application/json', accept: 'application/json' };
|
|
93
|
+
const registration = await fetch(
|
|
94
|
+
String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(snapshot.project_id),
|
|
95
|
+
{
|
|
96
|
+
method: 'PUT',
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
project_id: snapshot.project_id,
|
|
100
|
+
project_name: snapshot.project_name,
|
|
101
|
+
wendkeep_version: snapshot.wendkeep_version,
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
if (!registration.ok) throw new Error('Observer não registrou o projeto: HTTP ' + registration.status + '.');
|
|
106
|
+
const memory = await publishObserverMemory({
|
|
107
|
+
vaultBase: vault,
|
|
108
|
+
projectId: snapshot.project_id,
|
|
109
|
+
url,
|
|
110
|
+
});
|
|
111
|
+
const parity = await compareMemoryParity({
|
|
112
|
+
vaultBase: vault,
|
|
113
|
+
projectId: snapshot.project_id,
|
|
114
|
+
url,
|
|
115
|
+
});
|
|
116
|
+
const result = {
|
|
117
|
+
ok: memory.ok && parity.missing === 0 && parity.mismatched === 0,
|
|
118
|
+
project_id: snapshot.project_id,
|
|
119
|
+
memory,
|
|
120
|
+
parity,
|
|
121
|
+
};
|
|
122
|
+
print(result, asJson, write);
|
|
123
|
+
return result.ok ? 0 : 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!['register', 'publish'].includes(sub)) throw new Error('observer: subcomando desconhecido: ' + sub);
|
|
89
127
|
const root = projectRoot(argv);
|
|
90
128
|
const vault = vaultBase(argv, root);
|
|
91
129
|
const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
|
|
@@ -97,12 +135,12 @@ export async function runObserver(argv = []) {
|
|
|
97
135
|
wendkeepVersion: snapshot.wendkeep_version,
|
|
98
136
|
});
|
|
99
137
|
if (!result.registered) throw new Error(result.errors.join(' '));
|
|
100
|
-
print(result, asJson);
|
|
138
|
+
print(result, asJson, write);
|
|
101
139
|
return 0;
|
|
102
140
|
}
|
|
103
141
|
|
|
104
142
|
const result = appendObserverEvent(dir, snapshot);
|
|
105
143
|
if (!result.accepted && !result.duplicate) throw new Error(result.errors.join(' '));
|
|
106
|
-
print({ ok: true, ...result }, asJson);
|
|
144
|
+
print({ ok: true, ...result }, asJson, write);
|
|
107
145
|
return 0;
|
|
108
146
|
}
|