@nan0web/release 3.3.0 → 3.5.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.
Files changed (94) hide show
  1. package/README.md +0 -35
  2. package/bin/nan0release.js +33 -2
  3. package/bin/serve.js +73 -0
  4. package/package.json +46 -17
  5. package/src/Release/Document.js +1 -0
  6. package/src/Release/Scanner.js +3 -2
  7. package/src/Release.js +7 -7
  8. package/src/domain/App.js +33 -2
  9. package/src/domain/BumpCommand.js +10 -3
  10. package/src/domain/CheckCommand.js +504 -146
  11. package/src/domain/CheckCommand.story.js +92 -0
  12. package/src/domain/CloseCommand.js +13 -9
  13. package/src/domain/Dashboard/Dashboard.story.js +155 -0
  14. package/src/domain/Dashboard/DashboardModel.js +308 -0
  15. package/src/domain/Dashboard/ReleaseDiscovery.js +132 -0
  16. package/src/domain/Dashboard/ReleaseParser.js +226 -0
  17. package/src/domain/Dashboard/index.js +19 -0
  18. package/src/domain/DepsCommand.js +5 -2
  19. package/src/domain/InitCommand.js +18 -16
  20. package/src/domain/ListCommand.js +45 -0
  21. package/src/domain/PMaCParser.js +418 -0
  22. package/src/domain/PMaCParser.story.js +152 -0
  23. package/src/domain/PreviewCommand.js +39 -0
  24. package/src/domain/PublishCommand.js +7 -3
  25. package/src/domain/Registry/Registry.story.js +82 -0
  26. package/src/domain/Registry/RegistryModel.js +124 -0
  27. package/src/domain/ServeCommand.js +165 -0
  28. package/src/domain/ServeCommand.story.js +88 -0
  29. package/src/domain/SpecCommand.js +3 -1
  30. package/src/domain/StatusCommand.js +249 -3
  31. package/src/domain/ValidateCommand.js +426 -0
  32. package/src/domain/ValidateCommand.story.js +92 -0
  33. package/src/domain/ViewCommand.js +99 -0
  34. package/src/domain/WebCommand.js +140 -0
  35. package/src/domain/inspect/ReleaseAuditor.js +508 -0
  36. package/src/domain/inspect/ReleaseAuditor.story.js +136 -0
  37. package/src/domain/models/ReleaseIssueModel.js +27 -0
  38. package/src/domain/models/ReleaseMemberModel.js +21 -0
  39. package/src/domain/models/ReleaseModel.js +191 -0
  40. package/src/domain/models/ReleaseParityModel.js +27 -0
  41. package/src/domain/models/ReleaseTaskModel.js +33 -0
  42. package/src/domain/models/index.js +6 -0
  43. package/src/domain/templates/release.html +34 -0
  44. package/src/domain/templates/release.html.template.js +34 -0
  45. package/src/index.js +15 -0
  46. package/src/inspectors/inspect-js.js +32 -0
  47. package/src/releases/1/0/v1.0.3/task.test.js +1 -1
  48. package/src/utils/nodejs/GitTelemetry.js +154 -0
  49. package/types/Release/Document.d.ts +24 -27
  50. package/types/Release/MarkdownToTest.d.ts +1 -1
  51. package/types/Release/Person.d.ts +9 -7
  52. package/types/Release/Scanner.d.ts +41 -41
  53. package/types/Release.d.ts +42 -42
  54. package/types/architecture/ProjectManagementAsCode.d.ts +59 -64
  55. package/types/domain/App.d.ts +25 -7
  56. package/types/domain/BumpCommand.d.ts +11 -6
  57. package/types/domain/CheckCommand.d.ts +128 -16
  58. package/types/domain/CheckCommand.story.d.ts +1 -0
  59. package/types/domain/CloseCommand.d.ts +4 -6
  60. package/types/domain/Dashboard/Dashboard.story.d.ts +1 -0
  61. package/types/domain/Dashboard/DashboardModel.d.ts +273 -0
  62. package/types/domain/Dashboard/GitTelemetry.d.ts +23 -0
  63. package/types/domain/Dashboard/ReleaseDiscovery.d.ts +47 -0
  64. package/types/domain/Dashboard/ReleaseParser.d.ts +79 -0
  65. package/types/domain/Dashboard/index.d.ts +3 -0
  66. package/types/domain/DepsCommand.d.ts +6 -6
  67. package/types/domain/InitCommand.d.ts +4 -6
  68. package/types/domain/ListCommand.d.ts +18 -0
  69. package/types/domain/PMaCParser.d.ts +64 -0
  70. package/types/domain/PMaCParser.story.d.ts +1 -0
  71. package/types/domain/PreviewCommand.d.ts +6 -0
  72. package/types/domain/PublishCommand.d.ts +8 -6
  73. package/types/domain/Registry/Registry.story.d.ts +1 -0
  74. package/types/domain/Registry/RegistryModel.d.ts +61 -0
  75. package/types/domain/ServeCommand.d.ts +59 -0
  76. package/types/domain/ServeCommand.story.d.ts +1 -0
  77. package/types/domain/SpecCommand.d.ts +4 -6
  78. package/types/domain/StatusCommand.d.ts +38 -3
  79. package/types/domain/ValidateCommand.d.ts +69 -0
  80. package/types/domain/ValidateCommand.story.d.ts +1 -0
  81. package/types/domain/ViewCommand.d.ts +27 -0
  82. package/types/domain/WebCommand.d.ts +47 -0
  83. package/types/domain/inspect/ReleaseAuditor.d.ts +134 -0
  84. package/types/domain/inspect/ReleaseAuditor.story.d.ts +1 -0
  85. package/types/domain/models/ReleaseIssueModel.d.ts +35 -0
  86. package/types/domain/models/ReleaseMemberModel.d.ts +25 -0
  87. package/types/domain/models/ReleaseModel.d.ts +122 -0
  88. package/types/domain/models/ReleaseParityModel.d.ts +37 -0
  89. package/types/domain/models/ReleaseTaskModel.d.ts +46 -0
  90. package/types/domain/models/index.d.ts +5 -0
  91. package/types/domain/templates/release.html.template.d.ts +2 -0
  92. package/types/index.d.ts +21 -10
  93. package/types/inspectors/inspect-js.d.ts +2 -0
  94. package/types/utils/nodejs/GitTelemetry.d.ts +23 -0
