@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 +22 -2
- package/bin/template-agent.mjs +2 -1
- package/package.json +1 -1
- package/scripts/create-project.mjs +1 -1
- package/src/catalog.mjs +1 -1
- package/src/cli.mjs +23 -15
- package/src/terminal-ui.mjs +117 -0
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 | Когда выбирать | Не содержит |
|
package/bin/template-agent.mjs
CHANGED
|
@@ -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(
|
|
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
|
@@ -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.
|
|
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.
|
|
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(
|
|
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)
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|