@nan0web/release 1.0.1 → 1.0.3

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.
Files changed (109) hide show
  1. package/README.md +24 -42
  2. package/bin/nan0release.js +27 -42
  3. package/package.json +48 -42
  4. package/project.yaml +79 -0
  5. package/src/README.md.js +90 -93
  6. package/src/Release/Document.js +13 -19
  7. package/src/Release/MarkdownToTest.js +35 -33
  8. package/src/Release/Person.js +3 -8
  9. package/src/Release/Scanner.js +179 -0
  10. package/src/Release.js +19 -12
  11. package/src/Team.js +9 -9
  12. package/src/architecture/ProjectManagementAsCode.js +8 -8
  13. package/src/cli/index.js +9 -0
  14. package/src/co/AppCommandOptions.js +4 -6
  15. package/src/commands/CloseCommand.js +67 -0
  16. package/src/commands/Command.js +48 -0
  17. package/src/commands/DepsCommand.js +143 -0
  18. package/src/commands/InitCommand.js +48 -56
  19. package/src/commands/PublishCommand.js +80 -119
  20. package/src/commands/SpecCommand.js +41 -0
  21. package/src/commands/StatusCommand.js +26 -0
  22. package/src/db/ReleaseDB.js +5 -5
  23. package/src/index.js +18 -0
  24. package/src/ui/cli/ReleaseCLi.js +52 -0
  25. package/src/utils/runSpawn.js +0 -0
  26. package/types/Release/Document.d.ts +2 -2
  27. package/types/Release/Person.d.ts +3 -3
  28. package/types/Release/Scanner.d.ts +90 -0
  29. package/types/Release.d.ts +37 -33
  30. package/types/ReleaseCLI.d.ts +29 -28
  31. package/types/Team.d.ts +3 -3
  32. package/types/architecture/ProjectManagementAsCode.d.ts +1 -1
  33. package/types/cli/index.d.ts +2 -0
  34. package/types/co/AppCommandMessage.d.ts +10 -11
  35. package/types/co/AppCommandOptions.d.ts +1 -2
  36. package/types/commands/BaseCommand.d.ts +23 -0
  37. package/types/commands/ChatCommand.d.ts +9 -9
  38. package/types/commands/ChatMessage.d.ts +36 -0
  39. package/types/commands/CloseCommand.d.ts +25 -0
  40. package/types/commands/Command.d.ts +28 -0
  41. package/types/commands/DepsCommand.d.ts +32 -0
  42. package/types/commands/DepsMessage.d.ts +28 -0
  43. package/types/commands/HostCommand.d.ts +7 -7
  44. package/types/commands/HostMessage.d.ts +44 -0
  45. package/types/commands/InitCommand.d.ts +24 -19
  46. package/types/commands/InitMessage.d.ts +24 -0
  47. package/types/commands/ListCommand.d.ts +8 -8
  48. package/types/commands/ListMessage.d.ts +30 -0
  49. package/types/commands/PublishCommand.d.ts +36 -47
  50. package/types/commands/PublishMessage.d.ts +39 -0
  51. package/types/commands/SealCommand.d.ts +7 -7
  52. package/types/commands/SealMessage.d.ts +28 -0
  53. package/types/commands/ServeCommand.d.ts +7 -7
  54. package/types/commands/ServeMessage.d.ts +30 -0
  55. package/types/commands/ShowCommand.d.ts +7 -7
  56. package/types/commands/SpecCommand.d.ts +25 -0
  57. package/types/commands/StatusCommand.d.ts +9 -0
  58. package/types/commands/SubCommand.d.ts +9 -7
  59. package/types/commands/ValidateCommand.d.ts +7 -7
  60. package/types/commands/ValidateMessage.d.ts +20 -0
  61. package/types/db/ReleaseDB.d.ts +2 -2
  62. package/types/index.d.ts +10 -0
  63. package/types/ui/cli/ReleaseCLi.d.ts +10 -0
  64. package/types/ui/cli/index.d.ts +0 -0
  65. package/types/utils/runSpawn.d.ts +0 -0
  66. package/.datasets/README.dataset.jsonl +0 -19
  67. package/.editorconfig +0 -20
  68. package/CHANGELOG.md +0 -6
  69. package/CONTRIBUTING.md +0 -42
  70. package/bin/commands/InitCommand.js +0 -10
  71. package/bin/lib/functions.js +0 -240
  72. package/bin/lib/functions.test.js +0 -138
  73. package/bin/release.js +0 -12
  74. package/bin/release.test.js +0 -73
  75. package/bin/temp/package.json +0 -1
  76. package/docs/system.uk.md +0 -413
  77. package/docs/uk/README.md +0 -284
  78. package/legacy/PrepareCommand.js +0 -154
  79. package/legacy/PrepareCommand.test.js +0 -189
  80. package/legacy/changelog.js +0 -446
  81. package/releases/1/0/v1.0.0/models/Person.js +0 -26
  82. package/releases/1/0/v1.0.0/models/Person.test.js +0 -44
  83. package/releases/1/0/v1.0.0/models/company.js +0 -11
  84. package/releases/1/0/v1.0.0/models/members.js +0 -14
  85. package/releases/1/0/v1.0.0/models/members.test.js +0 -21
  86. package/releases/1/0/v1.0.0/models/teams/DevTeam.js +0 -6
  87. package/releases/1/0/v1.0.0/models/teams/DevTeam.test.js +0 -15
  88. package/releases/1/0/v1.0.0/models/teams/index.js +0 -5
  89. package/releases/1/0/v1.0.0/release.js +0 -12
  90. package/releases/1/0/v1.0.0/release.md +0 -121
  91. package/releases/1/0/v1.0.0/release.test.js +0 -36
  92. package/releases/1/0/v1.0.0/test/YaRaSLove/git-native.test.js +0 -14
  93. package/releases/current.js +0 -3
  94. package/releases/index.js +0 -11
  95. package/src/Release/MarkdownToTest.test.js +0 -60
  96. package/src/ReleaseCLI.js +0 -122
  97. package/src/architecture/ProjectManagementAsCode.test.js +0 -128
  98. package/src/co/AppCommandMessage.js +0 -22
  99. package/src/commands/ChatCommand.js +0 -56
  100. package/src/commands/HostCommand.js +0 -63
  101. package/src/commands/ListCommand.js +0 -56
  102. package/src/commands/SealCommand.js +0 -51
  103. package/src/commands/ServeCommand.js +0 -53
  104. package/src/commands/ShowCommand.js +0 -58
  105. package/src/commands/SubCommand.js +0 -21
  106. package/src/commands/ValidateCommand.js +0 -66
  107. package/system.md +0 -571
  108. package/tsconfig.json +0 -23
  109. /package/{bin/lib/temp/pnpm-lock.yaml → src/ui/cli/index.js} +0 -0
