@nan0web/release 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.datasets/README.dataset.jsonl +19 -0
- package/.editorconfig +20 -0
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +15 -0
- package/README.md +284 -0
- package/bin/commands/InitCommand.js +10 -0
- package/bin/lib/functions.js +240 -0
- package/bin/lib/functions.test.js +138 -0
- package/bin/lib/temp/pnpm-lock.yaml +0 -0
- package/bin/nan0release.js +57 -0
- package/bin/release.js +12 -0
- package/bin/release.test.js +73 -0
- package/bin/temp/package.json +1 -0
- package/docs/system.uk.md +413 -0
- package/docs/uk/README.md +284 -0
- package/legacy/PrepareCommand.js +154 -0
- package/legacy/PrepareCommand.test.js +189 -0
- package/legacy/changelog.js +446 -0
- package/package.json +43 -0
- package/releases/1/0/v1.0.0/models/Person.js +26 -0
- package/releases/1/0/v1.0.0/models/Person.test.js +44 -0
- package/releases/1/0/v1.0.0/models/company.js +11 -0
- package/releases/1/0/v1.0.0/models/members.js +14 -0
- package/releases/1/0/v1.0.0/models/members.test.js +21 -0
- package/releases/1/0/v1.0.0/models/teams/DevTeam.js +6 -0
- package/releases/1/0/v1.0.0/models/teams/DevTeam.test.js +15 -0
- package/releases/1/0/v1.0.0/models/teams/index.js +5 -0
- package/releases/1/0/v1.0.0/release.js +12 -0
- package/releases/1/0/v1.0.0/release.md +121 -0
- package/releases/1/0/v1.0.0/release.test.js +36 -0
- package/releases/1/0/v1.0.0/test/YaRaSLove/git-native.test.js +14 -0
- package/releases/current.js +3 -0
- package/releases/index.js +11 -0
- package/src/README.md.js +468 -0
- package/src/Release/Document.js +110 -0
- package/src/Release/MarkdownToTest.js +137 -0
- package/src/Release/MarkdownToTest.test.js +60 -0
- package/src/Release/Person.js +29 -0
- package/src/Release.js +132 -0
- package/src/ReleaseCLI.js +122 -0
- package/src/Team.js +35 -0
- package/src/architecture/ProjectManagementAsCode.js +264 -0
- package/src/architecture/ProjectManagementAsCode.test.js +128 -0
- package/src/co/AppCommandMessage.js +22 -0
- package/src/co/AppCommandOptions.js +27 -0
- package/src/commands/ChatCommand.js +56 -0
- package/src/commands/HostCommand.js +63 -0
- package/src/commands/InitCommand.js +65 -0
- package/src/commands/ListCommand.js +56 -0
- package/src/commands/PublishCommand.js +144 -0
- package/src/commands/SealCommand.js +51 -0
- package/src/commands/ServeCommand.js +53 -0
- package/src/commands/ShowCommand.js +58 -0
- package/src/commands/SubCommand.js +21 -0
- package/src/commands/ValidateCommand.js +66 -0
- package/src/db/ReleaseDB.js +54 -0
- package/system.md +571 -0
- package/tsconfig.json +23 -0
- package/types/Release/Document.d.ts +31 -0
- package/types/Release/MarkdownToTest.d.ts +47 -0
- package/types/Release/Person.d.ts +17 -0
- package/types/Release.d.ts +89 -0
- package/types/ReleaseCLI.d.ts +33 -0
- package/types/Team.d.ts +27 -0
- package/types/architecture/ProjectManagementAsCode.d.ts +159 -0
- package/types/co/AppCommandMessage.d.ts +12 -0
- package/types/co/AppCommandOptions.d.ts +23 -0
- package/types/commands/ChatCommand.d.ts +20 -0
- package/types/commands/HostCommand.d.ts +16 -0
- package/types/commands/InitCommand.d.ts +20 -0
- package/types/commands/ListCommand.d.ts +21 -0
- package/types/commands/PublishCommand.d.ts +49 -0
- package/types/commands/SealCommand.d.ts +21 -0
- package/types/commands/ServeCommand.d.ts +17 -0
- package/types/commands/ShowCommand.d.ts +19 -0
- package/types/commands/SubCommand.d.ts +8 -0
- package/types/commands/ValidateCommand.d.ts +19 -0
- package/types/db/ReleaseDB.d.ts +30 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# @nan0web/release
|
|
2
|
+
|
|
3
|
+
| Назва пакету | [Статус](https://github.com/nan0web/monorepo/blob/main/system.md#написання-сценаріїв) | Документація | Покриття тестами | Особливості | Версія npm |
|
|
4
|
+
|--------------|----------------------------------------------------------------------------------------|---------------|------------------|-------------|------------|
|
|
5
|
+
| [@nan0web/release](https://github.com/nan0web/release/) |🟢 `97.1%` | 🧪 [Англійською 🏴](https://github.com/nan0web/release/blob/main/README.md)<br />[Українською 🇺🇦](https://github.com/nan0web/release/blob/main/docs/uk/README.md) |🟡 `84.2%` | ✅ d.ts 📜 system.md 🕹️ playground | — |
|
|
6
|
+
|
|
7
|
+
Керування проєктами як код, орієнтоване на Git, підписане GPG та кероване тестами.
|
|
8
|
+
|
|
9
|
+
На відміну від традиційних інструментів керування проєктами, які потребують постійної синхронізації чи ручних оновлень,
|
|
10
|
+
`@nan0web/release` забезпечує структуру проєкту, де:
|
|
11
|
+
- Завдання представлені як тести у файлах `.test.js`.
|
|
12
|
+
- Статус відстежується через результати виконання тестів.
|
|
13
|
+
- Ролі в команді та погодження зберігаються у структурованих файлах.
|
|
14
|
+
- Всі зміни підписані, версійовані та компонуємі.
|
|
15
|
+
|
|
16
|
+
Це гарантує:
|
|
17
|
+
- Чітке, автоматизоване відстеження проєкту.
|
|
18
|
+
- Незмінні нотатки релізів і рефлексії після них.
|
|
19
|
+
- Нульову неоднозначність – завдання не існують, якщо не протестовані.
|
|
20
|
+
|
|
21
|
+
## Встановлення
|
|
22
|
+
|
|
23
|
+
Як встановити за допомогою pnpm?
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add @nan0web/release
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Як встановити за допомогою npm?
|
|
29
|
+
```bash
|
|
30
|
+
npm install @nan0web/release
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Як встановити за допомогою yarn?
|
|
34
|
+
```bash
|
|
35
|
+
yarn add @nan0web/release
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Використання CLI
|
|
39
|
+
|
|
40
|
+
Почніть із ініціалізації нового релізу:
|
|
41
|
+
|
|
42
|
+
Як ініціалізувати нову версію релізу?
|
|
43
|
+
```bash
|
|
44
|
+
release init v1.0.0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
const cli = new ReleaseCLI()
|
|
48
|
+
Показати деталі релізу:
|
|
49
|
+
|
|
50
|
+
Як показати інформацію про реліз?
|
|
51
|
+
```bash
|
|
52
|
+
release show [--full]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
const release = new Release({
|
|
56
|
+
version: "v1.0.0",
|
|
57
|
+
createdAt: new Date("2025-08-20")
|
|
58
|
+
})
|
|
59
|
+
console.info(release.version) // v1.0.0
|
|
60
|
+
Список усіх релізів у проєкті:
|
|
61
|
+
|
|
62
|
+
Як отримати список усіх релізів?
|
|
63
|
+
```bash
|
|
64
|
+
release list [--json]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
const cli = new ReleaseCLI()
|
|
68
|
+
Додати повідомлення до поточного релізу:
|
|
69
|
+
|
|
70
|
+
Як написати повідомлення до релізу?
|
|
71
|
+
```bash
|
|
72
|
+
release chat write --user alice "Проблема з пайплайном збірки"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
const cli = new ReleaseCLI()
|
|
76
|
+
Запустити статичний сервер для перегляду релізів:
|
|
77
|
+
|
|
78
|
+
Як запустити інтерфейс релізів?
|
|
79
|
+
```bash
|
|
80
|
+
release host [--webui] [--port 3000]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
const cli = new ReleaseCLI()
|
|
84
|
+
Обслуговувати всі файли релізу для локального перегляду:
|
|
85
|
+
|
|
86
|
+
Як обслуговувати статичні ресурси релізу?
|
|
87
|
+
```bash
|
|
88
|
+
release serve [--port 8080]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
const cli = new ReleaseCLI()
|
|
92
|
+
Перевірити завдання та цілісність релізу:
|
|
93
|
+
|
|
94
|
+
Як перевірити завдання релізу?
|
|
95
|
+
```bash
|
|
96
|
+
release validate [--ignore-fail]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
const release = new Release({
|
|
100
|
+
version: "v1.0.0",
|
|
101
|
+
createdAt: new Date("2025-08-20")
|
|
102
|
+
})
|
|
103
|
+
Завершити реліз з рефлексією:
|
|
104
|
+
|
|
105
|
+
Як завершити (запечатати) реліз?
|
|
106
|
+
```bash
|
|
107
|
+
release seal [--message "Усі основні API стабільні і протестовані"]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
const cli = new ReleaseCLI()
|
|
111
|
+
## Основні концепції
|
|
112
|
+
|
|
113
|
+
### 1. Реліз як об’єкт
|
|
114
|
+
|
|
115
|
+
Об’єкт `Release` включає:
|
|
116
|
+
- `version`: ідентифікатор релізу (vX.Y.Z).
|
|
117
|
+
- `createdAt`, `startAt`, `planAt`, `completeAt`: часові мітки.
|
|
118
|
+
- `document`: розібраний markdown-документ із структурною орієнтацією.
|
|
119
|
+
|
|
120
|
+
Усі властивості типізовані та автоматично перевіряються через JSDoc та runtime-аналіз.
|
|
121
|
+
|
|
122
|
+
Як створити об’єкт Release?
|
|
123
|
+
```js
|
|
124
|
+
import { Release } from '@nan0web/release'
|
|
125
|
+
const release = new Release({
|
|
126
|
+
version: "v1.0.0",
|
|
127
|
+
createdAt: "2025-08-20T10:00:00Z"
|
|
128
|
+
})
|
|
129
|
+
console.info(release.version) // ← v1.0.0
|
|
130
|
+
console.info(release.createdAt instanceof Date) // ← true
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
### 2. Розпізнавання документа релізу
|
|
134
|
+
|
|
135
|
+
`ReleaseDocument` розширює `@nan0web/markdown`:
|
|
136
|
+
- Структурний аналіз markdown-файлу нотаток релізу
|
|
137
|
+
- Видобування розділів і завдань
|
|
138
|
+
- Аналіз команд і ролей із frontmatter чи коду
|
|
139
|
+
|
|
140
|
+
Як перетворити markdown-нотатки релізу у структуровані дані?
|
|
141
|
+
```js
|
|
142
|
+
import { ReleaseDocument } from '@nan0web/release'
|
|
143
|
+
const md = `# v1.0.0 - 2025-08-20
|
|
144
|
+
|
|
145
|
+
## Огляд
|
|
146
|
+
Цей реліз включає поліпшення інтерфейсу та стабілізацію API.
|
|
147
|
+
|
|
148
|
+
### Завдання
|
|
149
|
+
### Done **Реалізувати основний дизайн інтерфейсу** [ui.core-design]
|
|
150
|
+
### InProgress **Виправити проблеми адаптивності на мобільних пристроях** [ui.mobile-fixes]`
|
|
151
|
+
|
|
152
|
+
const doc = ReleaseDocument.from(md)
|
|
153
|
+
console.info(doc.version) // ← v1.0.0
|
|
154
|
+
console.info(doc.date instanceof Date) // ← true
|
|
155
|
+
console.info(doc.document.children instanceof Array) // ← true
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
### 3. Структури персони та команди
|
|
159
|
+
|
|
160
|
+
Об’єкт `Person` включає:
|
|
161
|
+
- `name`: HumanName
|
|
162
|
+
- `gender`: HumanGender
|
|
163
|
+
- `contacts`: масив HumanContact
|
|
164
|
+
|
|
165
|
+
Як створити об’єкт Person із типізованими властивостями?
|
|
166
|
+
```js
|
|
167
|
+
import { Person } from '@nan0web/release'
|
|
168
|
+
const person = new Person({
|
|
169
|
+
name: ["Alice", "Developer"],
|
|
170
|
+
gender: "female",
|
|
171
|
+
contacts: ["mailto:alice@example.com"]
|
|
172
|
+
})
|
|
173
|
+
console.info(person.name.firstName) // ← Alice
|
|
174
|
+
console.info(person.contacts.length >= 0) // ← true
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
## Архітектура: керування проєктами як код
|
|
178
|
+
|
|
179
|
+
Ця архітектура розглядає завдання як тести, ролі — як класи, а прогрес — як підтверджені результати.
|
|
180
|
+
|
|
181
|
+
### Управління завданнями
|
|
182
|
+
|
|
183
|
+
Завдання фіксуються як `taskId` → `testFilePath` в `ProjectManagement`.
|
|
184
|
+
Статус визначається через запуск наборів тестів `node:test`.
|
|
185
|
+
|
|
186
|
+
Як зареєструвати та перевірити завдання за допомогою тестів?
|
|
187
|
+
```js
|
|
188
|
+
import { ProjectManagement } from '@nan0web/release'
|
|
189
|
+
const pm = new ProjectManagement()
|
|
190
|
+
pm.registerTask("task-1", "./tests/task1.test.js")
|
|
191
|
+
pm.registerTask("task-2", "./tests/task2.test.js")
|
|
192
|
+
|
|
193
|
+
const mockResults = {
|
|
194
|
+
passed: ["task-1"],
|
|
195
|
+
failed: [],
|
|
196
|
+
pending: ["task-2"]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pm.validateProjectState = async () => mockResults
|
|
200
|
+
const results = await pm.validateProjectState()
|
|
201
|
+
|
|
202
|
+
console.info(results.passed.includes("task-1")) // ← true
|
|
203
|
+
console.info(results.pending.includes("task-2")) // ← true
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
### Обробка релізу
|
|
207
|
+
|
|
208
|
+
`ReleaseManager` координує релізи і перевіряє готовність.
|
|
209
|
+
Він гарантує:
|
|
210
|
+
- Всі зареєстровані тести проходять
|
|
211
|
+
- Версія правильно збільшується
|
|
212
|
+
- Git-тег встановлюється, якщо всі перевірки успішні
|
|
213
|
+
|
|
214
|
+
Як виконати реліз після перевірки?
|
|
215
|
+
```js
|
|
216
|
+
import { ReleaseManager, ProjectManagement } from '@nan0web/release'
|
|
217
|
+
const pm = new ProjectManagement()
|
|
218
|
+
const rm = new ReleaseManager(pm)
|
|
219
|
+
|
|
220
|
+
rm.calculateVersion = () => "v1.0.1"
|
|
221
|
+
rm.publish = async () => true
|
|
222
|
+
|
|
223
|
+
const result = await rm.executeRelease("patch")
|
|
224
|
+
console.info(result.version) // ← v1.0.1
|
|
225
|
+
console.info(result.published) // ← true
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
### Інтеграція з CHANGELOG
|
|
229
|
+
|
|
230
|
+
Через `ChangelogTaskManager` можна:
|
|
231
|
+
- Видобувати означення завдань з записів змін
|
|
232
|
+
- Генерувати відповідні тести
|
|
233
|
+
- Автоматизувати перевірку завдань у CI
|
|
234
|
+
|
|
235
|
+
Як розібрати changelog і видобути завдання?
|
|
236
|
+
```js
|
|
237
|
+
import { ChangelogTaskManager } from '@nan0web/release'
|
|
238
|
+
const ctm = new ChangelogTaskManager()
|
|
239
|
+
const changelog = `# Зміни
|
|
240
|
+
|
|
241
|
+
## [1.0.0] - 2025-08-20
|
|
242
|
+
### Added
|
|
243
|
+
- Створено основну структуру [core.init]
|
|
244
|
+
- Додано підтримку нотаток релізу [docs.release-notes]`
|
|
245
|
+
|
|
246
|
+
const tasks = ctm.parseChangelog(changelog)
|
|
247
|
+
console.info(Array.isArray(tasks)) // ← true
|
|
248
|
+
console.info(tasks.length >= 0) // ← true
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
## Особливості Java•Script
|
|
252
|
+
|
|
253
|
+
Використовує JSDoc і .d.ts для автозаповнення
|
|
254
|
+
|
|
255
|
+
## Демонстрація CLI
|
|
256
|
+
|
|
257
|
+
Спробуйте пакет у терміналі:
|
|
258
|
+
|
|
259
|
+
Як запустити демо playground?
|
|
260
|
+
```bash
|
|
261
|
+
git clone https://github.com/nan0web/release.git
|
|
262
|
+
cd release
|
|
263
|
+
pnpm install
|
|
264
|
+
pnpm run playground
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Статус проєкту
|
|
268
|
+
|
|
269
|
+
Перевірити готовність проєкту можна так:
|
|
270
|
+
|
|
271
|
+
Як перевірити статус перед публікацією?
|
|
272
|
+
```bash
|
|
273
|
+
npm test
|
|
274
|
+
npm run test:coverage
|
|
275
|
+
npm run test:status
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Внески
|
|
279
|
+
|
|
280
|
+
Як зробити внесок? — [перегляньте тут](./CONTRIBUTING.md)
|
|
281
|
+
|
|
282
|
+
## Ліцензія
|
|
283
|
+
|
|
284
|
+
Як застосувати ліцензію ISC? — [перегляньте тут](./LICENSE)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import DB from '@nan0web/db-fs'
|
|
3
|
+
import Logger from '@nan0web/log'
|
|
4
|
+
import { Changelog } from '@nan0web/changelog'
|
|
5
|
+
import { Command } from '@nan0web/co'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} PrepareCommandOpts
|
|
9
|
+
* @property {boolean} laconic
|
|
10
|
+
* @property {boolean} descriptive
|
|
11
|
+
* @property {string} template
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Prepare command class
|
|
15
|
+
*/
|
|
16
|
+
class PrepareCommand extends Command {
|
|
17
|
+
/** @type {Logger} */
|
|
18
|
+
logger
|
|
19
|
+
/** @type {PrepareCommandOpts} */
|
|
20
|
+
opts
|
|
21
|
+
/** @type {string} */
|
|
22
|
+
cwd
|
|
23
|
+
/** @type {DB} */
|
|
24
|
+
db
|
|
25
|
+
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
logger = new Logger(),
|
|
29
|
+
cwd = ".",
|
|
30
|
+
} = options
|
|
31
|
+
super(options)
|
|
32
|
+
this.logger = Logger.from(logger)
|
|
33
|
+
this.cwd = String(cwd)
|
|
34
|
+
this.options = new Map([
|
|
35
|
+
["laconic", [false, "Generate laconic changelog"]],
|
|
36
|
+
["descriptive", [false, "Generate descriptive changelog"]],
|
|
37
|
+
["template", [String, "Template file or message for a changelog"]],
|
|
38
|
+
])
|
|
39
|
+
this.db = new DB({ cwd: this.cwd })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run the prepare command
|
|
44
|
+
* @param {string[]} args - Command arguments
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
async run(args) {
|
|
48
|
+
let content = await this.db.loadDocument('CHANGELOG.md', '')
|
|
49
|
+
|
|
50
|
+
const changelog = new Changelog()
|
|
51
|
+
changelog.parse(content)
|
|
52
|
+
changelog.init()
|
|
53
|
+
const latest = changelog.getLatestVersion()
|
|
54
|
+
const versions = changelog.getVersions()
|
|
55
|
+
|
|
56
|
+
// Get git changes
|
|
57
|
+
const changes = this.getGitChanges()
|
|
58
|
+
|
|
59
|
+
if (changes.length > 0) {
|
|
60
|
+
const promptContent = await this.generateChangelogPrompt(changes)
|
|
61
|
+
await this.db.writeDocument("me.md", String(promptContent))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Save updated changelog
|
|
65
|
+
await this.db.saveDocument('CHANGELOG.md', String(changelog.document))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get list of new and modified files from git
|
|
70
|
+
* @returns {string[]} - Array of file paths
|
|
71
|
+
*/
|
|
72
|
+
getGitChanges() {
|
|
73
|
+
try {
|
|
74
|
+
// Get uncommitted changes
|
|
75
|
+
const statusOutput = execSync('git status --porcelain', {
|
|
76
|
+
cwd: this.cwd,
|
|
77
|
+
encoding: 'utf8'
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const changes = statusOutput
|
|
81
|
+
.split('\n')
|
|
82
|
+
.filter(line => line.trim() !== '')
|
|
83
|
+
.map(line => {
|
|
84
|
+
const parts = line.trim().split(/\s+/)
|
|
85
|
+
return parts[1]
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return changes
|
|
89
|
+
} catch (e) {
|
|
90
|
+
this.logger.warn('Git repository not found or git command failed')
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate changelog prompt from file changes
|
|
97
|
+
* @param {string[]} changes - Array of changed file paths
|
|
98
|
+
* @returns {Promise<string>} - Prompt content
|
|
99
|
+
*/
|
|
100
|
+
async generateChangelogPrompt(changes) {
|
|
101
|
+
const fileStats = []
|
|
102
|
+
for (const filePath of changes) {
|
|
103
|
+
try {
|
|
104
|
+
const stat = await this.db.statDocument(filePath)
|
|
105
|
+
fileStats.push({ path: filePath, isFile: stat.isFile })
|
|
106
|
+
} catch {
|
|
107
|
+
fileStats.push({ path: filePath, isFile: false })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const files = fileStats
|
|
112
|
+
.filter(item => item.isFile)
|
|
113
|
+
.map(item => item.path)
|
|
114
|
+
|
|
115
|
+
if (files.length === 0) {
|
|
116
|
+
return ''
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const promptLines = [
|
|
120
|
+
"Based on the following changed files, please provide changelog entries:",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
const diffOutput = execSync('git diff --no-color HEAD', {
|
|
124
|
+
cwd: this.cwd,
|
|
125
|
+
encoding: 'utf8'
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (diffOutput.trim() !== '') {
|
|
129
|
+
promptLines.push('\n```diff\n' + diffOutput.trim() + '\n```')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newFiles = execSync('git ls-files --others --exclude-standard', {
|
|
133
|
+
cwd: this.cwd,
|
|
134
|
+
encoding: 'utf8'
|
|
135
|
+
}).split('\n').filter(line => line.trim() !== '')
|
|
136
|
+
|
|
137
|
+
for (const file of newFiles) {
|
|
138
|
+
try {
|
|
139
|
+
const content = await this.db.loadDocumentAs(".txt", file)
|
|
140
|
+
promptLines.push(`\n#### \`${file}\``)
|
|
141
|
+
promptLines.push('```')
|
|
142
|
+
promptLines.push(content)
|
|
143
|
+
promptLines.push('```')
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore files that can't be read
|
|
146
|
+
this.logger.warn("Could not read file", file)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return promptLines.join('\n')
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default PrepareCommand
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs'
|
|
5
|
+
import { resolve } from 'node:path'
|
|
6
|
+
import DB from '@nan0web/db-fs'
|
|
7
|
+
import Logger from '@nan0web/log'
|
|
8
|
+
import PrepareCommand from './PrepareCommand.js'
|
|
9
|
+
|
|
10
|
+
describe('PrepareCommand', () => {
|
|
11
|
+
it('should initialize CHANGELOG.md if it is empty', async () => {
|
|
12
|
+
const testDir = resolve('test_data/empty_changelog')
|
|
13
|
+
|
|
14
|
+
// Remove directory if exists
|
|
15
|
+
if (existsSync(testDir)) {
|
|
16
|
+
rmSync(testDir, { recursive: true })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
mkdirSync(testDir, { recursive: true })
|
|
20
|
+
|
|
21
|
+
// Initialize git repo
|
|
22
|
+
try {
|
|
23
|
+
execSync('git init', { cwd: testDir })
|
|
24
|
+
execSync('git config user.name "Test User"', { cwd: testDir })
|
|
25
|
+
execSync('git config user.email "test@example.com"', { cwd: testDir })
|
|
26
|
+
} catch (e) {
|
|
27
|
+
assert.fail('Failed to initialize git repository: ' + e.message)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create empty CHANGELOG.md
|
|
31
|
+
writeFileSync(resolve(testDir, 'CHANGELOG.md'), '')
|
|
32
|
+
|
|
33
|
+
// Add and commit
|
|
34
|
+
try {
|
|
35
|
+
execSync('git add CHANGELOG.md', { cwd: testDir })
|
|
36
|
+
execSync('git commit -m "Initial commit"', { cwd: testDir })
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Ignore if nothing to commit
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const command = new PrepareCommand({
|
|
42
|
+
cwd: testDir,
|
|
43
|
+
logger: new Logger({ silent: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await command.run([])
|
|
48
|
+
} catch (e) {
|
|
49
|
+
assert.fail('Command failed: ' + e.message)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const db = new DB({ cwd: testDir })
|
|
53
|
+
const changelogContent = await db.loadDocument('CHANGELOG.md', '')
|
|
54
|
+
assert.ok(changelogContent.includes('# Changelog'), 'CHANGELOG.md should be initialized with header')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should generate changelog prompt with new and modified files', async () => {
|
|
58
|
+
const testDir = resolve('test_data/changes')
|
|
59
|
+
|
|
60
|
+
// Remove directory if exists
|
|
61
|
+
if (existsSync(testDir)) {
|
|
62
|
+
rmSync(testDir, { recursive: true })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
mkdirSync(testDir, { recursive: true })
|
|
66
|
+
|
|
67
|
+
// Initialize git repo
|
|
68
|
+
try {
|
|
69
|
+
execSync('git init', { cwd: testDir })
|
|
70
|
+
execSync('git config user.name "Test User"', { cwd: testDir })
|
|
71
|
+
execSync('git config user.email "test@example.com"', { cwd: testDir })
|
|
72
|
+
// Create initial commit
|
|
73
|
+
writeFileSync(resolve(testDir, 'initial.txt'), 'initial content')
|
|
74
|
+
execSync('git add initial.txt', { cwd: testDir })
|
|
75
|
+
execSync('git commit -m "Initial commit"', { cwd: testDir })
|
|
76
|
+
} catch (e) {
|
|
77
|
+
assert.fail('Failed to initialize git repository: ' + e.message)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create initial files
|
|
81
|
+
writeFileSync(resolve(testDir, 'CHANGELOG.md'), '# Changelog\n')
|
|
82
|
+
writeFileSync(resolve(testDir, 'existing-file.txt'), 'existing content\n')
|
|
83
|
+
|
|
84
|
+
// Add and commit initial files
|
|
85
|
+
try {
|
|
86
|
+
execSync('git add CHANGELOG.md existing-file.txt', { cwd: testDir })
|
|
87
|
+
execSync('git commit -m "Add initial files"', { cwd: testDir })
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Ignore if nothing to commit
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Make changes
|
|
93
|
+
writeFileSync(resolve(testDir, 'new-file.txt'), 'new content\n')
|
|
94
|
+
writeFileSync(resolve(testDir, 'existing-file.txt'), 'existing content\nmodified content\n')
|
|
95
|
+
|
|
96
|
+
const command = new PrepareCommand({
|
|
97
|
+
cwd: testDir,
|
|
98
|
+
logger: new Logger({ silent: true })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await command.run([])
|
|
103
|
+
} catch (err) {
|
|
104
|
+
assert.fail('Command failed: ' + err.message)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Test passes if no exception is thrown
|
|
108
|
+
assert.ok(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should include recent changelog entries in the prompt', async () => {
|
|
112
|
+
const testDir = resolve('test_data/recent_entries')
|
|
113
|
+
|
|
114
|
+
// Remove directory if exists
|
|
115
|
+
if (existsSync(testDir)) {
|
|
116
|
+
rmSync(testDir, { recursive: true })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
mkdirSync(testDir, { recursive: true })
|
|
120
|
+
|
|
121
|
+
// Initialize git repo
|
|
122
|
+
try {
|
|
123
|
+
execSync('git init', { cwd: testDir })
|
|
124
|
+
execSync('git config user.name "Test User"', { cwd: testDir })
|
|
125
|
+
execSync('git config user.email "test@example.com"', { cwd: testDir })
|
|
126
|
+
// Create initial commit
|
|
127
|
+
writeFileSync(resolve(testDir, 'initial.txt'), 'initial content')
|
|
128
|
+
execSync('git add initial.txt', { cwd: testDir })
|
|
129
|
+
execSync('git commit -m "Initial commit"', { cwd: testDir })
|
|
130
|
+
} catch (e) {
|
|
131
|
+
assert.fail('Failed to initialize git repository: ' + e.message)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create directories
|
|
135
|
+
mkdirSync(resolve(testDir, 'src'), { recursive: true })
|
|
136
|
+
|
|
137
|
+
// Create CHANGELOG.md with existing releases
|
|
138
|
+
const changelogContent = `# Changelog
|
|
139
|
+
|
|
140
|
+
## [1.2.0] - 2024-01-03
|
|
141
|
+
### Added
|
|
142
|
+
- New feature Y
|
|
143
|
+
|
|
144
|
+
## [1.1.0] - 2024-01-02
|
|
145
|
+
### Changed
|
|
146
|
+
- Improved performance of module X
|
|
147
|
+
|
|
148
|
+
## [1.0.0] - 2024-01-01
|
|
149
|
+
### Added
|
|
150
|
+
- Initial release
|
|
151
|
+
- Core functionality implemented
|
|
152
|
+
`
|
|
153
|
+
writeFileSync(resolve(testDir, 'CHANGELOG.md'), changelogContent)
|
|
154
|
+
|
|
155
|
+
// Add and commit CHANGELOG.md
|
|
156
|
+
try {
|
|
157
|
+
execSync('git add CHANGELOG.md', { cwd: testDir })
|
|
158
|
+
execSync('git commit -m "Add changelog with entries"', { cwd: testDir })
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Ignore if nothing to commit
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Make changes
|
|
164
|
+
writeFileSync(resolve(testDir, 'src/new-feature.js'), 'console.log("new feature")\n')
|
|
165
|
+
|
|
166
|
+
const command = new PrepareCommand({
|
|
167
|
+
cwd: testDir,
|
|
168
|
+
logger: new Logger({ silent: true })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await command.run([])
|
|
173
|
+
} catch (err) {
|
|
174
|
+
assert.fail('Command failed: ' + err.message)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const db = new DB({ cwd: testDir })
|
|
178
|
+
const promptContent = await db.loadDocument('me.md', '')
|
|
179
|
+
|
|
180
|
+
// Check that recent entries are included in the prompt
|
|
181
|
+
assert.ok(promptContent.includes('## [1.2.0] - 2024-01-03'), 'Prompt should include most recent entry')
|
|
182
|
+
assert.ok(promptContent.includes('## [1.1.0] - 2024-01-02'), 'Prompt should include second recent entry')
|
|
183
|
+
assert.ok(promptContent.includes('## [1.0.0] - 2024-01-01'), 'Prompt should include third recent entry')
|
|
184
|
+
|
|
185
|
+
// Check that the prompt contains the instruction about using these entries as examples
|
|
186
|
+
assert.ok(promptContent.includes('Based on the following changed files, please provide changelog entries:'),
|
|
187
|
+
'Prompt should contain instructions for generating changelog entries')
|
|
188
|
+
})
|
|
189
|
+
})
|