@dev-kosaly/kagents 0.1.1 → 0.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/README.md CHANGED
@@ -50,16 +50,29 @@ Prérequis : Node 18 ou plus. Depuis la racine du projet :
50
50
 
51
51
  ```bash
52
52
  npx @dev-kosaly/kagents # ou : pnpm dlx @dev-kosaly/kagents
53
- npx @dev-kosaly/kagents --tools claude,cursor,agents
53
+ npx @dev-kosaly/kagents --tools claude,cursor,agents # sans question
54
+ npx @dev-kosaly/kagents --configure # rechoisir les outils
54
55
  npx @dev-kosaly/kagents --copy # copies au lieu de liens (utile sous Windows)
55
56
  npx @dev-kosaly/kagents uninstall
56
57
  ```
57
58
 
58
- Par défaut, le kit détecte les outils présents (`.claude/`, `.cursor/`) et branche aussi `.agents/skills/`.
59
+ Ajouter le paquet comme dépendance (`npm add @dev-kosaly/kagents` ou `pnpm add @dev-kosaly/kagents`) lance aussi l'installation automatiquement. Avec **pnpm**, les scripts des dépendances sont bloqués tant qu'ils ne sont pas approuvés : lancez `pnpm approve-builds` une fois (ou `pnpm exec kagents` à la main). `KAGENTS_SKIP_POSTINSTALL=1` désactive l'installation automatique.
60
+
61
+ Dans un terminal, l'installation pose la question :
62
+
63
+ ```text
64
+ ? Pour quels outils installer KAgents ? (↑↓ espace a entrée)
65
+ ❯ ◉ Claude Code (.claude/commands, .claude/agents)
66
+ ◯ Cursor (.cursor/commands, .cursor/agents, .cursor/rules)
67
+ ◉ Codex et autres outils AGENTS.md (.agents/skills)
68
+ ```
69
+
70
+ Le choix est mémorisé dans `.kagents/config.json` et rejoué aux mises à jour. Sans terminal (CI) ou avec `--yes`, les outils sont détectés automatiquement (`.claude/`, `.cursor/`) et `.agents/skills/` est ajouté.
59
71
 
60
72
  Ce que fait l'installation :
