@jaimevalasek/aioson 1.5.1 → 1.6.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.
Files changed (131) hide show
  1. package/README.md +6 -0
  2. package/docs/design-previews/aurora-command-ui-website.html +884 -0
  3. package/docs/design-previews/aurora-command-ui.html +682 -0
  4. package/docs/design-previews/bold-editorial-ui-website.html +658 -0
  5. package/docs/design-previews/bold-editorial-ui.html +717 -0
  6. package/docs/design-previews/clean-saas-ui-website.html +1202 -0
  7. package/docs/design-previews/clean-saas-ui.html +549 -0
  8. package/docs/design-previews/cognitive-core-ui-website.html +1009 -0
  9. package/docs/design-previews/cognitive-core-ui.html +463 -0
  10. package/docs/design-previews/glassmorphism-ui-website.html +572 -0
  11. package/docs/design-previews/glassmorphism-ui.html +886 -0
  12. package/docs/design-previews/index.html +699 -0
  13. package/docs/design-previews/interface-design-website.html +1187 -0
  14. package/docs/design-previews/interface-design.html +513 -0
  15. package/docs/design-previews/neo-brutalist-ui-website.html +621 -0
  16. package/docs/design-previews/neo-brutalist-ui.html +797 -0
  17. package/docs/design-previews/premium-command-center-ui-website.html +1217 -0
  18. package/docs/design-previews/premium-command-center-ui.html +552 -0
  19. package/docs/design-previews/warm-craft-ui-website.html +684 -0
  20. package/docs/design-previews/warm-craft-ui.html +739 -0
  21. package/docs/en/cli-reference.md +20 -9
  22. package/docs/pt/README.md +7 -0
  23. package/docs/pt/agent-sharding.md +132 -0
  24. package/docs/pt/agentes.md +8 -2
  25. package/docs/pt/busca-de-contexto.md +129 -0
  26. package/docs/pt/cache-de-contexto.md +156 -0
  27. package/docs/pt/comandos-cli.md +28 -0
  28. package/docs/pt/design-hybrid-forge.md +107 -0
  29. package/docs/pt/inicio-rapido.md +54 -3
  30. package/docs/pt/inteligencia-adaptativa.md +324 -0
  31. package/docs/pt/monitor-de-contexto.md +104 -0
  32. package/docs/pt/recuperacao-de-sessao.md +125 -0
  33. package/docs/pt/sandbox.md +125 -0
  34. package/docs/pt/skills.md +98 -6
  35. package/package.json +1 -1
  36. package/src/agent-loader.js +280 -0
  37. package/src/cli.js +94 -0
  38. package/src/commands/agent-loader.js +85 -0
  39. package/src/commands/context-cache.js +90 -0
  40. package/src/commands/context-monitor.js +92 -0
  41. package/src/commands/context-search.js +66 -0
  42. package/src/commands/design-hybrid-options.js +385 -0
  43. package/src/commands/health.js +214 -0
  44. package/src/commands/init.js +54 -13
  45. package/src/commands/install.js +52 -13
  46. package/src/commands/learning-evolve.js +355 -0
  47. package/src/commands/live.js +34 -0
  48. package/src/commands/recovery.js +43 -0
  49. package/src/commands/sandbox.js +37 -0
  50. package/src/commands/setup-context.js +22 -2
  51. package/src/commands/setup.js +178 -0
  52. package/src/commands/skill.js +79 -32
  53. package/src/commands/tool-registry-cmd.js +232 -0
  54. package/src/commands/update.js +7 -0
  55. package/src/constants.js +9 -0
  56. package/src/context-cache.js +159 -0
  57. package/src/context-search.js +326 -0
  58. package/src/design-variation-catalog.js +503 -0
  59. package/src/i18n/messages/en.js +32 -2
  60. package/src/i18n/messages/es.js +30 -2
  61. package/src/i18n/messages/fr.js +30 -2
  62. package/src/i18n/messages/pt-BR.js +32 -2
  63. package/src/install-animation.js +260 -0
  64. package/src/install-profile.js +143 -0
  65. package/src/install-wizard.js +474 -0
  66. package/src/installer.js +38 -10
  67. package/src/parser.js +7 -1
  68. package/src/recovery-context-session.js +154 -0
  69. package/src/runtime-store.js +97 -1
  70. package/src/sandbox.js +177 -0
  71. package/src/tool-executor.js +94 -0
  72. package/src/updater.js +11 -3
  73. package/template/.aioson/agents/analyst.md +58 -3
  74. package/template/.aioson/agents/architect.md +38 -0
  75. package/template/.aioson/agents/design-hybrid-forge.md +127 -0
  76. package/template/.aioson/agents/dev.md +103 -0
  77. package/template/.aioson/agents/deyvin.md +57 -0
  78. package/template/.aioson/agents/pm.md +58 -0
  79. package/template/.aioson/agents/product.md +28 -0
  80. package/template/.aioson/agents/qa.md +79 -0
  81. package/template/.aioson/agents/setup.md +65 -3
  82. package/template/.aioson/agents/sheldon.md +107 -6
  83. package/template/.aioson/agents/tester.md +156 -0
  84. package/template/.aioson/config.md +15 -0
  85. package/template/.aioson/context/forensics/.gitkeep +0 -0
  86. package/template/.aioson/context/seeds/seed-example.md +27 -0
  87. package/template/.aioson/context/user-profile.md +42 -0
  88. package/template/.aioson/locales/en/agents/setup.md +33 -1
  89. package/template/.aioson/locales/es/agents/setup.md +33 -1
  90. package/template/.aioson/locales/fr/agents/setup.md +33 -1
  91. package/template/.aioson/locales/pt-BR/agents/setup.md +33 -1
  92. package/template/.aioson/skills/design/aurora-command-ui/SKILL.md +243 -0
  93. package/template/.aioson/skills/design/aurora-command-ui/references/art-direction.md +293 -0
  94. package/template/.aioson/skills/design/aurora-command-ui/references/components.md +827 -0
  95. package/template/.aioson/skills/design/aurora-command-ui/references/dashboards.md +250 -0
  96. package/template/.aioson/skills/design/aurora-command-ui/references/design-tokens.md +585 -0
  97. package/template/.aioson/skills/design/aurora-command-ui/references/motion.md +365 -0
  98. package/template/.aioson/skills/design/aurora-command-ui/references/patterns.md +482 -0
  99. package/template/.aioson/skills/design/aurora-command-ui/references/websites.md +387 -0
  100. package/template/.aioson/skills/design/glassmorphism-ui/SKILL.md +222 -0
  101. package/template/.aioson/skills/design/glassmorphism-ui/references/art-direction.md +159 -0
  102. package/template/.aioson/skills/design/glassmorphism-ui/references/components.md +498 -0
  103. package/template/.aioson/skills/design/glassmorphism-ui/references/dashboards.md +236 -0
  104. package/template/.aioson/skills/design/glassmorphism-ui/references/design-tokens.md +274 -0
  105. package/template/.aioson/skills/design/glassmorphism-ui/references/motion.md +355 -0
  106. package/template/.aioson/skills/design/glassmorphism-ui/references/patterns.md +198 -0
  107. package/template/.aioson/skills/design/glassmorphism-ui/references/websites.md +307 -0
  108. package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +213 -0
  109. package/template/.aioson/skills/design/neo-brutalist-ui/references/art-direction.md +228 -0
  110. package/template/.aioson/skills/design/neo-brutalist-ui/references/components.md +855 -0
  111. package/template/.aioson/skills/design/neo-brutalist-ui/references/dashboards.md +334 -0
  112. package/template/.aioson/skills/design/neo-brutalist-ui/references/design-tokens.md +342 -0
  113. package/template/.aioson/skills/design/neo-brutalist-ui/references/motion.md +286 -0
  114. package/template/.aioson/skills/design/neo-brutalist-ui/references/patterns.md +458 -0
  115. package/template/.aioson/skills/design/neo-brutalist-ui/references/websites.md +723 -0
  116. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +45 -0
  117. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +109 -0
  118. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +44 -0
  119. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +37 -0
  120. package/template/.aioson/skills/process/aioson-spec-driven/references/hardening-lane.md +49 -0
  121. package/template/.aioson/skills/process/aioson-spec-driven/references/maintenance-and-state.md +66 -0
  122. package/template/.aioson/skills/process/aioson-spec-driven/references/ui-language.md +75 -0
  123. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +144 -0
  124. package/template/.aioson/skills/process/design-hybrid-forge/references/crossover-protocol.md +221 -0
  125. package/template/.aioson/skills/process/design-hybrid-forge/references/naming-registry.md +88 -0
  126. package/template/.aioson/skills/process/design-hybrid-forge/references/output-contract.md +291 -0
  127. package/template/.aioson/skills/process/design-hybrid-forge/references/pair-compatibility.md +117 -0
  128. package/template/.aioson/skills/process/design-hybrid-forge/references/quality-gates.md +188 -0
  129. package/template/.aioson/skills/process/design-hybrid-forge/references/variation-library.md +125 -0
  130. package/template/AGENTS.md +23 -1
  131. package/template/CLAUDE.md +1 -0
