@arka-labs/nemesis 1.2.0
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/LICENSE +201 -0
- package/README.md +668 -0
- package/lib/core/agent-launcher.js +193 -0
- package/lib/core/audit.js +210 -0
- package/lib/core/connexions.js +80 -0
- package/lib/core/flowmap/api.js +111 -0
- package/lib/core/flowmap/cli-helpers.js +80 -0
- package/lib/core/flowmap/machine.js +281 -0
- package/lib/core/flowmap/persistence.js +83 -0
- package/lib/core/generators.js +183 -0
- package/lib/core/inbox.js +275 -0
- package/lib/core/logger.js +20 -0
- package/lib/core/mission.js +109 -0
- package/lib/core/notewriter/config.js +36 -0
- package/lib/core/notewriter/cr.js +237 -0
- package/lib/core/notewriter/log.js +112 -0
- package/lib/core/notewriter/notes.js +168 -0
- package/lib/core/notewriter/paths.js +45 -0
- package/lib/core/notewriter/reader.js +121 -0
- package/lib/core/notewriter/registry.js +80 -0
- package/lib/core/odm.js +191 -0
- package/lib/core/profile-picker.js +323 -0
- package/lib/core/project.js +287 -0
- package/lib/core/registry.js +129 -0
- package/lib/core/secrets.js +137 -0
- package/lib/core/services.js +45 -0
- package/lib/core/team.js +287 -0
- package/lib/core/templates.js +80 -0
- package/lib/kairos/agent-runner.js +261 -0
- package/lib/kairos/claude-invoker.js +90 -0
- package/lib/kairos/context-injector.js +331 -0
- package/lib/kairos/context-loader.js +108 -0
- package/lib/kairos/context-writer.js +45 -0
- package/lib/kairos/dispatcher-router.js +173 -0
- package/lib/kairos/dispatcher.js +139 -0
- package/lib/kairos/event-bus.js +287 -0
- package/lib/kairos/event-router.js +131 -0
- package/lib/kairos/flowmap-bridge.js +120 -0
- package/lib/kairos/hook-handlers.js +351 -0
- package/lib/kairos/hook-installer.js +207 -0
- package/lib/kairos/hook-prompts.js +54 -0
- package/lib/kairos/leader-rules.js +94 -0
- package/lib/kairos/pid-checker.js +108 -0
- package/lib/kairos/situation-detector.js +123 -0
- package/lib/sync/fallback-engine.js +97 -0
- package/lib/sync/hcm-client.js +170 -0
- package/lib/sync/health.js +47 -0
- package/lib/sync/llm-client.js +387 -0
- package/lib/sync/nemesis-client.js +379 -0
- package/lib/sync/service-session.js +74 -0
- package/lib/sync/sync-engine.js +178 -0
- package/lib/ui/box.js +104 -0
- package/lib/ui/brand.js +42 -0
- package/lib/ui/colors.js +57 -0
- package/lib/ui/dashboard.js +580 -0
- package/lib/ui/error-hints.js +49 -0
- package/lib/ui/format.js +61 -0
- package/lib/ui/menu.js +306 -0
- package/lib/ui/note-card.js +198 -0
- package/lib/ui/note-colors.js +26 -0
- package/lib/ui/note-detail.js +297 -0
- package/lib/ui/note-filters.js +252 -0
- package/lib/ui/note-views.js +283 -0
- package/lib/ui/prompt.js +81 -0
- package/lib/ui/spinner.js +139 -0
- package/lib/ui/streambox.js +46 -0
- package/lib/ui/table.js +42 -0
- package/lib/ui/tree.js +33 -0
- package/package.json +53 -0
- package/src/cli.js +457 -0
- package/src/commands/_helpers.js +119 -0
- package/src/commands/audit.js +187 -0
- package/src/commands/auth.js +316 -0
- package/src/commands/doctor.js +243 -0
- package/src/commands/hcm.js +147 -0
- package/src/commands/inbox.js +333 -0
- package/src/commands/init.js +160 -0
- package/src/commands/kairos.js +216 -0
- package/src/commands/kars.js +134 -0
- package/src/commands/mission.js +275 -0
- package/src/commands/notes.js +316 -0
- package/src/commands/notewriter.js +296 -0
- package/src/commands/odm.js +329 -0
- package/src/commands/orch.js +68 -0
- package/src/commands/project.js +123 -0
- package/src/commands/run.js +123 -0
- package/src/commands/services.js +705 -0
- package/src/commands/status.js +231 -0
- package/src/commands/team.js +572 -0
- package/src/config.js +84 -0
- package/src/index.js +5 -0
- package/templates/project-context.json +10 -0
- package/templates/template_CONTRIB-NAME.json +22 -0
- package/templates/template_CR-ODM-NAME-000.exemple.json +32 -0
- package/templates/template_DEC-NAME-000.json +18 -0
- package/templates/template_INTV-NAME-000.json +15 -0
- package/templates/template_MISSION_CONTRACT.json +46 -0
- package/templates/template_ODM-NAME-000.json +89 -0
- package/templates/template_REGISTRY-PROJECT.json +26 -0
- package/templates/template_TXN-NAME-000.json +24 -0
package/lib/ui/menu.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive keyboard-navigable menu — zero deps.
|
|
3
|
+
* Uses readline.emitKeypressEvents for reliable arrow key handling.
|
|
4
|
+
* Arrow keys (up/down), Enter to select, q/Ctrl+C to quit.
|
|
5
|
+
* Supports scrollable viewport via maxVisible option.
|
|
6
|
+
* '/' activates filter mode for substring search.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
10
|
+
import { style, stripAnsi } from './colors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pure function — filter items by substring (case-insensitive) on label + description.
|
|
14
|
+
* @param {Array<{label: string, value: string, description?: string}>} items
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* @returns {Array<{label: string, value: string, description?: string, _origIndex: number}>}
|
|
17
|
+
*/
|
|
18
|
+
export function filterItems(items, text) {
|
|
19
|
+
if (!text) return items.map((item, i) => ({ ...item, _origIndex: i }));
|
|
20
|
+
const lower = text.toLowerCase();
|
|
21
|
+
return items
|
|
22
|
+
.map((item, i) => ({ ...item, _origIndex: i }))
|
|
23
|
+
.filter(item => {
|
|
24
|
+
const haystack = (stripAnsi(item.label) + ' ' + (item.description || '')).toLowerCase();
|
|
25
|
+
return haystack.includes(lower);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Array<{label: string, value: string, description?: string}>} items
|
|
31
|
+
* @param {object} opts
|
|
32
|
+
* @param {string} [opts.title]
|
|
33
|
+
* @param {string} [opts.hint]
|
|
34
|
+
* @param {number} [opts.maxVisible] — 0 = show all (default)
|
|
35
|
+
* @returns {Promise<string|null>} selected value or null if cancelled
|
|
36
|
+
*/
|
|
37
|
+
export function interactiveMenu(items, opts = {}) {
|
|
38
|
+
const {
|
|
39
|
+
title,
|
|
40
|
+
hint: customHint,
|
|
41
|
+
maxVisible = 0,
|
|
42
|
+
} = opts;
|
|
43
|
+
|
|
44
|
+
const defaultHint = 'Fleches \u2191\u2193 naviguer, Enter selectionner, / filtrer, q quitter';
|
|
45
|
+
|
|
46
|
+
return new Promise(resolve => {
|
|
47
|
+
let cursor = 0;
|
|
48
|
+
let viewOffset = 0;
|
|
49
|
+
let filterMode = false;
|
|
50
|
+
let filterText = '';
|
|
51
|
+
let filteredItems = items;
|
|
52
|
+
const { stdin, stdout } = process;
|
|
53
|
+
|
|
54
|
+
if (!stdin.isTTY) {
|
|
55
|
+
resolve(null);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getVisibleItems() {
|
|
60
|
+
return filterMode ? filteredItems : items;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getEffectiveMax() {
|
|
64
|
+
const vis = getVisibleItems();
|
|
65
|
+
return maxVisible > 0 && maxVisible < vis.length ? maxVisible : vis.length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getNeedsScroll() {
|
|
69
|
+
return getVisibleItems().length > getEffectiveMax();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
emitKeypressEvents(stdin);
|
|
73
|
+
stdin.setRawMode(true);
|
|
74
|
+
stdin.resume();
|
|
75
|
+
stdin.setEncoding('utf8');
|
|
76
|
+
|
|
77
|
+
// Fixed line count — scroll indicators always reserve space when scrollable
|
|
78
|
+
function fixedLineCount() {
|
|
79
|
+
let count = 0;
|
|
80
|
+
if (title) count += 2; // title + blank
|
|
81
|
+
if (getNeedsScroll()) count += 2; // ▲ + ▼ (or placeholders)
|
|
82
|
+
count += getEffectiveMax(); // visible items
|
|
83
|
+
count += 2; // blank + hint
|
|
84
|
+
if (filterMode) count += 1; // filter bar
|
|
85
|
+
return count;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function adjustViewport() {
|
|
89
|
+
const vis = getVisibleItems();
|
|
90
|
+
const eMax = getEffectiveMax();
|
|
91
|
+
if (vis.length === 0) { viewOffset = 0; return; }
|
|
92
|
+
if (cursor < viewOffset) viewOffset = cursor;
|
|
93
|
+
if (cursor >= viewOffset + eMax) viewOffset = cursor - eMax + 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function draw() {
|
|
97
|
+
const vis = getVisibleItems();
|
|
98
|
+
const eMax = getEffectiveMax();
|
|
99
|
+
const needsScroll = getNeedsScroll();
|
|
100
|
+
|
|
101
|
+
if (title) {
|
|
102
|
+
stdout.write(` ${style.bold(title)}\n`);
|
|
103
|
+
stdout.write('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Scroll up indicator
|
|
107
|
+
if (needsScroll) {
|
|
108
|
+
if (viewOffset > 0) {
|
|
109
|
+
stdout.write(` ${style.dim(`\u25B2 ${viewOffset} de plus`)}\n`);
|
|
110
|
+
} else {
|
|
111
|
+
stdout.write('\n');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Visible items
|
|
116
|
+
if (vis.length === 0) {
|
|
117
|
+
for (let i = 0; i < eMax || i < 1; i++) {
|
|
118
|
+
if (i === 0) {
|
|
119
|
+
stdout.write(` ${style.dim('(aucun resultat)')}\n`);
|
|
120
|
+
} else {
|
|
121
|
+
stdout.write('\n');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
for (let i = 0; i < eMax; i++) {
|
|
126
|
+
const realIndex = viewOffset + i;
|
|
127
|
+
if (realIndex >= vis.length) {
|
|
128
|
+
stdout.write('\n');
|
|
129
|
+
} else {
|
|
130
|
+
const item = vis[realIndex];
|
|
131
|
+
const active = realIndex === cursor;
|
|
132
|
+
const marker = active ? style.arkaRed('\u276f') : ' ';
|
|
133
|
+
const label = active ? style.bold(item.label) : item.label;
|
|
134
|
+
const desc = item.description ? style.gray(` \u2014 ${item.description}`) : '';
|
|
135
|
+
stdout.write(` ${marker} ${label}${desc}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Scroll down indicator
|
|
141
|
+
if (needsScroll) {
|
|
142
|
+
const remaining = vis.length - viewOffset - eMax;
|
|
143
|
+
if (remaining > 0) {
|
|
144
|
+
stdout.write(` ${style.dim(`\u25BC ${remaining} de plus`)}\n`);
|
|
145
|
+
} else {
|
|
146
|
+
stdout.write('\n');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Hint
|
|
151
|
+
const hint = customHint || defaultHint;
|
|
152
|
+
stdout.write(`\n ${style.dim(hint)}\n`);
|
|
153
|
+
|
|
154
|
+
// Filter bar
|
|
155
|
+
if (filterMode) {
|
|
156
|
+
stdout.write(` ${style.nemesisAccent('/')} ${filterText}${style.dim('_')}\n`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function redraw() {
|
|
161
|
+
stdout.write(`\x1b[${fixedLineCount()}A\x1b[J`);
|
|
162
|
+
draw();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cleanup() {
|
|
166
|
+
stdin.removeListener('keypress', onKeypress);
|
|
167
|
+
stdin.setRawMode(false);
|
|
168
|
+
stdin.pause();
|
|
169
|
+
stdout.write(`\x1b[${fixedLineCount() + 1}A\x1b[J`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function applyFilter() {
|
|
173
|
+
filteredItems = filterItems(items, filterText);
|
|
174
|
+
cursor = 0;
|
|
175
|
+
viewOffset = 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function onKeypress(ch, key) {
|
|
179
|
+
if (!key) return;
|
|
180
|
+
|
|
181
|
+
// Ctrl+C — always quit
|
|
182
|
+
if (key.ctrl && key.name === 'c') {
|
|
183
|
+
cleanup();
|
|
184
|
+
resolve(null);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Filter mode ---
|
|
189
|
+
if (filterMode) {
|
|
190
|
+
// Escape — exit filter mode
|
|
191
|
+
if (key.name === 'escape') {
|
|
192
|
+
filterMode = false;
|
|
193
|
+
filterText = '';
|
|
194
|
+
filteredItems = items;
|
|
195
|
+
cursor = 0;
|
|
196
|
+
viewOffset = 0;
|
|
197
|
+
redraw();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Enter — select current item in filtered list
|
|
202
|
+
if (key.name === 'return') {
|
|
203
|
+
const vis = getVisibleItems();
|
|
204
|
+
if (vis.length > 0) {
|
|
205
|
+
cleanup();
|
|
206
|
+
stdout.write('\n');
|
|
207
|
+
resolve(vis[cursor].value);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Backspace — delete last char
|
|
213
|
+
if (key.name === 'backspace') {
|
|
214
|
+
if (filterText.length > 0) {
|
|
215
|
+
filterText = filterText.slice(0, -1);
|
|
216
|
+
applyFilter();
|
|
217
|
+
}
|
|
218
|
+
redraw();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Arrow up
|
|
223
|
+
if (key.name === 'up') {
|
|
224
|
+
const vis = getVisibleItems();
|
|
225
|
+
if (vis.length > 0) {
|
|
226
|
+
cursor = (cursor - 1 + vis.length) % vis.length;
|
|
227
|
+
adjustViewport();
|
|
228
|
+
}
|
|
229
|
+
redraw();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Arrow down
|
|
234
|
+
if (key.name === 'down') {
|
|
235
|
+
const vis = getVisibleItems();
|
|
236
|
+
if (vis.length > 0) {
|
|
237
|
+
cursor = (cursor + 1) % vis.length;
|
|
238
|
+
adjustViewport();
|
|
239
|
+
}
|
|
240
|
+
redraw();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Printable character — append to filter
|
|
245
|
+
if (ch && ch.length === 1 && !key.ctrl && !key.meta) {
|
|
246
|
+
filterText += ch;
|
|
247
|
+
applyFilter();
|
|
248
|
+
redraw();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- Normal mode ---
|
|
256
|
+
|
|
257
|
+
// '/' — enter filter mode
|
|
258
|
+
if (ch === '/' && !key.ctrl && !key.meta) {
|
|
259
|
+
filterMode = true;
|
|
260
|
+
filterText = '';
|
|
261
|
+
filteredItems = filterItems(items, '');
|
|
262
|
+
cursor = 0;
|
|
263
|
+
viewOffset = 0;
|
|
264
|
+
redraw();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Escape or q
|
|
269
|
+
if (key.name === 'escape' || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
270
|
+
cleanup();
|
|
271
|
+
resolve(null);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Enter
|
|
276
|
+
if (key.name === 'return') {
|
|
277
|
+
cleanup();
|
|
278
|
+
stdout.write('\n');
|
|
279
|
+
resolve(items[cursor].value);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Arrow up or k
|
|
284
|
+
if (key.name === 'up' || ch === 'k') {
|
|
285
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
286
|
+
adjustViewport();
|
|
287
|
+
redraw();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Arrow down or j
|
|
292
|
+
if (key.name === 'down' || ch === 'j') {
|
|
293
|
+
cursor = (cursor + 1) % items.length;
|
|
294
|
+
adjustViewport();
|
|
295
|
+
redraw();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Initial draw
|
|
301
|
+
stdout.write('\n');
|
|
302
|
+
draw();
|
|
303
|
+
|
|
304
|
+
stdin.on('keypress', onKeypress);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { stringWidth, padEndVisible } from './box.js';
|
|
2
|
+
import { style } from './colors.js';
|
|
3
|
+
import { levelLabel, levelBorder } from './note-colors.js';
|
|
4
|
+
|
|
5
|
+
// NOTE — bordure simple
|
|
6
|
+
const SINGLE = { tl: '\u250c', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502' };
|
|
7
|
+
// CR — double bordure
|
|
8
|
+
const DOUBLE = { tl: '\u2554', tr: '\u2557', bl: '\u255a', br: '\u255d', h: '\u2550', v: '\u2551' };
|
|
9
|
+
|
|
10
|
+
export const CARD_WIDTH = 72;
|
|
11
|
+
|
|
12
|
+
// --- NOTE card ---
|
|
13
|
+
|
|
14
|
+
export function renderNoteCard(note, options = {}) {
|
|
15
|
+
const level = note.level || 'L1';
|
|
16
|
+
const color = (ch) => levelBorder(level, ch);
|
|
17
|
+
const agentShort = (note.agentId || '').replace(/^Agent_/i, '');
|
|
18
|
+
const time = formatTime(note.timestamp);
|
|
19
|
+
const lbl = levelLabel(level);
|
|
20
|
+
|
|
21
|
+
const lines = [];
|
|
22
|
+
|
|
23
|
+
// Header : ┌─ L4 · DECISION · Agent · 09:03 ─────────┐
|
|
24
|
+
const headerText = ` ${level} \u00b7 ${lbl} \u00b7 ${agentShort} \u00b7 ${time} `;
|
|
25
|
+
const headerFill = SINGLE.h.repeat(Math.max(0, CARD_WIDTH - stringWidth(headerText) - 1));
|
|
26
|
+
lines.push(color(SINGLE.tl + SINGLE.h) + color(headerText) + color(headerFill + SINGLE.tr));
|
|
27
|
+
|
|
28
|
+
// Ligne vide
|
|
29
|
+
lines.push(color(SINGLE.v) + padEndVisible('', CARD_WIDTH) + color(SINGLE.v));
|
|
30
|
+
|
|
31
|
+
if (!options.compact) {
|
|
32
|
+
// Contenu (word-wrap a CARD_WIDTH - 4 pour padding)
|
|
33
|
+
const contentLines = wrapText(note.content || '', CARD_WIDTH - 4);
|
|
34
|
+
for (const cl of contentLines) {
|
|
35
|
+
lines.push(color(SINGLE.v) + ' ' + padEndVisible(cl, CARD_WIDTH - 2) + color(SINGLE.v));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Actions
|
|
39
|
+
if (note.extractedActions?.length > 0) {
|
|
40
|
+
lines.push(color(SINGLE.v) + padEndVisible('', CARD_WIDTH) + color(SINGLE.v));
|
|
41
|
+
for (const action of note.extractedActions) {
|
|
42
|
+
const wrapped = wrapText(`\u21b3 ${action}`, CARD_WIDTH - 4);
|
|
43
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
44
|
+
const prefix = i === 0 ? ' ' : ' ';
|
|
45
|
+
lines.push(color(SINGLE.v) + padEndVisible(`${prefix}${wrapped[i]}`, CARD_WIDTH) + color(SINGLE.v));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ligne vide
|
|
51
|
+
lines.push(color(SINGLE.v) + padEndVisible('', CARD_WIDTH) + color(SINGLE.v));
|
|
52
|
+
|
|
53
|
+
// Footer : tags + provider + ID
|
|
54
|
+
const tags = (note.tags || []).map(t => `# ${t}`).join(' ');
|
|
55
|
+
const id = note.id || '';
|
|
56
|
+
const provider = note.llmProvider ? style.dim(`[${note.llmProvider}${note.llmFallback ? ' fb' : ''}]`) : '';
|
|
57
|
+
const footerLeft = ` ${tags}`;
|
|
58
|
+
const footerRight = provider ? `${provider} ${id} ` : `${id} `;
|
|
59
|
+
const totalNeeded = stringWidth(footerLeft) + 1 + stringWidth(footerRight);
|
|
60
|
+
if (totalNeeded <= CARD_WIDTH) {
|
|
61
|
+
const gap = CARD_WIDTH - stringWidth(footerLeft) - stringWidth(footerRight);
|
|
62
|
+
const footer = footerLeft + ' '.repeat(Math.max(1, gap)) + footerRight;
|
|
63
|
+
lines.push(color(SINGLE.v) + padEndVisible(footer, CARD_WIDTH) + color(SINGLE.v));
|
|
64
|
+
} else {
|
|
65
|
+
for (const tl of wrapText(tags, CARD_WIDTH - 4)) {
|
|
66
|
+
lines.push(color(SINGLE.v) + padEndVisible(` ${tl}`, CARD_WIDTH) + color(SINGLE.v));
|
|
67
|
+
}
|
|
68
|
+
lines.push(color(SINGLE.v) + padEndVisible(` ${footerRight.trim()}`, CARD_WIDTH) + color(SINGLE.v));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Bottom border
|
|
73
|
+
lines.push(color(SINGLE.bl + SINGLE.h.repeat(CARD_WIDTH) + SINGLE.br));
|
|
74
|
+
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- CR card ---
|
|
79
|
+
|
|
80
|
+
export function renderCrCard(cr, options = {}) {
|
|
81
|
+
const agentShort = (cr.agentId || '').replace(/^Agent_/i, '');
|
|
82
|
+
const date = formatDate(cr.period?.from);
|
|
83
|
+
const time = formatTime(cr.period?.from);
|
|
84
|
+
const endTime = formatTime(cr.period?.to);
|
|
85
|
+
|
|
86
|
+
const lines = [];
|
|
87
|
+
const dbl = (ch) => style.nemesisAccent(ch);
|
|
88
|
+
|
|
89
|
+
// Header double : ╔═ CR · Agent · date heure ═══╗
|
|
90
|
+
const headerText = ` CR \u00b7 ${agentShort} \u00b7 ${date} ${time} `;
|
|
91
|
+
const headerFill = DOUBLE.h.repeat(Math.max(0, CARD_WIDTH - stringWidth(headerText) - 1));
|
|
92
|
+
lines.push(dbl(DOUBLE.tl + DOUBLE.h) + dbl(headerText) + dbl(headerFill + DOUBLE.tr));
|
|
93
|
+
|
|
94
|
+
// Ligne vide
|
|
95
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible('', CARD_WIDTH) + dbl(DOUBLE.v));
|
|
96
|
+
|
|
97
|
+
// Titre
|
|
98
|
+
const titleLine = ` ${style.bold(cr.title || 'CR sans titre')}`;
|
|
99
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(titleLine, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
100
|
+
|
|
101
|
+
// Periode + notes + trigger
|
|
102
|
+
const metaLine = ` Periode : ${time} \u2192 ${endTime} \u00b7 ${cr.noteCount || 0} notes \u00b7 ${cr.trigger || '-'}`;
|
|
103
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(metaLine, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
104
|
+
|
|
105
|
+
if (!options.compact) {
|
|
106
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible('', CARD_WIDTH) + dbl(DOUBLE.v));
|
|
107
|
+
|
|
108
|
+
// Section CONTEXTE PRECEDENT
|
|
109
|
+
if (cr.previousCrSummary) {
|
|
110
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(` ${style.bold('\u25b6 CONTEXTE PRECEDENT')}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
111
|
+
for (const cl of wrapText(cr.previousCrSummary, CARD_WIDTH - 4)) {
|
|
112
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(` ${cl}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
113
|
+
}
|
|
114
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible('', CARD_WIDTH) + dbl(DOUBLE.v));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Section DECISIONS
|
|
118
|
+
if (cr.decisions?.length > 0) {
|
|
119
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(` ${style.bold('\u25b6 DECISIONS')}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
120
|
+
for (const d of cr.decisions) {
|
|
121
|
+
const wrapped = wrapText(`\u2022 ${d}`, CARD_WIDTH - 4);
|
|
122
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
123
|
+
const prefix = i === 0 ? ' ' : ' ';
|
|
124
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(`${prefix}${wrapped[i]}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible('', CARD_WIDTH) + dbl(DOUBLE.v));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Section ACTIONS EN ATTENTE
|
|
131
|
+
if (cr.actions?.length > 0) {
|
|
132
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(` ${style.bold('\u25b6 ACTIONS EN ATTENTE')}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
133
|
+
for (const a of cr.actions) {
|
|
134
|
+
const wrapped = wrapText(`\u2022 ${a}`, CARD_WIDTH - 4);
|
|
135
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
136
|
+
const prefix = i === 0 ? ' ' : ' ';
|
|
137
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(`${prefix}${wrapped[i]}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible('', CARD_WIDTH) + dbl(DOUBLE.v));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Footer : notes liees + ID
|
|
144
|
+
const noteIds = (cr.noteIds || []);
|
|
145
|
+
let notesRef = noteIds.slice(0, 3).join(' ');
|
|
146
|
+
if (noteIds.length > 3) notesRef += ` ... (+${noteIds.length - 3})`;
|
|
147
|
+
const footerLeft = ` Notes liees : ${notesRef}`;
|
|
148
|
+
const crId = cr.id || '';
|
|
149
|
+
const crIdRight = `${crId} `;
|
|
150
|
+
const totalNeeded = stringWidth(footerLeft) + 1 + stringWidth(crIdRight);
|
|
151
|
+
if (totalNeeded <= CARD_WIDTH) {
|
|
152
|
+
const gap = CARD_WIDTH - stringWidth(footerLeft) - stringWidth(crIdRight);
|
|
153
|
+
const footer = footerLeft + ' '.repeat(Math.max(1, gap)) + crIdRight;
|
|
154
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(footer, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
155
|
+
} else {
|
|
156
|
+
for (const fl of wrapText(`Notes liees : ${notesRef}`, CARD_WIDTH - 4)) {
|
|
157
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(` ${fl}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
158
|
+
}
|
|
159
|
+
lines.push(dbl(DOUBLE.v) + padEndVisible(` ${crId}`, CARD_WIDTH) + dbl(DOUBLE.v));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Bottom : bordure simple pour le bas (comme le mockup spec)
|
|
164
|
+
lines.push(SINGLE.bl + SINGLE.h.repeat(CARD_WIDTH) + SINGLE.br);
|
|
165
|
+
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Utilitaires ---
|
|
170
|
+
|
|
171
|
+
export function formatTime(isoString) {
|
|
172
|
+
if (!isoString) return '--:--';
|
|
173
|
+
const d = new Date(isoString);
|
|
174
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function formatDate(isoString) {
|
|
178
|
+
if (!isoString) return '--/--/----';
|
|
179
|
+
const d = new Date(isoString);
|
|
180
|
+
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function wrapText(text, maxWidth) {
|
|
184
|
+
if (!text) return [];
|
|
185
|
+
const words = text.split(/\s+/);
|
|
186
|
+
const lines = [];
|
|
187
|
+
let current = '';
|
|
188
|
+
for (const word of words) {
|
|
189
|
+
if (current && stringWidth(current + ' ' + word) > maxWidth) {
|
|
190
|
+
lines.push(current);
|
|
191
|
+
current = word;
|
|
192
|
+
} else {
|
|
193
|
+
current = current ? current + ' ' + word : word;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (current) lines.push(current);
|
|
197
|
+
return lines;
|
|
198
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { style } from './colors.js';
|
|
2
|
+
|
|
3
|
+
export const LEVEL_COLORS = {
|
|
4
|
+
L1: { r: 34, g: 197, b: 94, label: 'INFO' },
|
|
5
|
+
L2: { r: 59, g: 130, b: 246, label: 'ECHANGE' },
|
|
6
|
+
L3: { r: 139, g: 92, b: 246, label: 'CLARIFICATION' },
|
|
7
|
+
L4: { r: 249, g: 115, b: 22, label: 'DECISION' },
|
|
8
|
+
L5: { r: 199, g: 18, b: 68, label: 'CRITIQUE' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function levelColor(level, text) {
|
|
12
|
+
const c = LEVEL_COLORS[level] || LEVEL_COLORS.L1;
|
|
13
|
+
return style.rgb(c.r, c.g, c.b, text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function levelLabel(level) {
|
|
17
|
+
return (LEVEL_COLORS[level] || LEVEL_COLORS.L1).label;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function levelDot(level) {
|
|
21
|
+
return levelColor(level, '\u25CF');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function levelBorder(level, char) {
|
|
25
|
+
return levelColor(level, char);
|
|
26
|
+
}
|