61
- - Le kit est copié dans `.kagents/`. `.claude/`, `.cursor/` et `.agents/` ne contiennent que des liens relatifs.
73
+ - Le kit est copié dans `.kagents/`. `.claude/` et `.cursor/` reçoivent des liens relatifs vers les commandes et les agents (les skills restent dans `.kagents/skills/` pour ne pas polluer le menu `/`) ; `.agents/skills/` expose les skills aux outils qui le lisent.
62
74
  - Le bloc `<!-- kagents:start/end -->` de `AGENTS.md` est régénéré (mode d'emploi, agents, routage). Le reste du fichier n'est jamais modifié.
75
+ - `.gitignore` reçoit un bloc `# kagents:start/end` qui ignore le kit dans `.kagents/` mais pas `.kagents/docs/` (livrables et contexte, à versionner).
63
76
  - Un fichier existant qui n'est pas géré par KAgents n'est jamais écrasé.
64
77
 
65
78
  **Mise à jour** : relancer avec `@latest`. Les fichiers du kit dans `.kagents/` sont régénérés (ne pas les éditer). `.kagents/docs/`, qui contient les livrables des agents et `knowledge/context.md`, n'est jamais touché.
@@ -116,7 +116,7 @@ Handoff Developer : perimetre implementation **apres** validations ; ne pas defi
116
116
  | `agents/architect.md` | Qui, limites, modes, principes |
117
117
  | `rules/domains/architect-invariants.md` | Invariants non negociables |
118
118
  | `rules/domains/architect-chat.md` | Contrat de sortie conversationnelle |
119
- | `skills/architect-*` | Procedures |
119
+ | `skills/architect-*` | Procedures (installées dans `.kagents/skills/<nom>/SKILL.md` : les charger par ce chemin) |
120
120
  | `workflows/architect-*.md` | Enchainement |
121
121
  | `templates/architect-output/` | Structure livrable |
122
122
  | `commands/architect*.md` | Entrees explicites |
package/bin/kagents.js CHANGED
@@ -5,25 +5,29 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const readline = require('readline');
9
+ const tty = require('tty');
8
10
 
9
11
  const PKG_ROOT = path.resolve(__dirname, '..');
10
12
  const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
11
13
  const KIT_DIRS = ['agents', 'skills', 'commands', 'rules', 'templates', 'checklists', 'workflows', 'governance', 'docs'];
12
14
  const SKIP = new Set(['README.md', '.gitkeep']);
13
15
  const BLOCK_RE = /<!-- kagents:start -->[\s\S]*?<!-- kagents:end -->/;
16
+ const GITIGNORE_RE = /# kagents:start\n[\s\S]*?# kagents:end\n?/;
17
+ // On ignore le kit (régénérable) mais pas docs/ : livrables et contexte sont à versionner.
14
18
 
15
19
  // Adaptateurs : un outil = une liste [dossier du kit, destination, type].
16
20
  // type : files (fichiers), dirs (dossiers), agents (fichiers .md avec `name:`).
21
+ // Les skills ne sont volontairement PAS liées dans .claude/ ni .cursor/ : ces outils les listeraient
22
+ // comme commandes `/`. Les agents et commandes les chargent par chemin depuis .kagents/skills/.
17
23
  const ADAPTERS = {
18
24
  agents: [['skills', '.agents/skills', 'dirs']],
19
25
  claude: [
20
26
  ['commands', '.claude/commands', 'files'],
21
- ['skills', '.claude/skills', 'dirs'],
22
27
  ['agents', '.claude/agents', 'agents'],
23
28
  ],
24
29
  cursor: [
25
30
  ['commands', '.cursor/commands', 'files'],
26
- ['skills', '.cursor/skills', 'dirs'],
27
31
  ['agents', '.cursor/agents', 'agents'],
28
32
  ['rules/global', '.cursor/rules', 'files'],
29
33
  ['rules/domains', '.cursor/rules', 'files'],
@@ -32,14 +36,18 @@ const ADAPTERS = {
32
36
 
33
37
  const HELP = `kagents ${PKG.version}
34
38
 
35
- Usage : kagents [install] [--tools claude,cursor,agents|auto] [--copy]
39
+ Usage : kagents [install] [--tools claude,cursor,agents|auto|none] [--yes] [--copy]
40
+ kagents --configure (rechoisir les outils)
36
41
  kagents uninstall
37
42
  kagents --help | --version
38
43
 
39
- À lancer depuis la racine du projet.
40
- --tools outils à brancher (défaut : auto = agents + claude/cursor s'ils sont détectés)
41
- --copy copies au lieu de liens symboliques (aussi la variable KAGENTS_MODE=copy)
42
- uninstall retire ce que KAgents a installé (docs/ est conservé)
44
+ À lancer depuis la racine du projet. Dans un terminal, une liste permet de choisir
45
+ les outils à brancher ; le choix est mémorisé dans .kagents/config.json.
46
+ --tools outils à brancher, sans question
47
+ --yes, -y ne pose pas de question (détection automatique)
48
+ --configure pose à nouveau la question
49
+ --copy copies au lieu de liens symboliques (aussi KAGENTS_MODE=copy)
50
+ uninstall retire ce que KAgents a installé (docs/ est conservé)
43
51
  `;
44
52
 
45
53
  const log = (m) => console.log(`kagents: ${m}`);
@@ -51,12 +59,14 @@ const die = (m) => {
51
59
 
52
60
  // --- Arguments ----------------------------------------------------------------
53
61
  function parseArgs(argv) {
54
- const o = { cmd: 'install', tools: 'auto', copy: process.env.KAGENTS_MODE === 'copy' };
62
+ const o = { cmd: 'install', tools: null, yes: false, configure: false, copy: process.env.KAGENTS_MODE === 'copy' };
55
63
  for (let i = 0; i < argv.length; i++) {
56
64
  const a = argv[i];
57
65
  if (a === '-h' || a === '--help') o.cmd = 'help';
58
66
  else if (a === '-v' || a === '--version') o.cmd = 'version';
59
67
  else if (a === '--copy') o.copy = true;
68
+ else if (a === '-y' || a === '--yes') o.yes = true;
69
+ else if (a === '--configure') o.configure = true;
60
70
  else if (a === '--tools') o.tools = argv[++i] || die('--tools attend une valeur');
61
71
  else if (a.startsWith('--tools=')) o.tools = a.slice(8);
62
72
  else if (a === 'install' || a === 'uninstall') o.cmd = a;
@@ -102,6 +112,7 @@ class Installer {
102
112
  this.copy = copy;
103
113
  this.kagents = path.join(target, '.kagents');
104
114
  this.manifest = path.join(this.kagents, '.installed');
115
+ this.config = path.join(this.kagents, 'config.json');
105
116
  this.entries = [];
106
117
  }
107
118
 
@@ -266,15 +277,49 @@ Document partagé, écrit par l'utilisateur. Les agents le lisent, ne l'écriven
266
277
  fs.writeFileSync(file, out);
267
278
  }
268
279
 
269
- resolveTools(tools) {
270
- if (tools !== 'auto') return tools.split(',').filter(Boolean);
271
- const t = ['agents'];
280
+ // Bloc .gitignore : le kit et les liens que l'on a posés, jamais docs/ (sauf les fichiers du kit qui s'y trouvent).
281
+ gitignoreBlock() {
282
+ const extra = this.entries.filter((e) => !e.startsWith('.kagents/') || e.startsWith('.kagents/docs/')).sort();
283
+ return ['# kagents:start', '.kagents/*', '!.kagents/docs/', ...extra, '# kagents:end'].join('\n') + '\n';
284
+ }
285
+
286
+ writeGitignore() {
287
+ const file = path.join(this.target, '.gitignore');
288
+ if (!fs.existsSync(file) && !fs.existsSync(path.join(this.target, '.git'))) return;
289
+ const cur = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
290
+ const block = this.gitignoreBlock();
291
+ const out = GITIGNORE_RE.test(cur)
292
+ ? cur.replace(GITIGNORE_RE, () => block)
293
+ : (cur && !cur.endsWith('\n') ? cur + '\n' : cur) + (cur ? '\n' : '') + block;
294
+ fs.writeFileSync(file, out);
295
+ }
296
+
297
+ detectTools() {
272
298
  const has = (p) => fs.existsSync(path.join(this.target, p));
299
+ const t = [];
273
300
  if (has('.claude') || has('CLAUDE.md')) t.push('claude');
274
301
  if (has('.cursor')) t.push('cursor');
275
302
  return t;
276
303
  }
277
304
 
305
+ resolveTools(tools) {
306
+ return tools === 'auto' ? ['agents', ...this.detectTools()] : tools;
307
+ }
308
+
309
+ loadConfig() {
310
+ try {
311
+ const c = JSON.parse(fs.readFileSync(this.config, 'utf8'));
312
+ return Array.isArray(c.tools) ? c.tools : null;
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ saveConfig(tools) {
319
+ fs.mkdirSync(this.kagents, { recursive: true });
320
+ fs.writeFileSync(this.config, JSON.stringify({ tools }, null, 2) + '\n');
321
+ }
322
+
278
323
  runAdapters(tools) {
279
324
  for (const tool of this.resolveTools(tools)) {
280
325
  if (!ADAPTERS[tool]) {
@@ -326,6 +371,7 @@ Document partagé, écrit par l'utilisateur. Les agents le lisent, ne l'écriven
326
371
  this.createDocs();
327
372
  this.writeAgentsMd();
328
373
  this.runAdapters(tools);
374
+ this.writeGitignore();
329
375
  this.saveManifest();
330
376
  log(`installé dans ${this.kagents} (${this.copy ? 'copies' : 'liens'})`);
331
377
  }
@@ -341,22 +387,125 @@ Document partagé, écrit par l'utilisateur. Les agents le lisent, ne l'écriven
341
387
  else fs.rmSync(file);
342
388
  }
343
389
  }
390
+ const gi = path.join(this.target, '.gitignore');
391
+ if (fs.existsSync(gi) && GITIGNORE_RE.test(fs.readFileSync(gi, 'utf8'))) {
392
+ const out = fs.readFileSync(gi, 'utf8').replace(GITIGNORE_RE, '').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '');
393
+ if (out) fs.writeFileSync(gi, out + '\n');
394
+ else fs.rmSync(gi);
395
+ }
344
396
  fs.rmSync(this.manifest, { force: true });
397
+ fs.rmSync(this.config, { force: true });
345
398
  this.prune(removed);
346
399
  log('désinstallé (.kagents/docs/ conservé : ce sont vos livrables)');
347
400
  }
348
401
  }
349
402
 
403
+ // --- Question interactive -----------------------------------------------------
404
+ // Terminal utilisable : le nôtre, ou /dev/tty quand on est lancé par un postinstall (stdin/stdout détournés).
405
+ function openTTY() {
406
+ if (process.stdin.isTTY && process.stdout.isTTY) return { input: process.stdin, output: process.stdout, close() {} };
407
+ if (process.env.KAGENTS_FROM_POSTINSTALL && process.platform !== 'win32') {
408
+ try {
409
+ const rfd = fs.openSync('/dev/tty', 'r');
410
+ const wfd = fs.openSync('/dev/tty', 'w');
411
+ const input = new tty.ReadStream(rfd);
412
+ const output = new tty.WriteStream(wfd);
413
+ return { input, output, close: () => { input.destroy(); output.destroy(); } };
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+ return null;
419
+ }
420
+
421
+ // Liste à cocher : ↑↓ déplacer, espace cocher, a tout, entrée valider. Renvoie les ids cochés.
422
+ function multiselect(io, message, items) {
423
+ return new Promise((resolve) => {
424
+ const { input, output } = io;
425
+ const state = items.map((i) => ({ ...i }));
426
+ let cur = 0;
427
+ let drawn = false;
428
+ const draw = () => {
429
+ if (drawn) output.write(`\x1b[${state.length + 1}A`);
430
+ drawn = true;
431
+ output.write(`\x1b[2K\x1b[1m?\x1b[0m ${message}\n`);
432
+ state.forEach((it, i) => {
433
+ const box = it.checked ? '\x1b[32m◉\x1b[0m' : '◯';
434
+ output.write(`\x1b[2K ${i === cur ? '\x1b[36m❯\x1b[0m' : ' '} ${box} ${it.label}\n`);
435
+ });
436
+ };
437
+ const finish = () => {
438
+ input.removeListener('keypress', onKey);
439
+ input.setRawMode(false);
440
+ input.pause();
441
+ output.write('\x1b[?25h');
442
+ io.close();
443
+ resolve(state.filter((i) => i.checked).map((i) => i.id));
444
+ };
445
+ const onKey = (str, key = {}) => {
446
+ if (key.ctrl && key.name === 'c') {
447
+ output.write('\x1b[?25h\n');
448
+ process.exit(130);
449
+ }
450
+ if (key.name === 'up') cur = (cur + state.length - 1) % state.length;
451
+ else if (key.name === 'down') cur = (cur + 1) % state.length;
452
+ else if (key.name === 'space') state[cur].checked = !state[cur].checked;
453
+ else if (key.name === 'a') {
454
+ const all = state.every((i) => i.checked);
455
+ state.forEach((i) => (i.checked = !all));
456
+ } else if (key.name === 'return') {
457
+ draw();
458
+ return finish();
459
+ }
460
+ draw();
461
+ };
462
+ readline.emitKeypressEvents(input);
463
+ input.setRawMode(true);
464
+ input.resume();
465
+ output.write('\x1b[?25l');
466
+ input.on('keypress', onKey);
467
+ draw();
468
+ });
469
+ }
470
+
471
+ async function chooseTools(inst, o) {
472
+ if (o.tools) {
473
+ const t = o.tools === 'auto' ? 'auto' : o.tools === 'none' ? [] : o.tools.split(',').filter(Boolean);
474
+ if (t !== 'auto') inst.chosen = t;
475
+ return t;
476
+ }
477
+ if (!o.configure) {
478
+ const saved = inst.loadConfig();
479
+ if (saved) return saved;
480
+ }
481
+ if (!o.yes && !process.env.KAGENTS_NO_PROMPT && !process.env.CI) {
482
+ const io = openTTY();
483
+ if (io) {
484
+ const detected = inst.detectTools();
485
+ const picked = await multiselect(io, 'Pour quels outils installer KAgents ? (↑↓ espace a entrée)', [
486
+ { id: 'claude', label: 'Claude Code (.claude/commands, .claude/agents)', checked: detected.includes('claude') },
487
+ { id: 'cursor', label: 'Cursor (.cursor/commands, .cursor/agents, .cursor/rules)', checked: detected.includes('cursor') },
488
+ { id: 'agents', label: 'Codex et autres outils AGENTS.md (.agents/skills)', checked: true },
489
+ ]);
490
+ inst.chosen = picked;
491
+ return picked;
492
+ }
493
+ }
494
+ return 'auto';
495
+ }
496
+
350
497
  // --- Main ---------------------------------------------------------------------
351
- function main() {
498
+ async function main() {
352
499
  const o = parseArgs(process.argv.slice(2));
353
500
  if (o.cmd === 'help') return console.log(HELP);
354
501
  if (o.cmd === 'version') return console.log(PKG.version);
355
502
  const target = process.cwd();
356
503
  if (path.resolve(target) === PKG_ROOT) die("à lancer depuis la racine d'un projet, pas depuis le kit.");
357
504
  const inst = new Installer(target, o.copy);
358
- if (o.cmd === 'uninstall') inst.uninstall();
359
- else inst.install(o.tools);
505
+ if (o.cmd === 'uninstall') return inst.uninstall();
506
+ const tools = await chooseTools(inst, o);
507
+ inst.install(tools);
508
+ if (inst.chosen) inst.saveConfig(inst.chosen);
360
509
  }
361
510
 
362
511
  main();
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Lancé par npm/pnpm après `add @dev-kosaly/kagents` : installe le kit dans le projet.
4
+ // Ne doit jamais faire échouer l'installation des dépendances.
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const pkgRoot = path.resolve(__dirname, '..');
9
+ const project = process.env.INIT_CWD;
10
+
11
+ const skip = (why) => {
12
+ if (why) console.log(`kagents: installation automatique ignorée (${why}). Lancez : npx kagents`);
13
+ process.exit(0);
14
+ };
15
+
16
+ if (process.env.KAGENTS_SKIP_POSTINSTALL) skip();
17
+ if (!project) skip();
18
+ if (process.env.npm_config_global === 'true') skip('installation globale');
19
+ if (!pkgRoot.split(path.sep).includes('node_modules')) skip(); // développement du kit lui-même
20
+ if (path.resolve(project) === pkgRoot) skip();
21
+
22
+ try {
23
+ spawnSync(process.execPath, [path.join(__dirname, 'kagents.js')], { cwd: project, stdio: 'inherit', env: { ...process.env, KAGENTS_FROM_POSTINSTALL: '1' } });
24
+ } catch {
25
+ /* jamais bloquant */
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-kosaly/kagents",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Kit d'agents IA d'ingénierie installable dans un projet (Claude Code, Cursor, AGENTS.md)",
5
5
  "bin": {
6
6
  "kagents": "bin/kagents.js"
@@ -29,6 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "scripts": {
32
+ "postinstall": "node bin/postinstall.js",
32
33
  "test": "node scripts/smoke-test.js",
33
34
  "prepublishOnly": "npm test"
34
35
  },