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-утилита для генерации файлов из шаблонов.
|
||||
|
||||
## Установка
|
||||
|
||||
Глобально:
|
||||
|
||||
```bash
|
||||
npm i -g @gromlab/create
|
||||
```
|
||||
При запуске CLI проверяет доступность новой версии. Если вы выбираете «нет»,
|
||||
повторный запрос появится через 24 часа. Чтобы пропустить проверку, используйте
|
||||
флаг `--skip-update`.
|
||||
|
||||
|
||||
## Использование
|
||||
|
||||
```bash
|
||||
npx @gromlab/create <шаблон> <имя> [опции]
|
||||
npx @gromlab/create <шаблон> <имя> [путь] [опции]
|
||||
```
|
||||
|
||||
Если `[путь]` не указан, файлы создаются в директории, где запущен CLI.
|
||||
|
||||
## Пример
|
||||
|
||||
```bash
|
||||
# Создать компонент из шаблона
|
||||
npx @gromlab/create component Button
|
||||
|
||||
# Указать папку вывода
|
||||
npx @gromlab/create component Button --out src/components
|
||||
|
||||
# Превью без записи
|
||||
npx @gromlab/create component Button --dry-run
|
||||
# Указать папку вывода позиционно
|
||||
npx @gromlab/create component Button src/components
|
||||
```
|
||||
|
||||
## Шаблоны
|
||||
@@ -43,12 +54,12 @@ npx @gromlab/create component Button --dry-run
|
||||
- `{{name.kebab}}` — kebab-case
|
||||
- `{{name.snake}}` — snake_case
|
||||
|
||||
Переменная `name` задается только позиционным аргументом `<имя>`. Использование `--name` запрещено.
|
||||
|
||||
## Опции
|
||||
|
||||
| Опция | Описание |
|
||||
|-------|----------|
|
||||
| `--out <путь>` | Папка вывода (по умолчанию: `.`) |
|
||||
| `--templates <путь>` | Папка шаблонов (по умолчанию: `.templates`) |
|
||||
| `--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",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@gromlab/create",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"archy": "^1.0.0",
|
||||
"boxen": "^5.1.2",
|
||||
"chalk": "^4.1.2",
|
||||
"change-case-all": "^2.1.0",
|
||||
"directory-tree": "^3.5.2",
|
||||
"figures": "^3.2.0"
|
||||
"figures": "^3.2.0",
|
||||
"ora": "^5.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"create": "dist/cli.js",
|
||||
"gromlab-create": "dist/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -89,6 +92,37 @@
|
||||
"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": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
|
||||
@@ -111,6 +145,30 @@
|
||||
"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": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
@@ -169,6 +227,39 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -306,6 +397,18 @@
|
||||
"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": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.6.0.tgz",
|
||||
@@ -373,6 +476,32 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -382,12 +511,110 @@
|
||||
"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": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
|
||||
@@ -397,12 +624,60 @@
|
||||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz",
|
||||
"integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==",
|
||||
"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": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -537,6 +812,21 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"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",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Template-based file generator CLI",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bin": {
|
||||
"gromlab-create": "dist/cli.js"
|
||||
"gromlab-create": "dist/cli.js",
|
||||
"create": "dist/cli.js"
|
||||
},
|
||||
"main": "dist/cli.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"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",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
@@ -20,7 +26,8 @@
|
||||
"chalk": "^4.1.2",
|
||||
"change-case-all": "^2.1.0",
|
||||
"directory-tree": "^3.5.2",
|
||||
"figures": "^3.2.0"
|
||||
"figures": "^3.2.0",
|
||||
"ora": "^5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archy": "^0.0.31",
|
||||
|
||||
40
src/args.ts
40
src/args.ts
@@ -3,22 +3,22 @@ import { ParsedArgs } from './types';
|
||||
export function printHelp() {
|
||||
const lines = [
|
||||
'Использование:',
|
||||
' npx @gromlab/create <template> [name] [options]',
|
||||
' npx @gromlab/create <шаблон> <имя> [путь] [опции]',
|
||||
'',
|
||||
'Аргументы:',
|
||||
' <шаблон> Имя шаблона',
|
||||
' <имя> Значение переменной name (обязательный аргумент)',
|
||||
' [путь] Папка вывода (по умолчанию: текущая директория)',
|
||||
'',
|
||||
'Опции:',
|
||||
' --<var> <value> Переменная шаблона (поддерживается любой --key <value>)',
|
||||
' [name] Сокращение для --name',
|
||||
' --templates <path> Папка шаблонов (по умолчанию: .templates)',
|
||||
' --templates-path Алиас для --templates',
|
||||
' --out <path> Папка вывода (по умолчанию: текущая директория)',
|
||||
' --overwrite Перезаписывать существующие файлы',
|
||||
' --dry-run Показать результат без записи на диск',
|
||||
' --skip-update Не проверять обновления CLI',
|
||||
' -h, --help Показать эту справку',
|
||||
'',
|
||||
'Примеры:',
|
||||
' npx @gromlab/create component --name test',
|
||||
' npx @gromlab/create component --name test --out src/components',
|
||||
' npx @gromlab/create component --name test --templates /path/to/.templates'
|
||||
' npx @gromlab/create component Button',
|
||||
' npx @gromlab/create component Button src/components'
|
||||
];
|
||||
console.log(lines.join('\n'));
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
const parsed: ParsedArgs = {
|
||||
vars: {},
|
||||
overwrite: false,
|
||||
dryRun: false,
|
||||
skipUpdate: false,
|
||||
help: false,
|
||||
extra: []
|
||||
};
|
||||
@@ -52,11 +52,10 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
parsed.overwrite = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--dry-run') {
|
||||
parsed.dryRun = true;
|
||||
if (arg === '--skip-update') {
|
||||
parsed.skipUpdate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--')) {
|
||||
const eqIndex = arg.indexOf('=');
|
||||
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 === 'templates' || key === 'templatesPath' || key === 'templates-path') {
|
||||
const value = inlineValue ?? consumeValue(args, i, key);
|
||||
if (inlineValue === undefined) i++;
|
||||
parsed.templatesPath = value;
|
||||
continue;
|
||||
throw new Error('Опция --templates не поддерживается');
|
||||
}
|
||||
|
||||
if (key === 'out' || key === 'output') {
|
||||
const value = inlineValue ?? consumeValue(args, i, key);
|
||||
if (inlineValue === undefined) i++;
|
||||
parsed.outDir = value;
|
||||
continue;
|
||||
throw new Error('Опция --out не поддерживается');
|
||||
}
|
||||
|
||||
const value = inlineValue ?? consumeValue(args, i, key);
|
||||
@@ -94,6 +87,11 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parsed.positionalOutDir) {
|
||||
parsed.positionalOutDir = arg;
|
||||
continue;
|
||||
}
|
||||
|
||||
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 { normalizeArgs, resolveTemplateContext } from './validation';
|
||||
import { buildPlan, getCollisions, getExistingDirs, getRoots, getTopLevelDirs, writePlan } from './plan';
|
||||
import { maybeHandleUpdate } from './update';
|
||||
|
||||
function resolvePath(baseDir: string, inputPath: string): string {
|
||||
if (path.isAbsolute(inputPath)) return path.normalize(inputPath);
|
||||
return path.resolve(baseDir, inputPath);
|
||||
}
|
||||
|
||||
function run() {
|
||||
async function run() {
|
||||
const shouldContinue = await maybeHandleUpdate(process.argv.slice(2));
|
||||
if (!shouldContinue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseArgs(process.argv);
|
||||
@@ -38,8 +44,8 @@ function run() {
|
||||
const normalized = normalizedResult.normalized;
|
||||
|
||||
const cwd = process.cwd();
|
||||
const templatesDir = resolvePath(cwd, normalized.templatesPath ?? '.templates');
|
||||
const outDir = resolvePath(cwd, normalized.outDir ?? '.');
|
||||
const templatesDir = resolvePath(cwd, '.templates');
|
||||
const outDir = resolvePath(cwd, normalized.positionalOutDir ?? '.');
|
||||
|
||||
const templateResult = resolveTemplateContext(
|
||||
templatesDir,
|
||||
@@ -57,10 +63,9 @@ function run() {
|
||||
const existingDirs = getExistingDirs(outDir, topLevelDirs);
|
||||
|
||||
const redWarnings: string[][] = [];
|
||||
const warnings: string[][] = [];
|
||||
|
||||
if (existingDirs.length > 0) {
|
||||
if (!normalized.overwrite && !normalized.dryRun) {
|
||||
if (!normalized.overwrite) {
|
||||
printError(
|
||||
'Папка назначения уже существует',
|
||||
existingDirs.map((dir) => path.join(outDir, dir)),
|
||||
@@ -81,13 +86,6 @@ function run() {
|
||||
const collisions = getCollisions(plan);
|
||||
|
||||
if (collisions.length > 0 && !normalized.overwrite) {
|
||||
if (normalized.dryRun) {
|
||||
warnings.push([
|
||||
'Файлы уже существуют:',
|
||||
...collisions.map((target) => ` - ${path.relative(outDir, target)}`),
|
||||
'Используйте --overwrite для перезаписи.'
|
||||
]);
|
||||
} else {
|
||||
printError(
|
||||
'Файлы уже существуют',
|
||||
collisions.map((target) => path.relative(outDir, target)),
|
||||
@@ -97,25 +95,16 @@ function run() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
printSummary(plan, outDir, normalized.vars, false, normalized.templateName!, roots);
|
||||
printWarnings(redWarnings, warnings);
|
||||
printSummary(plan, outDir, normalized.vars, normalized.templateName!, roots);
|
||||
printWarnings(redWarnings, []);
|
||||
}
|
||||
|
||||
try {
|
||||
run();
|
||||
} catch (error) {
|
||||
run().catch((error) => {
|
||||
console.error(String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,44 +6,6 @@ import directoryTree = require('directory-tree');
|
||||
import figures = require('figures');
|
||||
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 {
|
||||
const tree = directoryTree(rootPath, { attributes: ['type'] }) as directoryTree.DirectoryTree | null;
|
||||
if (!tree) return '';
|
||||
@@ -109,7 +71,6 @@ export function printSummary(
|
||||
plan: PlanItem[],
|
||||
outDir: string,
|
||||
vars: Record<string, string>,
|
||||
isDryRun: boolean,
|
||||
templateName: string,
|
||||
roots: string[]
|
||||
) {
|
||||
@@ -119,9 +80,9 @@ export function printSummary(
|
||||
console.log('');
|
||||
|
||||
// Заголовок с иконкой
|
||||
const statusIcon = isDryRun ? figures.info : figures.tick;
|
||||
const statusColor = isDryRun ? chalk.blue : chalk.green;
|
||||
const statusText = isDryRun ? 'Планируется генерация' : 'Успешно создан';
|
||||
const statusIcon = figures.tick;
|
||||
const statusColor = chalk.green;
|
||||
const statusText = 'Успешно создан';
|
||||
|
||||
console.log(statusColor(`${statusIcon} ${statusText}: `) + chalk.bold.white(displayName));
|
||||
console.log('');
|
||||
@@ -145,11 +106,12 @@ export function printSummary(
|
||||
console.log(chalk.dim(' (пусто)'));
|
||||
} else {
|
||||
for (const rootPath of roots) {
|
||||
const treeOutput = (!isDryRun && fs.existsSync(rootPath))
|
||||
? buildTreeFromDirectory(rootPath)
|
||||
: buildTreeFromPaths(path.basename(rootPath) || rootPath, plan
|
||||
.map((item) => path.relative(rootPath, item.target))
|
||||
.filter((rel) => rel && !rel.startsWith('..')));
|
||||
const treeOutput = fs.existsSync(rootPath) ? buildTreeFromDirectory(rootPath) : '';
|
||||
|
||||
if (!treeOutput) {
|
||||
console.log(chalk.dim(' (пусто)'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Добавляем отступ к дереву
|
||||
const indentedTree = treeOutput
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export type ParsedArgs = {
|
||||
templateName?: string;
|
||||
positionalName?: string;
|
||||
templatesPath?: string;
|
||||
outDir?: string;
|
||||
positionalOutDir?: string;
|
||||
vars: Record<string, string>;
|
||||
overwrite: boolean;
|
||||
dryRun: boolean;
|
||||
skipUpdate: boolean;
|
||||
help: boolean;
|
||||
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 {
|
||||
normalized,
|
||||
error: {
|
||||
title: 'name задан дважды',
|
||||
details: ['позиционно и через --name']
|
||||
title: 'Переменная name задается только позиционно',
|
||||
details: ['уберите --name и укажите <имя> после шаблона']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.positionalName && !normalized.vars.name) {
|
||||
normalized.vars.name = normalized.positionalName;
|
||||
if (!normalized.positionalName) {
|
||||
return {
|
||||
normalized,
|
||||
error: {
|
||||
title: 'Требуется имя',
|
||||
details: ['укажите <имя> после шаблона'],
|
||||
showHelp: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
normalized.vars.name = normalized.positionalName;
|
||||
|
||||
if (normalized.extra.length > 0) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user