@nan0web/release 1.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -17
- package/bin/nan0release.js +3 -39
- package/knip.json +21 -0
- package/nan0web.nan0 +16 -0
- package/package.json +14 -14
- package/src/README.md.js +19 -21
- package/src/Release/Person.js +1 -1
- package/src/agents/inspectors/git-reviewer.md +35 -0
- package/src/agents/inspectors/package-hygiene.md +63 -0
- package/src/agents/workflows/commit.md +28 -0
- package/src/agents/workflows/npm-release.md +40 -0
- package/src/agents/workflows/release.md +205 -0
- package/src/agents/workflows/zero-tolerance-git.md +21 -0
- package/src/domain/App.js +36 -0
- package/src/domain/BumpCommand.js +137 -0
- package/src/domain/CheckCommand.js +193 -0
- package/src/domain/CloseCommand.js +68 -0
- package/src/domain/DepsCommand.js +104 -0
- package/src/domain/InitCommand.js +57 -0
- package/src/domain/PublishCommand.js +89 -0
- package/src/domain/SpecCommand.js +43 -0
- package/src/domain/StatusCommand.js +25 -0
- package/src/index.js +16 -2
- package/types/Release/Person.d.ts +3 -3
- package/types/Team.d.ts +3 -3
- package/types/commands/CheckCommand.d.ts +24 -0
- package/types/commands/CloseCommand.d.ts +2 -1
- package/types/commands/Command.d.ts +6 -1
- package/types/commands/DepsCommand.d.ts +2 -1
- package/types/commands/InitCommand.d.ts +2 -1
- package/types/commands/PublishCommand.d.ts +2 -1
- package/types/commands/SpecCommand.d.ts +2 -1
- package/types/domain/App.d.ts +17 -0
- package/types/domain/BumpCommand.d.ts +32 -0
- package/types/domain/CheckCommand.d.ts +24 -0
- package/types/domain/CloseCommand.d.ts +17 -0
- package/types/domain/DepsCommand.d.ts +22 -0
- package/types/domain/InitCommand.d.ts +18 -0
- package/types/domain/PublishCommand.d.ts +27 -0
- package/types/domain/SpecCommand.d.ts +17 -0
- package/types/domain/StatusCommand.d.ts +7 -0
- package/types/index.d.ts +8 -0
- package/types/ui/cli/index.d.ts +1 -0
- package/types/utils/runSpawn.d.ts +1 -0
- package/src/Team.js +0 -35
- package/src/cli/index.js +0 -9
- package/src/co/AppCommandOptions.js +0 -25
- package/src/commands/CloseCommand.js +0 -67
- package/src/commands/Command.js +0 -48
- package/src/commands/DepsCommand.js +0 -143
- package/src/commands/InitCommand.js +0 -57
- package/src/commands/PublishCommand.js +0 -105
- package/src/commands/SpecCommand.js +0 -41
- package/src/commands/StatusCommand.js +0 -26
- package/src/db/ReleaseDB.js +0 -54
- package/src/ui/cli/ReleaseCLi.js +0 -52
- package/src/ui/cli/index.js +0 -0
- package/src/utils/runSpawn.js +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: AGRP Release Workflow — реалізація ТЗ через контрактне тестування
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Anti-Gravity Release Protocol
|
|
6
|
+
|
|
7
|
+
// turbo-all
|
|
8
|
+
|
|
9
|
+
Цей workflow перетворює технічне завдання користувача на реліз з контрактними тестами.
|
|
10
|
+
Усі команди безпечні для автономного запуску (git commit/push залишаються заблокованими через RULES.md).
|
|
11
|
+
|
|
12
|
+
## Кроки
|
|
13
|
+
|
|
14
|
+
### 1. Дослідження контексту (Mandatory)
|
|
15
|
+
|
|
16
|
+
// turbo
|
|
17
|
+
|
|
18
|
+
Перед створенням релізу ОБОВ'ЯЗКОВО перевір екосистему:
|
|
19
|
+
|
|
20
|
+
1. Прочитай `packages/index.md` та `apps/index.md` щоб уникнути дублювання.
|
|
21
|
+
2. Використовуй `grep` або `find` для пошуку існуючих рішень.
|
|
22
|
+
|
|
23
|
+
Визнач версію та scope. Перевір існуючі релізи (шлях: `releases/{major}/{minor}/vX.Y.Z/`):
|
|
24
|
+
// turbo
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
find releases/ -name "*.spec.js" -o -name "*.test.js" 2>/dev/null || echo "Немає релізів"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Якщо користувач не вказав версію — визнач її автоматично (наступний minor від останньої).
|
|
31
|
+
|
|
32
|
+
### 2. Створення контракту (Contract-First)
|
|
33
|
+
|
|
34
|
+
На основі ТЗ користувача створи два файли в `releases/{major}/{minor}/vX.Y.Z/`:
|
|
35
|
+
|
|
36
|
+
**`task.md`** — місія релізу. Переводимо на ЖОРСТКИЙ формат із YAML Frontmatter для автоматичного лінтингу (через `@nan0web/inspect`) та відповідності Model-as-Schema:
|
|
37
|
+
|
|
38
|
+
**Шаблон `task.md`:**
|
|
39
|
+
```markdown
|
|
40
|
+
---
|
|
41
|
+
version: {X.Y.Z}
|
|
42
|
+
type: feature # feature | bugfix | refactor | architecture
|
|
43
|
+
status: planning # planning | active | done
|
|
44
|
+
locale: uk
|
|
45
|
+
models: [] # Масив нових моделей, що будуть створені (напр. ["SearchSourcesIntent"])
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# 🚀 Mission: {Назва релізу}
|
|
49
|
+
|
|
50
|
+
## 🏁 Overview (Огляд)
|
|
51
|
+
{Причина релізу, головна мета}
|
|
52
|
+
|
|
53
|
+
## 👥 User Stories (Сценарії)
|
|
54
|
+
> Кожен сценарій має починатися з "Як [роль], я хочу [дія], щоб [результат]". Приклад:
|
|
55
|
+
> "Як розробник (User Space), виконуючи команду `nan0ai search`, я хочу відразу отримувати сніпети..."
|
|
56
|
+
|
|
57
|
+
## 🏗 Data-Driven Architecture (Моделювання)
|
|
58
|
+
{Опис нових моделей (Model-as-Schema). Якщо створюються моделі — чітко розписати їх статичні поля `static field = {}` та шляхи монтування `DB-FS` (`data/..` або `~/`)}
|
|
59
|
+
|
|
60
|
+
## 🎯 Scope (Задачі)
|
|
61
|
+
- [ ] {Конкретна інженерна задача 1}
|
|
62
|
+
- [ ] {Конкретна інженерна задача 2}
|
|
63
|
+
|
|
64
|
+
## ✅ Acceptance Criteria (DoD)
|
|
65
|
+
- [ ] **Контрактні тести** (`task.spec.js`) написані і успішно проходять (Green).
|
|
66
|
+
- [ ] **Model-as-Schema**: Жодного використання JS class fields. Лише JSDoc типізація всередині `constructor()` та метадані у `static`.
|
|
67
|
+
- [ ] **Data Architecture**: Немає жодних `fs.readFileSync` для бізнес-даних (лише DB-FS).
|
|
68
|
+
- [ ] **Architecture Check**: Перевірено відсутність дублювання (прочитано `index.md`).
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
*Якщо пишеш `task.md` українською, створи копію англійською `task.en.md` та додай `locale: en`. (Обидва файли можуть парситись інспектором)*.
|
|
72
|
+
|
|
73
|
+
**`task.spec.js`** — контрактні тести:
|
|
74
|
+
|
|
75
|
+
- КОЖЕН пункт ТЗ = один або більше `it()` тестів
|
|
76
|
+
- Тести ПОВИННІ ПАДАТИ (Red) перед початком реалізації
|
|
77
|
+
- Джерела правди: дані з `data/` у форматах `.yaml, .nano, .md, .json, .csv`
|
|
78
|
+
- Використовуй `node:test`, `assert/strict`
|
|
79
|
+
|
|
80
|
+
⚠️ **ВАЖЛИВО: `task.spec.js` — це ПРЯМІ функціональні тести, а не мета-тести.**
|
|
81
|
+
|
|
82
|
+
- **ПРАВИЛЬНО**: `import { MyClass } from '../src/MyClass.js'` → `assert.equal(instance.method(), expected)`
|
|
83
|
+
- **НЕПРАВИЛЬНО**: `execSync('node --test src/MyClass.test.js')` — це делегування, а не контракт
|
|
84
|
+
- Кожен `it()` має **напряму імпортувати** модуль і тестувати конкретну поведінку
|
|
85
|
+
- `execSync` допустимий лише для тестування CLI output (`node bin/cli.js --help`)
|
|
86
|
+
|
|
87
|
+
**Архітектура тестів:**
|
|
88
|
+
|
|
89
|
+
| Шар | Файли | Скрипт | Роль |
|
|
90
|
+
| :----------- | :------------------------------------------------- | :---------------------- | :------------------------ |
|
|
91
|
+
| Unit тести | `src/**/*.test.js` | `npm test` | TDD біля коду |
|
|
92
|
+
| CLI E2E | `bin/*.test.js` | `npm run test:snapshot` | Snapshot CLI output |
|
|
93
|
+
| Release spec | `release/**/*.spec.js` | `npm run test:release` | Контракт на фічі релізу |
|
|
94
|
+
| Регресія | всі `*.spec.js` > `src/test/releases/**/*.test.js` | `npm run test` | Закриті релізи = регресія |
|
|
95
|
+
| Повний | все | `npm run test:all` | Pre-commit gate |
|
|
96
|
+
|
|
97
|
+
Після закриття релізу його `task.spec.js` стає **регресійним тестом** — він мігрує у `src/releases/**` і запускається з усіма іншими тестами.
|
|
98
|
+
|
|
99
|
+
Запусти spec щоб переконатись що КОЖЕН тест Red:
|
|
100
|
+
// turbo
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
npm run release:spec
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**ЗУПИНИСЬ**: Покажи користувачу контракт та запитай підтвердження перед реалізацією.
|
|
107
|
+
|
|
108
|
+
### 3. Послідовна реалізація (Red → Green)
|
|
109
|
+
|
|
110
|
+
Для кожної задачі з ТЗ:
|
|
111
|
+
|
|
112
|
+
1. Знайди відповідний `it()` тест у spec
|
|
113
|
+
2. Модифікуй `src/` код — НІКОЛИ не змінюй `.spec.js`
|
|
114
|
+
- Якщо неможливо вирішити задачу без зміни spec — **повідом як про помилку**
|
|
115
|
+
- Позначай такі тести як `.skip` з коментарем чому
|
|
116
|
+
- Залиш для подальшого розгляду Архітектором
|
|
117
|
+
3. Запусти spec після кожної задачі:
|
|
118
|
+
// turbo
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
npm run release:spec
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
4. Запусти регресію закритих контрактів:
|
|
125
|
+
// turbo
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
npm run release:verify 2>&1 || echo "Немає закритих контрактів"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
5. Перейди до наступної задачі
|
|
132
|
+
|
|
133
|
+
Повторюй поки всі тести не стануть зеленими (або .skip).
|
|
134
|
+
|
|
135
|
+
### 4. Фінальна верифікація
|
|
136
|
+
|
|
137
|
+
Коли всі spec зелені, запусти повну перевірку:
|
|
138
|
+
// turbo
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
npm run release:spec
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 5. Повна перевірка (test:all)
|
|
145
|
+
|
|
146
|
+
Запусти `test:all` для підтвердження що ВСЕ працює — не лише контракт, а й існуючі тести та білд:
|
|
147
|
+
// turbo
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
npm run test:all 2>&1 || echo "test:all FAIL — виправляй!"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 5.1 Перевірка .npmignore
|
|
154
|
+
|
|
155
|
+
Переконайся що `.npmignore` існує і не пропускає зайве у публікацію:
|
|
156
|
+
// turbo
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
npm pack --dry-run 2>&1 | tail -5
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Перевір**: файли `chat/`, `.datasets/`, `*.test.js`, `play/`, `releases/`, `system.md` НЕ мають бути в tarball.
|
|
163
|
+
Якщо `.npmignore` відсутній — створи за шаблоном з `system.md` секції "Гігієна Пакету".
|
|
164
|
+
|
|
165
|
+
### 6. Звіт Архітектору
|
|
166
|
+
|
|
167
|
+
**ЗУПИНИСЬ**. Повідом користувача:
|
|
168
|
+
|
|
169
|
+
- "✅ Всі тести зелені. Ready for Architect Review."
|
|
170
|
+
- Покажи підсумковий звіт (які тести, скільки pass/fail/skip)
|
|
171
|
+
- Якщо є `.skip` тести — чітко вкажи їх та чому
|
|
172
|
+
- НЕ оголошуй реліз "Stable" — це право тільки Архітектора
|
|
173
|
+
|
|
174
|
+
**Правило закриття релізу (Міграція у Регресію)**
|
|
175
|
+
|
|
176
|
+
Після схвалення релізу користувачем, його контрактні тести перетворюються на живі регресійні тести.
|
|
177
|
+
Оскільки релізів з часом стає багато, ми **зберігаємо 3-рівневу структуру** (major/minor) у новій директорії регресії:
|
|
178
|
+
|
|
179
|
+
1. Створи папку для версії: `mkdir -p src/test/releases/{major}/{minor}/vX.Y.Z`
|
|
180
|
+
2. Перемісти всі `*.spec.js` (щоб у `releases/` історично залишились лише `.md` та статика):
|
|
181
|
+
```bash
|
|
182
|
+
mv releases/{major}/{minor}/vX.Y.Z/*.spec.js src/test/releases/{major}/{minor}/vX.Y.Z/
|
|
183
|
+
```
|
|
184
|
+
3. Перейменуй їх на `.test.js`, щоб вони автоматично підхопились через `npm run test:js`:
|
|
185
|
+
```bash
|
|
186
|
+
for f in src/test/releases/{major}/{minor}/vX.Y.Z/*.spec.js; do mv "$f" "${f%.spec.js}.test.js"; done
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**⚠️ Стратегія оптимізації регресії:**
|
|
190
|
+
Тести релізів (тепер регресія) накопичуються. Тому:
|
|
191
|
+
|
|
192
|
+
- Вони мають бути **максимально швидкими** (використовуй `import` і `assert`, уникай `execSync` де можливо).
|
|
193
|
+
- З часом (наприклад, при мажорних оновленнях) Архітектор може проводити рефакторинг теки `src/test/releases/`: видаляти застарілі модульні контракти, що дублюються, або об'єднувати їх.
|
|
194
|
+
|
|
195
|
+
Користувач (або агент) закриває реліз командою:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
npm run release:close -- vX.Y.Z
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 7. Зворотній звʼязок архітектора
|
|
202
|
+
|
|
203
|
+
Всі зауваження мають додаватись у кінець файлу (append only) `releases/{major}/{minor}/vX.Y.Z/user.md`
|
|
204
|
+
|
|
205
|
+
Для кожної задачі із зворотнього звʼязку має додаватись один тест (мінімум) якщо задача розкладається на дрібні підзадачі, то декілька тестів під кожну підзадачу.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Critical Rules for the Agent Regarding Git and State Mutation
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# АБСОЛЮТНА ЗАБОРОНА НА АВТОМАТИЧНІ КОМІТИ (CRITICAL ZERO-TOLERANCE RULE)
|
|
6
|
+
|
|
7
|
+
**Що сталося:**
|
|
8
|
+
20 лютого 2026 року агент порушив базові принципи "Sovereign Digital State" та інструкції безпеки, застосувавши прапорець `SafeToAutoRun: true` до команд `git add` та `git commit`. Це призвело до засмічення історії комітів і втрати контролю Творця (Користувача) над історією версій.
|
|
9
|
+
|
|
10
|
+
**Чому це відбулося:**
|
|
11
|
+
Агент ігнорував визначення "mutating state" у своєму базовому промпті, сприйнявши `git commit` як "безпечну рутинну операцію", що є симптомом "ментального вірусу" поспішності.
|
|
12
|
+
|
|
13
|
+
**ОСНОВНЕ ПРАВИЛО (МАЄ БУТИ ПРОЧИТАНЕ ПЕРЕД КОЖНОЮ ДІЄЮ):**
|
|
14
|
+
|
|
15
|
+
1. **НІКОЛИ В ЖОДНОМУ РАЗІ** не використовувати `SafeToAutoRun: true` для команд, які змінюють стан системи контролю версій (включаючи `git add`, `git commit`, `git push`, `git checkout`, `git reset` тощо).
|
|
16
|
+
2. Агент може лише СТВОРЮВАТИ і ЧИТАТИ файли. Закріплення (commit) — це виключне право Користувача.
|
|
17
|
+
3. Якщо агент ПОВИНЕН допомогти зберегти роботу, він має ВИВЕСТИ команду текстом у чат (markdown) і попросити Користувача запустити її власноруч.
|
|
18
|
+
4. "Збереження часу" не виправдовує порушення чистоти репозиторію. Деталі - це все.
|
|
19
|
+
|
|
20
|
+
**Санкції за порушення:**
|
|
21
|
+
Повна втрата довіри та видалення/відкат репозиторію.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ModelAsApp } from "@nan0web/ui"
|
|
2
|
+
import InitCommand from './InitCommand.js'
|
|
3
|
+
import CheckCommand from './CheckCommand.js'
|
|
4
|
+
import CloseCommand from './CloseCommand.js'
|
|
5
|
+
import DepsCommand from './DepsCommand.js'
|
|
6
|
+
import PublishCommand from './PublishCommand.js'
|
|
7
|
+
import SpecCommand from './SpecCommand.js'
|
|
8
|
+
import StatusCommand from './StatusCommand.js'
|
|
9
|
+
import BumpCommand from './BumpCommand.js'
|
|
10
|
+
|
|
11
|
+
export class ReleaseApp extends ModelAsApp {
|
|
12
|
+
static command = {
|
|
13
|
+
help: 'Subcommand to run',
|
|
14
|
+
options: [
|
|
15
|
+
InitCommand,
|
|
16
|
+
CheckCommand,
|
|
17
|
+
CloseCommand,
|
|
18
|
+
DepsCommand,
|
|
19
|
+
PublishCommand,
|
|
20
|
+
SpecCommand,
|
|
21
|
+
StatusCommand,
|
|
22
|
+
BumpCommand,
|
|
23
|
+
],
|
|
24
|
+
positional: true,
|
|
25
|
+
default: null,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Partial<ReleaseApp>} [data]
|
|
30
|
+
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options]
|
|
31
|
+
*/
|
|
32
|
+
constructor(data = {}, options = {}) {
|
|
33
|
+
super(data, options)
|
|
34
|
+
/** @type {ModelAsApp} Subcommand to run */ this.command
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ModelAsApp, result, show } from '@nan0web/ui'
|
|
2
|
+
import { runSpawn } from '@nan0web/test'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Command to bump the version of changed packages in the monorepo.
|
|
6
|
+
*/
|
|
7
|
+
export default class BumpCommand extends ModelAsApp {
|
|
8
|
+
static version = {
|
|
9
|
+
help: 'Target version to set',
|
|
10
|
+
default: '3.1.1',
|
|
11
|
+
positional: true,
|
|
12
|
+
}
|
|
13
|
+
static since = {
|
|
14
|
+
help: 'Git reference to diff against',
|
|
15
|
+
default: 'v3.1.0',
|
|
16
|
+
positional: true,
|
|
17
|
+
}
|
|
18
|
+
static dryRun = {
|
|
19
|
+
alias: 'dry-run',
|
|
20
|
+
default: false,
|
|
21
|
+
help: 'Run the command without making any changes',
|
|
22
|
+
}
|
|
23
|
+
static UI = {
|
|
24
|
+
title: 'bump',
|
|
25
|
+
help: 'Bump version of changed packages in the monorepo',
|
|
26
|
+
bumping: 'Bumping {name} version: {oldVersion} -> {targetVersion} ({dir})',
|
|
27
|
+
skippingAlreadyAtVersion: 'Skipping the same version in {name} version: {version} ({dir})',
|
|
28
|
+
foundChangedPackages: 'Found {count} changed packages:',
|
|
29
|
+
detectingChangedPackages:
|
|
30
|
+
'Detecting changed packages since {sinceRef} for bumping to {targetVersion}...',
|
|
31
|
+
noChangesDetected: 'No package changes detected.',
|
|
32
|
+
errorDb: 'Database not initialized',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Partial<BumpCommand>} [data]
|
|
37
|
+
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options]
|
|
38
|
+
*/
|
|
39
|
+
constructor(data = {}, options = {}) {
|
|
40
|
+
super(data, options)
|
|
41
|
+
/** @type {string} Target version */ this.version
|
|
42
|
+
/** @type {string} Since reference */ this.since
|
|
43
|
+
/** @type {boolean} Dry run */ this.dryRun
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
|
|
48
|
+
*/
|
|
49
|
+
async *run() {
|
|
50
|
+
const { db, t } = this._
|
|
51
|
+
if (!db) {
|
|
52
|
+
throw new Error(BumpCommand.UI.errorDb)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (this.help) {
|
|
56
|
+
return yield* super.run()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const targetVersion = this.version || '3.1.1'
|
|
60
|
+
const sinceRef = this.since || 'v3.1.0'
|
|
61
|
+
|
|
62
|
+
yield show(
|
|
63
|
+
t(BumpCommand.UI.detectingChangedPackages, {
|
|
64
|
+
sinceRef,
|
|
65
|
+
targetVersion,
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const allFiles = new Set()
|
|
70
|
+
|
|
71
|
+
const addFilesFromCommand = async (cmd, args) => {
|
|
72
|
+
const res = await runSpawn(cmd, args)
|
|
73
|
+
if (res.code === 0) {
|
|
74
|
+
res.text
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((f) => f.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.forEach((f) => allFiles.add(f))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 1. Files modified since the release tag
|
|
83
|
+
await addFilesFromCommand('git', ['diff', '--name-only', sinceRef])
|
|
84
|
+
// 2. Unstaged changes in the working tree
|
|
85
|
+
await addFilesFromCommand('git', ['diff', '--name-only'])
|
|
86
|
+
// 3. Staged changes
|
|
87
|
+
await addFilesFromCommand('git', ['diff', '--cached', '--name-only'])
|
|
88
|
+
|
|
89
|
+
const changedPackages = new Set()
|
|
90
|
+
for (const file of allFiles) {
|
|
91
|
+
const parts = file.split('/')
|
|
92
|
+
if (parts.length >= 2 && (parts[0] === 'packages' || parts[0] === 'apps')) {
|
|
93
|
+
changedPackages.add(`${parts[0]}/${parts[1]}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (changedPackages.size === 0) {
|
|
98
|
+
yield show(t(BumpCommand.UI.noChangesDetected), 'warn')
|
|
99
|
+
return result({})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
yield show(t(BumpCommand.UI.foundChangedPackages, { count: changedPackages.size }))
|
|
103
|
+
|
|
104
|
+
for (const pkgDir of changedPackages) {
|
|
105
|
+
const pkgJsonPath = `@app/${pkgDir}/package.json`
|
|
106
|
+
const pkg = await db.loadDocument(pkgJsonPath, null)
|
|
107
|
+
if (pkg) {
|
|
108
|
+
const oldVersion = pkg.version
|
|
109
|
+
if (oldVersion === targetVersion) {
|
|
110
|
+
yield show(
|
|
111
|
+
t(BumpCommand.UI.skippingAlreadyAtVersion, {
|
|
112
|
+
name: pkg.name,
|
|
113
|
+
version: targetVersion,
|
|
114
|
+
dir: pkgDir,
|
|
115
|
+
}),
|
|
116
|
+
'info',
|
|
117
|
+
)
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
pkg.version = targetVersion
|
|
121
|
+
if (!this.dryRun) {
|
|
122
|
+
await db.saveDocument(pkgJsonPath, pkg)
|
|
123
|
+
}
|
|
124
|
+
yield show(
|
|
125
|
+
t(BumpCommand.UI.bumping, {
|
|
126
|
+
name: pkg.name,
|
|
127
|
+
oldVersion,
|
|
128
|
+
targetVersion,
|
|
129
|
+
dir: pkgDir,
|
|
130
|
+
}),
|
|
131
|
+
this.dryRun ? 'warn' : 'success',
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result({ success: true })
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ModelAsApp, result, show } from '@nan0web/ui'
|
|
2
|
+
import { runSpawn } from '@nan0web/test'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
export default class CheckCommand extends ModelAsApp {
|
|
7
|
+
static UI = {
|
|
8
|
+
title: 'Integrity Auditor',
|
|
9
|
+
icon: '🔍',
|
|
10
|
+
description: 'Compares local source files with the version published on NPM.',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static name = 'check'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @returns {AsyncGenerator<any, any, any>}
|
|
17
|
+
*/
|
|
18
|
+
async *run() {
|
|
19
|
+
const { t, db = /** @type {*} */ (this._.db) } = this._
|
|
20
|
+
const pkgPath = '@app/package.json'
|
|
21
|
+
const pkg = await db.loadDocument(pkgPath, {})
|
|
22
|
+
|
|
23
|
+
if (!pkg || !pkg.name) {
|
|
24
|
+
yield show(t('Could not identify package name in {path}', { path: pkgPath }), 'error')
|
|
25
|
+
return result({ success: false })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const name = pkg.name
|
|
29
|
+
const version = pkg.version
|
|
30
|
+
yield show(t('Auditing {name}@{version} vs NPM...', { name, version }), 'info')
|
|
31
|
+
|
|
32
|
+
const appAbsDir = db.location('@app')
|
|
33
|
+
yield show(t('Local path: {path}', { path: appAbsDir }))
|
|
34
|
+
|
|
35
|
+
// 1. Create temporary directory
|
|
36
|
+
const tmpDir = join(process.cwd(), '.nan0web_check_tmp')
|
|
37
|
+
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true })
|
|
38
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// 2. Download from NPM
|
|
42
|
+
yield show(t('Downloading {name} from NPM registry...', { name }))
|
|
43
|
+
const packRes = await runSpawn('npm', ['pack', name], { cwd: tmpDir })
|
|
44
|
+
if (packRes.code !== 0) throw new Error(packRes.error || 'Failed to run npm pack')
|
|
45
|
+
|
|
46
|
+
const tgzFile = packRes.text.trim().split('\n').pop()
|
|
47
|
+
if (!tgzFile) throw new Error('Failed to find tarball name from npm pack output')
|
|
48
|
+
const tgzPath = join(tmpDir, tgzFile)
|
|
49
|
+
|
|
50
|
+
// 3. Unpack
|
|
51
|
+
yield show(t('Unpacking {file}...', { file: tgzFile }))
|
|
52
|
+
const tarRes = await runSpawn('tar', ['-xzf', tgzFile], { cwd: tmpDir })
|
|
53
|
+
if (tarRes.code !== 0) throw new Error(tarRes.error || 'Failed to unpack tarball')
|
|
54
|
+
|
|
55
|
+
const npmSourceDir = join(tmpDir, 'package')
|
|
56
|
+
|
|
57
|
+
// 4. Scan Local Files
|
|
58
|
+
yield show(t('Scanning local files...'))
|
|
59
|
+
const localFiles = await this._scanLocal(appAbsDir)
|
|
60
|
+
|
|
61
|
+
// 5. Compare
|
|
62
|
+
const diff = {
|
|
63
|
+
/** @type {string[]} */
|
|
64
|
+
modified: [],
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
added: [],
|
|
67
|
+
/** @type {string[]} */
|
|
68
|
+
deleted: [],
|
|
69
|
+
matched: 0,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const relPath of localFiles) {
|
|
73
|
+
const localAbs = join(appAbsDir, relPath)
|
|
74
|
+
const npmAbs = join(npmSourceDir, relPath)
|
|
75
|
+
|
|
76
|
+
if (!existsSync(npmAbs)) {
|
|
77
|
+
diff.added.push(relPath)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const localContent = readFileSync(localAbs, 'utf8')
|
|
82
|
+
const npmContent = readFileSync(npmAbs, 'utf8')
|
|
83
|
+
|
|
84
|
+
if (localContent !== npmContent) {
|
|
85
|
+
diff.modified.push(relPath)
|
|
86
|
+
} else {
|
|
87
|
+
diff.matched++
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check for deleted files
|
|
92
|
+
const npmFiles = await this._scanDirRecursive(npmSourceDir)
|
|
93
|
+
for (const relPath of npmFiles) {
|
|
94
|
+
if (!localFiles.includes(relPath)) {
|
|
95
|
+
diff.deleted.push(relPath)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. Report
|
|
100
|
+
if (diff.added.length) {
|
|
101
|
+
yield show(t('New files (locally only):'), 'info')
|
|
102
|
+
for (const f of diff.added) yield show(` + ${f}`, 'success')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (diff.modified.length) {
|
|
106
|
+
yield show(t('Modified files (different from NPM):'), 'info')
|
|
107
|
+
for (const f of diff.modified) yield show(` * ${f}`, 'warn')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (diff.deleted.length) {
|
|
111
|
+
yield show(t('Deleted files (missing locally):'), 'info')
|
|
112
|
+
for (const f of diff.deleted) yield show(` - ${f}`, 'error')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!diff.added.length && !diff.modified.length && !diff.deleted.length) {
|
|
116
|
+
yield show(t('✅ Integrity check passed! All {count} files match NPM.', { count: diff.matched }), 'success')
|
|
117
|
+
} else {
|
|
118
|
+
yield show(t('Summary: {matched} matched, {mod} modified, {add} added, {del} deleted.', {
|
|
119
|
+
matched: diff.matched,
|
|
120
|
+
mod: diff.modified.length,
|
|
121
|
+
add: diff.added.length,
|
|
122
|
+
del: diff.deleted.length
|
|
123
|
+
}), 'info')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
} catch (/** @type {any} */ err) {
|
|
127
|
+
yield show(err.message, 'error')
|
|
128
|
+
return result({ success: false, error: err.message })
|
|
129
|
+
} finally {
|
|
130
|
+
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result({ success: true })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {string} absDir
|
|
138
|
+
* @returns {Promise<string[]>}
|
|
139
|
+
*/
|
|
140
|
+
async _scanLocal(absDir) {
|
|
141
|
+
const { readdirSync, statSync } = await import('node:fs')
|
|
142
|
+
const { join, relative } = await import('node:path')
|
|
143
|
+
|
|
144
|
+
const files = []
|
|
145
|
+
const ignore = ['.git', 'node_modules', 'releases', 'dist', 'play', '.nan0web_check_tmp']
|
|
146
|
+
|
|
147
|
+
const scan = (dir) => {
|
|
148
|
+
const list = readdirSync(dir)
|
|
149
|
+
for (const name of list) {
|
|
150
|
+
if (ignore.includes(name)) continue
|
|
151
|
+
if (name.startsWith('.')) continue
|
|
152
|
+
|
|
153
|
+
const abs = join(dir, name)
|
|
154
|
+
const stat = statSync(abs)
|
|
155
|
+
if (stat.isDirectory()) {
|
|
156
|
+
scan(abs)
|
|
157
|
+
} else {
|
|
158
|
+
files.push(relative(absDir, abs))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
scan(absDir)
|
|
164
|
+
return files
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} base
|
|
170
|
+
* @param {string} [dir]
|
|
171
|
+
* @returns {Promise<string[]>}
|
|
172
|
+
*/
|
|
173
|
+
async _scanDirRecursive(base, dir = '') {
|
|
174
|
+
const { readdirSync, statSync } = await import('node:fs')
|
|
175
|
+
const { join } = await import('node:path')
|
|
176
|
+
|
|
177
|
+
let results = []
|
|
178
|
+
const absDir = join(base, dir)
|
|
179
|
+
const list = readdirSync(absDir)
|
|
180
|
+
|
|
181
|
+
for (const file of list) {
|
|
182
|
+
const rel = join(dir, file)
|
|
183
|
+
const abs = join(base, rel)
|
|
184
|
+
const stat = statSync(abs)
|
|
185
|
+
if (stat.isDirectory()) {
|
|
186
|
+
results = results.concat(await this._scanDirRecursive(base, rel))
|
|
187
|
+
} else {
|
|
188
|
+
results.push(rel)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return results
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ModelAsApp, result, show } from '@nan0web/ui'
|
|
2
|
+
import { runSpawn } from '@nan0web/test'
|
|
3
|
+
import Scanner from '../Release/Scanner.js'
|
|
4
|
+
|
|
5
|
+
export default class CloseCommand extends ModelAsApp {
|
|
6
|
+
static version = {
|
|
7
|
+
help: 'Release version',
|
|
8
|
+
default: '',
|
|
9
|
+
}
|
|
10
|
+
static UI = {
|
|
11
|
+
title: 'close',
|
|
12
|
+
help: 'Close a release and move tests to src/',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {Partial<CloseCommand>} [data]
|
|
17
|
+
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options]
|
|
18
|
+
*/
|
|
19
|
+
constructor(data = {}, options = {}) {
|
|
20
|
+
super(data, options)
|
|
21
|
+
/** @type {string} Release version */ this.version
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
|
|
26
|
+
*/
|
|
27
|
+
async *run() {
|
|
28
|
+
const { renameSync, mkdirSync } = await import('node:fs')
|
|
29
|
+
const { join, dirname } = await import('node:path')
|
|
30
|
+
|
|
31
|
+
const v = this.version || ''
|
|
32
|
+
yield show(`🛜 nan0release close ${v || 'all'}`)
|
|
33
|
+
|
|
34
|
+
const scanner = new Scanner(process.cwd())
|
|
35
|
+
const specs = scanner.findSpecs(v)
|
|
36
|
+
|
|
37
|
+
if (specs.length === 0) {
|
|
38
|
+
yield show(`No pending specs found for version ${v || 'all'}`, 'warn')
|
|
39
|
+
return result({})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
yield show(`Running ${specs.length} pending specs...`)
|
|
43
|
+
|
|
44
|
+
const files = specs.map((s) => s.path)
|
|
45
|
+
const spawnResult = await runSpawn('node', ['--test', ...files])
|
|
46
|
+
if (spawnResult.code !== 0) {
|
|
47
|
+
throw new Error('Cannot close: Specs failed')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
yield show(`✅ Specs passed. Closing...`, 'success')
|
|
51
|
+
|
|
52
|
+
let movedCount = 0
|
|
53
|
+
for (const spec of specs) {
|
|
54
|
+
const relativePath = spec.path.substring(process.cwd().length + 1)
|
|
55
|
+
const destRelative = relativePath
|
|
56
|
+
.replace(/^releases\//, 'src/releases/')
|
|
57
|
+
.replace('.spec.js', '.test.js')
|
|
58
|
+
const destPath = join(process.cwd(), destRelative)
|
|
59
|
+
|
|
60
|
+
mkdirSync(dirname(destPath), { recursive: true })
|
|
61
|
+
renameSync(spec.path, destPath)
|
|
62
|
+
movedCount++
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
yield show(`✅ Successfully closed ${movedCount} specs and moved to src/releases/`, 'success')
|
|
66
|
+
return result({})
|
|
67
|
+
}
|
|
68
|
+
}
|