@@ -11,22 +11,22 @@ class MarkdownToTest {
11
11
  generateTests(markdown) {
12
12
  const lines = markdown.split('\n')
13
13
  const versionMatch = markdown.match(/^#\s+(v[\d.]+)\s*-\s*(\d{4}-\d{2}-\d{2})/)
14
-
14
+
15
15
  if (!versionMatch) {
16
16
  throw new Error('Markdown must start with version header: # vX.Y.Z - YYYY-MM-DD')
17
17
  }
18
-
18
+
19
19
  const [, version, date] = versionMatch
20
20
  let testCode = this.generateImports()
21
21
  testCode += this.generateDescribeHeader(version, date)
22
-
22
+
23
23
  const sections = this.extractSections(lines)
24
24
  testCode += this.generateSectionTests(sections)
25
-
25
+
26
26
  testCode += '})\n'
27
27
  return testCode
28
28
  }
29
-
29
+
30
30
  /**
31
31
  * Generate import statements for node:test
32
32
  * @returns {string} Import code
@@ -37,7 +37,7 @@ import { ok, strictEqual } from "node:assert"
37
37
 
38
38
  `
39
39
  }
40
-
40
+
41
41
  /**
42
42
  * Generate main describe block header
43
43
  * @param {string} version - Release version
@@ -48,7 +48,7 @@ import { ok, strictEqual } from "node:assert"
48
48
  return `describe("${version} - ${date}", () => {
49
49
  `
50
50
  }
51
-
51
+
52
52
  /**
53
53
  * Extract sections from markdown lines
54
54
  * @param {string[]} lines - Markdown lines
@@ -59,20 +59,20 @@ import { ok, strictEqual } from "node:assert"
59
59
  const sections = []
60
60
  /** @type {{title: string, tasks: Array<{content: string, status: string, slug: string}>} | null} */
61
61
  let currentSection = null
62
-
62
+
63
63
  for (let i = 0; i < lines.length; i++) {
64
64
  const line = lines[i]
65
-
65
+
66
66
  // H2 section
67
67
  if (line.startsWith('## ')) {
68
68
  currentSection = {
69
69
  title: line.slice(3).trim(),
70
- tasks: /** @type {Array<{content: string, status: string, slug: string}>} */([])
70
+ tasks: /** @type {Array<{content: string, status: string, slug: string}>} */ ([]),
71
71
  }
72
72
  sections.push(currentSection)
73
73
  continue
74
74
  }
75
-
75
+
76
76
  // H3 task
77
77
  if (line.startsWith('###') && currentSection) {
78
78
  const statusMatch = line.match(/^###( Draft| InProgress)? \*\*(.+)\*\* \[(.+)\]$/)
@@ -81,16 +81,16 @@ import { ok, strictEqual } from "node:assert"
81
81
  currentSection.tasks.push({
82
82
  content,
83
83
  status: status ? status.trim() : 'Done',
84
- slug
84
+ slug,
85
85
  })
86
86
  }
87
87
  continue
88
88
  }
89
89
  }
90
-
90
+
91
91
  return sections
92
92
  }
93
-
93
+
94
94
  /**
95
95
  * Generate test code for sections and their tasks
96
96
  * @param {Array<{title: string, tasks: Array}>} sections - Sections with tasks
@@ -98,40 +98,42 @@ import { ok, strictEqual } from "node:assert"
98
98
  */
99
99
  generateSectionTests(sections) {
100
100
  let code = ''
101
-
102
- sections.forEach(section => {
101
+
102
+ sections.forEach((section) => {
103
103
  code += ` describe("${section.title}", () => {
104
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}", () => {
105
+
106
+ section.tasks.forEach(
107
+ /** @param {{status: string, content: string}} task */ (task) => {
108
+ switch (task.status) {
109
+ case 'Draft':
110
+ code += ` it.todo("${task.content}", () => {
110
111
  ok(false, "Draft task - not yet implemented")
111
112
  })
112
113
  `
113
- break
114
- case 'InProgress':
115
- code += ` it.skip("${task.content}", () => {
114
+ break
115
+ case 'InProgress':
116
+ code += ` it.skip("${task.content}", () => {
116
117
  ok(false, "In progress task - not yet completed")
117
118
  })
118
119
  `
119
- break
120
- default: // Done
121
- code += ` it("${task.content}", () => {
120
+ break
121
+ default: // Done
122
+ code += ` it("${task.content}", () => {
122
123
  ok(true, "Task completed successfully")
123
124
  })
124
125
  `
125
- break
126
- }
127
- })
128
-
126
+ break
127
+ }
128
+ },
129
+ )
130
+
129
131
  code += ` })
130
132
  `
131
133
  })
132
-
134
+
133
135
  return code
134
136
  }
135
137
  }
136
138
 
137
- export default MarkdownToTest
139
+ export default MarkdownToTest
@@ -1,4 +1,4 @@
1
- import { HumanName, HumanContact, HumanGender } from "@nan0web/verse"
1
+ import { HumanName, HumanContact, HumanGender } from '@nan0web/verse'
2
2
 
3
3
  export default class Person {
4
4
  /** @type {HumanName} */
@@ -8,14 +8,10 @@ export default class Person {
8
8
  /** @type {HumanContact[]} */
9
9
  contacts
10
10
  constructor(input = {}) {
11
- const {
12
- name = new HumanName([]),
13
- gender = new HumanGender(-1),
14
- contacts = []
15
- } = input
11
+ const { name = new HumanName([]), gender = new HumanGender(-1), contacts = [] } = input
16
12
  this.name = HumanName.from(name)
17
13
  this.gender = HumanGender.from(gender)
18
- this.contacts = contacts.map(c => HumanContact.from(c))
14
+ this.contacts = contacts.map((c) => HumanContact.from(c))
19
15
  }
20
16
  /**
21
17
  * @param {*} input
@@ -26,4 +22,3 @@ export default class Person {
26
22
  return new Person(input)
27
23
  }
28
24
  }
29
-
@@ -0,0 +1,179 @@
1
+ import { readdirSync, existsSync, renameSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+
4
+ /**
5
+ * @typedef {Object} ContractEntry
6
+ * @property {string} version - Release version (e.g. "v1.0.0")
7
+ * @property {string} path - Absolute path to the contract file
8
+ * @property {string} type - "spec" or "test"
9
+ * @property {string} dir - Directory containing the contract
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} VersionStatus
14
+ * @property {string} version - Release version
15
+ * @property {'wip' | 'closed' | 'mixed'} state - Version state
16
+ * @property {string[]} specs - Paths to spec files
17
+ * @property {string[]} tests - Paths to test files
18
+ */
19
+
20
+ /**
21
+ * Рекурсивний сканер для пошуку release контрактів.
22
+ * Підтримує вкладену структуру: releases/{major}/{minor}/v{version}/
23
+ * та плоску: releases/v{version}/
24
+ */
25
+ export default class Scanner {
26
+ /** @type {string} */
27
+ root
28
+
29
+ /**
30
+ * @param {string} root - Root directory of the project
31
+ */
32
+ constructor(root) {
33
+ this.root = resolve(root)
34
+ }
35
+
36
+ /**
37
+ * Find all .spec.js files (WIP contracts)
38
+ * @param {string} [version] - Optional version filter
39
+ * @returns {ContractEntry[]}
40
+ */
41
+ findSpecs(version) {
42
+ return this.#scan(join(this.root, 'releases'), '.spec.js', version)
43
+ }
44
+
45
+ /**
46
+ * Find all .test.js files (Closed contracts)
47
+ * @param {string} [version] - Optional version filter
48
+ * @returns {ContractEntry[]}
49
+ */
50
+ findTests(version) {
51
+ const oldTests = this.#scan(join(this.root, 'releases'), '.test.js', version)
52
+ const newTests = this.#scan(join(this.root, 'src', 'releases'), '.test.js', version)
53
+ return [...oldTests, ...newTests]
54
+ }
55
+
56
+ /**
57
+ * Get status of all versions
58
+ * @returns {VersionStatus[]}
59
+ */
60
+ status() {
61
+ const specs = this.findSpecs()
62
+ const tests = this.findTests()
63
+
64
+ /** @type {Map<string, VersionStatus>} */
65
+ const map = new Map()
66
+
67
+ for (const entry of [...specs, ...tests]) {
68
+ let status = map.get(entry.version)
69
+ if (!status) {
70
+ status = {
71
+ version: entry.version,
72
+ state: 'wip',
73
+ specs: [],
74
+ tests: [],
75
+ }
76
+ map.set(entry.version, status)
77
+ }
78
+ if (entry.type === 'spec') status.specs.push(entry.path)
79
+ else status.tests.push(entry.path)
80
+ }
81
+
82
+ for (const status of map.values()) {
83
+ if (status.specs.length > 0 && status.tests.length > 0) {
84
+ status.state = 'mixed'
85
+ } else if (status.specs.length === 0 && status.tests.length > 0) {
86
+ status.state = 'closed'
87
+ } else {
88
+ status.state = 'wip'
89
+ }
90
+ }
91
+
92
+ return [...map.values()].sort((a, b) => a.version.localeCompare(b.version))
93
+ }
94
+
95
+ /**
96
+ * Close a release: rename all .spec.js → .test.js
97
+ * @param {string} [version] - Version to close. If omitted, closes all WIP.
98
+ * @returns {{ closed: number, files: string[] }}
99
+ */
100
+ close(version) {
101
+ const specs = this.findSpecs(version)
102
+
103
+ if (version && specs.length === 0) {
104
+ // Check if the version exists at all (might already be closed)
105
+ const tests = this.findTests(version)
106
+ if (tests.length === 0) {
107
+ throw new Error(`Version ${version} not found in releases/`)
108
+ }
109
+ return { closed: 0, files: [] }
110
+ }
111
+
112
+ const files = []
113
+ for (const spec of specs) {
114
+ const newPath = spec.path.replace('.spec.js', '.test.js')
115
+ renameSync(spec.path, newPath)
116
+ files.push(newPath)
117
+ }
118
+
119
+ return { closed: files.length, files }
120
+ }
121
+
122
+ /**
123
+ * Recursively scan directory for contract files
124
+ * @param {string} baseDir - Directory to scan
125
+ * @param {string} suffix - File suffix (.spec.js or .test.js)
126
+ * @param {string} [version] - Optional version filter
127
+ * @returns {ContractEntry[]}
128
+ */
129
+ #scan(baseDir, suffix, version) {
130
+ if (!existsSync(baseDir)) return []
131
+
132
+ /** @type {ContractEntry[]} */
133
+ const results = []
134
+ this.#walkDir(baseDir, suffix, version, results)
135
+ return results
136
+ }
137
+
138
+ /**
139
+ * @param {string} dir
140
+ * @param {string} suffix
141
+ * @param {string | undefined} version
142
+ * @param {ContractEntry[]} results
143
+ */
144
+ #walkDir(dir, suffix, version, results) {
145
+ let entries
146
+ try {
147
+ entries = readdirSync(dir, { withFileTypes: true })
148
+ } catch {
149
+ return
150
+ }
151
+
152
+ // Check if this directory is a version directory (starts with 'v' and has dots)
153
+ const dirName = dir.split('/').pop() || ''
154
+ const isVersionDir = /^v\d+\.\d+\.\d+/.test(dirName)
155
+
156
+ if (isVersionDir) {
157
+ if (version && dirName !== version) return
158
+
159
+ for (const entry of entries) {
160
+ if (entry.isFile() && entry.name.endsWith(suffix)) {
161
+ results.push({
162
+ version: dirName,
163
+ path: join(dir, entry.name),
164
+ type: suffix === '.spec.js' ? 'spec' : 'test',
165
+ dir,
166
+ })
167
+ }
168
+ }
169
+ return
170
+ }
171
+
172
+ // Recurse into subdirectories
173
+ for (const entry of entries) {
174
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
175
+ this.#walkDir(join(dir, entry.name), suffix, version, results)
176
+ }
177
+ }
178
+ }
179
+ }
package/src/Release.js CHANGED
@@ -1,10 +1,11 @@
1
- import ReleaseDocument from "./Release/Document.js"
1
+ import ReleaseDocument from './Release/Document.js'
2
2
  import Logger from '@nan0web/log'
3
3
 
4
4
  /**
5
5
  * @typedef {Object} ReleaseConfig
6
6
  * @property {string} [version] - Release version
7
7
  * @property {string | number | Date | undefined} [createdAt] -
8
+ * @property {string | number | Date | undefined} [date] - Alias for createdAt
8
9
  * @property {string | number | Date | undefined} [startAt] -
9
10
  * @property {string | number | Date | undefined} [planAt] -
10
11
  * @property {string | number | Date | undefined} [completeAt] -
@@ -15,7 +16,7 @@ import Logger from '@nan0web/log'
15
16
  /**
16
17
  * Release management class that coordinates task validation and release execution
17
18
  */
18
- class Release {
19
+ export default class Release {
19
20
  /** @type {string} */
20
21
  version
21
22
  /** @type {Date} */
@@ -40,12 +41,12 @@ class Release {
40
41
  constructor(config = {}) {
41
42
  const {
42
43
  version = '0.0.0',
43
- createdAt = new Date(),
44
+ createdAt = config.date || new Date(),
44
45
  startAt,
45
46
  planAt,
46
47
  completeAt,
47
48
  document,
48
- tasks = new Map()
49
+ tasks = new Map(),
49
50
  } = config
50
51
 
51
52
  this.version = String(version)
@@ -56,10 +57,18 @@ class Release {
56
57
  this.document = ReleaseDocument.from(document)
57
58
  this.tasks = tasks
58
59
  this.logger = new Logger()
60
+
61
+ // Store additional config properties
62
+ for (const [key, value] of Object.entries(config)) {
63
+ if (!(key in this)) {
64
+ /** @ts-ignore */
65
+ this[key] = value
66
+ }
67
+ }
59
68
  }
60
69
 
61
70
  get path() {
62
- return [...this.version.replace("v", "").split(".").slice(0, 2), this.version].join("/")
71
+ return [...this.version.replace('v', '').split('.').slice(0, 2), this.version].join('/')
63
72
  }
64
73
 
65
74
  /**
@@ -68,9 +77,9 @@ class Release {
68
77
  */
69
78
  async validate() {
70
79
  const results = {
71
- passed: /** @type {Array} */([]),
72
- failed: /** @type {Array<{taskId: any, error: string}>} */([]),
73
- pending: /** @type {Array} */([])
80
+ passed: /** @type {Array} */ ([]),
81
+ failed: /** @type {Array<{taskId: any, error: string}>} */ ([]),
82
+ pending: /** @type {Array} */ ([]),
74
83
  }
75
84
 
76
85
  for (const [taskId, testFn] of this.tasks.entries()) {
@@ -78,7 +87,7 @@ class Release {
78
87
  await testFn()
79
88
  results.passed.push(taskId)
80
89
  } catch (error) {
81
- results.failed.push({ taskId, error: /** @type {Error} */(error).message })
90
+ results.failed.push({ taskId, error: /** @type {Error} */ (error).message })
82
91
  }
83
92
  }
84
93
 
@@ -124,9 +133,7 @@ class Release {
124
133
  completed: completedTasks,
125
134
  failed: failedTasks,
126
135
  pending: pendingTasks,
127
- progress: totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0
136
+ progress: totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0,
128
137
  }
129
138
  }
130
139
  }
131
-
132
- export default Release
package/src/Team.js CHANGED
@@ -1,4 +1,4 @@
1
- import { HumanName, HumanGender, HumanContact } from "@nan0web/verse"
1
+ import { HumanName, HumanGender, HumanContact } from '@nan0web/verse'
2
2
 
3
3
  class Person {
4
4
  /** @type {HumanName} */
@@ -7,7 +7,7 @@ class Person {
7
7
  gender
8
8
  /** @type {HumanContact[]} */
9
9
  contacts
10
-
10
+
11
11
  constructor(name) {
12
12
  this.name = new HumanName(name)
13
13
  this.gender = new HumanGender(-1)
@@ -16,20 +16,20 @@ class Person {
16
16
  }
17
17
 
18
18
  class CLevelTeam {
19
- static name = "C Level"
20
- static ceo = new Person(["Yaroslav", "Wise"])
21
- static cfo = new Person(["Chingis"])
19
+ static name = 'C Level'
20
+ static ceo = new Person(['Yaroslav', 'Wise'])
21
+ static cfo = new Person(['Chingis'])
22
22
  }
23
23
 
24
24
  class UXTeam {
25
- static name = "UX"
26
- static designer = new Person(["Taras", "Shevchenko"])
25
+ static name = 'UX'
26
+ static designer = new Person(['Taras', 'Shevchenko'])
27
27
  }
28
28
 
29
29
  class Company {
30
- static name = "NaN•Web"
30
+ static name = 'NaN•Web'
31
31
  static c = CLevelTeam
32
32
  static ux = UXTeam
33
33
  }
34
34
 
35
- export default Company
35
+ export default Company
@@ -78,9 +78,9 @@ export class ProjectManagement {
78
78
  */
79
79
  async validateProjectState() {
80
80
  const results = {
81
- passed: /** @type {string[]} */([]),
82
- failed: /** @type {string[]} */([]),
83
- pending: /** @type {Array<{taskId: string, error: string}>} */([])
81
+ passed: /** @type {string[]} */ ([]),
82
+ failed: /** @type {string[]} */ ([]),
83
+ pending: /** @type {Array<{taskId: string, error: string}>} */ ([]),
84
84
  }
85
85
 
86
86
  for (const [taskId, testPath] of this.tasks.entries()) {
@@ -92,7 +92,7 @@ export class ProjectManagement {
92
92
  results.failed.push(taskId)
93
93
  }
94
94
  } catch (error) {
95
- results.pending.push({ taskId, error: /** @type {Error} */(error).message })
95
+ results.pending.push({ taskId, error: /** @type {Error} */ (error).message })
96
96
  }
97
97
  }
98
98
 
@@ -110,7 +110,7 @@ export class ProjectManagement {
110
110
  return {
111
111
  passed: true,
112
112
  duration: 0,
113
- assertions: 0
113
+ assertions: 0,
114
114
  }
115
115
  }
116
116
 
@@ -129,9 +129,9 @@ export class ProjectManagement {
129
129
  total: this.tasks.size,
130
130
  completed: projectState.passed.length,
131
131
  failed: projectState.failed.length,
132
- pending: projectState.pending.length
132
+ pending: projectState.pending.length,
133
133
  },
134
- ready: projectState.failed.length === 0
134
+ ready: projectState.failed.length === 0,
135
135
  }
136
136
  }
137
137
  }
@@ -171,7 +171,7 @@ export class ReleaseManager {
171
171
  return {
172
172
  version,
173
173
  validation,
174
- published: await this.publish(version, options)
174
+ published: await this.publish(version, options),
175
175
  }
176
176
  }
177
177
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Export the UI‑CLI based release CLI.
3
+ *
4
+ * @module release/cli
5
+ */
6
+
7
+ import createReleaseCLI from '../ui/cli/ReleaseCLi.js'
8
+
9
+ export default createReleaseCLI
@@ -1,4 +1,4 @@
1
- class AppCommandOptions {
1
+ export default class AppCommandOptions {
2
2
  /** @type {boolean} */
3
3
  webui = false
4
4
  /** @type {boolean} */
@@ -6,16 +6,16 @@ class AppCommandOptions {
6
6
  /** @type {boolean} */
7
7
  quiet = false
8
8
  /** @type {string} */
9
- releaseDir = "./releases"
9
+ releaseDir = './releases'
10
10
  /** @type {boolean} */
11
11
  write = false
12
12
  /** @type {string} */
13
- user = "anonymous"
13
+ user = 'anonymous'
14
14
  /** @type {boolean} */
15
15
  ignoreFailTests = false
16
16
  /** @type {boolean} */
17
17
  full = false
18
-
18
+
19
19
  /**
20
20
  * @param {Object} input
21
21
  */
@@ -23,5 +23,3 @@ class AppCommandOptions {
23
23
  Object.assign(this, input)
24
24
  }
25
25
  }
26
-
27
- export default AppCommandOptions
@@ -0,0 +1,67 @@
1
+ import { OutputMessage } from '@nan0web/co'
2
+ import Command from './Command.js'
3
+ import process from 'node:process'
4
+ import Scanner from '../Release/Scanner.js'
5
+
6
+ class CloseBody {
7
+ static version = { help: 'Release version', defaultValue: '' }
8
+ /** @type {string} */
9
+ version = ''
10
+ constructor({ version = '' } = {}) {
11
+ this.version = String(version)
12
+ }
13
+ }
14
+
15
+ export default class CloseCommand extends Command {
16
+ static name = 'close'
17
+ static help = 'Close a release and move tests to src/'
18
+ static Body = CloseBody
19
+
20
+ /** @param {Partial<Command> & { body?: Partial<CloseBody> }} [input={}] */
21
+ constructor(input = {}) {
22
+ super(input)
23
+ this.body = new CloseBody(/** @type {*} */ (input))
24
+ }
25
+
26
+ async *run() {
27
+ const v = this.body.version || ''
28
+
29
+ yield new OutputMessage(`🛜 nan0release close ${v || 'all'}`)
30
+
31
+ const scanner = new Scanner(process.cwd())
32
+ const specs = scanner.findSpecs(v)
33
+
34
+ if (specs.length === 0) {
35
+ yield new OutputMessage(`No pending specs found for version ${v || 'all'}`)
36
+ return
37
+ }
38
+
39
+ yield new OutputMessage(`Running ${specs.length} pending specs...`)
40
+
41
+ const files = specs.map((s) => s.path)
42
+ await this._run('node', ['--test', ...files], 'Cannot close: Specs failed')
43
+
44
+ yield new OutputMessage(`✅ Specs passed. Closing...`)
45
+
46
+ let movedCount = 0
47
+ const { renameSync, mkdirSync } = await import('node:fs')
48
+ const { join, dirname } = await import('node:path')
49
+
50
+ for (const spec of specs) {
51
+ const relativePath = spec.path.substring(process.cwd().length + 1)
52
+ const destRelative = relativePath
53
+ .replace(/^releases\//, 'src/releases/')
54
+ .replace('.spec.js', '.test.js')
55
+ const destPath = join(process.cwd(), destRelative)
56
+
57
+ mkdirSync(dirname(destPath), { recursive: true })
58
+ renameSync(spec.path, destPath)
59
+ movedCount++
60
+ }
61
+
62
+ yield new OutputMessage({
63
+ content: [`✅ Successfully closed ${movedCount} specs and moved to src/releases/`],
64
+ type: OutputMessage.TYPES.SUCCESS,
65
+ })
66
+ }
67
+ }
@@ -0,0 +1,48 @@
1
+ import DBFS from '@nan0web/db-fs'
2
+ import { OutputMessage } from '@nan0web/co'
3
+ import Logger from '@nan0web/log'
4
+ import { runSpawn } from '@nan0web/test'
5
+ import { UiMessage } from '@nan0web/ui'
6
+
7
+ /** @typedef {import("@nan0web/test/types/exec/runSpawn").SpawnResult} SpawnResult */
8
+
9
+ export default class Command extends UiMessage {
10
+ /** @type {DBFS} */
11
+ fs
12
+ /** @type {Logger} */
13
+ logger
14
+
15
+ /**
16
+ * @param {Partial<UiMessage> & { }} [input={}]
17
+ */
18
+ constructor(input = {}) {
19
+ super(input)
20
+ const { fs, logger } = UiMessage.parseBody(input, this.Body)
21
+ this.fs = fs ?? DBFS.from({})
22
+ this.logger = Logger.from(logger ?? {})
23
+ }
24
+ get Body() {
25
+ return /** @type {typeof Command} */ (this.constructor).Body
26
+ }
27
+ /**
28
+ * Run the command.
29
+ * @param {string} cmd
30
+ * @param {string[]} [args=[]]
31
+ * @param {string} [fail=""]
32
+ * @returns {Promise<SpawnResult>}
33
+ * @throws {Error}
34
+ */
35
+ async _run(cmd, args = [], fail = '') {
36
+ const result = await runSpawn(cmd, args)
37
+ if (0 !== result.code) {
38
+ this.logger.error(result.error)
39
+ throw new Error(fail)
40
+ }
41
+ return result
42
+ }
43
+
44
+ /** @returns {AsyncGenerator<OutputMessage>} */
45
+ async *run() {
46
+ throw new Error('Must be extended')
47
+ }
48
+ }