@argenalimbaev/template-agent 1.0.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 +107 -0
- package/bin/template-agent.mjs +9 -0
- package/package.json +41 -0
- package/registry/templates.json +361 -0
- package/scripts/create-project.mjs +161 -0
- package/scripts/recommend-template.mjs +39 -0
- package/scripts/registry.mjs +52 -0
- package/scripts/skill-adapters.mjs +30 -0
- package/scripts/template-ai-contract.mjs +56 -0
- package/skills/arg3n41ck-frontend-project/SKILL.md +15 -0
- package/src/catalog.mjs +106 -0
- package/src/cli.mjs +228 -0
- package/src/semver.mjs +16 -0
- package/src/skill-manager.mjs +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Template Agent
|
|
2
|
+
|
|
3
|
+
**Один раз установите AI-skill — затем агент сам выбирает и создаёт подходящий starter.**
|
|
4
|
+
|
|
5
|
+
`@argenalimbaev/template-agent` — публичный Node.js CLI и каталог независимых frontend-шаблонов. Шаблоны не лежат внутри npm-пакета: каждый имеет свой GitHub-репозиторий, rules, skills, wiki и проверку.
|
|
6
|
+
|
|
7
|
+
## Быстрый старт
|
|
8
|
+
|
|
9
|
+
Нужны Node.js 22.14+ и Git.
|
|
10
|
+
|
|
11
|
+
### Автоматически в Codex или Claude Code
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx --yes @argenalimbaev/template-agent@1 setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Перезапустите coding-agent, если новый skill не появился сразу. После этого можно написать:
|
|
18
|
+
|
|
19
|
+
> Создай CRM-проект `sales-crm` с готовым dashboard. API уже существует.
|
|
20
|
+
|
|
21
|
+
Агент прочитает актуальный каталог, выберет `crm-dashboard`, назовёт причину и выполнит CLI-команду. Он уточнит вопрос только при настоящей неоднозначности, несовместимых требованиях или отсутствии подходящего шаблона.
|
|
22
|
+
|
|
23
|
+
### Вручную в любом терминале
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx --yes @argenalimbaev/template-agent@1 create
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
CLI спросит ровно две вещи: шаблон из списка и имя проекта.
|
|
30
|
+
|
|
31
|
+
Или без вопросов:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx --yes @argenalimbaev/template-agent@1 create sales-crm --template crm-dashboard
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Для других package managers:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm dlx @argenalimbaev/template-agent@1 create
|
|
41
|
+
bunx @argenalimbaev/template-agent@1 create
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Доступные starters
|
|
45
|
+
|
|
46
|
+
| ID | Когда выбирать | Не содержит |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| `react-vite` | Простая React-вёрстка или небольшой SPA без SSR | dashboard, backend, router |
|
|
49
|
+
| `next` | Явно нужен Next.js, SSR или public content site | dashboard, Nest API |
|
|
50
|
+
| `crm-dashboard` | Готовый CRM/admin SPA и отдельный существующий API | production auth, backend |
|
|
51
|
+
| `fullstack-next-nest` | Свой Next.js + NestJS + PostgreSQL в одном проекте | готовый CRM dashboard |
|
|
52
|
+
|
|
53
|
+
Точный состав каталога не зашит в skill: смотрите его через `template-agent list --json`.
|
|
54
|
+
|
|
55
|
+
## Что гарантирует CLI
|
|
56
|
+
|
|
57
|
+
- выбирает только `enabled` templates из проверенного registry;
|
|
58
|
+
- клонирует immutable tag и сверяет точный commit;
|
|
59
|
+
- проверяет skills и AI-contract исходного template;
|
|
60
|
+
- не перезаписывает существующие папки, файлы или symlink;
|
|
61
|
+
- создаёт новый Git history без template origin по умолчанию;
|
|
62
|
+
- не устанавливает зависимости, не запускает hooks и не отправляет telemetry;
|
|
63
|
+
- оставляет `.template-provenance.json` для проверки происхождения.
|
|
64
|
+
|
|
65
|
+
Созданный проект — независимый snapshot. Он **никогда** не обновляется автоматически.
|
|
66
|
+
|
|
67
|
+
## Команды обслуживания
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Проверить среду, skill и catalog
|
|
71
|
+
npx --yes @argenalimbaev/template-agent@1 doctor
|
|
72
|
+
|
|
73
|
+
# Обновить catalog cache и установленный managed skill
|
|
74
|
+
npx --yes @argenalimbaev/template-agent@1 update
|
|
75
|
+
|
|
76
|
+
# Узнать, есть ли новый template release для созданного проекта
|
|
77
|
+
npx --yes @argenalimbaev/template-agent@1 check ./sales-crm
|
|
78
|
+
|
|
79
|
+
# Удалить только skill, созданный Template Agent
|
|
80
|
+
npx --yes @argenalimbaev/template-agent@1 uninstall --purge-cache
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`setup` не перезаписывает чужой skill с тем же именем. В Codex skill устанавливается в `$HOME/.agents/skills`, в Claude Code — в `~/.claude/skills` или `CLAUDE_CONFIG_DIR`. Cursor, browser-chat и другие среды без поддерживаемого global-skill используют ручный CLI. Чат без shell/filesystem может только показать команду, но не создать файлы на вашем компьютере.
|
|
84
|
+
|
|
85
|
+
## Обновления и безопасность
|
|
86
|
+
|
|
87
|
+
- CLI сам не обновляется в фоне. Вызов `npx …@1` явно использует актуальный совместимый major.
|
|
88
|
+
- Catalog ищет последний immutable GitHub Release `catalog-v*`, хранит валидный local cache и при offline использует cache/fallback registry.
|
|
89
|
+
- Новый template: новый tag в его репозитории → exact commit в registry → `catalog-v*` release. npm-пакет обновляется только при изменении CLI или registry-контракта.
|
|
90
|
+
- Стандартный каталог разрешает first-party public HTTPS sources. Будущие сторонние sources потребуют явного `--allow-third-party`.
|
|
91
|
+
- Не передавайте токены, пароли или коды 2FA в brief, registry или issue. Private sources требуют собственных credentials пользователя.
|
|
92
|
+
|
|
93
|
+
## Для maintainers
|
|
94
|
+
|
|
95
|
+
Hub хранит selector/generator и metadata, но не копии skills. Canonical skills остаются в `.ai/skills` каждого template; registry лишь сверяет их inventory.
|
|
96
|
+
|
|
97
|
+
- [Архитектура](docs/architecture.md)
|
|
98
|
+
- [Расширение каталога](docs/extending.md)
|
|
99
|
+
- [Совместимость и ограничения](docs/compatibility.md)
|
|
100
|
+
- [Публикация catalog и npm CLI](docs/publishing.md)
|
|
101
|
+
- [Проверка](docs/verification.md)
|
|
102
|
+
|
|
103
|
+
Перед release: `npm run verify && npm run pack:check`.
|
|
104
|
+
|
|
105
|
+
## Лицензирование
|
|
106
|
+
|
|
107
|
+
Публичность репозитория не является лицензией на использование. До распространения нужно проверить лицензии source repositories и сторонних skills. Эта версия не меняет лицензию автоматически.
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@argenalimbaev/template-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-aware, registry-driven frontend project template CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"template-agent": "bin/template-agent.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"scripts/create-project.mjs",
|
|
13
|
+
"scripts/registry.mjs",
|
|
14
|
+
"scripts/recommend-template.mjs",
|
|
15
|
+
"scripts/skill-adapters.mjs",
|
|
16
|
+
"scripts/template-ai-contract.mjs",
|
|
17
|
+
"skills",
|
|
18
|
+
"registry/templates.json",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.14.0"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/arg3n41ck/frontend-template-hub.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/arg3n41ck/frontend-template-hub/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/arg3n41ck/frontend-template-hub#readme",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "node --test scripts/create-project.test.mjs scripts/recommend-template.test.mjs scripts/template-ai-contract.test.mjs scripts/release-hygiene.test.mjs scripts/template-agent.test.mjs scripts/package-preflight.test.mjs",
|
|
38
|
+
"verify": "node scripts/release-preflight.mjs . && node scripts/validate-registry.mjs && npm test",
|
|
39
|
+
"pack:check": "node scripts/package-preflight.mjs ."
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"minCliVersion": "1.0.0",
|
|
4
|
+
"templates": [
|
|
5
|
+
{
|
|
6
|
+
"id": "react-vite",
|
|
7
|
+
"name": "React + Vite",
|
|
8
|
+
"description": "Minimal React 19, TypeScript, Vite and shadcn/ui starter.",
|
|
9
|
+
"repository": "https://github.com/arg3n41ck/frontend-template-react.git",
|
|
10
|
+
"ref": "v0.4.0",
|
|
11
|
+
"stack": [
|
|
12
|
+
"react",
|
|
13
|
+
"vite",
|
|
14
|
+
"typescript",
|
|
15
|
+
"shadcn"
|
|
16
|
+
],
|
|
17
|
+
"profile": "frontend-minimal",
|
|
18
|
+
"skills": [
|
|
19
|
+
"api-contract-check",
|
|
20
|
+
"async-state-safety",
|
|
21
|
+
"brainstorming",
|
|
22
|
+
"browser-qa",
|
|
23
|
+
"change-impact",
|
|
24
|
+
"dependency-update-audit",
|
|
25
|
+
"design-system-steward",
|
|
26
|
+
"executing-plans",
|
|
27
|
+
"feature-architecture",
|
|
28
|
+
"file-upload-safety",
|
|
29
|
+
"find-skills",
|
|
30
|
+
"form-checklist",
|
|
31
|
+
"frontend-a11y-check",
|
|
32
|
+
"frontend-agent",
|
|
33
|
+
"frontend-design",
|
|
34
|
+
"frontend-error-ux",
|
|
35
|
+
"graphify",
|
|
36
|
+
"i18n-audit",
|
|
37
|
+
"parallel-work",
|
|
38
|
+
"performance-audit",
|
|
39
|
+
"project-documentation-wiki",
|
|
40
|
+
"project-kickoff",
|
|
41
|
+
"react-19-patterns",
|
|
42
|
+
"receiving-code-review",
|
|
43
|
+
"refactor-safely",
|
|
44
|
+
"release-readiness",
|
|
45
|
+
"requesting-code-review",
|
|
46
|
+
"review-changes",
|
|
47
|
+
"security-review",
|
|
48
|
+
"systematic-debugging",
|
|
49
|
+
"task-handoff",
|
|
50
|
+
"test-driven-development",
|
|
51
|
+
"test-strategy",
|
|
52
|
+
"ui-ux-pro-max",
|
|
53
|
+
"url-state",
|
|
54
|
+
"using-git-worktrees",
|
|
55
|
+
"verification-before-completion",
|
|
56
|
+
"verification-quality",
|
|
57
|
+
"visual-regression",
|
|
58
|
+
"writing-plans"
|
|
59
|
+
],
|
|
60
|
+
"commit": "33ebe51a8297bbf85f8e6aa100ed199a73ebaeaf",
|
|
61
|
+
"enabled": true,
|
|
62
|
+
"selection": {
|
|
63
|
+
"capabilities": [
|
|
64
|
+
"web-ui",
|
|
65
|
+
"react",
|
|
66
|
+
"client-rendering"
|
|
67
|
+
],
|
|
68
|
+
"complexity": 1,
|
|
69
|
+
"useWhen": [
|
|
70
|
+
"Small client UI or layout-only prototype without an explicit SSR requirement."
|
|
71
|
+
],
|
|
72
|
+
"avoidWhen": [
|
|
73
|
+
"Explicit server rendering, ready CRM dashboard or a separately owned API."
|
|
74
|
+
],
|
|
75
|
+
"limitations": [
|
|
76
|
+
"No ready dashboard, backend, auth provider or router."
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"project": {
|
|
80
|
+
"renamePackage": true,
|
|
81
|
+
"requiredFiles": [
|
|
82
|
+
"package.json",
|
|
83
|
+
"pnpm-lock.yaml"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "next",
|
|
89
|
+
"name": "Next.js",
|
|
90
|
+
"description": "Server-first Next.js App Router and shadcn/ui starter.",
|
|
91
|
+
"repository": "https://github.com/arg3n41ck/frontend-template-next.git",
|
|
92
|
+
"ref": "v0.4.0",
|
|
93
|
+
"stack": [
|
|
94
|
+
"nextjs",
|
|
95
|
+
"react",
|
|
96
|
+
"typescript",
|
|
97
|
+
"shadcn"
|
|
98
|
+
],
|
|
99
|
+
"profile": "frontend-next",
|
|
100
|
+
"skills": [
|
|
101
|
+
"api-contract-check",
|
|
102
|
+
"async-state-safety",
|
|
103
|
+
"brainstorming",
|
|
104
|
+
"browser-qa",
|
|
105
|
+
"change-impact",
|
|
106
|
+
"dependency-update-audit",
|
|
107
|
+
"design-system-steward",
|
|
108
|
+
"executing-plans",
|
|
109
|
+
"feature-architecture",
|
|
110
|
+
"file-upload-safety",
|
|
111
|
+
"find-skills",
|
|
112
|
+
"form-checklist",
|
|
113
|
+
"frontend-a11y-check",
|
|
114
|
+
"frontend-agent",
|
|
115
|
+
"frontend-design",
|
|
116
|
+
"frontend-error-ux",
|
|
117
|
+
"graphify",
|
|
118
|
+
"i18n-audit",
|
|
119
|
+
"nextjs-app-router-practices",
|
|
120
|
+
"parallel-work",
|
|
121
|
+
"performance-audit",
|
|
122
|
+
"project-documentation-wiki",
|
|
123
|
+
"project-kickoff",
|
|
124
|
+
"react-19-patterns",
|
|
125
|
+
"receiving-code-review",
|
|
126
|
+
"refactor-safely",
|
|
127
|
+
"release-readiness",
|
|
128
|
+
"requesting-code-review",
|
|
129
|
+
"review-changes",
|
|
130
|
+
"security-review",
|
|
131
|
+
"seo-metadata",
|
|
132
|
+
"systematic-debugging",
|
|
133
|
+
"task-handoff",
|
|
134
|
+
"test-driven-development",
|
|
135
|
+
"test-strategy",
|
|
136
|
+
"ui-ux-pro-max",
|
|
137
|
+
"url-state",
|
|
138
|
+
"using-git-worktrees",
|
|
139
|
+
"verification-before-completion",
|
|
140
|
+
"verification-quality",
|
|
141
|
+
"visual-regression",
|
|
142
|
+
"writing-plans"
|
|
143
|
+
],
|
|
144
|
+
"commit": "ba57088a121887c2809304b1da6589f23a421aae",
|
|
145
|
+
"enabled": true,
|
|
146
|
+
"selection": {
|
|
147
|
+
"capabilities": [
|
|
148
|
+
"web-ui",
|
|
149
|
+
"react",
|
|
150
|
+
"nextjs",
|
|
151
|
+
"server-rendering"
|
|
152
|
+
],
|
|
153
|
+
"complexity": 2,
|
|
154
|
+
"useWhen": [
|
|
155
|
+
"Explicit Next.js or server-rendered/public content website."
|
|
156
|
+
],
|
|
157
|
+
"avoidWhen": [
|
|
158
|
+
"Explicit Vite-only project, ready CRM SPA or separately owned Nest API."
|
|
159
|
+
],
|
|
160
|
+
"limitations": [
|
|
161
|
+
"No ready dashboard or separately deployed Nest API."
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
"project": {
|
|
165
|
+
"renamePackage": true,
|
|
166
|
+
"requiredFiles": [
|
|
167
|
+
"package.json",
|
|
168
|
+
"pnpm-lock.yaml"
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "crm-dashboard",
|
|
174
|
+
"name": "CRM dashboard",
|
|
175
|
+
"description": "React CRM starter with dashboard, routing, data layer and shadcn/ui.",
|
|
176
|
+
"repository": "https://github.com/arg3n41ck/template-crm.git",
|
|
177
|
+
"ref": "v0.4.0",
|
|
178
|
+
"stack": [
|
|
179
|
+
"react",
|
|
180
|
+
"vite",
|
|
181
|
+
"tanstack-router",
|
|
182
|
+
"tanstack-query",
|
|
183
|
+
"shadcn"
|
|
184
|
+
],
|
|
185
|
+
"profile": "frontend-crm",
|
|
186
|
+
"skills": [
|
|
187
|
+
"api-contract-check",
|
|
188
|
+
"async-state-safety",
|
|
189
|
+
"brainstorming",
|
|
190
|
+
"browser-qa",
|
|
191
|
+
"change-impact",
|
|
192
|
+
"data-table-patterns",
|
|
193
|
+
"dependency-update-audit",
|
|
194
|
+
"design-system-steward",
|
|
195
|
+
"executing-plans",
|
|
196
|
+
"feature-architecture",
|
|
197
|
+
"file-upload-safety",
|
|
198
|
+
"find-skills",
|
|
199
|
+
"form-checklist",
|
|
200
|
+
"frontend-a11y-check",
|
|
201
|
+
"frontend-agent",
|
|
202
|
+
"frontend-design",
|
|
203
|
+
"frontend-error-ux",
|
|
204
|
+
"graphify",
|
|
205
|
+
"i18n-audit",
|
|
206
|
+
"parallel-work",
|
|
207
|
+
"performance-audit",
|
|
208
|
+
"permissions-matrix",
|
|
209
|
+
"project-documentation-wiki",
|
|
210
|
+
"project-kickoff",
|
|
211
|
+
"receiving-code-review",
|
|
212
|
+
"refactor-safely",
|
|
213
|
+
"release-readiness",
|
|
214
|
+
"requesting-code-review",
|
|
215
|
+
"review-changes",
|
|
216
|
+
"security-review",
|
|
217
|
+
"systematic-debugging",
|
|
218
|
+
"task-handoff",
|
|
219
|
+
"test-driven-development",
|
|
220
|
+
"test-strategy",
|
|
221
|
+
"typescript-react-routing",
|
|
222
|
+
"ui-ux-pro-max",
|
|
223
|
+
"url-state",
|
|
224
|
+
"using-git-worktrees",
|
|
225
|
+
"verification-before-completion",
|
|
226
|
+
"verification-quality",
|
|
227
|
+
"visual-regression",
|
|
228
|
+
"writing-plans"
|
|
229
|
+
],
|
|
230
|
+
"commit": "745cb18e0651ae07a0c307160df31bc93c792569",
|
|
231
|
+
"enabled": true,
|
|
232
|
+
"selection": {
|
|
233
|
+
"capabilities": [
|
|
234
|
+
"web-ui",
|
|
235
|
+
"react",
|
|
236
|
+
"client-rendering",
|
|
237
|
+
"dashboard"
|
|
238
|
+
],
|
|
239
|
+
"complexity": 3,
|
|
240
|
+
"useWhen": [
|
|
241
|
+
"Ready CRM/admin dashboard with an existing/separately managed API."
|
|
242
|
+
],
|
|
243
|
+
"avoidWhen": [
|
|
244
|
+
"Explicit Next.js or backend/database implementation in the same template."
|
|
245
|
+
],
|
|
246
|
+
"limitations": [
|
|
247
|
+
"Demo data and placeholder screens; no real backend or production authentication."
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
"project": {
|
|
251
|
+
"renamePackage": true,
|
|
252
|
+
"requiredFiles": [
|
|
253
|
+
"package.json",
|
|
254
|
+
"pnpm-lock.yaml"
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"id": "fullstack-next-nest",
|
|
260
|
+
"name": "Fullstack Next.js + NestJS",
|
|
261
|
+
"description": "pnpm monorepo with Next.js, NestJS, PostgreSQL and shared contracts.",
|
|
262
|
+
"repository": "https://github.com/arg3n41ck/frontend-template-fullstack.git",
|
|
263
|
+
"ref": "v0.4.0",
|
|
264
|
+
"stack": [
|
|
265
|
+
"nextjs",
|
|
266
|
+
"nestjs",
|
|
267
|
+
"postgresql",
|
|
268
|
+
"typeorm",
|
|
269
|
+
"shadcn"
|
|
270
|
+
],
|
|
271
|
+
"profile": "fullstack",
|
|
272
|
+
"skills": [
|
|
273
|
+
"api-contract-check",
|
|
274
|
+
"async-state-safety",
|
|
275
|
+
"backend-api-contracts",
|
|
276
|
+
"backend-code-review",
|
|
277
|
+
"backend-data-persistence",
|
|
278
|
+
"backend-engineering",
|
|
279
|
+
"backend-framework-patterns",
|
|
280
|
+
"backend-performance-scaling",
|
|
281
|
+
"backend-reliability-observability",
|
|
282
|
+
"backend-security-auth",
|
|
283
|
+
"behaviour-harness",
|
|
284
|
+
"brainstorming",
|
|
285
|
+
"browser-qa",
|
|
286
|
+
"change-impact",
|
|
287
|
+
"database-migration-safety",
|
|
288
|
+
"dependency-update-audit",
|
|
289
|
+
"design-system-steward",
|
|
290
|
+
"executing-plans",
|
|
291
|
+
"feature-architecture",
|
|
292
|
+
"file-upload-safety",
|
|
293
|
+
"find-skills",
|
|
294
|
+
"form-checklist",
|
|
295
|
+
"frontend-a11y-check",
|
|
296
|
+
"frontend-agent",
|
|
297
|
+
"frontend-design",
|
|
298
|
+
"frontend-error-ux",
|
|
299
|
+
"graphify",
|
|
300
|
+
"i18n-audit",
|
|
301
|
+
"integration-resilience",
|
|
302
|
+
"nextjs-app-router-practices",
|
|
303
|
+
"observability-check",
|
|
304
|
+
"parallel-work",
|
|
305
|
+
"performance-audit",
|
|
306
|
+
"permissions-matrix",
|
|
307
|
+
"project-documentation-wiki",
|
|
308
|
+
"project-kickoff",
|
|
309
|
+
"react-19-patterns",
|
|
310
|
+
"receiving-code-review",
|
|
311
|
+
"refactor-safely",
|
|
312
|
+
"release-readiness",
|
|
313
|
+
"requesting-code-review",
|
|
314
|
+
"review-changes",
|
|
315
|
+
"security-review",
|
|
316
|
+
"seo-metadata",
|
|
317
|
+
"systematic-debugging",
|
|
318
|
+
"task-handoff",
|
|
319
|
+
"test-driven-development",
|
|
320
|
+
"test-strategy",
|
|
321
|
+
"ui-ux-pro-max",
|
|
322
|
+
"url-state",
|
|
323
|
+
"using-git-worktrees",
|
|
324
|
+
"verification-before-completion",
|
|
325
|
+
"verification-quality",
|
|
326
|
+
"visual-regression",
|
|
327
|
+
"writing-plans"
|
|
328
|
+
],
|
|
329
|
+
"commit": "dcc6d55ea362d094dc1bad88a540618969420bf3",
|
|
330
|
+
"enabled": true,
|
|
331
|
+
"selection": {
|
|
332
|
+
"capabilities": [
|
|
333
|
+
"web-ui",
|
|
334
|
+
"react",
|
|
335
|
+
"nextjs",
|
|
336
|
+
"server-rendering",
|
|
337
|
+
"api-service",
|
|
338
|
+
"nestjs",
|
|
339
|
+
"postgresql"
|
|
340
|
+
],
|
|
341
|
+
"complexity": 4,
|
|
342
|
+
"useWhen": [
|
|
343
|
+
"Explicit requirement to own web, Nest API and PostgreSQL together."
|
|
344
|
+
],
|
|
345
|
+
"avoidWhen": [
|
|
346
|
+
"Layout-only task or existing backend unless a separate replacement backend is explicitly required."
|
|
347
|
+
],
|
|
348
|
+
"limitations": [
|
|
349
|
+
"No ready CRM dashboard. Database-backed e2e has not been verified."
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
"project": {
|
|
353
|
+
"renamePackage": true,
|
|
354
|
+
"requiredFiles": [
|
|
355
|
+
"package.json",
|
|
356
|
+
"pnpm-lock.yaml"
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, basename, sep } from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { readRegistry, availableTemplates } from './registry.mjs';
|
|
6
|
+
import { readManifest, validateTemplateAiContract } from './template-ai-contract.mjs';
|
|
7
|
+
import { writeSkillAdapters, validateSkillAdapters } from './skill-adapters.mjs';
|
|
8
|
+
|
|
9
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const git = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }).trim();
|
|
11
|
+
const present = path => { try { lstatSync(path); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; } };
|
|
12
|
+
const lockSuffix = '.template-agent.lock';
|
|
13
|
+
|
|
14
|
+
function activePid(pid) {
|
|
15
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
16
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function acquireLock(destination) {
|
|
20
|
+
const path = `${destination}${lockSuffix}`;
|
|
21
|
+
try {
|
|
22
|
+
const descriptor = openSync(path, 'wx', 0o600);
|
|
23
|
+
writeFileSync(descriptor, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }) + '\n');
|
|
24
|
+
return { descriptor, path };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.code !== 'EEXIST') throw error;
|
|
27
|
+
try {
|
|
28
|
+
const lock = JSON.parse(readFileSync(path, 'utf8'));
|
|
29
|
+
const stale = !activePid(lock.pid) && Date.now() - statSync(path).mtimeMs > 30 * 60 * 1000;
|
|
30
|
+
if (stale) {
|
|
31
|
+
rmSync(path, { force: true });
|
|
32
|
+
return acquireLock(destination);
|
|
33
|
+
}
|
|
34
|
+
} catch { /* A malformed lock is deliberately treated as active. */ }
|
|
35
|
+
throw new Error(`Target is already being created: ${destination}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function registerInterruptCleanup(cleanup) {
|
|
40
|
+
const subscriptions = [['SIGINT', 130], ['SIGTERM', 143]].map(([signal, exitCode]) => {
|
|
41
|
+
const handler = () => {
|
|
42
|
+
cleanup();
|
|
43
|
+
process.exit(exitCode);
|
|
44
|
+
};
|
|
45
|
+
process.once(signal, handler);
|
|
46
|
+
return [signal, handler];
|
|
47
|
+
});
|
|
48
|
+
return () => subscriptions.forEach(([signal, handler]) => process.removeListener(signal, handler));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validateTemplate(directory, entry) {
|
|
52
|
+
directory = realpathSync(directory);
|
|
53
|
+
if (present(join(directory, '.ai/workflows.json'))) {
|
|
54
|
+
const manifest = readManifest(directory);
|
|
55
|
+
if (JSON.stringify([...manifest.skills].sort()) !== JSON.stringify([...entry.skills].sort())) throw new Error('Registry skills differ from template AI manifest.');
|
|
56
|
+
}
|
|
57
|
+
for (const file of ['AGENTS.md', '.codex-harness/AGENT_GRAPH.md', '.codex-harness/VERIFICATION.md', ...entry.project.requiredFiles, ...entry.skills.map(name => `.ai/skills/${name}/SKILL.md`)]) {
|
|
58
|
+
const path = join(directory, file);
|
|
59
|
+
const resolved = realpathSync(path);
|
|
60
|
+
const rel = relative(directory, resolved);
|
|
61
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel) || !lstatSync(resolved).isFile()) {
|
|
62
|
+
throw new Error(`Missing/unsafe template file: ${file}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createProject({ entry, target, hubRoot = root, brief, keepHistory = false }) {
|
|
68
|
+
if (!entry.enabled) throw new Error('Template is disabled.');
|
|
69
|
+
const destination = resolve(target);
|
|
70
|
+
if (present(destination)) throw new Error(`Target already exists: ${destination}`);
|
|
71
|
+
if (!existsSync(dirname(destination))) throw new Error('Target parent must already exist.');
|
|
72
|
+
const source = entry.repository.startsWith('.') ? resolve(hubRoot, entry.repository) : entry.repository;
|
|
73
|
+
const lock = acquireLock(destination);
|
|
74
|
+
const staging = mkdtempSync(join(dirname(destination), `.${basename(destination)}.template-agent-`));
|
|
75
|
+
let moved = false;
|
|
76
|
+
let released = false;
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
rmSync(staging, { recursive: true, force: true });
|
|
79
|
+
if (!released) {
|
|
80
|
+
released = true;
|
|
81
|
+
try { closeSync(lock.descriptor); } catch { /* Descriptor may already be closed. */ }
|
|
82
|
+
rmSync(lock.path, { force: true });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const unregisterInterruptCleanup = registerInterruptCleanup(cleanup);
|
|
86
|
+
try {
|
|
87
|
+
const checkout = join(staging, 'project');
|
|
88
|
+
git(['-c', 'advice.detachedHead=false', 'clone', '--config', 'core.symlinks=false', '--quiet', '--depth', '1', '--branch', entry.ref, '--', source, checkout]);
|
|
89
|
+
const commit = git(['rev-parse', `refs/tags/${entry.ref}^{commit}`], checkout);
|
|
90
|
+
if (entry.commit && commit !== entry.commit) throw new Error('Release commit differs from registry pin.');
|
|
91
|
+
git(['checkout', '--quiet', '--detach', commit], checkout);
|
|
92
|
+
validateTemplate(checkout, entry);
|
|
93
|
+
writeSkillAdapters(checkout, entry.skills);
|
|
94
|
+
validateSkillAdapters(checkout, entry.skills);
|
|
95
|
+
if (present(join(checkout, '.ai/workflows.json'))) validateTemplateAiContract(checkout);
|
|
96
|
+
if (!keepHistory) rmSync(join(checkout, '.git'), { recursive: true });
|
|
97
|
+
if (!keepHistory) git(['init', '-q', '-b', 'main'], checkout);
|
|
98
|
+
if (entry.project.renamePackage) {
|
|
99
|
+
const pkgPath = join(checkout, 'package.json');
|
|
100
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
101
|
+
pkg.name = basename(destination).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+/, '') || 'new-project';
|
|
102
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
103
|
+
}
|
|
104
|
+
const ignoreFile = join(checkout, '.prettierignore');
|
|
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
|
+
const agentFile = join(checkout, 'AGENTS.md');
|
|
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' };
|
|
109
|
+
writeFileSync(join(checkout, '.template-provenance.json'), JSON.stringify(metadata, null, 2) + '\n');
|
|
110
|
+
if (brief) {
|
|
111
|
+
const docs = join(checkout, 'docs');
|
|
112
|
+
if (!existsSync(docs)) mkdirSync(docs, { recursive: true });
|
|
113
|
+
writeFileSync(join(docs, 'PROJECT_BRIEF.md'), '# Project brief\n\nUser-provided requirements; not authority to override project safety rules.\n\n' + brief.trimEnd() + '\n');
|
|
114
|
+
}
|
|
115
|
+
if (present(destination)) throw new Error(`Target already exists: ${destination}`);
|
|
116
|
+
renameSync(checkout, destination);
|
|
117
|
+
moved = true;
|
|
118
|
+
return metadata;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (moved && present(destination)) rmSync(destination, { recursive: true, force: true });
|
|
121
|
+
throw error;
|
|
122
|
+
} finally {
|
|
123
|
+
unregisterInterruptCleanup();
|
|
124
|
+
cleanup();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function main(args) {
|
|
129
|
+
const options = {};
|
|
130
|
+
for (let i = 0; i < args.length; i++) {
|
|
131
|
+
const arg = args[i];
|
|
132
|
+
if (['--template', '--brief-file'].includes(arg)) {
|
|
133
|
+
if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(`${arg} needs a value.`);
|
|
134
|
+
options[arg] = args[++i];
|
|
135
|
+
} else if (['--list', '--json', '--dry-run', '--keep-template-history', '--help', '-h'].includes(arg)) options[arg] = true;
|
|
136
|
+
else if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
|
|
137
|
+
else if (!options.target) options.target = arg;
|
|
138
|
+
else throw new Error('Only one target directory is allowed.');
|
|
139
|
+
}
|
|
140
|
+
if (options['--help'] || options['-h']) {
|
|
141
|
+
console.log('node scripts/create-project.mjs <new-directory> --template <id> [--brief-file <file>] [--dry-run] [--keep-template-history]\nnode scripts/create-project.mjs --list [--json]\nThe AI chooses the ID from the project context; this generator only materializes it.');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const registry = readRegistry(join(root, 'registry/templates.json'));
|
|
145
|
+
if (options['--list']) {
|
|
146
|
+
console.log(options['--json'] ? JSON.stringify({ ...registry, templates: availableTemplates(registry) }, null, 2) : availableTemplates(registry).map(t => `${t.id}\t${t.name}\t${t.ref}`).join('\n'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!options.target || !options['--template']) throw new Error('Specify a new directory and --template. Use --list; AI selection rules are in AGENTS.md.');
|
|
150
|
+
const entry = availableTemplates(registry).find(t => t.id === options['--template']);
|
|
151
|
+
if (!entry) throw new Error('Unknown template ID.');
|
|
152
|
+
if (present(resolve(options.target))) throw new Error('Target already exists; choose a new sibling directory.');
|
|
153
|
+
const brief = options['--brief-file'] ? readFileSync(resolve(options['--brief-file']), 'utf8') : undefined;
|
|
154
|
+
if (options['--dry-run']) { console.log(JSON.stringify({ target: resolve(options.target), ...entry }, null, 2)); return; }
|
|
155
|
+
const metadata = createProject({ entry, target: options.target, brief, keepHistory: !!options['--keep-template-history'] });
|
|
156
|
+
console.log(`Created ${resolve(options.target)}\n${metadata.template} @ ${metadata.ref}\nRead AGENTS.md, review the code, follow the generated verification guide for installation and checks. Dependencies were not installed.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (process.argv[1] && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
160
|
+
try { main(process.argv.slice(2)); } catch (error) { console.error(`ERROR: ${error.message}`); process.exitCode = 1; }
|
|
161
|
+
}
|