@argenalimbaev/template-agent 1.0.0 → 1.0.1

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
@@ -14,7 +14,7 @@
14
14
  npx --yes @argenalimbaev/template-agent@1 setup
15
15
  ```
16
16
 
17
- Перезапустите coding-agent, если новый skill не появился сразу. После этого можно написать:
17
+ После установки CLI покажет краткий success-screen и следующий шаг. Перезапустите coding-agent, если новый skill не появился сразу. Затем можно написать:
18
18
 
19
19
  > Создай CRM-проект `sales-crm` с готовым dashboard. API уже существует.
20
20
 
@@ -26,7 +26,7 @@ npx --yes @argenalimbaev/template-agent@1 setup
26
26
  npx --yes @argenalimbaev/template-agent@1 create
27
27
  ```
28
28
 
29
- CLI спросит ровно две вещи: шаблон из списка и имя проекта.
29
+ CLI спросит ровно две вещи: шаблон из списка и имя проекта. В интерактивном терминале номера, подсказки и success-screen подсвечиваются; JSON и неинтерактивный вывод остаются plain text.
30
30
 
31
31
  Или без вопросов:
32
32
 
@@ -41,6 +41,26 @@ pnpm dlx @argenalimbaev/template-agent@1 create
41
41
  bunx @argenalimbaev/template-agent@1 create
