@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/src/cli.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { renderBrandHeader, renderBrandBanner } from '../lib/ui/brand.js';
|
|
3
|
+
import { titledBox } from '../lib/ui/box.js';
|
|
4
|
+
import { style } from '../lib/ui/colors.js';
|
|
5
|
+
import { interactiveMenu } from '../lib/ui/menu.js';
|
|
6
|
+
import { clearScreen, waitForKey, BACK_ITEM, HOME_ITEM } from './commands/_helpers.js';
|
|
7
|
+
import { detectProject, storeHcmStatus } from '../lib/core/project.js';
|
|
8
|
+
import { readRegistry, getLanes, getPM } from '../lib/core/registry.js';
|
|
9
|
+
import { createHcmClient } from '../lib/sync/hcm-client.js';
|
|
10
|
+
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
init: () => import('./commands/init.js'),
|
|
13
|
+
team: () => import('./commands/team.js'),
|
|
14
|
+
project: () => import('./commands/project.js'),
|
|
15
|
+
status: () => import('./commands/status.js'),
|
|
16
|
+
doctor: () => import('./commands/doctor.js'),
|
|
17
|
+
mission: () => import('./commands/mission.js'),
|
|
18
|
+
odm: () => import('./commands/odm.js'),
|
|
19
|
+
hcm: () => import('./commands/hcm.js'),
|
|
20
|
+
audit: () => import('./commands/audit.js'),
|
|
21
|
+
services: () => import('./commands/services.js'),
|
|
22
|
+
kars: () => import('./commands/kars.js'),
|
|
23
|
+
notes: () => import('./commands/notes.js'),
|
|
24
|
+
notewriter: () => import('./commands/notewriter.js'),
|
|
25
|
+
kairos: () => import('./commands/kairos.js'),
|
|
26
|
+
auth: () => import('./commands/auth.js'),
|
|
27
|
+
run: () => import('./commands/run.js'),
|
|
28
|
+
orch: () => import('./commands/orch.js'),
|
|
29
|
+
inbox: () => import('./commands/inbox.js'),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const VERSION = '1.0.1';
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Menu principal — 2 zones : acces rapide agents + categories
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export function buildMainMenu(config) {
|
|
39
|
+
const items = [];
|
|
40
|
+
|
|
41
|
+
// --- Zone Acces rapide (si projet detecte) ---
|
|
42
|
+
const project = detectProject(config.cwd);
|
|
43
|
+
if (project) {
|
|
44
|
+
const registry = readRegistry(project.hcm_dir);
|
|
45
|
+
if (registry) {
|
|
46
|
+
const lanes = getLanes(registry);
|
|
47
|
+
const pm = getPM(registry);
|
|
48
|
+
// Agents directs du PM
|
|
49
|
+
const directReports = lanes.filter(l =>
|
|
50
|
+
l.reports_to === pm?.name || l.reports_to === pm?.id || l.reports_to === 'PM'
|
|
51
|
+
);
|
|
52
|
+
// Si aucun reports_to defini, afficher tous les agents (max 5)
|
|
53
|
+
const agents = directReports.length > 0 ? directReports : lanes;
|
|
54
|
+
|
|
55
|
+
for (const agent of agents.slice(0, 5)) {
|
|
56
|
+
const { checkAgentAlive } = require_team();
|
|
57
|
+
const alive = checkAgentAlive(agent.pid);
|
|
58
|
+
const dot = alive ? style.green('\u25CF') : agent.session_id ? style.yellow('\u25CF') : style.dim('\u25CB');
|
|
59
|
+
const statusText = alive ? 'actif' : agent.session_id ? 'en veille' : '';
|
|
60
|
+
items.push({
|
|
61
|
+
label: `${agent.name} ${dot} ${style.dim(statusText)}`,
|
|
62
|
+
value: `__agent__:${agent.name}`,
|
|
63
|
+
description: agent.role || '',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (lanes.length > 0) {
|
|
67
|
+
items.push({ label: style.dim('Voir tous \u2192'), value: '__all_agents__', description: '' });
|
|
68
|
+
}
|
|
69
|
+
// Separateur visuel
|
|
70
|
+
items.push({ label: style.dim('\u2500'.repeat(35)), value: '__sep__', description: '' });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Zone Categories ---
|
|
75
|
+
items.push(
|
|
76
|
+
{ label: 'Projet', value: '__cat_projet__', description: 'Status, init, diagnostic' },
|
|
77
|
+
{ label: 'Equipe', value: '__cat_equipe__', description: 'Agents, profils' },
|
|
78
|
+
{ label: 'Travail', value: '__cat_travail__', description: 'Missions, OdMs, audits' },
|
|
79
|
+
{ label: 'Memoire', value: '__cat_memoire__', description: 'Notes, CR, contexte agent' },
|
|
80
|
+
{ label: 'Infra', value: '__cat_infra__', description: 'Connexions LLM, HCM' },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Delegates to pid-checker for centralized PID logic
|
|
87
|
+
import { isPidAlive } from '../lib/kairos/pid-checker.js';
|
|
88
|
+
function require_team() {
|
|
89
|
+
return { checkAgentAlive: isPidAlive };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Sous-menus categories
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
const CAT_PROJET = [
|
|
97
|
+
{ label: 'status', value: 'status', description: 'Etat du projet + alertes' },
|
|
98
|
+
{ label: 'init', value: 'init', description: 'Scaffold structure projet' },
|
|
99
|
+
{ label: 'doctor', value: 'doctor', description: 'Diagnostic complet' },
|
|
100
|
+
BACK_ITEM, HOME_ITEM,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const CAT_EQUIPE = [
|
|
104
|
+
{ label: 'Organigramme', value: 'team:list', description: 'Vue hierarchique de l\'equipe' },
|
|
105
|
+
{ label: 'Agents', value: 'team:panneau', description: 'Espace de gestion des agents' },
|
|
106
|
+
{ label: 'Ajouter', value: 'team:add', description: 'Ajouter un agent' },
|
|
107
|
+
{ label: 'Profils', value: 'kars', description: 'Parcourir les profils disponibles' },
|
|
108
|
+
BACK_ITEM, HOME_ITEM,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const CAT_TRAVAIL = [
|
|
112
|
+
{ label: 'Orchestration', value: 'orch', description: 'Dashboard dispatches & agents' },
|
|
113
|
+
{ label: 'Inbox', value: 'inbox', description: 'Documents humains → MC' },
|
|
114
|
+
{ label: 'Missions', value: 'mission', description: 'Contrats de fonctionnalite' },
|
|
115
|
+
{ label: 'OdMs', value: 'odm', description: 'Ordres de mission' },
|
|
116
|
+
{ label: 'Audit', value: 'audit', description: 'Audit technique' },
|
|
117
|
+
BACK_ITEM, HOME_ITEM,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const CAT_MEMOIRE = [
|
|
121
|
+
{ label: 'Notes', value: 'notes', description: 'Consulter notes & CR des agents' },
|
|
122
|
+
{ label: 'Contexte', value: 'notewriter', description: 'Gestion du contexte agent' },
|
|
123
|
+
BACK_ITEM, HOME_ITEM,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const CAT_INFRA = [
|
|
127
|
+
{ label: 'Services', value: 'services', description: 'Connexions LLM' },
|
|
128
|
+
{ label: 'HCM', value: 'hcm', description: 'Synchronisation & recherche' },
|
|
129
|
+
{ label: 'Auth', value: 'auth', description: 'Connexion HCM' },
|
|
130
|
+
{ label: 'Kairos', value: 'kairos', description: 'Hooks & contexte agent' },
|
|
131
|
+
BACK_ITEM, HOME_ITEM,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
export const CATEGORY_MAP = {
|
|
135
|
+
'__cat_projet__': { items: CAT_PROJET, title: 'nemesis \u203A Projet' },
|
|
136
|
+
'__cat_equipe__': { items: CAT_EQUIPE, title: 'nemesis \u203A Equipe' },
|
|
137
|
+
'__cat_travail__': { items: CAT_TRAVAIL, title: 'nemesis \u203A Travail' },
|
|
138
|
+
'__cat_memoire__': { items: CAT_MEMOIRE, title: 'nemesis \u203A Memoire' },
|
|
139
|
+
'__cat_infra__': { items: CAT_INFRA, title: 'nemesis \u203A Infra' },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Export sub-menus for tests
|
|
143
|
+
export { CAT_PROJET, CAT_EQUIPE, CAT_TRAVAIL, CAT_MEMOIRE, CAT_INFRA };
|
|
144
|
+
|
|
145
|
+
// Export gate functions for tests
|
|
146
|
+
export { SKIP_HCM_COMMANDS, READ_ONLY_COMMANDS, warnIfHcmDown, blockIfHcmDown };
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Ecran rapide agent
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
async function handleQuickAgent(agentName, flags, config) {
|
|
153
|
+
const { teamInspect } = await import('../lib/core/team.js');
|
|
154
|
+
const result = teamInspect(agentName, config.cwd);
|
|
155
|
+
const agent = result.agent;
|
|
156
|
+
|
|
157
|
+
const alive = isPidAlive(agent.pid);
|
|
158
|
+
const dot = alive ? style.green('\u25CF') : agent.session_id ? style.yellow('\u25CF') : style.dim('\u25CB');
|
|
159
|
+
const statusText = alive ? 'actif' : agent.session_id ? 'en veille' : 'pas de session';
|
|
160
|
+
|
|
161
|
+
const choice = await interactiveMenu([
|
|
162
|
+
{ label: 'Rejoindre la session', value: 'join', description: '' },
|
|
163
|
+
{ label: 'Envoyer un message', value: 'inject', description: '' },
|
|
164
|
+
{ label: 'Panneau complet', value: 'panneau', description: '' },
|
|
165
|
+
BACK_ITEM,
|
|
166
|
+
], { title: `${agentName} ${dot} ${statusText}` });
|
|
167
|
+
|
|
168
|
+
if (!choice || choice === '__back__') return '__back__';
|
|
169
|
+
if (choice === '__home__') return '__home__';
|
|
170
|
+
|
|
171
|
+
const teamMod = await import('./commands/team.js');
|
|
172
|
+
switch (choice) {
|
|
173
|
+
case 'join': return teamMod.actionJoin(agent, [], flags, config);
|
|
174
|
+
case 'inject': return teamMod.actionInject(agent);
|
|
175
|
+
case 'panneau': return teamMod.handler({ args: ['panneau', agentName], flags, config });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// printHelp — mode direct (inchange)
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
function printHelp() {
|
|
184
|
+
const header = renderBrandHeader({ version: VERSION });
|
|
185
|
+
for (const line of header) console.log(line);
|
|
186
|
+
|
|
187
|
+
const cmdLines = [
|
|
188
|
+
`${style.bold('init')} ${style.gray('Scaffold structure projet')}`,
|
|
189
|
+
`${style.bold('team')} ${style.gray('Gestion equipe agents (add, list, panneau, remove)')}`,
|
|
190
|
+
`${style.bold('project')} ${style.gray('Vue projet (status, init)')}`,
|
|
191
|
+
`${style.bold('mission')} ${style.gray('Mission Contracts (init, list, inspect)')}`,
|
|
192
|
+
`${style.bold('odm')} ${style.gray('Ordres de Mission (init, list, inspect)')}`,
|
|
193
|
+
`${style.bold('status')} ${style.gray('Raccourci project status + alertes')}`,
|
|
194
|
+
`${style.bold('hcm')} ${style.gray('Synchronisation HCM (sync, status, search)')}`,
|
|
195
|
+
`${style.bold('kars')} ${style.gray('Recherche profils Kairos (search)')}`,
|
|
196
|
+
`${style.bold('audit')} ${style.gray('Audit technique (run, list, reports)')}`,
|
|
197
|
+
`${style.bold('services')} ${style.gray('Connexions LLM (connexions add/list/test/remove)')}`,
|
|
198
|
+
`${style.bold('notes')} ${style.gray('Notes & CR — consultation memoire')}`,
|
|
199
|
+
`${style.bold('notewriter')} ${style.gray('Memoire agent — notes, CR, replay')}`,
|
|
200
|
+
`${style.bold('kairos')} ${style.gray('Hooks & contexte agent')}`,
|
|
201
|
+
`${style.bold('auth')} ${style.gray('Connexion HCM (login, status, logout)')}`,
|
|
202
|
+
`${style.bold('inbox')} ${style.gray('Pipeline documents humains (add, list, process)')}`,
|
|
203
|
+
`${style.bold('orch')} ${style.gray('Orchestrateur daemon (dispatch, routing, supervision)')}`,
|
|
204
|
+
`${style.bold('run')} ${style.gray('Reprendre session agent (run agent:<NAME>)')}`,
|
|
205
|
+
`${style.bold('doctor')} ${style.gray('Diagnostic complet')}`,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const flagLines = [
|
|
209
|
+
`${style.bold('--format <fmt>')} ${style.gray('Format de sortie: json, text, yaml (default: text)')}`,
|
|
210
|
+
`${style.bold('--no-color')} ${style.gray('Desactiver les couleurs ANSI')}`,
|
|
211
|
+
`${style.bold('--verbose')} ${style.gray('Sortie detaillee')}`,
|
|
212
|
+
`${style.bold('-h, --help')} ${style.gray('Aide')}`,
|
|
213
|
+
`${style.bold('-V, --version')} ${style.gray('Version')}`,
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
console.log(titledBox('Commands', cmdLines, { border: style.arkaRed }));
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(titledBox('Flags', flagLines, { border: style.arkaRed }));
|
|
219
|
+
console.log('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// parseGlobalFlags
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
function parseGlobalFlags(args) {
|
|
227
|
+
const flags = {
|
|
228
|
+
format: 'text',
|
|
229
|
+
noColor: false,
|
|
230
|
+
verbose: false,
|
|
231
|
+
help: false,
|
|
232
|
+
version: false,
|
|
233
|
+
};
|
|
234
|
+
const remaining = [];
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < args.length; i++) {
|
|
237
|
+
const arg = args[i];
|
|
238
|
+
if (arg === '--format' && i + 1 < args.length) {
|
|
239
|
+
flags.format = args[++i];
|
|
240
|
+
} else if (arg === '--no-color') {
|
|
241
|
+
flags.noColor = true;
|
|
242
|
+
} else if (arg === '--verbose') {
|
|
243
|
+
flags.verbose = true;
|
|
244
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
245
|
+
flags.help = true;
|
|
246
|
+
} else if (arg === '-V' || arg === '--version') {
|
|
247
|
+
flags.version = true;
|
|
248
|
+
} else {
|
|
249
|
+
remaining.push(arg);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { flags, remaining };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// HCM gate
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
async function checkHcm(config) {
|
|
261
|
+
const project = detectProject(config.cwd);
|
|
262
|
+
const client = createHcmClient({ timeout: 3000 });
|
|
263
|
+
const ping = await client.ping();
|
|
264
|
+
|
|
265
|
+
if (project) {
|
|
266
|
+
storeHcmStatus(project.hcm_dir, project.id, {
|
|
267
|
+
connected: ping.ok,
|
|
268
|
+
url: client.baseUrl,
|
|
269
|
+
latency: ping.latency,
|
|
270
|
+
checked_at: new Date().toISOString(),
|
|
271
|
+
...(ping.error ? { error: ping.error } : {}),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return ping;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function blockIfHcmDown(ping) {
|
|
279
|
+
if (ping.ok) return;
|
|
280
|
+
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(` ${style.red('\u2717')} ${style.bold('HCM non connecte')}`);
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log(` La CLI necessite une connexion au HCM pour fonctionner.`);
|
|
285
|
+
if (ping.error) {
|
|
286
|
+
console.log(` ${style.dim('\u2192')} ${style.dim(ping.error)}`);
|
|
287
|
+
}
|
|
288
|
+
console.log('');
|
|
289
|
+
console.log(` ${style.bold('Connectez-vous avec :')} nemesis auth login`);
|
|
290
|
+
console.log(` ${style.bold('Dashboard HCM :')} ${style.nemesisAccent('https://hcm.arkalabs.app')}`);
|
|
291
|
+
console.log('');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const SKIP_HCM_COMMANDS = new Set(['auth', 'init', 'notewriter', 'notes', 'kairos', 'orch', 'inbox']);
|
|
296
|
+
|
|
297
|
+
const READ_ONLY_COMMANDS = new Set(['status', 'project', 'doctor', 'audit', 'team', 'odm', 'mission']);
|
|
298
|
+
|
|
299
|
+
function warnIfHcmDown(ping) {
|
|
300
|
+
if (ping.ok) return;
|
|
301
|
+
console.log('');
|
|
302
|
+
console.log(` ${style.yellow('\u26A0')} ${style.bold('HCM non connecte')} \u2014 mode lecture seule`);
|
|
303
|
+
console.log(` ${style.dim('\u2192')} Reconnectez-vous : ${style.bold('nemesis auth login')}`);
|
|
304
|
+
console.log('');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Boucle principale
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
export async function run(argv) {
|
|
312
|
+
const { flags, remaining } = parseGlobalFlags(argv);
|
|
313
|
+
|
|
314
|
+
if (flags.noColor) {
|
|
315
|
+
process.env.NO_COLOR = '1';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (flags.version) {
|
|
319
|
+
console.log(`nemesis v${VERSION}`);
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const commandName = remaining[0];
|
|
324
|
+
const commandArgs = remaining.slice(1);
|
|
325
|
+
|
|
326
|
+
// --help flag without command -> static help
|
|
327
|
+
if (flags.help && !commandName) {
|
|
328
|
+
printHelp();
|
|
329
|
+
process.exit(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// No command -> interactive menu loop (screen-based)
|
|
333
|
+
if (!commandName) {
|
|
334
|
+
const config = await loadConfig();
|
|
335
|
+
|
|
336
|
+
// HCM gate (interactive = warn only)
|
|
337
|
+
const ping = await checkHcm(config);
|
|
338
|
+
warnIfHcmDown(ping);
|
|
339
|
+
|
|
340
|
+
while (true) {
|
|
341
|
+
clearScreen();
|
|
342
|
+
const header = renderBrandHeader({ version: VERSION });
|
|
343
|
+
for (const line of header) console.log(line);
|
|
344
|
+
|
|
345
|
+
const menuItems = buildMainMenu(config);
|
|
346
|
+
const selected = await interactiveMenu(menuItems, {
|
|
347
|
+
title: 'Que souhaitez-vous faire ?',
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (!selected) {
|
|
351
|
+
clearScreen();
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (selected === '__sep__' || selected === '__coming_soon__') continue;
|
|
356
|
+
|
|
357
|
+
// Acces rapide agent
|
|
358
|
+
if (selected.startsWith('__agent__:')) {
|
|
359
|
+
clearScreen();
|
|
360
|
+
const agentName = selected.replace('__agent__:', '');
|
|
361
|
+
process.stdout.write(renderBrandBanner({ version: VERSION, section: agentName }));
|
|
362
|
+
try {
|
|
363
|
+
const result = await handleQuickAgent(agentName, flags, config);
|
|
364
|
+
if (result !== '__back__' && result !== '__home__') await waitForKey();
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (flags.verbose) console.error(err);
|
|
367
|
+
else console.error(` ${style.red('Erreur')}: ${err.message}\n`);
|
|
368
|
+
await waitForKey();
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Voir tous -> Equipe panneau
|
|
374
|
+
if (selected === '__all_agents__') {
|
|
375
|
+
clearScreen();
|
|
376
|
+
process.stdout.write(renderBrandBanner({ version: VERSION, section: 'Equipe' }));
|
|
377
|
+
const mod = await COMMANDS.team();
|
|
378
|
+
const result = await mod.handler({ args: ['panneau'], flags, config });
|
|
379
|
+
if (result !== '__back__' && result !== '__home__') await waitForKey();
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Categorie -> sous-menu
|
|
384
|
+
const cat = CATEGORY_MAP[selected];
|
|
385
|
+
if (cat) {
|
|
386
|
+
let stayInCategory = true;
|
|
387
|
+
while (stayInCategory) {
|
|
388
|
+
clearScreen();
|
|
389
|
+
process.stdout.write(renderBrandBanner({ version: VERSION, section: cat.title.split(' \u203A ')[1] }));
|
|
390
|
+
|
|
391
|
+
const sub = await interactiveMenu(cat.items, { title: cat.title });
|
|
392
|
+
if (!sub || sub === '__back__') { break; }
|
|
393
|
+
if (sub === '__home__') { break; }
|
|
394
|
+
if (sub === '__coming_soon__') continue;
|
|
395
|
+
|
|
396
|
+
// Dispatch commande
|
|
397
|
+
clearScreen();
|
|
398
|
+
let cmdName = sub;
|
|
399
|
+
let cmdArgs = [];
|
|
400
|
+
if (sub.includes(':')) {
|
|
401
|
+
const [cmd, ...rest] = sub.split(':');
|
|
402
|
+
cmdName = cmd;
|
|
403
|
+
cmdArgs = rest;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
process.stdout.write(renderBrandBanner({ version: VERSION, section: cmdName }));
|
|
407
|
+
const loader = COMMANDS[cmdName];
|
|
408
|
+
if (!loader) continue;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const mod = await loader();
|
|
412
|
+
const result = await mod.handler({ args: cmdArgs, flags, config });
|
|
413
|
+
if (result === '__home__') { stayInCategory = false; break; }
|
|
414
|
+
if (result !== '__back__') await waitForKey();
|
|
415
|
+
} catch (err) {
|
|
416
|
+
if (flags.verbose) console.error(err);
|
|
417
|
+
else console.error(` ${style.red('Erreur')}: ${err.message}\n`);
|
|
418
|
+
await waitForKey();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Direct command path
|
|
427
|
+
const loader = COMMANDS[commandName];
|
|
428
|
+
if (!loader) {
|
|
429
|
+
console.error(`Commande inconnue : ${commandName}`);
|
|
430
|
+
console.error(`Tapez "nemesis --help" pour voir les commandes disponibles.`);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const config = await loadConfig();
|
|
435
|
+
|
|
436
|
+
// HCM gate: skip → no ping, read-only → warn, others → block
|
|
437
|
+
if (!SKIP_HCM_COMMANDS.has(commandName)) {
|
|
438
|
+
const ping = await checkHcm(config);
|
|
439
|
+
if (READ_ONLY_COMMANDS.has(commandName)) {
|
|
440
|
+
warnIfHcmDown(ping);
|
|
441
|
+
} else {
|
|
442
|
+
blockIfHcmDown(ping);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const mod = await loader();
|
|
448
|
+
await mod.handler({ args: commandArgs, flags, config });
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (flags.verbose) {
|
|
451
|
+
console.error(err);
|
|
452
|
+
} else {
|
|
453
|
+
console.error(`Erreur: ${err.message}`);
|
|
454
|
+
}
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { detectProject } from '../../lib/core/project.js';
|
|
3
|
+
import { askConfirm } from '../../lib/ui/prompt.js';
|
|
4
|
+
import { interactiveMenu } from '../../lib/ui/menu.js';
|
|
5
|
+
import { style } from '../../lib/ui/colors.js';
|
|
6
|
+
|
|
7
|
+
const BACK_ITEM = { label: style.dim('\u2190 Retour'), value: '__back__', description: '' };
|
|
8
|
+
const HOME_ITEM = { label: style.dim('\u2302 Accueil'), value: '__home__', description: '' };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect project or propose interactive init. Returns project or null.
|
|
12
|
+
*/
|
|
13
|
+
export async function ensureProject(config, flags) {
|
|
14
|
+
let project = detectProject(config.cwd);
|
|
15
|
+
if (project) return project;
|
|
16
|
+
|
|
17
|
+
console.log(`\n ${style.yellow('\u26A0')} Aucun projet detecte.`);
|
|
18
|
+
const doInit = await askConfirm('Initialiser un projet maintenant ?', true);
|
|
19
|
+
if (doInit) {
|
|
20
|
+
const initMod = await import('./init.js');
|
|
21
|
+
await initMod.handler({ args: [], flags, config });
|
|
22
|
+
project = detectProject(config.cwd);
|
|
23
|
+
}
|
|
24
|
+
if (!project) {
|
|
25
|
+
console.log(` ${style.dim('Utilisez')} nemesis init ${style.dim('pour creer un projet.')}\n`);
|
|
26
|
+
}
|
|
27
|
+
return project || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pick an item from a list via interactive menu.
|
|
32
|
+
* labelFn is used for display — description is NOT auto-populated from item.title
|
|
33
|
+
* to avoid duplication when labelFn already includes it.
|
|
34
|
+
* Returns selected value or null if cancelled.
|
|
35
|
+
*/
|
|
36
|
+
export async function pickFromList(items, opts = {}) {
|
|
37
|
+
const {
|
|
38
|
+
title = 'Choisir',
|
|
39
|
+
labelFn = (item) => item.id || item.label,
|
|
40
|
+
maxVisible = 0,
|
|
41
|
+
} = opts;
|
|
42
|
+
|
|
43
|
+
if (items.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const menuItems = items.map(item => ({
|
|
46
|
+
label: labelFn(item),
|
|
47
|
+
value: item.id || item.value,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
return interactiveMenu(menuItems, { title, maxVisible });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Auto-generate the next sequential ID.
|
|
55
|
+
* @param {string} prefix - e.g. "MCT-NEMESIS" or "ODM-NEMESIS"
|
|
56
|
+
* @param {string[]} existingIds - list of existing IDs
|
|
57
|
+
* @returns {string} next ID like "MCT-NEMESIS-003"
|
|
58
|
+
*/
|
|
59
|
+
export function nextId(prefix, existingIds) {
|
|
60
|
+
let max = 0;
|
|
61
|
+
for (const id of existingIds) {
|
|
62
|
+
const match = id.match(/-(\d+)$/);
|
|
63
|
+
if (match) {
|
|
64
|
+
const num = parseInt(match[1], 10);
|
|
65
|
+
if (num > max) max = num;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return `${prefix}-${String(max + 1).padStart(3, '0')}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clear the terminal screen and move cursor to top-left.
|
|
73
|
+
*/
|
|
74
|
+
export function clearScreen() {
|
|
75
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wait for the user to press Enter before returning.
|
|
80
|
+
* Used after command output so user can read it before screen clears.
|
|
81
|
+
*/
|
|
82
|
+
export async function waitForKey() {
|
|
83
|
+
if (!process.stdin.isTTY) return;
|
|
84
|
+
console.log(`\n ${style.dim('Appuyez sur Entree pour revenir au menu...')}`);
|
|
85
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
86
|
+
return new Promise(resolve => {
|
|
87
|
+
rl.once('line', () => { rl.close(); resolve(); });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract a flag value from args array: --flag <value>
|
|
93
|
+
*/
|
|
94
|
+
export function getFlag(args, flag) {
|
|
95
|
+
const idx = args.indexOf(flag);
|
|
96
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sanitize agent name: strip existing Agent_ prefix, replace spaces/special chars, re-prefix.
|
|
101
|
+
* "Agent stratege" → "Agent_Stratege", "Agent_Agent_Foo" → "Agent_Foo"
|
|
102
|
+
*/
|
|
103
|
+
export function sanitizeName(raw) {
|
|
104
|
+
let name = raw.trim();
|
|
105
|
+
// Strip existing Agent_ or Agent prefix (with underscore or space)
|
|
106
|
+
name = name.replace(/^Agent[_ ]?/i, '');
|
|
107
|
+
// Replace spaces and special chars with underscores, collapse multiples
|
|
108
|
+
name = name.replace(/[^a-zA-Z0-9_-]/g, '_').replace(/_+/g, '_').replace(/^[_-]+|[_-]+$/g, '');
|
|
109
|
+
if (!name) return 'Agent_Unknown';
|
|
110
|
+
// Capitalize first letter
|
|
111
|
+
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
112
|
+
return `Agent_${name}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Back menu item — add as last option in submenus.
|
|
117
|
+
* Home menu item — returns to main CLI menu from any depth.
|
|
118
|
+
*/
|
|
119
|
+
export { BACK_ITEM, HOME_ITEM };
|