@@ -0,0 +1,474 @@
1
+ 'use strict';
2
+
3
+ const readline = require('node:readline');
4
+ const { getCliVersionSync } = require('./version');
5
+
6
+ const TOOLS = [
7
+ { id: 'claude', label: 'Claude Code', desc: 'Slash commands, CLAUDE.md, .claude/' },
8
+ { id: 'codex', label: 'Codex (OpenAI)', desc: 'AGENTS.md protocol' },
9
+ { id: 'gemini', label: 'Gemini CLI', desc: 'GEMINI.md + .gemini/commands/' },
10
+ { id: 'opencode', label: 'OpenCode', desc: 'OPENCODE.md protocol' }
11
+ ];
12
+
13
+ const USES = [
14
+ {
15
+ id: 'development',
16
+ label: 'Development',
17
+ desc: 'Agent workflow: setup → analyst → architect → dev → qa',
18
+ locked: true
19
+ },
20
+ {
21
+ id: 'squads',
22
+ label: 'Squads',
23
+ desc: 'Create and run AI squads (squad, genome, orache, profiler)',
24
+ locked: false
25
+ }
26
+ ];
27
+
28
+ const DESIGNS = [
29
+ { id: 'none', label: 'None', desc: 'No design system installed' },
30
+ { id: 'clean-saas-ui', label: 'Clean SaaS UI', desc: 'Minimal, functional — dashboards & tools' },
31
+ { id: 'aurora-command-ui', label: 'Aurora Command UI', desc: 'Dark, glowing — command centers & apps' },
32
+ { id: 'cognitive-core-ui', label: 'Cognitive Core UI', desc: 'Information-dense — data & analytics' },
33
+ { id: 'bold-editorial-ui', label: 'Bold Editorial UI', desc: 'High contrast typography — content sites' },
34
+ { id: 'warm-craft-ui', label: 'Warm Craft UI', desc: 'Warm tones, organic — consumer & lifestyle' },
35
+ { id: 'glassmorphism-ui', label: 'Glassmorphism UI', desc: 'Translucent layers — immersive interfaces' },
36
+ { id: 'neo-brutalist-ui', label: 'Neo-Brutalist UI', desc: 'Raw, high-contrast — bold statements' },
37
+ { id: 'premium-command-center-ui',label: 'Premium Command Center UI',desc: 'Enterprise-grade — ops & monitoring' },
38
+ { id: 'interface-design', label: 'Interface Design', desc: 'Foundational system — general purpose' }
39
+ ];
40
+
41
+ const LOCALES = [
42
+ { id: 'en', label: 'English', flag: '🇺🇸' },
43
+ { id: 'pt-BR', label: 'Português (Brasil)', flag: '🇧🇷' },
44
+ { id: 'es', label: 'Español', flag: '🇪🇸' },
45
+ { id: 'fr', label: 'Français', flag: '🇫🇷' }
46
+ ];
47
+
48
+ const BANNER_ART = [
49
+ '█████╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
50
+ '██╔══██╗██║██╔═══██╗██╔════╝██╔═══██╗████╗ ██║',
51
+ '███████║██║██║ ██║███████╗██║ ██║██╔██╗ ██║',
52
+ '██╔══██║██║██║ ██║╚════██║██║ ██║██║╚██╗██║',
53
+ '██║ ██║██║╚██████╔╝███████║╚██████╔╝██║ ╚████║',
54
+ '╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝'
55
+ ];
56
+
57
+ function getBanner(version, stdout) {
58
+ const cols = (stdout && stdout.columns) || 80;
59
+ const noColor = process.env.NO_COLOR !== undefined;
60
+ const dumb = process.env.TERM === 'dumb';
61
+
62
+ if (dumb || cols < 60) {
63
+ return `AIOSON v${version}\n\n`;
64
+ }
65
+
66
+ const cyan = noColor ? '' : '\x1b[1;36m';
67
+ const border = noColor ? '' : '\x1b[36m';
68
+ const dim = noColor ? '' : '\x1b[90m';
69
+ const reset = noColor ? '' : '\x1b[0m';
70
+
71
+ const artWidth = Math.max(...BANNER_ART.map(r => r.length));
72
+ const sidePad = 3;
73
+ const inner = artWidth + sidePad * 2;
74
+ const dashes = '─'.repeat(inner);
75
+
76
+ function centered(content, visibleLen) {
77
+ const left = Math.floor((inner - visibleLen) / 2);
78
+ const right = inner - left - visibleLen;
79
+ return ` ${border}│${reset}${' '.repeat(left)}${content}${' '.repeat(right)}${border}│${reset}`;
80
+ }
81
+
82
+ const emptyRow = ` ${border}│${reset}${' '.repeat(inner)}${border}│${reset}`;
83
+ const tagline = `AI Operating Framework v${version}`;
84
+
85
+ return [
86
+ ` ${border}╭${dashes}╮${reset}`,
87
+ emptyRow,
88
+ ...BANNER_ART.map(row => {
89
+ const left = Math.floor((inner - row.length) / 2);
90
+ const right = inner - left - row.length;
91
+ return ` ${border}│${reset}${' '.repeat(left)}${cyan}${row}${reset}${' '.repeat(right)}${border}│${reset}`;
92
+ }),
93
+ emptyRow,
94
+ centered(`${dim}${tagline}${reset}`, tagline.length),
95
+ emptyRow,
96
+ ` ${border}╰${dashes}╯${reset}`,
97
+ ''
98
+ ].join('\n') + '\n';
99
+ }
100
+
101
+ function header(screen, total, stdout) {
102
+ stdout.write('\x1Bc');
103
+ stdout.write(getBanner(getCliVersionSync(), stdout));
104
+ stdout.write(` AIOSON — Installation Wizard (${screen}/${total})\n\n`);
105
+ }
106
+
107
+ function renderScreen1(cursor, selected, warn, stdout) {
108
+ header(1, 4, stdout);
109
+ stdout.write(' Which AI tools will you use in this project?\n');
110
+ stdout.write(' (↑/↓ to move, space to select, enter to continue)\n\n');
111
+ for (let i = 0; i < TOOLS.length; i++) {
112
+ const tool = TOOLS[i];
113
+ const pointer = i === cursor ? '►' : ' ';
114
+ const check = selected.has(tool.id) ? '✓' : ' ';
115
+ stdout.write(` ${pointer} [${check}] ${tool.label.padEnd(20)} ${tool.desc}\n`);
116
+ }
117
+ if (warn) stdout.write('\n ⚠ Select at least one tool to continue.\n');
118
+ stdout.write('\n');
119
+ }
120
+
121
+ function renderScreen2(cursor, selected, stdout) {
122
+ header(2, 4, stdout);
123
+ stdout.write(' What will you do with AIOSON?\n');
124
+ stdout.write(' (space to select, enter to continue)\n\n');
125
+ for (let i = 0; i < USES.length; i++) {
126
+ const use = USES[i];
127
+ const pointer = i === cursor ? '►' : ' ';
128
+ const check = selected.has(use.id) ? '✓' : ' ';
129
+ const lock = use.locked ? ' (always on)' : '';
130
+ stdout.write(` ${pointer} [${check}] ${use.label}${lock}\n`);
131
+ stdout.write(` ${use.desc}\n`);
132
+ }
133
+ stdout.write('\n');
134
+ }
135
+
136
+ function renderScreen3(cursor, selected, warn, stdout) {
137
+ header(3, 4, stdout);
138
+ stdout.write(' Which design system? (optional — select multiple)\n');
139
+ stdout.write(' (↑/↓ to move, space to toggle, enter to continue)\n\n');
140
+ for (let i = 0; i < DESIGNS.length; i++) {
141
+ const d = DESIGNS[i];
142
+ const pointer = i === cursor ? '►' : ' ';
143
+ const check = selected.has(d.id) ? '✓' : ' ';
144
+ stdout.write(` ${pointer} [${check}] ${d.label.padEnd(28)} ${d.desc}\n`);
145
+ }
146
+ if (warn) stdout.write('\n ⚠ Select "None" or at least one design skill.\n');
147
+ stdout.write('\n');
148
+ }
149
+
150
+ function renderScreen4(cursor, stdout) {
151
+ header(4, 4, stdout);
152
+ stdout.write(' Which language for agents?\n');
153
+ stdout.write(' (↑/↓ to move, enter to select)\n\n');
154
+ for (let i = 0; i < LOCALES.length; i++) {
155
+ const loc = LOCALES[i];
156
+ const pointer = i === cursor ? '►' : ' ';
157
+ const bullet = i === cursor ? '◉' : '○';
158
+ stdout.write(` ${pointer} ${bullet} ${loc.flag} ${loc.label}\n`);
159
+ }
160
+ stdout.write('\n');
161
+ }
162
+
163
+ function renderConfirm(tools, uses, design, locale, existingProfile, t, stdout) {
164
+ const TOOL_NAMES = { claude: 'Claude Code', codex: 'Codex', gemini: 'Gemini CLI', opencode: 'OpenCode' };
165
+ const toolNames = tools.map(id => TOOL_NAMES[id] || id).join(', ');
166
+ const modeLabel = uses.includes('squads') ? 'Development + Squads' : 'Development';
167
+ // design can be string (single/id/all) or string[] (multiple)
168
+ const designList = Array.isArray(design)
169
+ ? design.map(id => DESIGNS.find(d => d.id === id)?.label || id)
170
+ : [DESIGNS.find(d => d.id === design)?.label || design];
171
+ const designLabel = designList.join(', ');
172
+ const localeName = LOCALES.find(l => l.id === locale)?.label || locale;
173
+
174
+ stdout.write('\x1Bc');
175
+ stdout.write(` ${t('install_wizard.ready_to_install')}\n\n`);
176
+ stdout.write(` Tools → ${toolNames}\n`);
177
+ stdout.write(` Mode → ${modeLabel}\n`);
178
+ stdout.write(` Design → ${designLabel}\n`);
179
+ stdout.write(` Locale → ${localeName}\n\n`);
180
+
181
+ // Warn if reconfigure has deselected items (we don't auto-remove files)
182
+ if (existingProfile) {
183
+ const prevTools = new Set(Array.isArray(existingProfile.tools) ? existingProfile.tools : [existingProfile.tools]);
184
+ const prevDesign = new Set(Array.isArray(existingProfile.design) ? existingProfile.design : [existingProfile.design]);
185
+ const currTools = new Set(tools);
186
+ const currDesign = new Set(Array.isArray(design) ? design : [design]);
187
+
188
+ const removedTools = [...prevTools].filter(t => !currTools.has(t) && t !== 'none');
189
+ const removedDesign = [...prevDesign].filter(d => !currDesign.has(d) && d !== 'none' && d !== 'all');
190
+
191
+ if (removedTools.length > 0 || removedDesign.length > 0) {
192
+ stdout.write(` ${t('install_wizard.deselected_warning')}\n`);
193
+ stdout.write(`${t('install_wizard.deselected_hint')}\n\n`);
194
+ }
195
+ }
196
+
197
+ stdout.write(` ${t('install_wizard.press_enter_to_install')}\n\n`);
198
+ }
199
+
200
+ function makeRawSession(io) {
201
+ const stdin = io.stdin || process.stdin;
202
+ const wasRaw = Boolean(stdin.isRaw);
203
+ const wasPaused = typeof stdin.isPaused === 'function' ? stdin.isPaused() : true;
204
+
205
+ readline.emitKeypressEvents(stdin);
206
+ if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true);
207
+ if (typeof stdin.resume === 'function') stdin.resume();
208
+
209
+ function cleanupListeners(onKeypress) {
210
+ stdin.removeListener('keypress', onKeypress);
211
+ if (stdin.listenerCount('keypress') === 0 && stdin.listenerCount('data') > 0) {
212
+ stdin.emit('data', Buffer.alloc(0));
213
+ }
214
+ if (typeof stdin.setRawMode === 'function') stdin.setRawMode(wasRaw);
215
+ if (wasPaused && typeof stdin.pause === 'function') stdin.pause();
216
+ }
217
+
218
+ return { stdin, cleanupListeners };
219
+ }
220
+
221
+ // Generic multi-select prompt (checkbox)
222
+ async function promptCheckbox({ items, defaultSelected, lockFirst, render, io = {} }) {
223
+ const stdout = io.stdout || process.stdout;
224
+ const { stdin, cleanupListeners } = makeRawSession(io);
225
+ let cursor = 0;
226
+ const selected = new Set(defaultSelected);
227
+ let warn = false;
228
+
229
+ render(cursor, selected, warn, stdout);
230
+
231
+ return new Promise((resolve) => {
232
+ let cleanedUp = false;
233
+ function cleanup() {
234
+ if (cleanedUp) return;
235
+ cleanedUp = true;
236
+ cleanupListeners(onKeypress);
237
+ }
238
+ function onKeypress(_str, key) {
239
+ if (!key) return;
240
+ if ((key.ctrl && key.name === 'c') || key.name === 'q') { cleanup(); resolve(null); return; }
241
+ if (key.name === 'up') { cursor = cursor === 0 ? items.length - 1 : cursor - 1; render(cursor, selected, warn, stdout); return; }
242
+ if (key.name === 'down') { cursor = cursor === items.length - 1 ? 0 : cursor + 1; render(cursor, selected, warn, stdout); return; }
243
+ if (key.name === 'space') {
244
+ const item = items[cursor];
245
+ if (lockFirst && item.locked) return;
246
+ if (selected.has(item.id)) selected.delete(item.id);
247
+ else selected.add(item.id);
248
+ warn = false;
249
+ render(cursor, selected, warn, stdout);
250
+ return;
251
+ }
252
+ if (key.name === 'return') {
253
+ if (selected.size === 0) { warn = true; render(cursor, selected, warn, stdout); return; }
254
+ cleanup();
255
+ resolve([...selected]);
256
+ }
257
+ }
258
+ stdin.on('keypress', onKeypress);
259
+ });
260
+ }
261
+
262
+ // Generic single-select prompt (radio)
263
+ async function promptRadio({ items, defaultIndex, render, io = {} }) {
264
+ const stdout = io.stdout || process.stdout;
265
+ const { stdin, cleanupListeners } = makeRawSession(io);
266
+ let cursor = defaultIndex || 0;
267
+
268
+ render(cursor, stdout);
269
+
270
+ return new Promise((resolve) => {
271
+ let cleanedUp = false;
272
+ function cleanup() {
273
+ if (cleanedUp) return;
274
+ cleanedUp = true;
275
+ cleanupListeners(onKeypress);
276
+ }
277
+ function onKeypress(_str, key) {
278
+ if (!key) return;
279
+ if ((key.ctrl && key.name === 'c') || key.name === 'q') { cleanup(); resolve(null); return; }
280
+ if (key.name === 'up') { cursor = cursor === 0 ? items.length - 1 : cursor - 1; render(cursor, stdout); return; }
281
+ if (key.name === 'down') { cursor = cursor === items.length - 1 ? 0 : cursor + 1; render(cursor, stdout); return; }
282
+ if (key.name === 'return') { cleanup(); resolve(items[cursor].id); }
283
+ }
284
+ stdin.on('keypress', onKeypress);
285
+ });
286
+ }
287
+
288
+ // Multi-select with exclusive option: when 'noneId' is selected it clears all others;
289
+ // when any other is selected it clears 'noneId'.
290
+ async function promptDesignCheckbox({ items, noneId, defaultSelected, render, io = {} }) {
291
+ const stdout = io.stdout || process.stdout;
292
+ const { stdin, cleanupListeners } = makeRawSession(io);
293
+ let cursor = 0;
294
+ const selected = new Set(defaultSelected);
295
+ let warn = false;
296
+
297
+ render(cursor, selected, warn, stdout);
298
+
299
+ return new Promise((resolve) => {
300
+ let cleanedUp = false;
301
+ function cleanup() {
302
+ if (cleanedUp) return;
303
+ cleanedUp = true;
304
+ cleanupListeners(onKeypress);
305
+ }
306
+ function onKeypress(_str, key) {
307
+ if (!key) return;
308
+ if ((key.ctrl && key.name === 'c') || key.name === 'q') { cleanup(); resolve(null); return; }
309
+ if (key.name === 'up') { cursor = cursor === 0 ? items.length - 1 : cursor - 1; render(cursor, selected, warn, stdout); return; }
310
+ if (key.name === 'down') { cursor = cursor === items.length - 1 ? 0 : cursor + 1; render(cursor, selected, warn, stdout); return; }
311
+ if (key.name === 'space') {
312
+ const item = items[cursor];
313
+ if (item.id === noneId) {
314
+ // Exclusive: selecting 'none' clears everything else
315
+ selected.clear();
316
+ selected.add(noneId);
317
+ } else {
318
+ // Selecting any other clears 'none'
319
+ selected.delete(noneId);
320
+ if (selected.has(item.id)) selected.delete(item.id);
321
+ else selected.add(item.id);
322
+ }
323
+ warn = false;
324
+ render(cursor, selected, warn, stdout);
325
+ return;
326
+ }
327
+ if (key.name === 'return') {
328
+ if (selected.size === 0) { warn = true; render(cursor, selected, warn, stdout); return; }
329
+ cleanup();
330
+ resolve([...selected]);
331
+ }
332
+ }
333
+ stdin.on('keypress', onKeypress);
334
+ });
335
+ }
336
+
337
+ async function promptConfirmScreen(tools, uses, design, locale, existingProfile, t, io = {}) {
338
+ const stdout = io.stdout || process.stdout;
339
+ const { stdin, cleanupListeners } = makeRawSession(io);
340
+
341
+ renderConfirm(tools, uses, design, locale, existingProfile, t, stdout);
342
+
343
+ return new Promise((resolve) => {
344
+ let cleanedUp = false;
345
+ function cleanup() {
346
+ if (cleanedUp) return;
347
+ cleanedUp = true;
348
+ cleanupListeners(onKeypress);
349
+ }
350
+ function onKeypress(_str, key) {
351
+ if (!key) return;
352
+ if ((key.ctrl && key.name === 'c') || key.name === 'q') { cleanup(); resolve(false); return; }
353
+ if (key.name === 'return') { cleanup(); resolve(true); }
354
+ }
355
+ stdin.on('keypress', onKeypress);
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Runs the interactive install wizard.
361
+ * Returns { tools, uses, design, locale } or null (cancelled / non-TTY / --no-interactive).
362
+ * @param {object} options
363
+ * @param {object} [options.existingProfile] - Pre-existing profile to pre-select in wizard
364
+ * @param {function} [options.t] - Translator function for i18n strings
365
+ */
366
+ async function runInstallWizard(options = {}, io = {}) {
367
+ const stdin = io.stdin || process.stdin;
368
+ const stdout = io.stdout || process.stdout;
369
+ const existingProfile = options.existingProfile || null;
370
+ const t = options.t || ((key) => key);
371
+
372
+ if (!stdin.isTTY || !stdout.isTTY) return null;
373
+ if (options.noInteractive) return null;
374
+
375
+ function finalCleanup() {
376
+ if (stdin === process.stdin) {
377
+ if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false);
378
+ stdin.pause();
379
+ if (typeof stdin.unref === 'function') stdin.unref();
380
+ }
381
+ }
382
+
383
+ // Derive defaults from existing profile (supports both string and array design)
384
+ const defaultTools = existingProfile
385
+ ? (Array.isArray(existingProfile.tools) ? existingProfile.tools : [existingProfile.tools])
386
+ : ['claude'];
387
+ const defaultUses = existingProfile
388
+ ? (Array.isArray(existingProfile.uses) ? existingProfile.uses : [existingProfile.uses])
389
+ : ['development'];
390
+ const defaultDesign = existingProfile
391
+ ? (Array.isArray(existingProfile.design)
392
+ ? existingProfile.design
393
+ : (existingProfile.design === 'none' || existingProfile.design === 'all'
394
+ ? [existingProfile.design]
395
+ : [existingProfile.design])) // single design skill
396
+ : ['none'];
397
+ const defaultLocale = existingProfile
398
+ ? (LOCALES.findIndex(l => l.id === existingProfile.locale) || 0)
399
+ : 0;
400
+
401
+ // Screen 1 — Tools (multi-select)
402
+ const tools = await promptCheckbox({
403
+ items: TOOLS,
404
+ defaultSelected: defaultTools,
405
+ lockFirst: false,
406
+ render: (cursor, selected, warn, out) => renderScreen1(cursor, selected, warn, out),
407
+ io
408
+ });
409
+ if (!tools) { finalCleanup(); return null; }
410
+
411
+ // Screen 2 — Uses (multi-select, development locked)
412
+ const uses = await promptCheckbox({
413
+ items: USES,
414
+ defaultSelected: defaultUses,
415
+ lockFirst: true,
416
+ render: (cursor, selected, _warn, out) => renderScreen2(cursor, selected, out),
417
+ io
418
+ });
419
+ if (!uses) { finalCleanup(); return null; }
420
+
421
+ // Screen 3 — Design (multi-select with exclusive 'none')
422
+ const design = await promptDesignCheckbox({
423
+ items: DESIGNS,
424
+ noneId: 'none',
425
+ defaultSelected: defaultDesign,
426
+ render: (cursor, selected, warn, out) => renderScreen3(cursor, selected, warn, out),
427
+ io
428
+ });
429
+ if (design === null) { finalCleanup(); return null; }
430
+
431
+ // Screen 4 — Locale (single-select / radio)
432
+ const locale = await promptRadio({
433
+ items: LOCALES,
434
+ defaultIndex: defaultLocale,
435
+ render: (cursor, out) => renderScreen4(cursor, out),
436
+ io
437
+ });
438
+ if (locale === null) { finalCleanup(); return null; }
439
+
440
+ // Confirm screen
441
+ const confirmed = await promptConfirmScreen(tools, uses, design, locale, existingProfile, t, io);
442
+ if (!confirmed) { finalCleanup(); return null; }
443
+
444
+ stdout.write('\x1Bc');
445
+ finalCleanup();
446
+
447
+ // Normalize design: empty array → 'none', single 'none' → 'none'
448
+ const normalizedDesign = (design.length === 0 || (design.length === 1 && design[0] === 'none'))
449
+ ? 'none'
450
+ : (design.length === 1 && design[0] === 'all')
451
+ ? 'all'
452
+ : design;
453
+
454
+ return { tools, uses, design: normalizedDesign, locale };
455
+ }
456
+
457
+ module.exports = {
458
+ runInstallWizard,
459
+ __test__: {
460
+ renderScreen1,
461
+ renderScreen2,
462
+ renderScreen3,
463
+ renderScreen4,
464
+ renderConfirm,
465
+ getBanner,
466
+ TOOLS,
467
+ USES,
468
+ DESIGNS,
469
+ LOCALES,
470
+ promptCheckbox,
471
+ promptRadio,
472
+ promptDesignCheckbox
473
+ }
474
+ };
package/src/installer.js CHANGED
@@ -6,6 +6,7 @@ const { MANAGED_FILES } = require('./constants');
6
6
  const { getCliVersion } = require('./version');
7
7
  const { exists, ensureDir, copyFileWithDir, nowStamp, toRelativeSafe } = require('./utils');
8
8
  const { ensureProjectRuntime } = require('./execution-gateway');
9
+ const { shouldIncludeForProfile } = require('./install-profile');
9
10
 
10
11
  const ROOT_DIR = path.join(__dirname, '..');
11
12
  const TEMPLATE_DIR = path.join(ROOT_DIR, 'template');
@@ -121,17 +122,24 @@ async function listFilesRecursive(dir) {
121
122
  return out;
122
123
  }
123
124
 
124
- function shouldSkipTemplatePath(rel) {
125
- if (rel.startsWith('.aioson/context/')) return true;
125
+ /**
126
+ * Returns a skip-reason string if the file should be skipped, or false if it should be installed.
127
+ */
128
+ function shouldSkipTemplatePath(rel, profile = null) {
126
129
  if (rel === '.aioson/context/.gitkeep') return false;
130
+ if (rel.startsWith('.aioson/context/')) return 'context-protected';
127
131
  // Never overwrite user-installed skills (only the .gitkeep is created)
128
- if (rel.startsWith('.aioson/installed-skills/') && rel !== '.aioson/installed-skills/.gitkeep') return true;
132
+ if (rel.startsWith('.aioson/installed-skills/') && rel !== '.aioson/installed-skills/.gitkeep') return 'context-protected';
129
133
  // Never overwrite custom agents (only the .gitkeep is created)
130
- if (rel.startsWith('.aioson/my-agents/') && rel !== '.aioson/my-agents/.gitkeep') return true;
134
+ if (rel.startsWith('.aioson/my-agents/') && rel !== '.aioson/my-agents/.gitkeep') return 'context-protected';
135
+
136
+ // Profile-based filtering
137
+ if (profile && !shouldIncludeForProfile(rel, profile)) return 'not-in-profile';
138
+
131
139
  return false;
132
140
  }
133
141
 
134
- async function writeInstallMetadata(targetDir, action, frameworkDetection) {
142
+ async function writeInstallMetadata(targetDir, action, frameworkDetection, installProfile) {
135
143
  const metaPath = path.join(targetDir, '.aioson/install.json');
136
144
  await ensureDir(path.dirname(metaPath));
137
145
  const existing = (await exists(metaPath)) ? JSON.parse(await fs.readFile(metaPath, 'utf8')) : {};
@@ -143,12 +151,24 @@ async function writeInstallMetadata(targetDir, action, frameworkDetection) {
143
151
  template_version: version,
144
152
  last_action: action,
145
153
  last_action_at: new Date().toISOString(),
146
- framework_detected: frameworkDetection || existing.framework_detected || null
154
+ framework_detected: frameworkDetection || existing.framework_detected || null,
155
+ install_profile: installProfile || existing.install_profile || null
147
156
  };
148
157
 
149
158
  await fs.writeFile(metaPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
150
159
  }
151
160
 
161
+ async function readInstallProfile(targetDir) {
162
+ const metaPath = path.join(targetDir, '.aioson/install.json');
163
+ if (!(await exists(metaPath))) return null;
164
+ try {
165
+ const data = JSON.parse(await fs.readFile(metaPath, 'utf8'));
166
+ return data.install_profile || null;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
152
172
  async function backupManagedFile(targetDir, relPath, backupRoot) {
153
173
  const source = path.join(targetDir, relPath);
154
174
  if (!(await exists(source))) return null;
@@ -164,7 +184,9 @@ async function installTemplate(targetDir, options = {}) {
164
184
  dryRun = false,
165
185
  mode = 'install',
166
186
  backupOnOverwrite = mode === 'update',
167
- frameworkDetection = null
187
+ frameworkDetection = null,
188
+ installProfile = null,
189
+ onProgress = null
168
190
  } = options;
169
191
 
170
192
  await ensureDir(targetDir);
@@ -183,8 +205,9 @@ async function installTemplate(targetDir, options = {}) {
183
205
 
184
206
  for (const absPath of templateFiles) {
185
207
  const rel = toRelativeSafe(TEMPLATE_DIR, absPath);
186
- if (shouldSkipTemplatePath(rel)) {
187
- skipped.push({ path: rel, reason: 'context-protected' });
208
+ const skipReason = shouldSkipTemplatePath(rel, installProfile);
209
+ if (skipReason) {
210
+ skipped.push({ path: rel, reason: skipReason });
188
211
  continue;
189
212
  }
190
213
 
@@ -215,6 +238,10 @@ async function installTemplate(targetDir, options = {}) {
215
238
  }
216
239
 
217
240
  copied.push(rel);
241
+
242
+ if (onProgress) {
243
+ onProgress({ copied: copied.length, total: templateFiles.length, file: rel });
244
+ }
218
245
  }
219
246
 
220
247
  if (!dryRun) {
@@ -225,7 +252,7 @@ async function installTemplate(targetDir, options = {}) {
225
252
  await fs.writeFile(gitkeep, '', 'utf8');
226
253
  }
227
254
 
228
- await writeInstallMetadata(targetDir, mode, frameworkDetection);
255
+ await writeInstallMetadata(targetDir, mode, frameworkDetection, installProfile);
229
256
 
230
257
  await ensureProjectGitignorePolicy(targetDir);
231
258
 
@@ -252,6 +279,7 @@ module.exports = {
252
279
  TEMPLATE_DIR,
253
280
  detectExistingInstall,
254
281
  installTemplate,
282
+ readInstallProfile,
255
283
  listFilesRecursive,
256
284
  ensureGitignoreEntry,
257
285
  ensureGitignoreEntries,
package/src/parser.js CHANGED
@@ -16,8 +16,14 @@ function parseArgv(argv) {
16
16
  continue;
17
17
  }
18
18
 
19
+ // Boolean-only flags that never consume the next token
20
+ const boolOnly = new Set([
21
+ 'all', 'force', 'dry-run', 'no-interactive', 'fix', 'json',
22
+ 'help', 'version', 'no-launch', 'attach'
23
+ ]);
24
+
19
25
  const next = tokens[i + 1];
20
- if (next && !next.startsWith('-')) {
26
+ if (next && !next.startsWith('-') && !boolOnly.has(k)) {
21
27
  options[k] = next;
22
28
  i += 1;
23
29
  } else {