@@ -0,0 +1,82 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import DB from '@nan0web/db'
4
+ import { RegistryModel } from './RegistryModel.js'
5
+
6
+ describe('RegistryModel OLMUI Scenario', () => {
7
+ it('parses releases.txt and extracts valid project paths ignoring comments and empty lines', async () => {
8
+ const rawContent = `# Global Registry
9
+ .
10
+ # packages
11
+ packages/release
12
+
13
+ # apps
14
+ apps/release.app
15
+ apps/3rdparty/industrialbank/bank
16
+ `
17
+ const db = new DB({
18
+ predefined: [
19
+ ['releases.txt', rawContent],
20
+ ['package.json', { name: '@nan0web/monorepo', version: '1.0.0' }],
21
+ ['packages/release/package.json', { name: '@nan0web/release', version: '3.3.0' }],
22
+ ['apps/release.app/package.json', { name: 'release.app', version: '0.1.0' }],
23
+ ['apps/3rdparty/industrialbank/bank/package.json', { name: '@industrialbank/bank', version: '3.3.0' }],
24
+ ],
25
+ })
26
+ await db.connect()
27
+
28
+ const registry = new RegistryModel({ filePath: 'releases.txt' }, { db })
29
+ const projects = await registry.loadProjects()
30
+
31
+ assert.equal(projects.length, 4)
32
+ assert.deepEqual(projects[0], {
33
+ path: '.',
34
+ name: '@nan0web/monorepo',
35
+ version: '1.0.0',
36
+ })
37
+ assert.deepEqual(projects[1], {
38
+ path: 'packages/release',
39
+ name: '@nan0web/release',
40
+ version: '3.3.0',
41
+ })
42
+ assert.deepEqual(projects[2], {
43
+ path: 'apps/release.app',
44
+ name: 'release.app',
45
+ version: '0.1.0',
46
+ })
47
+ assert.deepEqual(projects[3], {
48
+ path: 'apps/3rdparty/industrialbank/bank',
49
+ name: '@industrialbank/bank',
50
+ version: '3.3.0',
51
+ })
52
+ })
53
+
54
+ it('handles projects without package.json gracefully', async () => {
55
+ const db = new DB({
56
+ predefined: [
57
+ ['releases.txt', 'packages/custom-pkg\n'],
58
+ ],
59
+ })
60
+ await db.connect()
61
+
62
+ const registry = new RegistryModel({ filePath: 'releases.txt' }, { db })
63
+ const projects = await registry.loadProjects()
64
+
65
+ assert.equal(projects.length, 1)
66
+ assert.equal(projects[0].name, 'packages/custom-pkg')
67
+ assert.equal(projects[0].version, '0.0.0')
68
+ })
69
+
70
+ it('parses raw text paths via static parsePaths utility', () => {
71
+ const text = `
72
+ # Comment 1
73
+ /path/one
74
+
75
+ /path/two # inline comment
76
+ # Comment 2
77
+ /path/three
78
+ `
79
+ const paths = RegistryModel.parsePaths(text)
80
+ assert.deepEqual(paths, ['/path/one', '/path/two', '/path/three'])
81
+ })
82
+ })
@@ -0,0 +1,124 @@
1
+ import { Model } from '@nan0web/types'
2
+
3
+ /**
4
+ * @typedef {Object} ProjectEntry
5
+ * @property {string} path - Project relative or absolute path
6
+ * @property {string} name - Package name from package.json or folder basename
7
+ * @property {string} version - Package version from package.json
8
+ */
9
+
10
+ /**
11
+ * RegistryModel - PM-as-Code Project Registry manager.
12
+ * Reads project paths from releases.txt and discovers package metadata.
13
+ */
14
+ export class RegistryModel extends Model {
15
+ static filePath = {
16
+ help: 'Path to releases.txt registry file',
17
+ default: 'releases.txt',
18
+ type: 'string',
19
+ }
20
+
21
+ static projects = {
22
+ help: 'List of registered projects',
23
+ default: () => [],
24
+ type: 'array',
25
+ }
26
+
27
+ static UI = {
28
+ fileNotFound: 'Registry file {$path} not found',
29
+ loadingProjects: 'Loading projects from registry {$path}...',
30
+ projectsLoaded: 'Loaded {$count} projects from registry',
31
+ }
32
+
33
+ /**
34
+ * @param {Partial<RegistryModel>} [data]
35
+ * @param {import('@nan0web/types').ModelOptions} [options]
36
+ */
37
+ constructor(data = {}, options = {}) {
38
+ super(data, options)
39
+ /** @type {string} Path to registry file */ this.filePath
40
+ /** @type {ProjectEntry[]} List of registered projects */ this.projects
41
+ }
42
+
43
+ /**
44
+ * Parse raw content of releases.txt into array of clean paths
45
+ * @param {string} content
46
+ * @returns {string[]}
47
+ */
48
+ static parsePaths(content = '') {
49
+ if (typeof content !== 'string') return []
50
+ return content
51
+ .split('\n')
52
+ .map((line) => {
53
+ // Strip comments starting with #
54
+ const withoutComment = line.split('#')[0]
55
+ return withoutComment.trim()
56
+ })
57
+ .filter((line) => line.length > 0)
58
+ }
59
+
60
+ /**
61
+ * Load project entries from registry file using injected DB instance
62
+ * @returns {Promise<ProjectEntry[]>}
63
+ */
64
+ async loadProjects() {
65
+ const db = this._.db
66
+ if (!db) {
67
+ throw new Error('Database instance is required in options ({ db }) to load projects')
68
+ }
69
+
70
+ let content = await db.loadDocument(this.filePath, '')
71
+ let rootPrefix = ''
72
+ if (!content && this.filePath === 'releases.txt') {
73
+ try {
74
+ const parent2 = await db.loadDocument('../../releases.txt', '')
75
+ if (parent2) {
76
+ content = parent2
77
+ rootPrefix = '../../'
78
+ } else {
79
+ const parent1 = await db.loadDocument('../releases.txt', '')
80
+ if (parent1) {
81
+ content = parent1
82
+ rootPrefix = '../'
83
+ }
84
+ }
85
+ } catch {
86
+ // ignore
87
+ }
88
+ }
89
+
90
+ const paths = RegistryModel.parsePaths(String(content || ''))
91
+
92
+ /** @type {ProjectEntry[]} */
93
+ const result = []
94
+
95
+ for (const projectPath of paths) {
96
+ const effectivePath = rootPrefix
97
+ ? projectPath === '.'
98
+ ? rootPrefix.slice(0, -1)
99
+ : `${rootPrefix}${projectPath}`
100
+ : projectPath
101
+ const pkgPath = effectivePath === '.' ? 'package.json' : `${effectivePath.replace(/\/+$/, '')}/package.json`
102
+ let pkg = null
103
+ try {
104
+ pkg = await db.loadDocument(pkgPath, null)
105
+ } catch {
106
+ pkg = null
107
+ }
108
+
109
+ const name = pkg?.name || projectPath
110
+ const version = pkg?.version || '0.0.0'
111
+
112
+ result.push({
113
+ path: projectPath,
114
+ name,
115
+ version,
116
+ })
117
+ }
118
+
119
+ this.projects = result
120
+ return result
121
+ }
122
+ }
123
+
124
+ export default RegistryModel
@@ -0,0 +1,165 @@
1
+ import { ModelAsApp, result, show } from '@nan0web/ui'
2
+ import Markdown from '@nan0web/markdown'
3
+ import DEFAULT_TEMPLATE from './templates/release.html.template.js'
4
+
5
+ /**
6
+ * ServeCommand - Universal ModelAsApp for data or release catalogs.
7
+ *
8
+ * Generates HTML content from README.md and release templates using $db and Markdown.
9
+ * Pure OLMUI model: does not start HTTP server, does not call node:fs or child_process.
10
+ *
11
+ * Usage:
12
+ * pnpm exec serve releases/ # Runs via CLI bootstrap
13
+ */
14
+ export default class ServeCommand extends ModelAsApp {
15
+ static alias = 'serve'
16
+
17
+ static UI = {
18
+ title: 'serve',
19
+ help: 'Serve data or releases catalog over HTTP',
20
+ serving: '🌐 Serving {$dir} on http://localhost:{$port} (Press Ctrl+C to stop)',
21
+ dirNotFound: 'Directory {$dir} does not exist.',
22
+ }
23
+
24
+ static dir = {
25
+ help: 'Directory to serve (e.g. releases/ or data/)',
26
+ positional: true,
27
+ type: 'string',
28
+ default: 'releases',
29
+ }
30
+
31
+ static port = {
32
+ help: 'HTTP server port',
33
+ default: 3131,
34
+ type: 'number',
35
+ alias: 'p',
36
+ }
37
+
38
+ static open = {
39
+ help: 'Automatically open in browser',
40
+ default: true,
41
+ type: 'boolean',
42
+ }
43
+
44
+ /**
45
+ * @param {Partial<ServeCommand>} [data]
46
+ * @param {import('@nan0web/types').ModelOptions} [options]
47
+ */
48
+ constructor(data = {}, options = {}) {
49
+ super(data, options)
50
+ /** @type {string} */
51
+ this.dir = String(data.dir || (options && 'dir' in options ? /** @type {any} */ (options).dir : 'releases'))
52
+ /** @type {number} */
53
+ this.port = Number(data.port ?? (options && 'port' in options ? /** @type {any} */ (options).port : 3131))
54
+ /** @type {boolean} */
55
+ this.open = Boolean(data.open ?? (options && 'open' in options ? /** @type {any} */ (options).open : true))
56
+ }
57
+
58
+ /**
59
+ * Convert markdown string to HTML using @nan0web/markdown
60
+ * @param {string} mdRaw
61
+ * @returns {string}
62
+ */
63
+ renderMarkdownToHtml(mdRaw) {
64
+ if (!mdRaw || typeof mdRaw !== 'string') return ''
65
+ const md = new Markdown(mdRaw)
66
+ return md.stringify()
67
+ }
68
+
69
+ /**
70
+ * Render HTML content for the target directory
71
+ * @returns {Promise<string>}
72
+ */
73
+ async renderHtml() {
74
+ const db = this.$db
75
+ let readmeContent = ''
76
+ const readmePath = db.resolveSync ? db.resolveSync(this.dir, 'README.md') : `${this.dir}/README.md`
77
+
78
+ try {
79
+ const mdRaw = await db.loadDocument(readmePath, null)
80
+ if (mdRaw) {
81
+ readmeContent = this.renderMarkdownToHtml(typeof mdRaw === 'string' ? mdRaw : mdRaw.content || String(mdRaw))
82
+ }
83
+ } catch {}
84
+
85
+ if (!readmeContent) {
86
+ // If no README.md in directory, scan directory entries and list them nicely
87
+ let listItems = []
88
+ try {
89
+ if (typeof db.listDir === 'function') {
90
+ const entries = await db.listDir(this.dir)
91
+ if (entries && Array.isArray(entries)) {
92
+ listItems = entries
93
+ .map((e) => e?.name || e?.path || String(e))
94
+ .filter((name) => !name.startsWith('.'))
95
+ }
96
+ }
97
+ } catch {}
98
+
99
+ const title = this.dir === 'releases' ? 'Releases Catalog' : `Index of ${this.dir}`
100
+ let md = `# ${title}\n\n`
101
+ if (listItems.length > 0) {
102
+ md += `Found **${listItems.length}** item(s) in \`${this.dir}\`:\n\n`
103
+ for (const item of listItems) {
104
+ md += `- **\`${item}\`**\n`
105
+ }
106
+ } else {
107
+ md += `Directory \`${this.dir}\` is currently empty or contains no catalog entries.\n`
108
+ }
109
+ readmeContent = this.renderMarkdownToHtml(md)
110
+ }
111
+
112
+ // Load template via DB or fallback to bundled template
113
+ let templateRaw = null
114
+ try {
115
+ templateRaw = await db.loadDocument('packages/release/src/domain/templates/release.html', null)
116
+ } catch {}
117
+
118
+ if (!templateRaw) {
119
+ templateRaw = DEFAULT_TEMPLATE
120
+ }
121
+
122
+ return String(templateRaw).replace('{{CONTENT}}', readmeContent)
123
+ }
124
+
125
+ async *run() {
126
+ const db = this.$db
127
+
128
+ // Check if directory exists via db.listDir or db.stat
129
+ let dirExists = false
130
+ try {
131
+ if (typeof db.listDir === 'function') {
132
+ const entries = await db.listDir(this.dir)
133
+ dirExists = entries && entries.length > 0
134
+ }
135
+ if (!dirExists && typeof db.stat === 'function') {
136
+ const stat = await db.stat(this.dir)
137
+ dirExists = Boolean(stat && (stat.exists || stat.isDirectory || stat.isFile))
138
+ }
139
+ } catch {
140
+ dirExists = false
141
+ }
142
+
143
+ if (!dirExists) {
144
+ yield show(ServeCommand.UI.dirNotFound.replace('{$dir}', this.dir), 'error')
145
+ return result({ success: false })
146
+ }
147
+
148
+ const html = await this.renderHtml()
149
+
150
+ yield show(
151
+ ServeCommand.UI.serving.replace('{$dir}', this.dir).replace('{$port}', String(this.port)),
152
+ 'success'
153
+ )
154
+
155
+ return result({
156
+ success: true,
157
+ dir: this.dir,
158
+ port: this.port,
159
+ open: this.open,
160
+ html,
161
+ })
162
+ }
163
+ }
164
+
165
+ export { ServeCommand }
@@ -0,0 +1,88 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import DB from '@nan0web/db'
4
+ import ServeCommand from './ServeCommand.js'
5
+
6
+ describe('ServeCommand OLMUI Scenario & Model Test', () => {
7
+ it('generates HTML from releases/README.md when directory exists', async () => {
8
+ const readmeMd = `# Releases
9
+
10
+ This is the main catalog of releases.
11
+
12
+ ## Active Projects
13
+ - @nan0web/ui
14
+ - @nan0web/db
15
+ `
16
+ const templateHtml = `<!DOCTYPE html><html><body><main>{{CONTENT}}</main></body></html>`
17
+
18
+ const db = new DB({
19
+ predefined: [
20
+ ['releases/README.md', readmeMd],
21
+ ['packages/release/src/domain/templates/release.html', templateHtml],
22
+ ],
23
+ })
24
+ await db.connect()
25
+
26
+ const cmd = new ServeCommand({ dir: 'releases', port: 3131 }, { db })
27
+ const gen = cmd.run()
28
+
29
+ let step = await gen.next()
30
+ const outputs = []
31
+ while (!step.done) {
32
+ outputs.push(step.value)
33
+ step = await gen.next()
34
+ }
35
+
36
+ const res = step.value?.data || step.value
37
+ assert.ok(res.success !== false, 'Execution should succeed')
38
+ assert.equal(res.dir, 'releases')
39
+ assert.equal(res.port, 3131)
40
+ assert.ok(typeof res.html === 'string', 'Should return html string')
41
+ assert.ok(res.html.includes('Releases'), 'HTML should contain rendered markdown title')
42
+ assert.ok(res.html.includes('main'), 'HTML should use the template')
43
+ })
44
+
45
+ it('yields error when target directory does not exist', async () => {
46
+ const db = new DB({
47
+ predefined: [],
48
+ })
49
+ await db.connect()
50
+
51
+ const cmd = new ServeCommand({ dir: 'nonexistent-dir' }, { db })
52
+ const gen = cmd.run()
53
+
54
+ let step = await gen.next()
55
+ const outputs = []
56
+ while (!step.done) {
57
+ outputs.push(step.value)
58
+ step = await gen.next()
59
+ }
60
+
61
+ const res = step.value?.data || step.value
62
+ assert.equal(res.success, false)
63
+ assert.ok(outputs.some((o) => o?.type === 'show' || o?.tag === 'show' || String(o).includes('Directory') || o?.content?.includes('nonexistent-dir')))
64
+ })
65
+
66
+ it('handles missing README.md with fallback content', async () => {
67
+ const templateHtml = `<!DOCTYPE html><html><body>{{CONTENT}}</body></html>`
68
+ const db = new DB({
69
+ predefined: [
70
+ ['releases/.gitkeep', ''],
71
+ ['packages/release/src/domain/templates/release.html', templateHtml],
72
+ ],
73
+ })
74
+ await db.connect()
75
+
76
+ const cmd = new ServeCommand({ dir: 'releases' }, { db })
77
+ const gen = cmd.run()
78
+
79
+ let step = await gen.next()
80
+ while (!step.done) {
81
+ step = await gen.next()
82
+ }
83
+
84
+ const res = step.value?.data || step.value
85
+ assert.ok(res.success !== false)
86
+ assert.ok(res.html.includes('Releases'))
87
+ })
88
+ })
@@ -3,6 +3,9 @@ import { runSpawn } from '@nan0web/test'
3
3
  import Scanner from '../Release/Scanner.js'
4
4
 
5
5
  export default class SpecCommand extends ModelAsApp {
6
+ /** @type {string} Release version filter */
7
+ version = ''
8
+
6
9
  static version = {
7
10
  help: 'Release version filter',
8
11
  default: '',
@@ -18,7 +21,6 @@ export default class SpecCommand extends ModelAsApp {
18
21
  */
19
22
  constructor(data = {}, options = {}) {
20
23
  super(data, options)
21
- /** @type {string} Release version filter */ this.version
22
24
  }
23
25
 
24
26
  /**