42
42
  ```
43
43
 
44
+ ## Terminal UX
45
+
46
+ В обычном терминале CLI показывает компактные экраны: выбор template с цветными номерами, статус установки global skill, создание проекта и конкретные команды запуска.
47
+
48
+ ```text
49
+ ╭─ ✦ Template Agent ────────────────╮
50
+ │ ✓ Проект создан
51
+ ╰───────────────────────────────────╯
52
+
53
+ Дальше:
54
+ cd './sales-crm'
55
+ pnpm install
56
+ pnpm dev
57
+ ```
58
+
59
+ - `--json` всегда печатает только JSON без ANSI-кодов — для скриптов и агентов.
60
+ - Ошибки в интерактивном терминале выделяются красным, а успех — зелёным.
61
+ - `NO_COLOR=1` отключает цвета, не меняя команды и результат.
62
+ - Вне TTY (CI, pipe) CLI не добавляет цветовые коды.
63
+
44
64
  ## Доступные starters
45
65
 
46
66
  | ID | Когда выбирать | Не содержит |
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCli } from '../src/cli.mjs';
3
+ import { formatCliError, shouldUseColor } from '../src/terminal-ui.mjs';
3
4
 
4
5
  try {
5
6
  await runCli(process.argv.slice(2));
6
7
  } catch (error) {
7
- console.error(`ERROR: ${error.message}`);
8
+ console.error(formatCliError(error.message, { color: shouldUseColor({ isTTY: process.stderr.isTTY, json: process.argv.includes('--json'), noColor: !!process.env.NO_COLOR }) }));
8
9
  process.exitCode = 1;
9
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@argenalimbaev/template-agent",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI-aware, registry-driven frontend project template CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -105,7 +105,7 @@ export function createProject({ entry, target, hubRoot = root, brief, keepHistor
105
105
  if (existsSync(ignoreFile)) writeFileSync(ignoreFile, readFileSync(ignoreFile, 'utf8').trimEnd() + '\n\n# Generated skill forwarding files\n.agents/skills\n.claude/skills\n.codex/skills\n');
106
106
  const agentFile = join(checkout, 'AGENTS.md');
107
107
  writeFileSync(agentFile, '# Hub-generated project\n\nRead `docs/PROJECT_BRIEF.md` when present. Canonical skills live in `.ai/skills`; `.agents/skills`, `.claude/skills` and `.codex/skills` contain portable forwarding files, not symlinks. This overrides older link descriptions below. Any coding agent may read these Markdown files directly; no provider plugin or global installation is required.\n\n' + readFileSync(agentFile, 'utf8'));
108
- const metadata = { template: entry.id, repository: entry.repository, ref: entry.ref, commit, profile: entry.profile, skills: entry.skills, generatorVersion: '1.0.0', adapterMode: 'portable-forwarders' };
108
+ const metadata = { template: entry.id, repository: entry.repository, ref: entry.ref, commit, profile: entry.profile, skills: entry.skills, generatorVersion: '1.0.1', adapterMode: 'portable-forwarders' };
109
109
  writeFileSync(join(checkout, '.template-provenance.json'), JSON.stringify(metadata, null, 2) + '\n');
110
110
  if (brief) {
111
111
  const docs = join(checkout, 'docs');
package/src/catalog.mjs CHANGED
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
4
4
  import { validateRegistry } from '../scripts/registry.mjs';
5
5
  import { compareSemver } from './semver.mjs';
6
6
 
7
- export const CLI_VERSION = '1.0.0';
7
+ export const CLI_VERSION = '1.0.1';
8
8
  export const CATALOG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
9
9
  export const MAX_CATALOG_BYTES = 1024 * 1024;
10
10
  export const DEFAULT_RELEASE_API_URL = 'https://api.github.com/repos/arg3n41ck/frontend-template-hub/releases?per_page=100';
package/src/cli.mjs CHANGED
@@ -9,6 +9,7 @@ import { recommend } from '../scripts/recommend-template.mjs';
9
9
  import { CLI_VERSION, defaultCacheDirectory, resolveCatalog } from './catalog.mjs';
10
10
  import { compareSemver } from './semver.mjs';
11
11
  import { detectClients, findDuplicateSkillNames, installSkill, skillTargets, uninstallSkill } from './skill-manager.mjs';
12
+ import { formatCreatePreview, formatCreateSuccess, formatDoctor, formatProjectCheck, formatSkillSummary, formatTemplates, formatUpdate, shouldUseColor, terminalPalette } from './terminal-ui.mjs';
12
13
 
13
14
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
14
15
  const skillSource = join(root, 'skills', 'arg3n41ck-frontend-project', 'SKILL.md');
@@ -74,10 +75,6 @@ function assertSourcePolicy(entry, allowThirdParty) {
74
75
  }
75
76
  }
76
77
 
77
- function formatTemplates(templates) {
78
- return templates.map((entry, index) => `${index + 1}. ${entry.name} (${entry.id}) — ${entry.description}`).join('\n');
79
- }
80
-
81
78
  async function interactivePrompt() {
82
79
  const readline = createInterface({ input: process.stdin, output: process.stdout });
83
80
  return {
@@ -86,16 +83,19 @@ async function interactivePrompt() {
86
83
  };
87
84
  }
88
85
 
89
- async function chooseCreateArguments({ templates, target, template, prompt }) {
86
+ async function chooseCreateArguments({ templates, target, template, prompt, color }) {
90
87
  let selectedId = template;
91
88
  if (!selectedId) {
92
- const answer = await prompt.ask(`Choose a template:\n${formatTemplates(templates)}\n> `);
89
+ const answer = await prompt.ask(formatTemplates(templates, { color, interactive: true }));
93
90
  const selected = templates[Number(answer) - 1];
94
91
  if (!selected) throw new Error('Choose a template number from the list.');
95
92
  selectedId = selected.id;
96
93
  }
97
94
  let selectedTarget = target;
98
- if (!selectedTarget) selectedTarget = validateInteractiveProjectName(await prompt.ask('Project name: '));
95
+ if (!selectedTarget) {
96
+ const ui = terminalPalette(color);
97
+ selectedTarget = validateInteractiveProjectName(await prompt.ask(`${ui.accent('Название проекта')}: `));
98
+ }
99
99
  return { template: selectedId, target: selectedTarget };
100
100
  }
101
101
 
@@ -124,6 +124,7 @@ export async function runCli(argv, dependencies = {}) {
124
124
  const output = dependencies.output || (value => console.log(value));
125
125
  const cwd = dependencies.cwd || process.cwd();
126
126
  const environment = dependencies.environment || {};
127
+ const color = dependencies.color ?? shouldUseColor({ isTTY: process.stdout.isTTY, json: !!parsed.options.json, noColor: !!process.env.NO_COLOR });
127
128
  if (parsed.options.help || parsed.command === 'help') {
128
129
  output(usage());
129
130
  return { status: 'help' };
@@ -142,7 +143,8 @@ export async function runCli(argv, dependencies = {}) {
142
143
  if (!parsed.options.dryRun) rmSync(cache, { recursive: true, force: true });
143
144
  response.cache = parsed.options.dryRun ? 'would-remove' : 'removed';
144
145
  }
145
- print(response, { json: !!parsed.options.json, output });
146
+ if (parsed.options.json) print(response, { json: true, output });
147
+ else output(formatSkillSummary({ skills: result, action: parsed.command, dryRun: !!parsed.options.dryRun, color }));
146
148
  return response;
147
149
  }
148
150
  if (parsed.command === 'doctor') {
@@ -159,7 +161,8 @@ export async function runCli(argv, dependencies = {}) {
159
161
  duplicateSkillFiles: findDuplicateSkillNames(environment),
160
162
  ok: nodeOk && gitOk,
161
163
  };
162
- print(result, { json: !!parsed.options.json, output });
164
+ if (parsed.options.json) print(result, { json: true, output });
165
+ else output(formatDoctor(result, color));
163
166
  return result;
164
167
  }
165
168
  if (parsed.command === 'update') {
@@ -167,14 +170,15 @@ export async function runCli(argv, dependencies = {}) {
167
170
  const clients = detectClients(environment);
168
171
  const skills = clients.length ? installSkill({ targets: skillTargets({ clients, ...environment }), source: skillSource, dryRun: !!parsed.options.dryRun }) : [];
169
172
  const result = { catalog: { source: catalog.source, stale: catalog.stale, warning: catalog.warning || null }, skills };
170
- print(result, { json: !!parsed.options.json, output });
173
+ if (parsed.options.json) print(result, { json: true, output });
174
+ else output(formatUpdate(result, color));
171
175
  return result;
172
176
  }
173
177
  const catalog = await catalogForCommand({}, dependencies);
174
178
  const templates = availableTemplates(catalog.registry);
175
179
  if (parsed.command === 'list') {
176
180
  const result = { source: catalog.source, templates };
177
- print(parsed.options.json ? result : formatTemplates(templates), { json: !!parsed.options.json, output });
181
+ print(parsed.options.json ? result : formatTemplates(templates, { color }), { json: !!parsed.options.json, output });
178
182
  return result;
179
183
  }
180
184
  if (parsed.command === 'recommend') {
@@ -197,7 +201,8 @@ export async function runCli(argv, dependencies = {}) {
197
201
  generated: { ref: provenance.ref, commit: provenance.commit },
198
202
  updateAvailable: !!current && (current.ref !== provenance.ref || current.commit !== provenance.commit),
199
203
  };
200
- print(result, { json: !!parsed.options.json, output });
204
+ if (parsed.options.json) print(result, { json: true, output });
205
+ else output(formatProjectCheck(result, color));
201
206
  return result;
202
207
  }
203
208
  if (parsed.command !== 'create') throw new Error(`Unsupported command: ${parsed.command}`);
@@ -207,20 +212,23 @@ export async function runCli(argv, dependencies = {}) {
207
212
  throw new Error('create needs a project name and --template outside an interactive terminal.');
208
213
  }
209
214
  if (!parsed.options.template || !parsed.target) prompt = dependencies.prompt || await interactivePrompt();
210
- const selected = await chooseCreateArguments({ templates, target: parsed.target, template: parsed.options.template, prompt });
215
+ const selected = await chooseCreateArguments({ templates, target: parsed.target, template: parsed.options.template, prompt, color });
211
216
  const entry = templates.find(item => item.id === selected.template);
212
217
  if (!entry) throw new Error('Unknown template ID. Run template-agent list.');
213
218
  assertSourcePolicy(entry, !!parsed.options.allowThirdParty);
214
219
  const target = isAbsolute(selected.target) ? selected.target : resolve(cwd, selected.target);
215
220
  if (parsed.options.dryRun) {
216
221
  const result = { target, template: entry.id, source: catalog.source, dryRun: true };
217
- print(result, { json: true, output });
222
+ if (parsed.options.json) print(result, { json: true, output });
223
+ else output(formatCreatePreview({ target, entry, color }));
218
224
  return result;
219
225
  }
226
+ if (!parsed.options.json) output(terminalPalette(color).accent(`✦ Создаю ${entry.name}…`));
220
227
  const brief = parsed.options.briefFile ? readFileSync(resolve(cwd, parsed.options.briefFile), 'utf8') : undefined;
221
228
  const metadata = createProject({ entry, target, brief, keepHistory: !!parsed.options.keepTemplateHistory });
222
229
  const result = { target, source: catalog.source, metadata };
223
- print(result, { json: true, output });
230
+ if (parsed.options.json) print(result, { json: true, output });
231
+ else output(formatCreateSuccess({ target, entry, color }));
224
232
  return result;
225
233
  } finally {
226
234
  prompt?.close?.();
@@ -0,0 +1,117 @@
1
+ export function shouldUseColor({ isTTY, json = false, noColor = false }) {
2
+ return !!isTTY && !json && !noColor;
3
+ }
4
+
5
+ export function terminalPalette(color) {
6
+ const paint = (code, value) => color ? `\x1b[${code}m${value}\x1b[0m` : value;
7
+ return {
8
+ accent: value => paint('96', value),
9
+ bold: value => paint('1', value),
10
+ success: value => paint('32', value),
11
+ warning: value => paint('33', value),
12
+ danger: value => paint('31', value),
13
+ muted: value => paint('2', value),
14
+ };
15
+ }
16
+
17
+ function panel(title, color) {
18
+ const ui = terminalPalette(color);
19
+ return [
20
+ ui.accent('╭─ ✦ Template Agent ────────────────╮'),
21
+ `${ui.accent('│')} ${ui.success('✓')} ${ui.bold(title)}`,
22
+ ui.accent('╰───────────────────────────────────╯'),
23
+ ].join('\n');
24
+ }
25
+
26
+ function shellQuote(value) {
27
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
28
+ }
29
+
30
+ export function formatCliError(message, { color = false } = {}) {
31
+ return terminalPalette(color).danger(`ERROR: ${message}`);
32
+ }
33
+
34
+ export function formatTemplates(templates, { color = false, interactive = false } = {}) {
35
+ const ui = terminalPalette(color);
36
+ const lines = templates.flatMap((entry, index) => [
37
+ `${ui.warning(String(index + 1))} ${ui.bold(entry.name)} ${ui.muted(`(${entry.id})`)}`,
38
+ ` ${ui.muted(entry.description)}`,
39
+ ]);
40
+ if (!interactive) return lines.join('\n');
41
+ return [
42
+ ui.accent('✦ Новый проект'),
43
+ ui.muted('Выбери template. Нужны только номер и название проекта.'),
44
+ '',
45
+ ...lines,
46
+ '',
47
+ `${ui.accent('Введите номер')}: `,
48
+ ].join('\n');
49
+ }
50
+
51
+ export function formatCreateSuccess({ target, entry, color = false }) {
52
+ const ui = terminalPalette(color);
53
+ return [
54
+ panel('Проект создан', color),
55
+ `${ui.muted('Template:')} ${ui.bold(entry.name)} ${ui.muted(`(${entry.id})`)}`,
56
+ `${ui.muted('Папка:')} ${target}`,
57
+ '',
58
+ ui.accent('Дальше:'),
59
+ ` cd ${shellQuote(target)}`,
60
+ ' pnpm install',
61
+ ' pnpm dev',
62
+ ].join('\n');
63
+ }
64
+
65
+ export function formatCreatePreview({ target, entry, color }) {
66
+ const ui = terminalPalette(color);
67
+ return [
68
+ panel('Проверка создания', color),
69
+ `${ui.muted('Template:')} ${ui.bold(entry.name)} ${ui.muted(`(${entry.id})`)}`,
70
+ `${ui.muted('Папка:')} ${target}`,
71
+ ui.warning('Файлы не созданы: включён --dry-run.'),
72
+ ].join('\n');
73
+ }
74
+
75
+ export function formatSkillSummary({ skills, action, dryRun, color }) {
76
+ const ui = terminalPalette(color);
77
+ const installed = skills.some(item => item.status === 'installed' || item.status === 'updated');
78
+ const title = dryRun
79
+ ? `Проверка: ${action}`
80
+ : action === 'setup'
81
+ ? installed ? 'AI-skill готов' : 'AI-skill уже актуален'
82
+ : 'AI-skill удалён';
83
+ const details = skills.map(item => ` ${ui.muted('•')} ${ui.bold(item.client)}: ${item.status}`).join('\n');
84
+ const next = action === 'setup'
85
+ ? ['','Перезапусти Codex или Claude Code, если skill не появился сразу.', 'Затем просто опиши новый проект — агент выберет подходящий template.']
86
+ : [];
87
+ return [panel(title, color), details, ...next].filter(Boolean).join('\n');
88
+ }
89
+
90
+ export function formatDoctor(result, color) {
91
+ const ui = terminalPalette(color);
92
+ const state = value => value ? ui.success('✓') : ui.danger('✗');
93
+ return [
94
+ panel(result.ok ? 'Среда готова' : 'Среда требует внимания', color),
95
+ `${state(result.node.supported)} Node.js ${result.node.version}`,
96
+ `${state(result.git.supported)} Git`,
97
+ `${ui.muted('Catalog:')} ${result.catalog.source}${result.catalog.stale ? ' (stale)' : ''}`,
98
+ ].join('\n');
99
+ }
100
+
101
+ export function formatUpdate(result, color) {
102
+ const ui = terminalPalette(color);
103
+ return [
104
+ panel('Catalog обновлён', color),
105
+ `${ui.muted('Источник:')} ${result.catalog.source}`,
106
+ result.skills.length ? `${ui.muted('Skills:')} ${result.skills.map(item => `${item.client} — ${item.status}`).join(', ')}` : ui.muted('Skills: поддерживаемые клиенты не найдены.'),
107
+ ].join('\n');
108
+ }
109
+
110
+ export function formatProjectCheck(result, color) {
111
+ const ui = terminalPalette(color);
112
+ return [
113
+ panel(result.updateAvailable ? 'Есть обновление template' : 'Template актуален', color),
114
+ `${ui.muted('Проект:')} ${result.project}`,
115
+ `${ui.muted('Template:')} ${result.template}`,
116
+ ].join('\n');
117
+ }