feat: автообновление, шаблон zustand-store, документация
- Добавлено автоматическое обновление со спиннером (ora) - Новый шаблон zustand-store - Документация (FEATURES.md, LICENSE, README)
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './{{name.camelCase}}Store.type';
|
||||||
|
export * from './{{name.camelCase}}Store';
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
import { immer } from 'zustand/middleware/immer';
|
||||||
|
import type { {{name.pascalCase}}State, {{name.pascalCase}}Store } from './{{name.camelCase}}Store.type';
|
||||||
|
|
||||||
|
export const defaultInitState: {{name.pascalCase}}State = {
|
||||||
|
deviceId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDeviceId = (state: {{name.pascalCase}}Store, deviceId: string) => {
|
||||||
|
state.deviceId = deviceId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const use{{name.pascalCase}}Store = create<{{name.pascalCase}}Store>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
immer<{{name.pascalCase}}Store>((set) => ({
|
||||||
|
...defaultInitState,
|
||||||
|
setDeviceId: (deviceId) => set((state) => setDeviceId(state, deviceId)),
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: '{{name.kebabCase}}-storage',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{ name: '{{name.pascalCase}}Store' }
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type {{name.pascalCase}}State = {
|
||||||
|
deviceId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}Actions = {
|
||||||
|
setDeviceId: (deviceId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}Store = {{name.pascalCase}}State & {{name.pascalCase}}Actions;
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Gromlab
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
29
README.md
29
README.md
@@ -2,23 +2,34 @@
|
|||||||
|
|
||||||
CLI-утилита для генерации файлов из шаблонов.
|
CLI-утилита для генерации файлов из шаблонов.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
Глобально:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -g @gromlab/create
|
||||||
|
```
|
||||||
|
При запуске CLI проверяет доступность новой версии. Если вы выбираете «нет»,
|
||||||
|
повторный запрос появится через 24 часа. Чтобы пропустить проверку, используйте
|
||||||
|
флаг `--skip-update`.
|
||||||
|
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @gromlab/create <шаблон> <имя> [опции]
|
npx @gromlab/create <шаблон> <имя> [путь] [опции]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Если `[путь]` не указан, файлы создаются в директории, где запущен CLI.
|
||||||
|
|
||||||
## Пример
|
## Пример
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Создать компонент из шаблона
|
# Создать компонент из шаблона
|
||||||
npx @gromlab/create component Button
|
npx @gromlab/create component Button
|
||||||
|
|
||||||
# Указать папку вывода
|
# Указать папку вывода позиционно
|
||||||
npx @gromlab/create component Button --out src/components
|
npx @gromlab/create component Button src/components
|
||||||
|
|
||||||
# Превью без записи
|
|
||||||
npx @gromlab/create component Button --dry-run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Шаблоны
|
## Шаблоны
|
||||||
@@ -43,12 +54,12 @@ npx @gromlab/create component Button --dry-run
|
|||||||
- `{{name.kebab}}` — kebab-case
|
- `{{name.kebab}}` — kebab-case
|
||||||
- `{{name.snake}}` — snake_case
|
- `{{name.snake}}` — snake_case
|
||||||
|
|
||||||
|
Переменная `name` задается только позиционным аргументом `<имя>`. Использование `--name` запрещено.
|
||||||
|
|
||||||
## Опции
|
## Опции
|
||||||
|
|
||||||
| Опция | Описание |
|
| Опция | Описание |
|
||||||
|-------|----------|
|
|-------|----------|
|
||||||
| `--out <путь>` | Папка вывода (по умолчанию: `.`) |
|
|
||||||
| `--templates <путь>` | Папка шаблонов (по умолчанию: `.templates`) |
|
|
||||||
| `--overwrite` | Перезаписать существующие файлы |
|
| `--overwrite` | Перезаписать существующие файлы |
|
||||||
| `--dry-run` | Показать результат без записи |
|
| `--skip-update` | Не проверять обновления CLI |
|
||||||
| `--<переменная> <значение>` | Произвольная переменная шаблона |
|
| `--<переменная> <значение>` | Произвольная переменная шаблона |
|
||||||
|
|||||||
52
docs/ru/FEATURES.md
Normal file
52
docs/ru/FEATURES.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Возможности CLI
|
||||||
|
|
||||||
|
Ниже перечислены текущие возможности `@gromlab/create`.
|
||||||
|
|
||||||
|
## Основные
|
||||||
|
|
||||||
|
- Генерация файлов и папок по шаблонам из каталога `.templates/`.
|
||||||
|
- Обязательный позиционный аргумент `<имя>` (переменная `name`).
|
||||||
|
- Опциональный позиционный `[путь]` — папка вывода (по умолчанию текущая директория).
|
||||||
|
- Произвольные переменные шаблонов через `--<var> <value>`.
|
||||||
|
- Подстановка переменных в **содержимом** файлов и **путях**.
|
||||||
|
|
||||||
|
## Модификаторы переменных
|
||||||
|
|
||||||
|
Поддерживаются модификаторы для `{{var}}`:
|
||||||
|
- `{{name.pascalCase}}`
|
||||||
|
- `{{name.camelCase}}`
|
||||||
|
- `{{name.snakeCase}}`
|
||||||
|
- `{{name.kebabCase}}`
|
||||||
|
- `{{name.screamingSnakeCase}}`
|
||||||
|
- `{{name.upperCase}}`
|
||||||
|
- `{{name.lowerCase}}`
|
||||||
|
- `{{name.upperCaseAll}}`
|
||||||
|
- `{{name.lowerCaseAll}}`
|
||||||
|
|
||||||
|
## Режимы и защита от перезаписи
|
||||||
|
|
||||||
|
- `--overwrite` — разрешает перезапись существующих файлов.
|
||||||
|
- Проверка коллизий файлов и существующих директорий (с понятными ошибками/предупреждениями).
|
||||||
|
|
||||||
|
## Отчёт и визуализация
|
||||||
|
|
||||||
|
- Печать итогового отчёта: имя, путь(и), дерево файлов, список переменных.
|
||||||
|
|
||||||
|
## Диагностика и подсказки
|
||||||
|
|
||||||
|
- Подробные ошибки валидации аргументов.
|
||||||
|
- Список доступных шаблонов, если запрошенный не найден.
|
||||||
|
- Подсказки для недостающих переменных.
|
||||||
|
- `-h`/`--help` — вывод справки.
|
||||||
|
- `--skip-update` — запуск без проверки обновлений.
|
||||||
|
|
||||||
|
## Обновления CLI
|
||||||
|
|
||||||
|
- При запуске (не через `npx`) проверяется доступность новой версии в npm.
|
||||||
|
- Если пользователь отвечает «нет», повторный запрос появится через 24 часа.
|
||||||
|
- В неинтерактивном режиме (без TTY) запрос не показывается.
|
||||||
|
|
||||||
|
## Бины
|
||||||
|
|
||||||
|
- Основной бин: `gromlab-create`.
|
||||||
|
- Дополнительный алиас: `create`.
|
||||||
296
package-lock.json
generated
296
package-lock.json
generated
@@ -1,21 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "@gromlab/create",
|
"name": "@gromlab/create",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@gromlab/create",
|
"name": "@gromlab/create",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archy": "^1.0.0",
|
"archy": "^1.0.0",
|
||||||
"boxen": "^5.1.2",
|
"boxen": "^5.1.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"change-case-all": "^2.1.0",
|
"change-case-all": "^2.1.0",
|
||||||
"directory-tree": "^3.5.2",
|
"directory-tree": "^3.5.2",
|
||||||
"figures": "^3.2.0"
|
"figures": "^3.2.0",
|
||||||
|
"ora": "^5.4.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
"create": "dist/cli.js",
|
||||||
"gromlab-create": "dist/cli.js"
|
"gromlab-create": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -89,6 +92,37 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/boxen": {
|
"node_modules/boxen": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
|
||||||
@@ -111,6 +145,30 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camelcase": {
|
"node_modules/camelcase": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||||
@@ -169,6 +227,39 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-cursor": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"restore-cursor": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-spinners": {
|
||||||
|
"version": "2.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||||
|
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -306,6 +397,18 @@
|
|||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/defaults": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clone": "^1.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/directory-tree": {
|
"node_modules/directory-tree": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.6.0.tgz",
|
||||||
@@ -373,6 +476,32 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -382,12 +511,110 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-interactive": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-unicode-supported": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lodash.camelcase": {
|
"node_modules/lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/log-symbols": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"is-unicode-supported": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mimic-fn": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/onetime": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-fn": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ora": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.1.0",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"cli-cursor": "^3.1.0",
|
||||||
|
"cli-spinners": "^2.5.0",
|
||||||
|
"is-interactive": "^1.0.0",
|
||||||
|
"is-unicode-supported": "^0.1.0",
|
||||||
|
"log-symbols": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wcwidth": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reduce-flatten": {
|
"node_modules/reduce-flatten": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
|
||||||
@@ -397,12 +624,60 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/restore-cursor": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"onetime": "^5.1.0",
|
||||||
|
"signal-exit": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/sponge-case": {
|
"node_modules/sponge-case": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz",
|
||||||
"integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==",
|
"integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
@@ -537,6 +812,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wcwidth": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"defaults": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/widest-line": {
|
"node_modules/widest-line": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,16 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "@gromlab/create",
|
"name": "@gromlab/create",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "Template-based file generator CLI",
|
"description": "Template-based file generator CLI",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"gromlab-create": "dist/cli.js"
|
"gromlab-create": "dist/cli.js",
|
||||||
|
"create": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"main": "dist/cli.js",
|
"main": "dist/cli.js",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"clear": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||||
|
"build": "npm run clear && tsc -p tsconfig.json",
|
||||||
"dev": "tsc -p tsconfig.json -w",
|
"dev": "tsc -p tsconfig.json -w",
|
||||||
"prepare": "npm run build"
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
@@ -20,7 +26,8 @@
|
|||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"change-case-all": "^2.1.0",
|
"change-case-all": "^2.1.0",
|
||||||
"directory-tree": "^3.5.2",
|
"directory-tree": "^3.5.2",
|
||||||
"figures": "^3.2.0"
|
"figures": "^3.2.0",
|
||||||
|
"ora": "^5.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/archy": "^0.0.31",
|
"@types/archy": "^0.0.31",
|
||||||
|
|||||||
40
src/args.ts
40
src/args.ts
@@ -3,22 +3,22 @@ import { ParsedArgs } from './types';
|
|||||||
export function printHelp() {
|
export function printHelp() {
|
||||||
const lines = [
|
const lines = [
|
||||||
'Использование:',
|
'Использование:',
|
||||||
' npx @gromlab/create <template> [name] [options]',
|
' npx @gromlab/create <шаблон> <имя> [путь] [опции]',
|
||||||
|
'',
|
||||||
|
'Аргументы:',
|
||||||
|
' <шаблон> Имя шаблона',
|
||||||
|
' <имя> Значение переменной name (обязательный аргумент)',
|
||||||
|
' [путь] Папка вывода (по умолчанию: текущая директория)',
|
||||||
'',
|
'',
|
||||||
'Опции:',
|
'Опции:',
|
||||||
' --<var> <value> Переменная шаблона (поддерживается любой --key <value>)',
|
' --<var> <value> Переменная шаблона (поддерживается любой --key <value>)',
|
||||||
' [name] Сокращение для --name',
|
|
||||||
' --templates <path> Папка шаблонов (по умолчанию: .templates)',
|
|
||||||
' --templates-path Алиас для --templates',
|
|
||||||
' --out <path> Папка вывода (по умолчанию: текущая директория)',
|
|
||||||
' --overwrite Перезаписывать существующие файлы',
|
' --overwrite Перезаписывать существующие файлы',
|
||||||
' --dry-run Показать результат без записи на диск',
|
' --skip-update Не проверять обновления CLI',
|
||||||
' -h, --help Показать эту справку',
|
' -h, --help Показать эту справку',
|
||||||
'',
|
'',
|
||||||
'Примеры:',
|
'Примеры:',
|
||||||
' npx @gromlab/create component --name test',
|
' npx @gromlab/create component Button',
|
||||||
' npx @gromlab/create component --name test --out src/components',
|
' npx @gromlab/create component Button src/components'
|
||||||
' npx @gromlab/create component --name test --templates /path/to/.templates'
|
|
||||||
];
|
];
|
||||||
console.log(lines.join('\n'));
|
console.log(lines.join('\n'));
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|||||||
const parsed: ParsedArgs = {
|
const parsed: ParsedArgs = {
|
||||||
vars: {},
|
vars: {},
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
dryRun: false,
|
skipUpdate: false,
|
||||||
help: false,
|
help: false,
|
||||||
extra: []
|
extra: []
|
||||||
};
|
};
|
||||||
@@ -52,11 +52,10 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|||||||
parsed.overwrite = true;
|
parsed.overwrite = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (arg === '--dry-run') {
|
if (arg === '--skip-update') {
|
||||||
parsed.dryRun = true;
|
parsed.skipUpdate = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.startsWith('--')) {
|
if (arg.startsWith('--')) {
|
||||||
const eqIndex = arg.indexOf('=');
|
const eqIndex = arg.indexOf('=');
|
||||||
const key = eqIndex === -1 ? arg.slice(2) : arg.slice(2, eqIndex);
|
const key = eqIndex === -1 ? arg.slice(2) : arg.slice(2, eqIndex);
|
||||||
@@ -65,17 +64,11 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
|
|
||||||
if (key === 'templates' || key === 'templatesPath' || key === 'templates-path') {
|
if (key === 'templates' || key === 'templatesPath' || key === 'templates-path') {
|
||||||
const value = inlineValue ?? consumeValue(args, i, key);
|
throw new Error('Опция --templates не поддерживается');
|
||||||
if (inlineValue === undefined) i++;
|
|
||||||
parsed.templatesPath = value;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'out' || key === 'output') {
|
if (key === 'out' || key === 'output') {
|
||||||
const value = inlineValue ?? consumeValue(args, i, key);
|
throw new Error('Опция --out не поддерживается');
|
||||||
if (inlineValue === undefined) i++;
|
|
||||||
parsed.outDir = value;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = inlineValue ?? consumeValue(args, i, key);
|
const value = inlineValue ?? consumeValue(args, i, key);
|
||||||
@@ -94,6 +87,11 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!parsed.positionalOutDir) {
|
||||||
|
parsed.positionalOutDir = arg;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
parsed.extra.push(arg);
|
parsed.extra.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
src/cli.ts
39
src/cli.ts
@@ -5,13 +5,19 @@ import { printError, printWarnings, printSummary } from './output';
|
|||||||
import { PlanItem } from './types';
|
import { PlanItem } from './types';
|
||||||
import { normalizeArgs, resolveTemplateContext } from './validation';
|
import { normalizeArgs, resolveTemplateContext } from './validation';
|
||||||
import { buildPlan, getCollisions, getExistingDirs, getRoots, getTopLevelDirs, writePlan } from './plan';
|
import { buildPlan, getCollisions, getExistingDirs, getRoots, getTopLevelDirs, writePlan } from './plan';
|
||||||
|
import { maybeHandleUpdate } from './update';
|
||||||
|
|
||||||
function resolvePath(baseDir: string, inputPath: string): string {
|
function resolvePath(baseDir: string, inputPath: string): string {
|
||||||
if (path.isAbsolute(inputPath)) return path.normalize(inputPath);
|
if (path.isAbsolute(inputPath)) return path.normalize(inputPath);
|
||||||
return path.resolve(baseDir, inputPath);
|
return path.resolve(baseDir, inputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
async function run() {
|
||||||
|
const shouldContinue = await maybeHandleUpdate(process.argv.slice(2));
|
||||||
|
if (!shouldContinue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = parseArgs(process.argv);
|
parsed = parseArgs(process.argv);
|
||||||
@@ -38,8 +44,8 @@ function run() {
|
|||||||
const normalized = normalizedResult.normalized;
|
const normalized = normalizedResult.normalized;
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const templatesDir = resolvePath(cwd, normalized.templatesPath ?? '.templates');
|
const templatesDir = resolvePath(cwd, '.templates');
|
||||||
const outDir = resolvePath(cwd, normalized.outDir ?? '.');
|
const outDir = resolvePath(cwd, normalized.positionalOutDir ?? '.');
|
||||||
|
|
||||||
const templateResult = resolveTemplateContext(
|
const templateResult = resolveTemplateContext(
|
||||||
templatesDir,
|
templatesDir,
|
||||||
@@ -57,10 +63,9 @@ function run() {
|
|||||||
const existingDirs = getExistingDirs(outDir, topLevelDirs);
|
const existingDirs = getExistingDirs(outDir, topLevelDirs);
|
||||||
|
|
||||||
const redWarnings: string[][] = [];
|
const redWarnings: string[][] = [];
|
||||||
const warnings: string[][] = [];
|
|
||||||
|
|
||||||
if (existingDirs.length > 0) {
|
if (existingDirs.length > 0) {
|
||||||
if (!normalized.overwrite && !normalized.dryRun) {
|
if (!normalized.overwrite) {
|
||||||
printError(
|
printError(
|
||||||
'Папка назначения уже существует',
|
'Папка назначения уже существует',
|
||||||
existingDirs.map((dir) => path.join(outDir, dir)),
|
existingDirs.map((dir) => path.join(outDir, dir)),
|
||||||
@@ -81,13 +86,6 @@ function run() {
|
|||||||
const collisions = getCollisions(plan);
|
const collisions = getCollisions(plan);
|
||||||
|
|
||||||
if (collisions.length > 0 && !normalized.overwrite) {
|
if (collisions.length > 0 && !normalized.overwrite) {
|
||||||
if (normalized.dryRun) {
|
|
||||||
warnings.push([
|
|
||||||
'Файлы уже существуют:',
|
|
||||||
...collisions.map((target) => ` - ${path.relative(outDir, target)}`),
|
|
||||||
'Используйте --overwrite для перезаписи.'
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
printError(
|
printError(
|
||||||
'Файлы уже существуют',
|
'Файлы уже существуют',
|
||||||
collisions.map((target) => path.relative(outDir, target)),
|
collisions.map((target) => path.relative(outDir, target)),
|
||||||
@@ -97,25 +95,16 @@ function run() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const roots = getRoots(outDir, plan);
|
const roots = getRoots(outDir, plan);
|
||||||
|
|
||||||
if (normalized.dryRun) {
|
|
||||||
printSummary(plan, outDir, normalized.vars, true, normalized.templateName!, roots);
|
|
||||||
printWarnings(redWarnings, warnings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writePlan(plan, normalized.vars, normalized.overwrite);
|
writePlan(plan, normalized.vars, normalized.overwrite);
|
||||||
|
|
||||||
printSummary(plan, outDir, normalized.vars, false, normalized.templateName!, roots);
|
printSummary(plan, outDir, normalized.vars, normalized.templateName!, roots);
|
||||||
printWarnings(redWarnings, warnings);
|
printWarnings(redWarnings, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
run().catch((error) => {
|
||||||
run();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(String(error));
|
console.error(String(error));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -6,44 +6,6 @@ import directoryTree = require('directory-tree');
|
|||||||
import figures = require('figures');
|
import figures = require('figures');
|
||||||
import { PlanItem } from './types';
|
import { PlanItem } from './types';
|
||||||
|
|
||||||
type TreeNode = {
|
|
||||||
name: string;
|
|
||||||
children: Map<string, TreeNode>;
|
|
||||||
isFile: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildTreeFromPaths(rootLabel: string, paths: string[]): string {
|
|
||||||
const root: TreeNode = { name: rootLabel, children: new Map(), isFile: false };
|
|
||||||
|
|
||||||
for (const rawPath of paths) {
|
|
||||||
const parts = rawPath.split(path.sep).filter(Boolean);
|
|
||||||
let current = root;
|
|
||||||
parts.forEach((part, index) => {
|
|
||||||
const isLast = index === parts.length - 1;
|
|
||||||
let child = current.children.get(part);
|
|
||||||
if (!child) {
|
|
||||||
child = { name: part, children: new Map(), isFile: isLast };
|
|
||||||
current.children.set(part, child);
|
|
||||||
}
|
|
||||||
if (isLast) {
|
|
||||||
child.isFile = true;
|
|
||||||
}
|
|
||||||
current = child;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const toArchyNode = (node: TreeNode): archy.Data => {
|
|
||||||
const entries = Array.from(node.children.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
const isDir = node.children.size > 0;
|
|
||||||
return {
|
|
||||||
label: isDir ? chalk.cyan(`${node.name}/`) : chalk.white(node.name),
|
|
||||||
nodes: entries.map((child) => toArchyNode(child))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return archy(toArchyNode(root));
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTreeFromDirectory(rootPath: string): string {
|
function buildTreeFromDirectory(rootPath: string): string {
|
||||||
const tree = directoryTree(rootPath, { attributes: ['type'] }) as directoryTree.DirectoryTree | null;
|
const tree = directoryTree(rootPath, { attributes: ['type'] }) as directoryTree.DirectoryTree | null;
|
||||||
if (!tree) return '';
|
if (!tree) return '';
|
||||||
@@ -109,7 +71,6 @@ export function printSummary(
|
|||||||
plan: PlanItem[],
|
plan: PlanItem[],
|
||||||
outDir: string,
|
outDir: string,
|
||||||
vars: Record<string, string>,
|
vars: Record<string, string>,
|
||||||
isDryRun: boolean,
|
|
||||||
templateName: string,
|
templateName: string,
|
||||||
roots: string[]
|
roots: string[]
|
||||||
) {
|
) {
|
||||||
@@ -119,9 +80,9 @@ export function printSummary(
|
|||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Заголовок с иконкой
|
// Заголовок с иконкой
|
||||||
const statusIcon = isDryRun ? figures.info : figures.tick;
|
const statusIcon = figures.tick;
|
||||||
const statusColor = isDryRun ? chalk.blue : chalk.green;
|
const statusColor = chalk.green;
|
||||||
const statusText = isDryRun ? 'Планируется генерация' : 'Успешно создан';
|
const statusText = 'Успешно создан';
|
||||||
|
|
||||||
console.log(statusColor(`${statusIcon} ${statusText}: `) + chalk.bold.white(displayName));
|
console.log(statusColor(`${statusIcon} ${statusText}: `) + chalk.bold.white(displayName));
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -145,11 +106,12 @@ export function printSummary(
|
|||||||
console.log(chalk.dim(' (пусто)'));
|
console.log(chalk.dim(' (пусто)'));
|
||||||
} else {
|
} else {
|
||||||
for (const rootPath of roots) {
|
for (const rootPath of roots) {
|
||||||
const treeOutput = (!isDryRun && fs.existsSync(rootPath))
|
const treeOutput = fs.existsSync(rootPath) ? buildTreeFromDirectory(rootPath) : '';
|
||||||
? buildTreeFromDirectory(rootPath)
|
|
||||||
: buildTreeFromPaths(path.basename(rootPath) || rootPath, plan
|
if (!treeOutput) {
|
||||||
.map((item) => path.relative(rootPath, item.target))
|
console.log(chalk.dim(' (пусто)'));
|
||||||
.filter((rel) => rel && !rel.startsWith('..')));
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем отступ к дереву
|
// Добавляем отступ к дереву
|
||||||
const indentedTree = treeOutput
|
const indentedTree = treeOutput
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
export type ParsedArgs = {
|
export type ParsedArgs = {
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
positionalName?: string;
|
positionalName?: string;
|
||||||
templatesPath?: string;
|
positionalOutDir?: string;
|
||||||
outDir?: string;
|
|
||||||
vars: Record<string, string>;
|
vars: Record<string, string>;
|
||||||
overwrite: boolean;
|
overwrite: boolean;
|
||||||
dryRun: boolean;
|
skipUpdate: boolean;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
extra: string[];
|
extra: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
225
src/update.ts
Normal file
225
src/update.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as readline from 'readline';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import ora = require('ora');
|
||||||
|
|
||||||
|
const PACKAGE_NAME = '@gromlab/create';
|
||||||
|
const UPDATE_TIMEOUT_MS = 2000;
|
||||||
|
const UPDATE_PROMPT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function isNpxInvocation(): boolean {
|
||||||
|
const argv1 = process.argv[1] ?? '';
|
||||||
|
const npmCommand = process.env.npm_command ?? '';
|
||||||
|
const npmExecPath = process.env.npm_execpath ?? '';
|
||||||
|
|
||||||
|
if (argv1.includes(`${path.sep}_npx${path.sep}`)) return true;
|
||||||
|
if (npmCommand === 'exec') return true;
|
||||||
|
if (npmExecPath.includes('npx-cli.js')) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdateStatePath(): string {
|
||||||
|
const xdgHome = process.env.XDG_CONFIG_HOME;
|
||||||
|
if (xdgHome) {
|
||||||
|
return path.join(xdgHome, 'gromlab-create', 'update.json');
|
||||||
|
}
|
||||||
|
const appData = process.env.APPDATA;
|
||||||
|
if (appData) {
|
||||||
|
return path.join(appData, 'gromlab-create', 'update.json');
|
||||||
|
}
|
||||||
|
return path.join(os.homedir(), '.gromlab-create', 'update.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDeclinedAt(): number | undefined {
|
||||||
|
const statePath = getUpdateStatePath();
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(statePath, 'utf8');
|
||||||
|
const data = JSON.parse(raw) as { declinedAt?: number };
|
||||||
|
if (typeof data.declinedAt === 'number') return data.declinedAt;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDeclinedAt(timestamp: number) {
|
||||||
|
const statePath = getUpdateStatePath();
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
||||||
|
fs.writeFileSync(statePath, JSON.stringify({ declinedAt: timestamp }), 'utf8');
|
||||||
|
} catch {
|
||||||
|
// ignore write errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDeclinedAt() {
|
||||||
|
const statePath = getUpdateStatePath();
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(statePath);
|
||||||
|
} catch {
|
||||||
|
// ignore remove errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCurrentVersion(): string | undefined {
|
||||||
|
try {
|
||||||
|
const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
|
||||||
|
const raw = fs.readFileSync(packageJsonPath, 'utf8');
|
||||||
|
const data = JSON.parse(raw) as { version?: string };
|
||||||
|
return typeof data.version === 'string' ? data.version : undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSemver(version: string): number[] | null {
|
||||||
|
const clean = version.split('+')[0].split('-')[0].trim();
|
||||||
|
if (!clean) return null;
|
||||||
|
const parts = clean.split('.').map((part) => Number(part));
|
||||||
|
if (parts.some((value) => Number.isNaN(value))) return null;
|
||||||
|
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareSemver(a: string, b: string): number {
|
||||||
|
const aParts = parseSemver(a);
|
||||||
|
const bParts = parseSemver(b);
|
||||||
|
if (!aParts || !bParts) return 0;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (aParts[i] > bParts[i]) return 1;
|
||||||
|
if (aParts[i] < bParts[i]) return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchLatestVersion(packageName: string, timeoutMs: number): Promise<string | undefined> {
|
||||||
|
const encodedName = encodeURIComponent(packageName);
|
||||||
|
const url = `https://registry.npmjs.org/${encodedName}/latest`;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const finish = (value?: string) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.get(url, { headers: { Accept: 'application/json' } }, (res) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
res.resume();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data) as { version?: string };
|
||||||
|
const version = typeof json.version === 'string' ? json.version : undefined;
|
||||||
|
finish(version);
|
||||||
|
} catch {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', () => finish());
|
||||||
|
req.setTimeout(timeoutMs, () => {
|
||||||
|
req.destroy();
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInteractive(): boolean {
|
||||||
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function askYesNo(question: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
rl.close();
|
||||||
|
const normalized = answer.trim().toLowerCase();
|
||||||
|
resolve(['y', 'yes', 'д', 'да'].includes(normalized));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runUpdate(packageName: string): boolean {
|
||||||
|
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
|
const result = spawnSync(npmCmd, ['i', '-g', `${packageName}@latest`], { stdio: 'inherit' });
|
||||||
|
if (result.error) {
|
||||||
|
console.error(String(result.error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerunCommand(args: string[]): never {
|
||||||
|
const binName = process.platform === 'win32' ? 'gromlab-create.cmd' : 'gromlab-create';
|
||||||
|
const result = spawnSync(binName, args, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
const nodeResult = spawnSync(process.execPath, [process.argv[1], ...args], { stdio: 'inherit' });
|
||||||
|
process.exit(nodeResult.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(result.status ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSkipUpdateFlag(args: string[]): boolean {
|
||||||
|
return args.some((arg) => arg === '--skip-update' || arg.startsWith('--skip-update='));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function maybeHandleUpdate(args: string[]): Promise<boolean> {
|
||||||
|
if (isNpxInvocation()) return true;
|
||||||
|
if (hasSkipUpdateFlag(args)) return true;
|
||||||
|
if (!isInteractive()) return true;
|
||||||
|
|
||||||
|
const currentVersion = readCurrentVersion();
|
||||||
|
if (!currentVersion) return true;
|
||||||
|
|
||||||
|
const declinedAt = readDeclinedAt();
|
||||||
|
if (declinedAt && Date.now() - declinedAt < UPDATE_PROMPT_COOLDOWN_MS) return true;
|
||||||
|
|
||||||
|
const spinner = ora('Проверка обновлений...').start();
|
||||||
|
|
||||||
|
const latestVersion = await fetchLatestVersion(PACKAGE_NAME, UPDATE_TIMEOUT_MS);
|
||||||
|
if (!latestVersion) {
|
||||||
|
spinner.stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compareSemver(latestVersion, currentVersion) <= 0) {
|
||||||
|
spinner.stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.succeed(`Доступна новая версия: ${currentVersion} → ${latestVersion}`);
|
||||||
|
|
||||||
|
const shouldUpdate = await askYesNo('Обновить сейчас? [y/N] ');
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
writeDeclinedAt(Date.now());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSpinner = ora('Обновление...').start();
|
||||||
|
const updated = runUpdate(PACKAGE_NAME);
|
||||||
|
if (!updated) {
|
||||||
|
updateSpinner.fail('Не удалось обновить');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSpinner.succeed('Обновлено');
|
||||||
|
clearDeclinedAt();
|
||||||
|
rerunCommand(args);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -20,19 +20,28 @@ export function normalizeArgs(parsed: ParsedArgs): { normalized: ParsedArgs; err
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.positionalName && normalized.vars.name) {
|
if (normalized.vars.name) {
|
||||||
return {
|
return {
|
||||||
normalized,
|
normalized,
|
||||||
error: {
|
error: {
|
||||||
title: 'name задан дважды',
|
title: 'Переменная name задается только позиционно',
|
||||||
details: ['позиционно и через --name']
|
details: ['уберите --name и укажите <имя> после шаблона']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.positionalName && !normalized.vars.name) {
|
if (!normalized.positionalName) {
|
||||||
normalized.vars.name = normalized.positionalName;
|
return {
|
||||||
|
normalized,
|
||||||
|
error: {
|
||||||
|
title: 'Требуется имя',
|
||||||
|
details: ['укажите <имя> после шаблона'],
|
||||||
|
showHelp: true
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.vars.name = normalized.positionalName;
|
||||||
|
|
||||||
if (normalized.extra.length > 0) {
|
if (normalized.extra.length > 0) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user