@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/core/odm.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { writeFileSync, existsSync, readdirSync, readFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Valid OdM status transitions.
|
|
6
|
+
*/
|
|
7
|
+
const VALID_TRANSITIONS = {
|
|
8
|
+
DRAFT: ['ASSIGNED', 'READY_TO_DISPATCH'],
|
|
9
|
+
ASSIGNED: ['READY_TO_DISPATCH', 'COMPLETED'],
|
|
10
|
+
READY_TO_DISPATCH: ['DISPATCHING'],
|
|
11
|
+
DISPATCHING: ['ASSIGNED', 'READY_TO_DISPATCH', 'COMPLETED'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Set the status of an OdM with transition validation + atomic write.
|
|
16
|
+
* @param {string} id — OdM ID
|
|
17
|
+
* @param {string} newStatus — target status
|
|
18
|
+
* @param {string} hcmDir — path to .nemesis/HCM
|
|
19
|
+
* @returns {{ id: string, oldStatus: string, newStatus: string }}
|
|
20
|
+
*/
|
|
21
|
+
export function setOdmStatus(id, newStatus, hcmDir) {
|
|
22
|
+
const odmDir = join(hcmDir, 'odm');
|
|
23
|
+
const filepath = join(odmDir, `${id}.json`);
|
|
24
|
+
|
|
25
|
+
if (!existsSync(filepath)) {
|
|
26
|
+
throw new Error(`OdM ${id} introuvable.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const odm = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
30
|
+
const oldStatus = odm.odm_meta?.status;
|
|
31
|
+
|
|
32
|
+
if (!oldStatus) {
|
|
33
|
+
throw new Error(`OdM ${id} n'a pas de status.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const allowed = VALID_TRANSITIONS[oldStatus];
|
|
37
|
+
if (!allowed || !allowed.includes(newStatus)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Transition invalide : ${oldStatus} → ${newStatus}. ` +
|
|
40
|
+
`Transitions valides depuis ${oldStatus} : ${(allowed || []).join(', ') || '(aucune)'}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
odm.odm_meta.status = newStatus;
|
|
45
|
+
odm.odm_meta.updated_at = new Date().toISOString();
|
|
46
|
+
|
|
47
|
+
// Atomic write: tmp + rename
|
|
48
|
+
const tmpPath = filepath + '.tmp';
|
|
49
|
+
writeFileSync(tmpPath, JSON.stringify(odm, null, 2) + '\n', 'utf-8');
|
|
50
|
+
renameSync(tmpPath, filepath);
|
|
51
|
+
|
|
52
|
+
return { id, oldStatus, newStatus };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Assign an OdM to an agent (sets assigned_to field).
|
|
57
|
+
* @param {string} id — OdM ID
|
|
58
|
+
* @param {string} agentId — agent name/id
|
|
59
|
+
* @param {string} hcmDir — path to .nemesis/HCM
|
|
60
|
+
* @returns {{ id: string, assignedTo: string }}
|
|
61
|
+
*/
|
|
62
|
+
export function assignOdm(id, agentId, hcmDir) {
|
|
63
|
+
const odmDir = join(hcmDir, 'odm');
|
|
64
|
+
const filepath = join(odmDir, `${id}.json`);
|
|
65
|
+
|
|
66
|
+
if (!existsSync(filepath)) {
|
|
67
|
+
throw new Error(`OdM ${id} introuvable.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const odm = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
71
|
+
odm.odm_meta.assigned_to = { actor_type: 'agent', actor_id: agentId };
|
|
72
|
+
odm.odm_meta.updated_at = new Date().toISOString();
|
|
73
|
+
|
|
74
|
+
const tmpPath = filepath + '.tmp';
|
|
75
|
+
writeFileSync(tmpPath, JSON.stringify(odm, null, 2) + '\n', 'utf-8');
|
|
76
|
+
renameSync(tmpPath, filepath);
|
|
77
|
+
|
|
78
|
+
return { id, assignedTo: agentId };
|
|
79
|
+
}
|
|
80
|
+
import { loadTemplate, mergeTemplate } from './templates.js';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Init an OdM — creates a DRAFT ODM-*.json.
|
|
84
|
+
*/
|
|
85
|
+
export function initOdm(id, opts = {}) {
|
|
86
|
+
const {
|
|
87
|
+
title = '', assignedTo = '', priority = 'HAUTE',
|
|
88
|
+
root = process.cwd(), projectId = '',
|
|
89
|
+
} = opts;
|
|
90
|
+
|
|
91
|
+
const hcmDir = join(root, '.nemesis', 'HCM');
|
|
92
|
+
const odmDir = join(hcmDir, 'odm');
|
|
93
|
+
|
|
94
|
+
if (!existsSync(odmDir)) {
|
|
95
|
+
throw new Error('.nemesis/HCM/odm/ introuvable. Lancez "nemesis init" d\'abord.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const template = loadTemplate('odm', root);
|
|
99
|
+
const odm = mergeTemplate(template, {
|
|
100
|
+
odm_meta: {
|
|
101
|
+
odm_id: id,
|
|
102
|
+
project_id: projectId,
|
|
103
|
+
status: 'DRAFT',
|
|
104
|
+
priority,
|
|
105
|
+
created_at: new Date().toISOString(),
|
|
106
|
+
created_by: { actor_type: 'human', actor_id: 'PM' },
|
|
107
|
+
assigned_to: assignedTo ? { actor_type: 'agent', actor_id: assignedTo } : { actor_type: '', actor_id: '' },
|
|
108
|
+
},
|
|
109
|
+
odm_payload: {
|
|
110
|
+
cadrage: { title },
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const filepath = join(odmDir, `${id}.json`);
|
|
115
|
+
if (existsSync(filepath)) {
|
|
116
|
+
throw new Error(`OdM ${id} existe deja.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
writeFileSync(filepath, JSON.stringify(odm, null, 2) + '\n', 'utf-8');
|
|
120
|
+
return { filepath, id, status: 'DRAFT' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* List all OdMs — ignores legacy_* files.
|
|
125
|
+
*/
|
|
126
|
+
export function listOdm(hcmDir) {
|
|
127
|
+
const odmDir = join(hcmDir, 'odm');
|
|
128
|
+
if (!existsSync(odmDir)) return [];
|
|
129
|
+
|
|
130
|
+
return readdirSync(odmDir)
|
|
131
|
+
.filter(f => f.endsWith('.json') && !f.startsWith('legacy_'))
|
|
132
|
+
.map(f => {
|
|
133
|
+
try {
|
|
134
|
+
const data = JSON.parse(readFileSync(join(odmDir, f), 'utf-8'));
|
|
135
|
+
return {
|
|
136
|
+
id: data.odm_meta?.odm_id || f.replace('.json', ''),
|
|
137
|
+
title: data.odm_payload?.cadrage?.title || '',
|
|
138
|
+
assignee: data.odm_meta?.assigned_to?.actor_id || '',
|
|
139
|
+
priority: data.odm_meta?.priority || '',
|
|
140
|
+
status: data.odm_meta?.status || 'UNKNOWN',
|
|
141
|
+
};
|
|
142
|
+
} catch (_e) {
|
|
143
|
+
/* fallback: malformed OdM */
|
|
144
|
+
return { id: f.replace('.json', ''), title: '', assignee: '', priority: '', status: 'ERROR' };
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Inspect a specific OdM — full content + associated CRs.
|
|
151
|
+
*/
|
|
152
|
+
export function inspectOdm(id, hcmDir) {
|
|
153
|
+
const odmDir = join(hcmDir, 'odm');
|
|
154
|
+
const filepath = join(odmDir, `${id}.json`);
|
|
155
|
+
|
|
156
|
+
if (!existsSync(filepath)) {
|
|
157
|
+
throw new Error(`OdM ${id} introuvable.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const odm = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
161
|
+
|
|
162
|
+
// Find associated CRs
|
|
163
|
+
const crDir = join(hcmDir, 'cr');
|
|
164
|
+
let associatedCrs = [];
|
|
165
|
+
if (existsSync(crDir)) {
|
|
166
|
+
associatedCrs = readdirSync(crDir)
|
|
167
|
+
.filter(f => f.endsWith('.json'))
|
|
168
|
+
.map(f => {
|
|
169
|
+
try {
|
|
170
|
+
const data = JSON.parse(readFileSync(join(crDir, f), 'utf-8'));
|
|
171
|
+
return { file: f, data };
|
|
172
|
+
} catch (_e) {
|
|
173
|
+
/* fallback: malformed CR */
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.filter(cr => {
|
|
179
|
+
const odmRef = cr.data.metadata?.odm_id || '';
|
|
180
|
+
return odmRef === id || cr.file.includes(id.replace('ODM-', ''));
|
|
181
|
+
})
|
|
182
|
+
.map(cr => ({
|
|
183
|
+
id: cr.data.metadata?.odm_id || cr.file,
|
|
184
|
+
title: cr.data.title || '',
|
|
185
|
+
status: cr.data.metadata?.status || 'UNKNOWN',
|
|
186
|
+
date: cr.data.metadata?.date_execution || '',
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { odm, associatedCrs };
|
|
191
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { interactiveMenu } from '../ui/menu.js';
|
|
2
|
+
import { titledBox } from '../ui/box.js';
|
|
3
|
+
import { style } from '../ui/colors.js';
|
|
4
|
+
import { askText, askConfirm } from '../ui/prompt.js';
|
|
5
|
+
import { createSpinner } from '../ui/spinner.js';
|
|
6
|
+
import { debug } from './logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Search profiles by sector via interactive picker.
|
|
10
|
+
* Returns selected profileId or null if cancelled.
|
|
11
|
+
* @param {object} client
|
|
12
|
+
* @param {function} write — UI output callback
|
|
13
|
+
*/
|
|
14
|
+
async function searchBySector(client, write) {
|
|
15
|
+
const spinner = createSpinner('Chargement des secteurs...');
|
|
16
|
+
spinner.start();
|
|
17
|
+
|
|
18
|
+
let profiles;
|
|
19
|
+
try {
|
|
20
|
+
// q is required by the API — use 'a' to match all profiles
|
|
21
|
+
const all = await client.searchProfiles('a');
|
|
22
|
+
profiles = Array.isArray(all) ? all : [];
|
|
23
|
+
} catch (e) {
|
|
24
|
+
debug(`searchBySector: ${e.message}`);
|
|
25
|
+
spinner.fail('Recherche indisponible');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sectorSet = new Set();
|
|
30
|
+
for (const p of profiles) {
|
|
31
|
+
const sectors = Array.isArray(p.sectors) ? p.sectors : [];
|
|
32
|
+
for (const s of sectors) {
|
|
33
|
+
if (s) sectorSet.add(s);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const sectors = [...sectorSet].sort((a, b) => a.localeCompare(b, 'fr'));
|
|
37
|
+
spinner.stop(`${sectors.length} secteurs`);
|
|
38
|
+
|
|
39
|
+
if (sectors.length === 0) {
|
|
40
|
+
write(` ${style.yellow('\u26A0')} Aucun secteur disponible.\n`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sector = await interactiveMenu(
|
|
45
|
+
sectors.map(s => ({ label: s, value: s })),
|
|
46
|
+
{ title: 'Filtrer par secteur', maxVisible: 15 },
|
|
47
|
+
);
|
|
48
|
+
if (!sector) return null;
|
|
49
|
+
|
|
50
|
+
const sp2 = createSpinner(`Recherche secteur : ${sector}...`);
|
|
51
|
+
sp2.start();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const results = await client.searchProfiles(null, sector);
|
|
55
|
+
const filtered = Array.isArray(results) ? results : [];
|
|
56
|
+
sp2.stop(`${filtered.length} profils`);
|
|
57
|
+
|
|
58
|
+
if (filtered.length === 0) {
|
|
59
|
+
write(` Aucun profil pour le secteur "${sector}".\n`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const items = filtered.map(p => ({
|
|
64
|
+
label: p.id,
|
|
65
|
+
value: p.id,
|
|
66
|
+
description: p.role || '',
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
return interactiveMenu(items, {
|
|
70
|
+
title: `Profils — ${sector}`,
|
|
71
|
+
maxVisible: 10,
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
debug(`searchBySector filter: ${e.message}`);
|
|
75
|
+
sp2.fail('Erreur recherche');
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch profile list from HCM, enriched with role from NUCLEUS.
|
|
82
|
+
* Priority: /profiles/summary (1 request, id+role pre-joined).
|
|
83
|
+
* Fallback: listProfiles + fetchAllRoles (searchAtoms batch or per-profile).
|
|
84
|
+
* Returns sorted array of { id, role } or empty array.
|
|
85
|
+
*/
|
|
86
|
+
export async function fetchProfileList(client) {
|
|
87
|
+
// Fast path: /profiles/summary returns [{ id, role }] in 1 request
|
|
88
|
+
if (typeof client.listProfilesSummary === 'function') {
|
|
89
|
+
try {
|
|
90
|
+
const summary = await client.listProfilesSummary();
|
|
91
|
+
if (Array.isArray(summary) && summary.length > 0) {
|
|
92
|
+
return summary
|
|
93
|
+
.map(p => ({ id: p.id || '', role: p.role || '' }))
|
|
94
|
+
.filter(p => p.id)
|
|
95
|
+
.sort((a, b) => a.id.localeCompare(b.id, 'fr'));
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
debug(`fetchProfileList summary: ${e.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fallback: listProfiles + enrich with roles
|
|
103
|
+
const profiles = await client.listProfiles();
|
|
104
|
+
if (!Array.isArray(profiles) || profiles.length === 0) return [];
|
|
105
|
+
|
|
106
|
+
const list = profiles
|
|
107
|
+
.map(p => ({ id: p.label || p.profileId || p.id, role: '' }))
|
|
108
|
+
.filter(p => p.id)
|
|
109
|
+
.sort((a, b) => a.id.localeCompare(b.id, 'fr'));
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const roleMap = await fetchAllRoles(client);
|
|
113
|
+
for (const item of list) {
|
|
114
|
+
if (roleMap[item.id]) {
|
|
115
|
+
item.role = roleMap[item.id];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
debug(`fetchProfileList roles: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return list;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fetch all NUCLEUS role atoms in one call, return { profileId → shortRole } map.
|
|
127
|
+
*/
|
|
128
|
+
async function fetchAllRoles(client) {
|
|
129
|
+
const map = {};
|
|
130
|
+
|
|
131
|
+
// Try batch searchAtoms first (1 call)
|
|
132
|
+
if (typeof client.searchAtoms === 'function') {
|
|
133
|
+
try {
|
|
134
|
+
const atoms = await client.searchAtoms({ sousFamille: 'NUCLEUS', type: 'role', limit: 200 });
|
|
135
|
+
for (const a of atoms) {
|
|
136
|
+
const profileId = a.famille || a.profileId || '';
|
|
137
|
+
const occ = a.occurrence || a.node?.data?.occurrence || a.data?.occurrence || '';
|
|
138
|
+
if (profileId && occ) {
|
|
139
|
+
map[profileId] = occ.split(' \u2014 ')[0] || occ;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (Object.keys(map).length > 0) return map;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
debug(`fetchAllRoles searchAtoms: ${e.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: per-profile fetch, 5 at a time max
|
|
149
|
+
if (typeof client.getProfileAtoms === 'function') {
|
|
150
|
+
try {
|
|
151
|
+
const profiles = await client.listProfiles();
|
|
152
|
+
const ids = (Array.isArray(profiles) ? profiles : [])
|
|
153
|
+
.map(p => p.label || p.profileId || p.id)
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
|
|
156
|
+
const BATCH = 5;
|
|
157
|
+
for (let i = 0; i < ids.length; i += BATCH) {
|
|
158
|
+
const batch = ids.slice(i, i + BATCH);
|
|
159
|
+
const results = await Promise.allSettled(
|
|
160
|
+
batch.map(id => client.getProfileAtoms(id, ['NUCLEUS']).catch(() => []))
|
|
161
|
+
);
|
|
162
|
+
for (let j = 0; j < batch.length; j++) {
|
|
163
|
+
if (results[j].status === 'fulfilled') {
|
|
164
|
+
const atoms = results[j].value;
|
|
165
|
+
const items = Array.isArray(atoms) ? atoms : (atoms?.atoms || []);
|
|
166
|
+
const nucleus = extractNucleus(items);
|
|
167
|
+
if (nucleus.role) {
|
|
168
|
+
map[batch[j]] = nucleus.role.split(' \u2014 ')[0] || nucleus.role;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
debug(`fetchAllRoles per-profile: ${e.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return map;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetch NUCLEUS atoms for a single profile (role, mission, mindset).
|
|
183
|
+
* Returns { role, mission, mindset } or null.
|
|
184
|
+
*/
|
|
185
|
+
export async function fetchProfileDetail(client, profileId) {
|
|
186
|
+
try {
|
|
187
|
+
const atoms = await client.getProfileAtoms(profileId, ['NUCLEUS']);
|
|
188
|
+
const items = Array.isArray(atoms) ? atoms : (atoms?.atoms || []);
|
|
189
|
+
return extractNucleus(items);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
debug(`fetchProfileDetail ${profileId}: ${e.message}`);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extract role/mission/mindset from NUCLEUS atoms array.
|
|
198
|
+
*/
|
|
199
|
+
export function extractNucleus(atoms) {
|
|
200
|
+
const get = (label) => {
|
|
201
|
+
const atom = atoms.find(a =>
|
|
202
|
+
(a.node?.label || a.label || '').toLowerCase() === label
|
|
203
|
+
);
|
|
204
|
+
return atom?.node?.data?.occurrence || atom?.data?.occurrence || '';
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
role: get('role'),
|
|
208
|
+
mission: get('mission'),
|
|
209
|
+
mindset: get('mindset'),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Render a profile confirmation box using titledBox.
|
|
215
|
+
* Returns the formatted string.
|
|
216
|
+
*/
|
|
217
|
+
export function renderProfileConfirmation(profileId, detail) {
|
|
218
|
+
const lines = [];
|
|
219
|
+
if (detail.role) {
|
|
220
|
+
lines.push(`${style.bold('Role')} ${detail.role}`);
|
|
221
|
+
}
|
|
222
|
+
if (detail.mission) {
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push(`${style.bold('Mission')} ${detail.mission}`);
|
|
225
|
+
}
|
|
226
|
+
if (detail.mindset) {
|
|
227
|
+
lines.push('');
|
|
228
|
+
lines.push(`${style.bold('Mindset')} ${style.dim(detail.mindset)}`);
|
|
229
|
+
}
|
|
230
|
+
return titledBox(`Profil : ${profileId}`, lines, {
|
|
231
|
+
border: style.nemesisAccent,
|
|
232
|
+
padding: 1,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Full interactive profile picker: list → select → confirm → return profileId.
|
|
238
|
+
* @param {object} client — HCM client
|
|
239
|
+
* @param {{ write?: function }} [opts] — write callback for UI output (default: no-op)
|
|
240
|
+
* @returns {Promise<{ id: string, kairosData: object|null }>}
|
|
241
|
+
*/
|
|
242
|
+
export async function pickProfile(client, { write = () => {} } = {}) {
|
|
243
|
+
let profileItems = [{ label: style.dim('(saisie manuelle)'), value: '__custom__' }];
|
|
244
|
+
let hcmConnected = false;
|
|
245
|
+
|
|
246
|
+
const spinner = createSpinner('Chargement des profils Kairos...');
|
|
247
|
+
spinner.start();
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const profiles = await fetchProfileList(client);
|
|
251
|
+
hcmConnected = true;
|
|
252
|
+
spinner.stop(`${profiles.length} profils charges`);
|
|
253
|
+
if (profiles.length > 0) {
|
|
254
|
+
profileItems = [
|
|
255
|
+
{ label: style.nemesisAccent('(rechercher par secteur)'), value: '__search__' },
|
|
256
|
+
...profiles.map(p => ({
|
|
257
|
+
label: p.id,
|
|
258
|
+
value: p.id,
|
|
259
|
+
description: p.role || '',
|
|
260
|
+
})),
|
|
261
|
+
{ label: style.dim('(saisie manuelle)'), value: '__custom__' },
|
|
262
|
+
];
|
|
263
|
+
} else {
|
|
264
|
+
write(` ${style.yellow('\u26A0')} HCM connecte mais aucun profil Kairos disponible.\n`);
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
spinner.fail('HCM non connecte');
|
|
268
|
+
write(`\n ${style.yellow('\u26A0')} HCM non connecte \u2014 saisie manuelle du profil.`);
|
|
269
|
+
write(` ${style.dim('\u2192')} ${style.dim(err.message || String(err))}`);
|
|
270
|
+
write(` ${style.dim('\u2192 Configurez avec')} nemesis auth login\n`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Selection loop — allows retry on confirmation decline
|
|
274
|
+
while (true) {
|
|
275
|
+
const profileCount = profileItems.filter(p => p.value !== '__custom__').length;
|
|
276
|
+
const titleSuffix = hcmConnected && profileCount > 0 ? ` (${profileCount} profils HCM)` : '';
|
|
277
|
+
const selected = await interactiveMenu(profileItems, {
|
|
278
|
+
title: `Profil Kairos${titleSuffix}`,
|
|
279
|
+
maxVisible: 10,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (selected === '__search__') {
|
|
283
|
+
const found = await searchBySector(client, write);
|
|
284
|
+
if (found) {
|
|
285
|
+
const spinner2 = createSpinner('Chargement du profil...');
|
|
286
|
+
spinner2.start();
|
|
287
|
+
const detail = await fetchProfileDetail(client, found);
|
|
288
|
+
spinner2.stop(found);
|
|
289
|
+
if (detail && (detail.role || detail.mission)) {
|
|
290
|
+
write('');
|
|
291
|
+
write(renderProfileConfirmation(found, detail));
|
|
292
|
+
const confirmed = await askConfirm('Confirmer ce profil ?', true);
|
|
293
|
+
if (confirmed) return { id: found, kairosData: detail };
|
|
294
|
+
} else {
|
|
295
|
+
return { id: found, kairosData: null };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (selected === '__custom__' || selected === null) {
|
|
302
|
+
const customId = await askText('Profil Kairos ID (optionnel)', '');
|
|
303
|
+
return { id: customId, kairosData: null };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Confirmation screen — fetch NUCLEUS for the selected profile only
|
|
307
|
+
if (hcmConnected) {
|
|
308
|
+
const spinner3 = createSpinner('Chargement du profil...');
|
|
309
|
+
spinner3.start();
|
|
310
|
+
const detail = await fetchProfileDetail(client, selected);
|
|
311
|
+
spinner3.stop(selected);
|
|
312
|
+
if (detail && (detail.role || detail.mission)) {
|
|
313
|
+
write('');
|
|
314
|
+
write(renderProfileConfirmation(selected, detail));
|
|
315
|
+
const confirmed = await askConfirm('Confirmer ce profil ?', true);
|
|
316
|
+
if (confirmed) return { id: selected, kairosData: detail };
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { id: selected, kairosData: null };
|
|
322
|
+
}
|
|
323
|
+
}
|