@nan0web/release 3.1.0 → 3.2.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/bin/nan0release.js +2 -2
- package/bin/release.test.js +81 -0
- package/package.json +20 -12
- package/src/README.md.js +7 -7
- package/src/Release/MarkdownToTest.test.js +60 -0
- package/src/Release/Scanner.close.test.js +91 -0
- package/src/Release/Scanner.test.js +134 -0
- package/src/architecture/ProjectManagementAsCode.test.js +128 -0
- package/src/domain/App.js +5 -2
- package/src/domain/BumpCommand.js +137 -0
- package/src/index.js +8 -3
- package/src/releases/1/0/v1.0.2/test.test.js +6 -0
- package/src/releases/1/0/v1.0.3/task.test.js +16 -0
- package/types/domain/App.d.ts +4 -3
- package/types/domain/BumpCommand.d.ts +39 -0
- package/types/index.d.ts +1 -1
- package/knip.json +0 -21
- package/nan0web.nan0 +0 -16
- package/project.yaml +0 -79
package/bin/nan0release.js
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import FS from '@nan0web/db-fs'
|
|
4
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { resolve, dirname } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import main from './release.js'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = dirname(__filename)
|
|
12
|
+
const tempDir = resolve(__dirname, 'temp')
|
|
13
|
+
const tempPackageJson = resolve(tempDir, 'package.json')
|
|
14
|
+
|
|
15
|
+
const fs = new FS()
|
|
16
|
+
|
|
17
|
+
describe('release.js', async () => {
|
|
18
|
+
await fs.connect()
|
|
19
|
+
const tempDir = 'tmp/release'
|
|
20
|
+
const db = fs.extract(tempDir)
|
|
21
|
+
await db.writeDocument('package.json', {
|
|
22
|
+
name: 'test-package',
|
|
23
|
+
version: '0.0.1-test',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Mock process.cwd to return tempDir
|
|
27
|
+
const originalCwd = process.cwd
|
|
28
|
+
process.cwd = () => tempDir
|
|
29
|
+
|
|
30
|
+
it('main function processes release command', async () => {
|
|
31
|
+
// Test with ignore flags to avoid actual git operations
|
|
32
|
+
const result = await main(['--ignore-uncommitted', '--ignore-fail-tests', '--ignore-tag'])
|
|
33
|
+
assert.ok(result.includes('Release completed'))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('main function processes prepare command', async () => {
|
|
37
|
+
const resultP = await main(['prepare'])
|
|
38
|
+
assert.ok(result.includes('Prepare completed'))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('ReleaseCommand handles package.json validation', async () => {
|
|
42
|
+
// Create a package.json without required scripts
|
|
43
|
+
writeFileSync(
|
|
44
|
+
tempPackageJson,
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
name: 'test-package',
|
|
47
|
+
version: '0.0.1-test',
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Should exit with error when missing scripts
|
|
52
|
+
try {
|
|
53
|
+
await main(['--ignore-uncommitted', '--ignore-fail-tests', '--ignore-tag'])
|
|
54
|
+
assert.fail('Should have exited with error')
|
|
55
|
+
} catch (err) {
|
|
56
|
+
assert.ok(err.message.includes('Missing required scripts'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Should fix when --fix flag is used
|
|
60
|
+
const result = await main([
|
|
61
|
+
'--ignore-uncommitted',
|
|
62
|
+
'--ignore-fail-tests',
|
|
63
|
+
'--ignore-tag',
|
|
64
|
+
'--fix',
|
|
65
|
+
])
|
|
66
|
+
assert.ok(result.includes('Release completed'))
|
|
67
|
+
|
|
68
|
+
// Verify package.json was fixed
|
|
69
|
+
const pkg = JSON.parse(readFileSync(tempPackageJson, 'utf8'))
|
|
70
|
+
assert.ok(pkg.scripts.dev)
|
|
71
|
+
assert.ok(pkg.scripts.build)
|
|
72
|
+
assert.ok(pkg.scripts.preview)
|
|
73
|
+
assert.ok(pkg.scripts.test)
|
|
74
|
+
assert.ok(pkg.scripts.clean)
|
|
75
|
+
assert.ok(pkg.devDependencies.husky)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Cleanup
|
|
79
|
+
process.cwd = originalCwd
|
|
80
|
+
await db.dropDocument('package.json')
|
|
81
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/release",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Project Management as Code, Git-Native, GPG-Signed, Test-Driven",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -10,23 +10,31 @@
|
|
|
10
10
|
"release": "./bin/nan0release.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@nan0web/
|
|
14
|
-
"@nan0web/
|
|
15
|
-
"@nan0web/
|
|
16
|
-
"@nan0web/
|
|
17
|
-
"@nan0web/
|
|
18
|
-
"@nan0web/
|
|
19
|
-
"@nan0web/
|
|
20
|
-
"@nan0web/
|
|
21
|
-
"@nan0web/
|
|
22
|
-
"@nan0web/
|
|
13
|
+
"@nan0web/co": "3.2.0",
|
|
14
|
+
"@nan0web/db": "3.2.0",
|
|
15
|
+
"@nan0web/ui": "3.2.0",
|
|
16
|
+
"@nan0web/types": "3.2.0",
|
|
17
|
+
"@nan0web/db-fs": "3.2.0",
|
|
18
|
+
"@nan0web/log": "3.2.0",
|
|
19
|
+
"@nan0web/markdown": "3.2.0",
|
|
20
|
+
"@nan0web/changelog": "3.2.0",
|
|
21
|
+
"@nan0web/ui-cli": "3.2.0",
|
|
22
|
+
"@nan0web/logos": "3.2.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^25.6.0",
|
|
26
26
|
"husky": "^9.1.7",
|
|
27
27
|
"knip": "^5.86.0",
|
|
28
|
-
"@nan0web/test": "3.
|
|
28
|
+
"@nan0web/test": "3.2.0"
|
|
29
29
|
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"src",
|
|
33
|
+
"types",
|
|
34
|
+
"data",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
30
38
|
"scripts": {
|
|
31
39
|
"build": "tsc",
|
|
32
40
|
"clean": "rm -rf .cache/ && rm -rf dist/",
|
package/src/README.md.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
4
4
|
import FS from '@nan0web/db-fs'
|
|
5
5
|
import { NoConsole } from '@nan0web/log'
|
|
6
6
|
import { DatasetParser, DocsParser, runSpawn } from '@nan0web/test'
|
|
7
|
-
import {
|
|
7
|
+
import { ReleaseApp } from './index.js'
|
|
8
8
|
import Release from './Release.js'
|
|
9
9
|
import ReleaseDocument from './Release/Document.js'
|
|
10
10
|
import Person from './Release/Person.js'
|
|
@@ -103,7 +103,7 @@ function testRender() {
|
|
|
103
103
|
* nan0release init v1.0.0
|
|
104
104
|
* ```
|
|
105
105
|
*/
|
|
106
|
-
assert.ok(new
|
|
106
|
+
assert.ok(new ReleaseApp().constructor.command.options)
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
/**
|
|
@@ -134,7 +134,7 @@ function testRender() {
|
|
|
134
134
|
* nan0release list [--json]
|
|
135
135
|
* ```
|
|
136
136
|
*/
|
|
137
|
-
assert.ok(new
|
|
137
|
+
assert.ok(new ReleaseApp().constructor.command.options)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
/**
|
|
@@ -147,7 +147,7 @@ function testRender() {
|
|
|
147
147
|
* nan0release chat write --user alice "Issue with the build pipeline"
|
|
148
148
|
* ```
|
|
149
149
|
*/
|
|
150
|
-
assert.ok(new
|
|
150
|
+
assert.ok(new ReleaseApp().constructor.command.options)
|
|
151
151
|
})
|
|
152
152
|
|
|
153
153
|
/**
|
|
@@ -160,7 +160,7 @@ function testRender() {
|
|
|
160
160
|
* nan0release host [--webui] [--port 3000]
|
|
161
161
|
* ```
|
|
162
162
|
*/
|
|
163
|
-
assert.ok(new
|
|
163
|
+
assert.ok(new ReleaseApp().constructor.command.options)
|
|
164
164
|
})
|
|
165
165
|
|
|
166
166
|
/**
|
|
@@ -173,7 +173,7 @@ function testRender() {
|
|
|
173
173
|
* nan0release serve [--port 8080]
|
|
174
174
|
* ```
|
|
175
175
|
*/
|
|
176
|
-
assert.ok(new
|
|
176
|
+
assert.ok(new ReleaseApp().constructor.command.options)
|
|
177
177
|
})
|
|
178
178
|
|
|
179
179
|
/**
|
|
@@ -204,7 +204,7 @@ function testRender() {
|
|
|
204
204
|
* nan0release seal [--message "All core APIs are stable and tested"]
|
|
205
205
|
* ```
|
|
206
206
|
*/
|
|
207
|
-
assert.ok(new
|
|
207
|
+
assert.ok(new ReleaseApp().constructor.command.options)
|
|
208
208
|
})
|
|
209
209
|
|
|
210
210
|
/**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import { strictEqual } from 'node:assert'
|
|
3
|
+
import MarkdownToTest from './MarkdownToTest.js'
|
|
4
|
+
|
|
5
|
+
describe.skip('MarkdownToTest', () => {
|
|
6
|
+
it('should generate node:test code from markdown release notes', () => {
|
|
7
|
+
const parser = new MarkdownToTest()
|
|
8
|
+
const markdown = `# v1.1.0 - 2025-08-19
|
|
9
|
+
|
|
10
|
+
## CLI application
|
|
11
|
+
### **Treats project management like code: everything is versioned, auditable, reversible.** [treats-project-management-like-code-everything-is-versioned-auditable-reversible]
|
|
12
|
+
### Draft **No external tools or databases needed — just Git.** [no-external-tools-or-databases-needed-just-git]
|
|
13
|
+
### **Natural for developers; lowers barrier to entry for dev-centric teams.** [natural-for-developers-lowers-barrier-to-entry-for-dev-centric-teams]
|
|
14
|
+
|
|
15
|
+
## UI application
|
|
16
|
+
### **Drag-and-drop interface** [drag-and-drop-interface]
|
|
17
|
+
### **Visual tree structure** [visual-tree-structure]`
|
|
18
|
+
|
|
19
|
+
const expected = `import { describe, it } from "node:test"
|
|
20
|
+
import { ok, strictEqual } from "node:assert"
|
|
21
|
+
|
|
22
|
+
describe("v1.1.0 - 2025-08-19", () => {
|
|
23
|
+
describe("CLI application", () => {
|
|
24
|
+
it("Treats project management like code: everything is versioned, auditable, reversible.", () => {
|
|
25
|
+
ok(true, "Task completed successfully")
|
|
26
|
+
})
|
|
27
|
+
it.skip("No external tools or databases needed — just Git.", () => {
|
|
28
|
+
ok(false, "Draft task - not yet implemented")
|
|
29
|
+
})
|
|
30
|
+
it("Natural for developers; lowers barrier to entry for dev-centric teams.", () => {
|
|
31
|
+
ok(true, "Task completed successfully")
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
describe("UI application", () => {
|
|
35
|
+
it("Drag-and-drop interface", () => {
|
|
36
|
+
ok(true, "Task completed successfully")
|
|
37
|
+
})
|
|
38
|
+
it("Visual tree structure", () => {
|
|
39
|
+
ok(true, "Task completed successfully")
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})`
|
|
43
|
+
|
|
44
|
+
const result = parser.generateTests(markdown)
|
|
45
|
+
strictEqual(result.trim(), expected.trim())
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("should throw error if markdown doesn't have version header", () => {
|
|
49
|
+
const parser = new MarkdownToTest()
|
|
50
|
+
const markdown = `## CLI application
|
|
51
|
+
### **Treats project management like code** [treats-project-management-like-code]`
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
parser.generateTests(markdown)
|
|
55
|
+
throw new Error('Should have thrown an error')
|
|
56
|
+
} catch (error) {
|
|
57
|
+
strictEqual(error.message, 'Markdown must start with version header: # vX.Y.Z - YYYY-MM-DD')
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import Scanner from './Scanner.js'
|
|
8
|
+
|
|
9
|
+
const TMP = join(tmpdir(), 'nan0release-close-test-' + Date.now())
|
|
10
|
+
|
|
11
|
+
describe('Scanner.close()', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mkdirSync(TMP, { recursive: true })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(TMP, { recursive: true, force: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renames spec → test for a specific version', () => {
|
|
21
|
+
const dir = join(TMP, 'releases/2/0/v2.0.0')
|
|
22
|
+
mkdirSync(dir, { recursive: true })
|
|
23
|
+
writeFileSync(join(dir, 'task.spec.js'), '// contract')
|
|
24
|
+
|
|
25
|
+
const scanner = new Scanner(TMP)
|
|
26
|
+
const result = scanner.close('v2.0.0')
|
|
27
|
+
|
|
28
|
+
assert.equal(result.closed, 1)
|
|
29
|
+
assert.ok(!existsSync(join(dir, 'task.spec.js')), 'spec should be gone')
|
|
30
|
+
assert.ok(existsSync(join(dir, 'task.test.js')), 'test should exist')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('renames multiple spec files in the same version', () => {
|
|
34
|
+
const dir = join(TMP, 'releases/2/0/v2.0.0')
|
|
35
|
+
mkdirSync(dir, { recursive: true })
|
|
36
|
+
writeFileSync(join(dir, 'task.spec.js'), '// contract 1')
|
|
37
|
+
writeFileSync(join(dir, 'api.spec.js'), '// contract 2')
|
|
38
|
+
|
|
39
|
+
const scanner = new Scanner(TMP)
|
|
40
|
+
const result = scanner.close('v2.0.0')
|
|
41
|
+
|
|
42
|
+
assert.equal(result.closed, 2)
|
|
43
|
+
assert.ok(existsSync(join(dir, 'task.test.js')))
|
|
44
|
+
assert.ok(existsSync(join(dir, 'api.test.js')))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns 0 when no specs found for version', () => {
|
|
48
|
+
mkdirSync(join(TMP, 'releases/1/0/v1.0.0'), { recursive: true })
|
|
49
|
+
writeFileSync(join(TMP, 'releases/1/0/v1.0.0/task.test.js'), '// already closed')
|
|
50
|
+
|
|
51
|
+
const scanner = new Scanner(TMP)
|
|
52
|
+
const result = scanner.close('v1.0.0')
|
|
53
|
+
|
|
54
|
+
assert.equal(result.closed, 0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('throws when version not found', () => {
|
|
58
|
+
mkdirSync(join(TMP, 'releases'), { recursive: true })
|
|
59
|
+
|
|
60
|
+
const scanner = new Scanner(TMP)
|
|
61
|
+
assert.throws(() => scanner.close('v9.9.9'), /not found/i)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('works with flat structure (releases/v2.0.0/)', () => {
|
|
65
|
+
const dir = join(TMP, 'releases/v2.0.0')
|
|
66
|
+
mkdirSync(dir, { recursive: true })
|
|
67
|
+
writeFileSync(join(dir, 'task.spec.js'), '// flat')
|
|
68
|
+
|
|
69
|
+
const scanner = new Scanner(TMP)
|
|
70
|
+
const result = scanner.close('v2.0.0')
|
|
71
|
+
|
|
72
|
+
assert.equal(result.closed, 1)
|
|
73
|
+
assert.ok(existsSync(join(dir, 'task.test.js')))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('close --all renames all specs across versions', () => {
|
|
77
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
78
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
79
|
+
mkdirSync(v1, { recursive: true })
|
|
80
|
+
mkdirSync(v2, { recursive: true })
|
|
81
|
+
writeFileSync(join(v1, 'task.spec.js'), '// v1')
|
|
82
|
+
writeFileSync(join(v2, 'task.spec.js'), '// v2')
|
|
83
|
+
|
|
84
|
+
const scanner = new Scanner(TMP)
|
|
85
|
+
const result = scanner.close()
|
|
86
|
+
|
|
87
|
+
assert.equal(result.closed, 2)
|
|
88
|
+
assert.ok(existsSync(join(v1, 'task.test.js')))
|
|
89
|
+
assert.ok(existsSync(join(v2, 'task.test.js')))
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import Scanner from './Scanner.js'
|
|
8
|
+
|
|
9
|
+
const TMP = join(tmpdir(), 'nan0release-scanner-test-' + Date.now())
|
|
10
|
+
|
|
11
|
+
describe('Scanner', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mkdirSync(TMP, { recursive: true })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(TMP, { recursive: true, force: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('nested version structure', () => {
|
|
21
|
+
it('finds spec files in nested directories (releases/1/0/v1.0.0/)', () => {
|
|
22
|
+
const dir = join(TMP, 'releases/1/0/v1.0.0')
|
|
23
|
+
mkdirSync(dir, { recursive: true })
|
|
24
|
+
writeFileSync(join(dir, 'task.spec.js'), '// spec')
|
|
25
|
+
|
|
26
|
+
const scanner = new Scanner(TMP)
|
|
27
|
+
const specs = scanner.findSpecs()
|
|
28
|
+
|
|
29
|
+
assert.equal(specs.length, 1)
|
|
30
|
+
assert.equal(specs[0].version, 'v1.0.0')
|
|
31
|
+
assert.ok(specs[0].path.endsWith('task.spec.js'))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('finds test files (closed contracts)', () => {
|
|
35
|
+
const dir = join(TMP, 'releases/1/0/v1.0.0')
|
|
36
|
+
mkdirSync(dir, { recursive: true })
|
|
37
|
+
writeFileSync(join(dir, 'task.test.js'), '// test')
|
|
38
|
+
|
|
39
|
+
const scanner = new Scanner(TMP)
|
|
40
|
+
const tests = scanner.findTests()
|
|
41
|
+
|
|
42
|
+
assert.equal(tests.length, 1)
|
|
43
|
+
assert.equal(tests[0].version, 'v1.0.0')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('finds flat structure too (releases/v2.0.0/)', () => {
|
|
47
|
+
const dir = join(TMP, 'releases/v2.0.0')
|
|
48
|
+
mkdirSync(dir, { recursive: true })
|
|
49
|
+
writeFileSync(join(dir, 'task.spec.js'), '// spec')
|
|
50
|
+
|
|
51
|
+
const scanner = new Scanner(TMP)
|
|
52
|
+
const specs = scanner.findSpecs()
|
|
53
|
+
|
|
54
|
+
assert.equal(specs.length, 1)
|
|
55
|
+
assert.equal(specs[0].version, 'v2.0.0')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('multiple versions', () => {
|
|
60
|
+
it('finds all specs across multiple versions', () => {
|
|
61
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
62
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
63
|
+
mkdirSync(v1, { recursive: true })
|
|
64
|
+
mkdirSync(v2, { recursive: true })
|
|
65
|
+
writeFileSync(join(v1, 'task.spec.js'), '// v1 spec')
|
|
66
|
+
writeFileSync(join(v2, 'task.spec.js'), '// v2 spec')
|
|
67
|
+
|
|
68
|
+
const scanner = new Scanner(TMP)
|
|
69
|
+
const specs = scanner.findSpecs()
|
|
70
|
+
|
|
71
|
+
assert.equal(specs.length, 2)
|
|
72
|
+
const versions = specs.map((s) => s.version).sort()
|
|
73
|
+
assert.deepEqual(versions, ['v1.0.0', 'v2.0.0'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('separates specs from tests', () => {
|
|
77
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
78
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
79
|
+
mkdirSync(v1, { recursive: true })
|
|
80
|
+
mkdirSync(v2, { recursive: true })
|
|
81
|
+
writeFileSync(join(v1, 'task.test.js'), '// closed')
|
|
82
|
+
writeFileSync(join(v2, 'task.spec.js'), '// wip')
|
|
83
|
+
|
|
84
|
+
const scanner = new Scanner(TMP)
|
|
85
|
+
assert.equal(scanner.findSpecs().length, 1)
|
|
86
|
+
assert.equal(scanner.findTests().length, 1)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('filter by version', () => {
|
|
91
|
+
it('finds specs for a specific version', () => {
|
|
92
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
93
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
94
|
+
mkdirSync(v1, { recursive: true })
|
|
95
|
+
mkdirSync(v2, { recursive: true })
|
|
96
|
+
writeFileSync(join(v1, 'task.spec.js'), '// v1')
|
|
97
|
+
writeFileSync(join(v2, 'task.spec.js'), '// v2')
|
|
98
|
+
|
|
99
|
+
const scanner = new Scanner(TMP)
|
|
100
|
+
const specs = scanner.findSpecs('v2.0.0')
|
|
101
|
+
|
|
102
|
+
assert.equal(specs.length, 1)
|
|
103
|
+
assert.equal(specs[0].version, 'v2.0.0')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('status', () => {
|
|
108
|
+
it('returns status for all versions', () => {
|
|
109
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
110
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
111
|
+
mkdirSync(v1, { recursive: true })
|
|
112
|
+
mkdirSync(v2, { recursive: true })
|
|
113
|
+
writeFileSync(join(v1, 'task.test.js'), '// closed')
|
|
114
|
+
writeFileSync(join(v2, 'task.spec.js'), '// wip')
|
|
115
|
+
|
|
116
|
+
const scanner = new Scanner(TMP)
|
|
117
|
+
const status = scanner.status()
|
|
118
|
+
|
|
119
|
+
assert.equal(status.length, 2)
|
|
120
|
+
const v1s = status.find((s) => s.version === 'v1.0.0')
|
|
121
|
+
const v2s = status.find((s) => s.version === 'v2.0.0')
|
|
122
|
+
assert.equal(v1s.state, 'closed')
|
|
123
|
+
assert.equal(v2s.state, 'wip')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('empty releases', () => {
|
|
128
|
+
it('returns empty arrays when no releases directory', () => {
|
|
129
|
+
const scanner = new Scanner(TMP)
|
|
130
|
+
assert.deepEqual(scanner.findSpecs(), [])
|
|
131
|
+
assert.deepEqual(scanner.findTests(), [])
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import {
|
|
4
|
+
ProjectManagement,
|
|
5
|
+
ReleaseManager,
|
|
6
|
+
TaskTestSuite,
|
|
7
|
+
ChangelogTaskManager,
|
|
8
|
+
} from './ProjectManagementAsCode.js'
|
|
9
|
+
|
|
10
|
+
describe('ProjectManagementAsCode Architecture', () => {
|
|
11
|
+
describe('ProjectManagement', () => {
|
|
12
|
+
it('should initialize with default configuration', () => {
|
|
13
|
+
const pm = new ProjectManagement()
|
|
14
|
+
assert.equal(pm.projectPath, '.')
|
|
15
|
+
assert.equal(pm.autoTrack, false)
|
|
16
|
+
assert.ok(pm.tasks instanceof Map)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should register tasks with their test files', () => {
|
|
20
|
+
const pm = new ProjectManagement()
|
|
21
|
+
pm.registerTask('task-1', './tests/task1.test.js')
|
|
22
|
+
pm.registerTask('task-2', './tests/task2.test.js')
|
|
23
|
+
|
|
24
|
+
assert.equal(pm.tasks.size, 2)
|
|
25
|
+
assert.equal(pm.tasks.get('task-1'), './tests/task1.test.js')
|
|
26
|
+
assert.equal(pm.tasks.get('task-2'), './tests/task2.test.js')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should validate project state by running task tests', async () => {
|
|
30
|
+
const pm = new ProjectManagement()
|
|
31
|
+
pm.registerTask('task-1', './tests/task1.test.js')
|
|
32
|
+
pm.registerTask('task-2', './tests/task2.test.js')
|
|
33
|
+
|
|
34
|
+
const results = await pm.validateProjectState()
|
|
35
|
+
assert.ok(results.passed.length >= 0)
|
|
36
|
+
assert.ok(results.failed.length >= 0)
|
|
37
|
+
assert.ok(results.pending.length >= 0)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('ReleaseManager', () => {
|
|
42
|
+
it('should initialize with project manager', () => {
|
|
43
|
+
const pm = new ProjectManagement()
|
|
44
|
+
const rm = new ReleaseManager(pm)
|
|
45
|
+
|
|
46
|
+
assert.equal(rm.projectManager, pm)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it.skip('should validate release with version calculation', async () => {
|
|
50
|
+
const pm = new ProjectManagement()
|
|
51
|
+
const rm = new ReleaseManager(pm)
|
|
52
|
+
|
|
53
|
+
const validation = await rm.validateRelease('1.0.0')
|
|
54
|
+
assert.equal(validation.version, '1.0.0')
|
|
55
|
+
assert.ok(validation.timestamp)
|
|
56
|
+
assert.ok(validation.tasks.total >= 0)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should execute release process with validation', async () => {
|
|
60
|
+
const pm = new ProjectManagement()
|
|
61
|
+
const rm = new ReleaseManager(pm)
|
|
62
|
+
|
|
63
|
+
const result = await rm.executeRelease('patch')
|
|
64
|
+
assert.ok(result.version)
|
|
65
|
+
assert.ok(result.validation)
|
|
66
|
+
assert.equal(typeof result.published, 'boolean')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should block release when tests fail and ignore option is not set', async () => {
|
|
70
|
+
const pm = new ProjectManagement()
|
|
71
|
+
const rm = new ReleaseManager(pm)
|
|
72
|
+
|
|
73
|
+
// Mock validation to simulate failed tasks
|
|
74
|
+
rm.projectManager.validateProjectState = async () => ({
|
|
75
|
+
passed: [],
|
|
76
|
+
failed: ['task-1'],
|
|
77
|
+
pending: [],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await rm.executeRelease('patch')
|
|
82
|
+
assert.fail('Should have thrown an error')
|
|
83
|
+
} catch (error) {
|
|
84
|
+
assert.ok(error.message.includes('Release blocked'))
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('TaskTestSuite', () => {
|
|
90
|
+
it.skip('should extend TestSuite with task metadata', () => {
|
|
91
|
+
const testFunction = () => {}
|
|
92
|
+
const taskSuite = new TaskTestSuite('task-1', 'Test task description', testFunction)
|
|
93
|
+
|
|
94
|
+
assert.equal(taskSuite.taskId, 'task-1')
|
|
95
|
+
assert.equal(taskSuite.description, 'Test task description')
|
|
96
|
+
assert.equal(taskSuite.testFunction, testFunction)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('ChangelogTaskManager', () => {
|
|
101
|
+
it('should parse changelog content to extract tasks', () => {
|
|
102
|
+
const ctm = new ChangelogTaskManager()
|
|
103
|
+
const changelogContent = `
|
|
104
|
+
# Changelog
|
|
105
|
+
|
|
106
|
+
## [1.0.0] - 2024-01-01
|
|
107
|
+
### Added
|
|
108
|
+
- Initial release features
|
|
109
|
+
- Core functionality implementation
|
|
110
|
+
`
|
|
111
|
+
|
|
112
|
+
const tasks = ctm.parseChangelog(changelogContent)
|
|
113
|
+
assert.ok(Array.isArray(tasks))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should generate task tests from changelog entries', async () => {
|
|
117
|
+
const ctm = new ChangelogTaskManager()
|
|
118
|
+
const tasks = [
|
|
119
|
+
{ id: 'task-1', description: 'Implement feature A' },
|
|
120
|
+
{ id: 'task-2', description: 'Fix bug B' },
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
// This should not throw an error
|
|
124
|
+
await ctm.generateTaskTests(tasks)
|
|
125
|
+
assert.ok(true)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
package/src/domain/App.js
CHANGED
|
@@ -6,8 +6,9 @@ import DepsCommand from './DepsCommand.js'
|
|
|
6
6
|
import PublishCommand from './PublishCommand.js'
|
|
7
7
|
import SpecCommand from './SpecCommand.js'
|
|
8
8
|
import StatusCommand from './StatusCommand.js'
|
|
9
|
+
import BumpCommand from './BumpCommand.js'
|
|
9
10
|
|
|
10
|
-
export class
|
|
11
|
+
export class ReleaseApp extends ModelAsApp {
|
|
11
12
|
static command = {
|
|
12
13
|
help: 'Subcommand to run',
|
|
13
14
|
options: [
|
|
@@ -18,12 +19,14 @@ export class App extends ModelAsApp {
|
|
|
18
19
|
PublishCommand,
|
|
19
20
|
SpecCommand,
|
|
20
21
|
StatusCommand,
|
|
22
|
+
BumpCommand,
|
|
21
23
|
],
|
|
24
|
+
positional: true,
|
|
22
25
|
default: null,
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
|
-
* @param {Partial<
|
|
29
|
+
* @param {Partial<ReleaseApp>} [data]
|
|
27
30
|
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options]
|
|
28
31
|
*/
|
|
29
32
|
constructor(data = {}, options = {}) {
|
|
@@ -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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,12 @@ import ReleaseDocument from './Release/Document.js'
|
|
|
3
3
|
import Person from './Release/Person.js'
|
|
4
4
|
import Scanner from './Release/Scanner.js'
|
|
5
5
|
import MarkdownToTest from './Release/MarkdownToTest.js'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ProjectManagement,
|
|
8
|
+
ReleaseManager,
|
|
9
|
+
TaskTestSuite,
|
|
10
|
+
ChangelogTaskManager,
|
|
11
|
+
} from './architecture/ProjectManagementAsCode.js'
|
|
7
12
|
|
|
8
13
|
export {
|
|
9
14
|
Release,
|
|
@@ -14,10 +19,10 @@ export {
|
|
|
14
19
|
ProjectManagement,
|
|
15
20
|
ReleaseManager,
|
|
16
21
|
TaskTestSuite,
|
|
17
|
-
ChangelogTaskManager
|
|
22
|
+
ChangelogTaskManager,
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
export {
|
|
25
|
+
export { ReleaseApp } from './domain/App.js'
|
|
21
26
|
export { default as InitCommand } from './domain/InitCommand.js'
|
|
22
27
|
export { default as CheckCommand } from './domain/CheckCommand.js'
|
|
23
28
|
export { default as CloseCommand } from './domain/CloseCommand.js'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
describe('Release v3.0.0 - Dependency Alignment', () => {
|
|
8
|
+
it('should have correct versions for @nan0web packages', async () => {
|
|
9
|
+
const pkgPath = join(process.cwd(), 'package.json')
|
|
10
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
11
|
+
|
|
12
|
+
assert.ok(/^3\.1\.\d+$/.test(pkg.version), 'Version should start with 3.1')
|
|
13
|
+
assert.ok(/^(\^1\.8\.0|workspace:\*)$/.test(pkg.dependencies['@nan0web/ui']), '@nan0web/ui should be ^1.8.0 or workspace:*')
|
|
14
|
+
assert.ok(/^(\^2\.9\.0|workspace:\*)$/.test(pkg.dependencies['@nan0web/ui-cli']), '@nan0web/ui-cli should be ^2.9.0 or workspace:*')
|
|
15
|
+
})
|
|
16
|
+
})
|
package/types/domain/App.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
export class
|
|
1
|
+
export class ReleaseApp extends ModelAsApp {
|
|
2
2
|
static command: {
|
|
3
3
|
help: string;
|
|
4
4
|
options: (typeof CheckCommand | typeof StatusCommand)[];
|
|
5
|
+
positional: boolean;
|
|
5
6
|
default: null;
|
|
6
7
|
};
|
|
7
8
|
/**
|
|
8
|
-
* @param {Partial<
|
|
9
|
+
* @param {Partial<ReleaseApp>} [data]
|
|
9
10
|
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options]
|
|
10
11
|
*/
|
|
11
|
-
constructor(data?: Partial<
|
|
12
|
+
constructor(data?: Partial<ReleaseApp>, options?: Partial<import("@nan0web/ui").ModelAsAppOptions>);
|
|
12
13
|
/** @type {ModelAsApp} Subcommand to run */ command: ModelAsApp;
|
|
13
14
|
}
|
|
14
15
|
import { ModelAsApp } from "@nan0web/ui";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to bump the version of changed packages in the monorepo.
|
|
3
|
+
*/
|
|
4
|
+
export default class BumpCommand extends ModelAsApp {
|
|
5
|
+
static version: {
|
|
6
|
+
help: string;
|
|
7
|
+
default: string;
|
|
8
|
+
positional: boolean;
|
|
9
|
+
};
|
|
10
|
+
static since: {
|
|
11
|
+
help: string;
|
|
12
|
+
default: string;
|
|
13
|
+
positional: boolean;
|
|
14
|
+
};
|
|
15
|
+
static dryRun: {
|
|
16
|
+
alias: string;
|
|
17
|
+
default: boolean;
|
|
18
|
+
help: string;
|
|
19
|
+
};
|
|
20
|
+
static UI: {
|
|
21
|
+
title: string;
|
|
22
|
+
help: string;
|
|
23
|
+
bumping: string;
|
|
24
|
+
skippingAlreadyAtVersion: string;
|
|
25
|
+
foundChangedPackages: string;
|
|
26
|
+
detectingChangedPackages: string;
|
|
27
|
+
noChangesDetected: string;
|
|
28
|
+
errorDb: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* @param {Partial<BumpCommand>} [data]
|
|
32
|
+
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options]
|
|
33
|
+
*/
|
|
34
|
+
constructor(data?: Partial<BumpCommand>, options?: Partial<import("@nan0web/ui").ModelAsAppOptions>);
|
|
35
|
+
/** @type {string} Target version */ version: string;
|
|
36
|
+
/** @type {string} Since reference */ since: string;
|
|
37
|
+
/** @type {boolean} Dry run */ dryRun: boolean;
|
|
38
|
+
}
|
|
39
|
+
import { ModelAsApp } from '@nan0web/ui';
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ReleaseApp } from "./domain/App.js";
|
|
2
2
|
export { default as InitCommand } from "./domain/InitCommand.js";
|
|
3
3
|
export { default as CheckCommand } from "./domain/CheckCommand.js";
|
|
4
4
|
export { default as CloseCommand } from "./domain/CloseCommand.js";
|
package/knip.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://unpkg.com/knip@5/schema.json",
|
|
3
|
-
"entry": [
|
|
4
|
-
"src/index.js",
|
|
5
|
-
"bin/nan0release.js",
|
|
6
|
-
"src/README.md.js"
|
|
7
|
-
],
|
|
8
|
-
"project": ["src/**/*.js", "bin/*.js"],
|
|
9
|
-
"ignoreDependencies": [
|
|
10
|
-
"@nan0web/log",
|
|
11
|
-
"@nan0web/ui",
|
|
12
|
-
"@nan0web/ui-cli",
|
|
13
|
-
"@nan0web/logos",
|
|
14
|
-
"@nan0web/markdown",
|
|
15
|
-
"@nan0web/db",
|
|
16
|
-
"@nan0web/db-fs",
|
|
17
|
-
"@nan0web/types",
|
|
18
|
-
"@nan0web/co",
|
|
19
|
-
"@nan0web/changelog"
|
|
20
|
-
]
|
|
21
|
-
}
|
package/nan0web.nan0
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
name: "@nan0web/release"
|
|
2
|
-
agents:
|
|
3
|
-
- id: 'release-manager'
|
|
4
|
-
description: 'Release & NPM Publication Manager'
|
|
5
|
-
workflows:
|
|
6
|
-
- 'src/agents/workflows/release.md'
|
|
7
|
-
- 'src/agents/workflows/npm-release.md'
|
|
8
|
-
inspectors:
|
|
9
|
-
- 'src/agents/inspectors/package-hygiene.md'
|
|
10
|
-
- id: 'git-reviewer'
|
|
11
|
-
description: 'Architectural Git Commit Validator'
|
|
12
|
-
workflows:
|
|
13
|
-
- 'src/agents/workflows/commit.md'
|
|
14
|
-
- 'src/agents/workflows/zero-tolerance-git.md'
|
|
15
|
-
inspectors:
|
|
16
|
-
- 'src/agents/inspectors/git-reviewer.md'
|
package/project.yaml
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
metadata:
|
|
3
|
-
description: ""
|
|
4
|
-
tags: []
|
|
5
|
-
status: planned
|
|
6
|
-
locale: uk
|
|
7
|
-
sections:
|
|
8
|
-
- title: "🧭 Фаза 1: Філософія та Абстракція (The Seed)"
|
|
9
|
-
content:
|
|
10
|
-
- "**Місія:** Управління проєктами як код (Project Management as Code). Ми відмовляємось від зовнішніх трекерів задач (Jira, Trello). Проєкт — це набір технічних **контрактів**, які проходять шлях від наміру до імплементації, забезпечуючи реальний статус прогресу автоматично."
|
|
11
|
-
- "**Базова Абстракція:** Пакет відповідає за логіку та CLI, яка читає Markdown, генерує контракти (`.spec.js`), а потім закриває їх як тести (`.test.js`) з подальшою вічною регресією (тести копіюються в кодову базу)."
|
|
12
|
-
- "**Глосарій:**"
|
|
13
|
-
- "- **Release Document**: `release.md` з ТЗ та відомостями про команду."
|
|
14
|
-
- "- **WIP Контракт**: `*.spec.js`, який падає/у стані `todo`, доки задачу не взяли і не зробили. Оновлює прогрес автоматично при виконанні."
|
|
15
|
-
- "- **Закритий Контракт**: `*.test.js` (результат команди `close`). Він стає законом і гарантує регресію через `pnpm test`."
|
|
16
|
-
- "---"
|
|
17
|
-
tasks: []
|
|
18
|
-
- title: "📐 Фаза 2: Доменне Моделювання (Data-Driven Models)"
|
|
19
|
-
content:
|
|
20
|
-
- "Сутності і моделі, які керують процесом релізу, не мають жорсткої прив'язки до вебу, але описуються як чисті об'єкти:"
|
|
21
|
-
- "- `Release`: Координує стан (версія, час початку, час завершення)."
|
|
22
|
-
- "- `ReleaseDocument`: Структурує Markdown у модель (дістає задачі)."
|
|
23
|
-
- "- `Person`: Типізована перевірка того, хто схвалив дії (через GPG або JSON підпис)."
|
|
24
|
-
- "- `Scanner`: Абстракція доступу до файлової системи (`releases/`)."
|
|
25
|
-
- "Статуси відстежуються динамічно через статус тестів: `it.todo` -> `it.skip` -> `it()`."
|
|
26
|
-
- "---"
|
|
27
|
-
tasks: []
|
|
28
|
-
- title: "🛠 Фаза 3: Верифікація Логіки (CLI-First)"
|
|
29
|
-
content:
|
|
30
|
-
- "Уся бізнес-логіка @nan0web/release 100% покривається Unit-тестами та CLI командами. Жодного веб-коду. Використовуємо Message-based архітектуру `@nan0web/ui-cli`."
|
|
31
|
-
- "1. **Команди CLI (`bin/nan0release.js`):**"
|
|
32
|
-
- "- `deps`, `publish` (реалізовані)."
|
|
33
|
-
- "- Заплановані (`init`, `spec`, `close`, `status`) інкапсулюють керування `Scanner` та `Release` моделями."
|
|
34
|
-
- "2. **Contract Lifecycle (Автоматизація прогресу):**"
|
|
35
|
-
- "- На старті `.spec.js` мають `it.todo()` → задача в беклозі."
|
|
36
|
-
- "- Коли розробник бере задачу → генерує логіку, змінює на `it()`. При коміті реальний прогрес проекту видно \"зеленим\"."
|
|
37
|
-
- "- `nan0release close` перейменовує `spec → test` і переносить його в `src/releases/` для 100% автоматичної регресії."
|
|
38
|
-
- "---"
|
|
39
|
-
tasks: []
|
|
40
|
-
- title: "🪐 Фаза 4: Sovereign Workbench (The Master IDE)"
|
|
41
|
-
content:
|
|
42
|
-
- "_(Для цього пакету застосовується опціонально, оскільки тут CLI-First)_"
|
|
43
|
-
- "Пакет `release` є постачальником даних для інших Sandbox-середовищ, де може виводитись:"
|
|
44
|
-
- "- Dashboard релізу (таблиці статусів `wip/closed/mixed`)."
|
|
45
|
-
- "- Для цього ми генеруємо `.datasets/README.dataset.jsonl` за допомогою `test:docs`, щоб створювати машиночитабельні знання для LLM-агентів для донавчання або можна користуватись README.md як індексу у реальному часі."
|
|
46
|
-
- "---"
|
|
47
|
-
tasks: []
|
|
48
|
-
- title: "🎨 Фаза 5: Тематизація та Інтерфейс (Theming)"
|
|
49
|
-
content:
|
|
50
|
-
- "_(Може застосовуватися при запуску HTML звітів про реліз або Dashboard)_"
|
|
51
|
-
- "Логіка `release UI` (якщо така з'явиться у додатку `@nan0web/release.app`), буде імпортувати компоненти з `@nan0web/ui-lit` зі строгим слідуванням CSS-змінним (напр. `--co` для кольорів бренду) без створення кастомних стилів."
|
|
52
|
-
- "---"
|
|
53
|
-
tasks: []
|
|
54
|
-
- title: "🏁 Чек-лист готовності пакету (Definition of Done)"
|
|
55
|
-
content: []
|
|
56
|
-
tasks:
|
|
57
|
-
- title: "General"
|
|
58
|
-
content: []
|
|
59
|
-
items:
|
|
60
|
-
- text: "Визначена місія та філософія у `system.md` та `project.md`."
|
|
61
|
-
completed: true
|
|
62
|
-
- text: "Робота з Markdown-нотатками та базова доменна логіка (`Scanner`, `Release`)."
|
|
63
|
-
completed: true
|
|
64
|
-
- text: "Атомарна операція закриття контрактів (`Scanner.close()`)."
|
|
65
|
-
completed: true
|
|
66
|
-
- text: "Всі існуючі специфікації проходять через CLI та `node:test` (22/24 pass, 2 skip)."
|
|
67
|
-
completed: true
|
|
68
|
-
- text: "Прогрес автоматизується зміною статусу `spec.js` (todo/skip/pass)."
|
|
69
|
-
completed: true
|
|
70
|
-
- text: "Усі CLI сценарії для AGRP (spec, close, init, status) завершені."
|
|
71
|
-
completed: false
|
|
72
|
-
- text: "Створені API-ендпоінти для відображення релізного прогресу у React-Dashboard."
|
|
73
|
-
completed: false
|
|
74
|
-
total: 7
|
|
75
|
-
completed: 5
|
|
76
|
-
stats:
|
|
77
|
-
totalTasks: 7
|
|
78
|
-
completedTasks: 5
|
|
79
|
-
progress: 71
|