@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,110 @@
|
|
|
1
|
+
import Markdown, { MDElement, MDHeading1, MDHeading2, MDHeading3 } from "@nan0web/markdown"
|
|
2
|
+
import Person from "./Person.js"
|
|
3
|
+
|
|
4
|
+
class ReleaseDocument extends Markdown {
|
|
5
|
+
/** @type {Person[]} */
|
|
6
|
+
team
|
|
7
|
+
/** @type {Map<string, Person[]>} */
|
|
8
|
+
roles
|
|
9
|
+
/** @type {string} */
|
|
10
|
+
version
|
|
11
|
+
/** @type {Date | undefined} */
|
|
12
|
+
date
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Array} [options.team] - Release team members
|
|
17
|
+
* @param {Map | Array} [options.roles] - Release roles map with team members
|
|
18
|
+
* @param {string} [options.version] - Release version
|
|
19
|
+
* @param {string} [options.date] - Release date
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super()
|
|
23
|
+
const {
|
|
24
|
+
team = [],
|
|
25
|
+
roles = new Map(),
|
|
26
|
+
version = "",
|
|
27
|
+
date
|
|
28
|
+
} = options
|
|
29
|
+
this.version = String(version)
|
|
30
|
+
this.date = date ? new Date(date) : undefined
|
|
31
|
+
this.team = team.map(t => Person.from(t))
|
|
32
|
+
this.roles = new Map(roles)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse release document content
|
|
37
|
+
* Extracts version and date from H1 heading
|
|
38
|
+
* @param {string} input - Markdown content
|
|
39
|
+
* @returns {MDElement[]}
|
|
40
|
+
*/
|
|
41
|
+
parse(input) {
|
|
42
|
+
const result = super.parse(input)
|
|
43
|
+
|
|
44
|
+
const h1s = this.document.filter((el) => el instanceof MDHeading1)
|
|
45
|
+
if (h1s.length !== 1) {
|
|
46
|
+
throw new TypeError("Release document must have exactly one H1 heading with version and date")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const h1Content = h1s[0].content ?? ""
|
|
50
|
+
const [versionPart, ...dateParts] = h1Content.split(" - ")
|
|
51
|
+
this.version = versionPart.trim()
|
|
52
|
+
this.date = new Date(dateParts.join(" - ").trim())
|
|
53
|
+
|
|
54
|
+
const sections = []
|
|
55
|
+
/** @type {{title: string, tasks: Array} | null} */
|
|
56
|
+
let currentSection = null
|
|
57
|
+
for (const el of result) {
|
|
58
|
+
if (el instanceof MDHeading2) {
|
|
59
|
+
currentSection = {
|
|
60
|
+
title: el.content.trim(),
|
|
61
|
+
tasks: []
|
|
62
|
+
}
|
|
63
|
+
sections.push(currentSection)
|
|
64
|
+
}
|
|
65
|
+
else if (el instanceof MDHeading3 && currentSection) {
|
|
66
|
+
const content = el.content.trim()
|
|
67
|
+
const statusMatch = content.match(/^(\w+)\s*(.+)$/)
|
|
68
|
+
const slugMatch = content.match(/\[([^\]]+)\]$/)
|
|
69
|
+
|
|
70
|
+
let status = 'todo'
|
|
71
|
+
let taskContent = content
|
|
72
|
+
let slug = ''
|
|
73
|
+
|
|
74
|
+
if (statusMatch) {
|
|
75
|
+
status = statusMatch[1]
|
|
76
|
+
taskContent = statusMatch[2]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (slugMatch) {
|
|
80
|
+
slug = slugMatch[1]
|
|
81
|
+
taskContent = taskContent.replace(/\[([^\]]+)\]$/, '').trim()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
currentSection.tasks.push({
|
|
85
|
+
content: taskContent,
|
|
86
|
+
status: status,
|
|
87
|
+
slug: slug
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {*} input
|
|
97
|
+
* @returns {ReleaseDocument}
|
|
98
|
+
*/
|
|
99
|
+
static from(input) {
|
|
100
|
+
if (input instanceof ReleaseDocument) return input
|
|
101
|
+
if (typeof input === "string") {
|
|
102
|
+
const doc = new ReleaseDocument()
|
|
103
|
+
doc.parse(input)
|
|
104
|
+
return doc
|
|
105
|
+
}
|
|
106
|
+
return new ReleaseDocument(input)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default ReleaseDocument
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownToTest - generates node:test code from markdown release notes
|
|
3
|
+
* Enables writing release notes first, then generating executable tests
|
|
4
|
+
*/
|
|
5
|
+
class MarkdownToTest {
|
|
6
|
+
/**
|
|
7
|
+
* Convert markdown release notes to node:test code
|
|
8
|
+
* @param {string} markdown - Release notes markdown content
|
|
9
|
+
* @returns {string} Generated test code
|
|
10
|
+
*/
|
|
11
|
+
generateTests(markdown) {
|
|
12
|
+
const lines = markdown.split('\n')
|
|
13
|
+
const versionMatch = markdown.match(/^#\s+(v[\d.]+)\s*-\s*(\d{4}-\d{2}-\d{2})/)
|
|
14
|
+
|
|
15
|
+
if (!versionMatch) {
|
|
16
|
+
throw new Error('Markdown must start with version header: # vX.Y.Z - YYYY-MM-DD')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const [, version, date] = versionMatch
|
|
20
|
+
let testCode = this.generateImports()
|
|
21
|
+
testCode += this.generateDescribeHeader(version, date)
|
|
22
|
+
|
|
23
|
+
const sections = this.extractSections(lines)
|
|
24
|
+
testCode += this.generateSectionTests(sections)
|
|
25
|
+
|
|
26
|
+
testCode += '})\n'
|
|
27
|
+
return testCode
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate import statements for node:test
|
|
32
|
+
* @returns {string} Import code
|
|
33
|
+
*/
|
|
34
|
+
generateImports() {
|
|
35
|
+
return `import { describe, it } from "node:test"
|
|
36
|
+
import { ok, strictEqual } from "node:assert"
|
|
37
|
+
|
|
38
|
+
`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate main describe block header
|
|
43
|
+
* @param {string} version - Release version
|
|
44
|
+
* @param {string} date - Release date
|
|
45
|
+
* @returns {string} Describe header code
|
|
46
|
+
*/
|
|
47
|
+
generateDescribeHeader(version, date) {
|
|
48
|
+
return `describe("${version} - ${date}", () => {
|
|
49
|
+
`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract sections from markdown lines
|
|
54
|
+
* @param {string[]} lines - Markdown lines
|
|
55
|
+
* @returns {Array<{title: string, tasks: Array<{content: string, status: string, slug: string}>}>} Sections with tasks
|
|
56
|
+
*/
|
|
57
|
+
extractSections(lines) {
|
|
58
|
+
/** @type {Array<{title: string, tasks: Array<{content: string, status: string, slug: string}>}>} */
|
|
59
|
+
const sections = []
|
|
60
|
+
/** @type {{title: string, tasks: Array<{content: string, status: string, slug: string}>} | null} */
|
|
61
|
+
let currentSection = null
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
const line = lines[i]
|
|
65
|
+
|
|
66
|
+
// H2 section
|
|
67
|
+
if (line.startsWith('## ')) {
|
|
68
|
+
currentSection = {
|
|
69
|
+
title: line.slice(3).trim(),
|
|
70
|
+
tasks: /** @type {Array<{content: string, status: string, slug: string}>} */([])
|
|
71
|
+
}
|
|
72
|
+
sections.push(currentSection)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// H3 task
|
|
77
|
+
if (line.startsWith('###') && currentSection) {
|
|
78
|
+
const statusMatch = line.match(/^###( Draft| InProgress)? \*\*(.+)\*\* \[(.+)\]$/)
|
|
79
|
+
if (statusMatch) {
|
|
80
|
+
const [, status, content, slug] = statusMatch
|
|
81
|
+
currentSection.tasks.push({
|
|
82
|
+
content,
|
|
83
|
+
status: status ? status.trim() : 'Done',
|
|
84
|
+
slug
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return sections
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate test code for sections and their tasks
|
|
96
|
+
* @param {Array<{title: string, tasks: Array}>} sections - Sections with tasks
|
|
97
|
+
* @returns {string} Generated test code
|
|
98
|
+
*/
|
|
99
|
+
generateSectionTests(sections) {
|
|
100
|
+
let code = ''
|
|
101
|
+
|
|
102
|
+
sections.forEach(section => {
|
|
103
|
+
code += ` describe("${section.title}", () => {
|
|
104
|
+
`
|
|
105
|
+
|
|
106
|
+
section.tasks.forEach(/** @param {{status: string, content: string}} task */task => {
|
|
107
|
+
switch (task.status) {
|
|
108
|
+
case 'Draft':
|
|
109
|
+
code += ` it.todo("${task.content}", () => {
|
|
110
|
+
ok(false, "Draft task - not yet implemented")
|
|
111
|
+
})
|
|
112
|
+
`
|
|
113
|
+
break
|
|
114
|
+
case 'InProgress':
|
|
115
|
+
code += ` it.skip("${task.content}", () => {
|
|
116
|
+
ok(false, "In progress task - not yet completed")
|
|
117
|
+
})
|
|
118
|
+
`
|
|
119
|
+
break
|
|
120
|
+
default: // Done
|
|
121
|
+
code += ` it("${task.content}", () => {
|
|
122
|
+
ok(true, "Task completed successfully")
|
|
123
|
+
})
|
|
124
|
+
`
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
code += ` })
|
|
130
|
+
`
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return code
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default MarkdownToTest
|
|
@@ -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,29 @@
|
|
|
1
|
+
import { HumanName, HumanContact, HumanGender } from "@nan0web/verse"
|
|
2
|
+
|
|
3
|
+
export default class Person {
|
|
4
|
+
/** @type {HumanName} */
|
|
5
|
+
name
|
|
6
|
+
/** @type {HumanGender} */
|
|
7
|
+
gender
|
|
8
|
+
/** @type {HumanContact[]} */
|
|
9
|
+
contacts
|
|
10
|
+
constructor(input = {}) {
|
|
11
|
+
const {
|
|
12
|
+
name = new HumanName([]),
|
|
13
|
+
gender = new HumanGender(-1),
|
|
14
|
+
contacts = []
|
|
15
|
+
} = input
|
|
16
|
+
this.name = HumanName.from(name)
|
|
17
|
+
this.gender = HumanGender.from(gender)
|
|
18
|
+
this.contacts = contacts.map(c => HumanContact.from(c))
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @param {*} input
|
|
22
|
+
* @returns {Person}
|
|
23
|
+
*/
|
|
24
|
+
static from(input) {
|
|
25
|
+
if (input instanceof Person) return input
|
|
26
|
+
return new Person(input)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
package/src/Release.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import ReleaseDocument from "./Release/Document.js"
|
|
2
|
+
import Logger from '@nan0web/log'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} ReleaseConfig
|
|
6
|
+
* @property {string} [version] - Release version
|
|
7
|
+
* @property {string | number | Date | undefined} [createdAt] -
|
|
8
|
+
* @property {string | number | Date | undefined} [startAt] -
|
|
9
|
+
* @property {string | number | Date | undefined} [planAt] -
|
|
10
|
+
* @property {string | number | Date | undefined} [completeAt] -
|
|
11
|
+
* @property {string | ReleaseDocument} [document] -
|
|
12
|
+
* @property {Map<string, Function>} [tasks] - Task map with test functions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Release management class that coordinates task validation and release execution
|
|
17
|
+
*/
|
|
18
|
+
class Release {
|
|
19
|
+
/** @type {string} */
|
|
20
|
+
version
|
|
21
|
+
/** @type {Date} */
|
|
22
|
+
createdAt
|
|
23
|
+
/** @type {Date?} */
|
|
24
|
+
startAt
|
|
25
|
+
/** @type {Date?} */
|
|
26
|
+
planAt
|
|
27
|
+
/** @type {Date?} */
|
|
28
|
+
completeAt
|
|
29
|
+
/** @type {Map<string, Function>} */
|
|
30
|
+
tasks
|
|
31
|
+
/** @type {ReleaseDocument} */
|
|
32
|
+
document
|
|
33
|
+
/** @type {Logger} */
|
|
34
|
+
logger
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a Release instance
|
|
38
|
+
* @param {ReleaseConfig} config - Release configuration
|
|
39
|
+
*/
|
|
40
|
+
constructor(config = {}) {
|
|
41
|
+
const {
|
|
42
|
+
version = '0.0.0',
|
|
43
|
+
createdAt = new Date(),
|
|
44
|
+
startAt,
|
|
45
|
+
planAt,
|
|
46
|
+
completeAt,
|
|
47
|
+
document,
|
|
48
|
+
tasks = new Map()
|
|
49
|
+
} = config
|
|
50
|
+
|
|
51
|
+
this.version = String(version)
|
|
52
|
+
this.createdAt = new Date(createdAt)
|
|
53
|
+
this.startAt = startAt ? new Date(startAt) : null
|
|
54
|
+
this.planAt = planAt ? new Date(planAt) : null
|
|
55
|
+
this.completeAt = completeAt ? new Date(completeAt) : null
|
|
56
|
+
this.document = ReleaseDocument.from(document)
|
|
57
|
+
this.tasks = tasks
|
|
58
|
+
this.logger = new Logger()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get path() {
|
|
62
|
+
return [...this.version.replace("v", "").split(".").slice(0, 2), this.version].join("/")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate release by running all task tests
|
|
67
|
+
* @returns {Promise<Object>} Validation results
|
|
68
|
+
*/
|
|
69
|
+
async validate() {
|
|
70
|
+
const results = {
|
|
71
|
+
passed: /** @type {Array} */([]),
|
|
72
|
+
failed: /** @type {Array<{taskId: any, error: string}>} */([]),
|
|
73
|
+
pending: /** @type {Array} */([])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const [taskId, testFn] of this.tasks.entries()) {
|
|
77
|
+
try {
|
|
78
|
+
await testFn()
|
|
79
|
+
results.passed.push(taskId)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
results.failed.push({ taskId, error: /** @type {Error} */(error).message })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return results
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Execute release process
|
|
90
|
+
* @param {Object} options - Release options
|
|
91
|
+
* @param {boolean} [options.ignoreFailTests=false] - Ignore failed tests
|
|
92
|
+
* @returns {Promise<boolean>} Release success status
|
|
93
|
+
*/
|
|
94
|
+
async execute(options = {}) {
|
|
95
|
+
const { ignoreFailTests = false } = options
|
|
96
|
+
const validation = await this.validate()
|
|
97
|
+
|
|
98
|
+
if (!ignoreFailTests && validation.failed.length > 0) {
|
|
99
|
+
this.logger.error(`Release blocked: ${validation.failed.length} tasks failed`)
|
|
100
|
+
for (const failure of validation.failed) {
|
|
101
|
+
this.logger.error(` ${failure.taskId}: ${failure.error}`)
|
|
102
|
+
}
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.logger.info(`Release ${this.version} executed successfully`)
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get release progress statistics
|
|
112
|
+
* @returns {Promise<Object>} Progress statistics
|
|
113
|
+
*/
|
|
114
|
+
async getProgress() {
|
|
115
|
+
const validation = await this.validate()
|
|
116
|
+
const totalTasks = this.tasks.size
|
|
117
|
+
const completedTasks = validation.passed.length
|
|
118
|
+
const failedTasks = validation.failed.length
|
|
119
|
+
const pendingTasks = validation.pending.length
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
version: this.version,
|
|
123
|
+
total: totalTasks,
|
|
124
|
+
completed: completedTasks,
|
|
125
|
+
failed: failedTasks,
|
|
126
|
+
pending: pendingTasks,
|
|
127
|
+
progress: totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default Release
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from '@nan0web/co'
|
|
4
|
+
import Logger from '@nan0web/log'
|
|
5
|
+
import FS from "@nan0web/db-fs"
|
|
6
|
+
import DB from "./db/ReleaseDB.js"
|
|
7
|
+
import Release from './Release.js'
|
|
8
|
+
import ChatCommand from './commands/ChatCommand.js'
|
|
9
|
+
import HostCommand from './commands/HostCommand.js'
|
|
10
|
+
import InitCommand from './commands/InitCommand.js'
|
|
11
|
+
import ListCommand from './commands/ListCommand.js'
|
|
12
|
+
import SealCommand from './commands/SealCommand.js'
|
|
13
|
+
import ServeCommand from './commands/ServeCommand.js'
|
|
14
|
+
import ShowCommand from './commands/ShowCommand.js'
|
|
15
|
+
import AppCommandMessage from './co/AppCommandMessage.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CLI application for release management
|
|
19
|
+
*/
|
|
20
|
+
export default class ReleaseCLI extends Command {
|
|
21
|
+
static Message = AppCommandMessage
|
|
22
|
+
/** @type {Logger} */
|
|
23
|
+
logger
|
|
24
|
+
/** @type {DB?} */
|
|
25
|
+
db
|
|
26
|
+
/** @type {FS} */
|
|
27
|
+
fs
|
|
28
|
+
/** @type {Record<string, Release>} */
|
|
29
|
+
releases = {}
|
|
30
|
+
/** @type {Release?} */
|
|
31
|
+
current = null
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
super({
|
|
35
|
+
name: 'release',
|
|
36
|
+
help: 'Release management CLI tool'
|
|
37
|
+
})
|
|
38
|
+
this.logger = new Logger({ icons: true })
|
|
39
|
+
this.fs = new FS()
|
|
40
|
+
this.db = null
|
|
41
|
+
this.addOption('releaseDir', String, '.', 'Release dir')
|
|
42
|
+
this.addOption('version', String, '0.0.0', 'Release version')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string[] | string} argv
|
|
47
|
+
* @returns {AppCommandMessage}
|
|
48
|
+
*/
|
|
49
|
+
parse(argv = []) {
|
|
50
|
+
return AppCommandMessage.from(super.parse(argv))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run CLI application
|
|
55
|
+
* @param {string[]} argv - Command line arguments
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
async run(argv) {
|
|
59
|
+
const msg = this.parse(argv)
|
|
60
|
+
await this.fs.connect()
|
|
61
|
+
|
|
62
|
+
const rootDb = this.fs.extract(/** @type {string} */(msg.opts.releaseDir))
|
|
63
|
+
|
|
64
|
+
const indexStat = await rootDb.statDocument("index.js")
|
|
65
|
+
const currentStat = await rootDb.statDocument("current.js")
|
|
66
|
+
if (indexStat.exists && currentStat.exists) {
|
|
67
|
+
const indexJs = rootDb.absolute("index.js")
|
|
68
|
+
const currentJs = rootDb.absolute("current.js")
|
|
69
|
+
try {
|
|
70
|
+
this.releases = (await import(indexJs)).default
|
|
71
|
+
this.current = (await import(currentJs)).default
|
|
72
|
+
this.logger.info(Object.keys(this.releases).length, "release(s) found")
|
|
73
|
+
} catch (e) {
|
|
74
|
+
this.logger.error("Failed to load release database", e)
|
|
75
|
+
this.current = null
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
this.logger.info("No releases initialized. Use 'release init <version>' to create your first release.")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const globalOpts = { logger: this.logger }
|
|
82
|
+
this.addSubcommand(new ListCommand(globalOpts))
|
|
83
|
+
this.addSubcommand(new HostCommand(globalOpts))
|
|
84
|
+
this.addSubcommand(new ServeCommand(globalOpts))
|
|
85
|
+
|
|
86
|
+
if (this.current) {
|
|
87
|
+
this.logger.info("Current release", this.current.version)
|
|
88
|
+
this.db = DB.from(rootDb.extract(this.current.path))
|
|
89
|
+
await this.db.connect()
|
|
90
|
+
const document = await this.db.loadDocument("release.md")
|
|
91
|
+
const release = new Release({ document, version: this.current.version })
|
|
92
|
+
const currentOpts = { ...globalOpts, db: this.db }
|
|
93
|
+
this.addSubcommand(new InitCommand(currentOpts))
|
|
94
|
+
this.addSubcommand(new ShowCommand(currentOpts))
|
|
95
|
+
this.addSubcommand(new ChatCommand(currentOpts))
|
|
96
|
+
this.addSubcommand(new SealCommand(currentOpts))
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.logger.info("Provide a release version")
|
|
100
|
+
this.logger.info(this.generateHelp())
|
|
101
|
+
}
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async loadReleaseFiles() {
|
|
106
|
+
if (!this.db) return
|
|
107
|
+
const stream = this.db.findStream("")
|
|
108
|
+
const started = Date.now()
|
|
109
|
+
for await (const entry of stream) {
|
|
110
|
+
const spent = ((Date.now() - started) / 1000).toFixed(3)
|
|
111
|
+
this.logger.clearLine(this.logger.cursorUp())
|
|
112
|
+
this.logger.info("Loading ..", spent + "s", this.db.data.size, entry.file.path)
|
|
113
|
+
}
|
|
114
|
+
const spent = ((Date.now() - started) / 1000).toFixed(3)
|
|
115
|
+
this.logger.clearLine(this.logger.cursorUp(/** @type {DB} */(this.db).data.size & 1))
|
|
116
|
+
const info = [
|
|
117
|
+
["Loaded in", spent + "s", /** @type {DB} */(this.db).data.size, "document(s)"],
|
|
118
|
+
["", "", /** @type {DB} */(this.db).releases.length, "release(s)"],
|
|
119
|
+
]
|
|
120
|
+
this.logger.table(info, [], { padding: 1 })
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/Team.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { HumanName, HumanGender, HumanContact } from "@nan0web/verse"
|
|
2
|
+
|
|
3
|
+
class Person {
|
|
4
|
+
/** @type {HumanName} */
|
|
5
|
+
name
|
|
6
|
+
/** @type {HumanGender} */
|
|
7
|
+
gender
|
|
8
|
+
/** @type {HumanContact[]} */
|
|
9
|
+
contacts
|
|
10
|
+
|
|
11
|
+
constructor(name) {
|
|
12
|
+
this.name = new HumanName(name)
|
|
13
|
+
this.gender = new HumanGender(-1)
|
|
14
|
+
this.contacts = []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class CLevelTeam {
|
|
19
|
+
static name = "C Level"
|
|
20
|
+
static ceo = new Person(["Yaroslav", "Wise"])
|
|
21
|
+
static cfo = new Person(["Chingis"])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class UXTeam {
|
|
25
|
+
static name = "UX"
|
|
26
|
+
static designer = new Person(["Taras", "Shevchenko"])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class Company {
|
|
30
|
+
static name = "NaN•Web"
|
|
31
|
+
static c = CLevelTeam
|
|
32
|
+
static ux = UXTeam
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default Company
|