@nan0web/release 1.0.1 → 1.0.2

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 (108) hide show
  1. package/README.md +24 -42
  2. package/bin/nan0release.js +27 -42
  3. package/package.json +21 -14
  4. package/src/Release/Document.js +13 -19
  5. package/src/Release/MarkdownToTest.js +35 -33
  6. package/src/Release/Person.js +3 -8
  7. package/src/Release/Scanner.js +179 -0
  8. package/src/Release.js +19 -12
  9. package/src/Team.js +9 -9
  10. package/src/architecture/ProjectManagementAsCode.js +8 -8
  11. package/src/cli/index.js +9 -0
  12. package/src/co/AppCommandOptions.js +4 -6
  13. package/src/commands/CloseCommand.js +67 -0
  14. package/src/commands/Command.js +48 -0
  15. package/src/commands/DepsCommand.js +143 -0
  16. package/src/commands/InitCommand.js +48 -56
  17. package/src/commands/PublishCommand.js +80 -119
  18. package/src/commands/SpecCommand.js +41 -0
  19. package/src/commands/StatusCommand.js +26 -0
  20. package/src/db/ReleaseDB.js +5 -5
  21. package/src/index.js +18 -0
  22. package/src/ui/cli/ReleaseCLi.js +52 -0
  23. package/src/utils/runSpawn.js +0 -0
  24. package/types/Release/Document.d.ts +2 -2
  25. package/types/Release/Person.d.ts +3 -3
  26. package/types/Release/Scanner.d.ts +90 -0
  27. package/types/Release.d.ts +37 -33
  28. package/types/ReleaseCLI.d.ts +29 -28
  29. package/types/Team.d.ts +3 -3
  30. package/types/architecture/ProjectManagementAsCode.d.ts +1 -1
  31. package/types/cli/index.d.ts +2 -0
  32. package/types/co/AppCommandMessage.d.ts +10 -11
  33. package/types/co/AppCommandOptions.d.ts +1 -2
  34. package/types/commands/BaseCommand.d.ts +23 -0
  35. package/types/commands/ChatCommand.d.ts +9 -9
  36. package/types/commands/ChatMessage.d.ts +36 -0
  37. package/types/commands/CloseCommand.d.ts +25 -0
  38. package/types/commands/Command.d.ts +28 -0
  39. package/types/commands/DepsCommand.d.ts +32 -0
  40. package/types/commands/DepsMessage.d.ts +28 -0
  41. package/types/commands/HostCommand.d.ts +7 -7
  42. package/types/commands/HostMessage.d.ts +44 -0
  43. package/types/commands/InitCommand.d.ts +24 -19
  44. package/types/commands/InitMessage.d.ts +24 -0
  45. package/types/commands/ListCommand.d.ts +8 -8
  46. package/types/commands/ListMessage.d.ts +30 -0
  47. package/types/commands/PublishCommand.d.ts +36 -47
  48. package/types/commands/PublishMessage.d.ts +39 -0
  49. package/types/commands/SealCommand.d.ts +7 -7
  50. package/types/commands/SealMessage.d.ts +28 -0
  51. package/types/commands/ServeCommand.d.ts +7 -7
  52. package/types/commands/ServeMessage.d.ts +30 -0
  53. package/types/commands/ShowCommand.d.ts +7 -7
  54. package/types/commands/SpecCommand.d.ts +25 -0
  55. package/types/commands/StatusCommand.d.ts +9 -0
  56. package/types/commands/SubCommand.d.ts +9 -7
  57. package/types/commands/ValidateCommand.d.ts +7 -7
  58. package/types/commands/ValidateMessage.d.ts +20 -0
  59. package/types/db/ReleaseDB.d.ts +2 -2
  60. package/types/index.d.ts +10 -0
  61. package/types/ui/cli/ReleaseCLi.d.ts +10 -0
  62. package/types/ui/cli/index.d.ts +0 -0
  63. package/types/utils/runSpawn.d.ts +0 -0
  64. package/.datasets/README.dataset.jsonl +0 -19
  65. package/.editorconfig +0 -20
  66. package/CHANGELOG.md +0 -6
  67. package/CONTRIBUTING.md +0 -42
  68. package/bin/commands/InitCommand.js +0 -10
  69. package/bin/lib/functions.js +0 -240
  70. package/bin/lib/functions.test.js +0 -138
  71. package/bin/release.js +0 -12
  72. package/bin/release.test.js +0 -73
  73. package/bin/temp/package.json +0 -1
  74. package/docs/system.uk.md +0 -413
  75. package/docs/uk/README.md +0 -284
  76. package/legacy/PrepareCommand.js +0 -154
  77. package/legacy/PrepareCommand.test.js +0 -189
  78. package/legacy/changelog.js +0 -446
  79. package/releases/1/0/v1.0.0/models/Person.js +0 -26
  80. package/releases/1/0/v1.0.0/models/Person.test.js +0 -44
  81. package/releases/1/0/v1.0.0/models/company.js +0 -11
  82. package/releases/1/0/v1.0.0/models/members.js +0 -14
  83. package/releases/1/0/v1.0.0/models/members.test.js +0 -21
  84. package/releases/1/0/v1.0.0/models/teams/DevTeam.js +0 -6
  85. package/releases/1/0/v1.0.0/models/teams/DevTeam.test.js +0 -15
  86. package/releases/1/0/v1.0.0/models/teams/index.js +0 -5
  87. package/releases/1/0/v1.0.0/release.js +0 -12
  88. package/releases/1/0/v1.0.0/release.md +0 -121
  89. package/releases/1/0/v1.0.0/release.test.js +0 -36
  90. package/releases/1/0/v1.0.0/test/YaRaSLove/git-native.test.js +0 -14
  91. package/releases/current.js +0 -3
  92. package/releases/index.js +0 -11
  93. package/src/README.md.js +0 -468
  94. package/src/Release/MarkdownToTest.test.js +0 -60
  95. package/src/ReleaseCLI.js +0 -122
  96. package/src/architecture/ProjectManagementAsCode.test.js +0 -128
  97. package/src/co/AppCommandMessage.js +0 -22
  98. package/src/commands/ChatCommand.js +0 -56
  99. package/src/commands/HostCommand.js +0 -63
  100. package/src/commands/ListCommand.js +0 -56
  101. package/src/commands/SealCommand.js +0 -51
  102. package/src/commands/ServeCommand.js +0 -53
  103. package/src/commands/ShowCommand.js +0 -58
  104. package/src/commands/SubCommand.js +0 -21
  105. package/src/commands/ValidateCommand.js +0 -66
  106. package/system.md +0 -571
  107. package/tsconfig.json +0 -23
  108. /package/{bin/lib/temp/pnpm-lock.yaml → src/ui/cli/index.js} +0 -0
@@ -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
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * DepsCommand – Message‑based version of the old `DepsCommand`.
3
+ *
4
+ * Checks and optionally fixes workspace / link dependencies.
5
+ *
6
+ * Body schema:
7
+ * fix – boolean – whether to apply fixes (default: false)
8
+ *
9
+ * @module release/commands/DepsCommand
10
+ */
11
+
12
+ import { Message, OutputMessage } from '@nan0web/co'
13
+
14
+ import Command from './Command.js'
15
+
16
+ class DepsBody {
17
+ /** @type {boolean} */
18
+ fix = false
19
+ static fix = {
20
+ help: 'Apply fixes automatically',
21
+ defaultValue: false,
22
+ }
23
+ /** @type {boolean} */
24
+ latest = false
25
+ static latest = {
26
+ help: 'Update @nan0web packages to the latests available versions',
27
+ defaultValue: false,
28
+ }
29
+ constructor(input = {}) {
30
+ const { fix = this.fix, latest = this.latest } = Message.parseBody(input, DepsBody)
31
+ this.fix = Boolean(fix)
32
+ this.latest = Boolean(latest)
33
+ }
34
+ }
35
+
36
+ export default class DepsCommand extends Command {
37
+ static name = 'deps'
38
+ static help = 'Dependencies manager'
39
+ static Body = DepsBody
40
+ /** @type {DepsBody} */
41
+ body
42
+
43
+ /** @param {Partial<DepsBody>} [input={}] */
44
+ constructor(input = {}) {
45
+ super({})
46
+ this.body = Command.parseBody(input, DepsBody)
47
+ }
48
+
49
+ /**
50
+ * @param {DepsCommand} [msg]
51
+ * @returns {AsyncGenerator<OutputMessage>}
52
+ */
53
+ async *run(msg = new DepsCommand()) {
54
+ const pkg = await this.fs.loadDocument('package.json', {})
55
+ yield new OutputMessage(['Loaded package.json', JSON.stringify(pkg).length + ' bytes'])
56
+ const deps = {
57
+ devDependencies: pkg?.devDependencies ?? {},
58
+ dependencies: pkg?.dependencies ?? {},
59
+ peerDependencies: pkg?.peerDependencies ?? {},
60
+ }
61
+ const fixes = new Map()
62
+ const fails = new Set()
63
+
64
+ const isLocal = (ver) =>
65
+ ['workspace:', 'link:'].some((p) => String(ver).toLowerCase().startsWith(p))
66
+
67
+ const changes = new Map()
68
+
69
+ for (const [group, map] of Object.entries(deps)) {
70
+ yield new OutputMessage(`${group} (${Object.keys(map).length} items)`)
71
+ for (const [dep, ver] of Object.entries(map ?? {})) {
72
+ if (isLocal(ver) || msg.body.latest) {
73
+ const result = await this._run(
74
+ 'npm',
75
+ ['info', dep, '--json'],
76
+ 'Cannot retrieve npm package info',
77
+ )
78
+ try {
79
+ const data = JSON.parse(result.text)
80
+ if (data.version) {
81
+ fixes.set(dep, data.version)
82
+ yield new OutputMessage({
83
+ content: [` ${dep}: ${ver} → ${data.version}`],
84
+ type: OutputMessage.TYPES.WARNING,
85
+ })
86
+ } else {
87
+ fails.add(dep)
88
+ yield new OutputMessage({
89
+ content: [` ${dep}: 404 — not found`],
90
+ type: OutputMessage.TYPES.ERROR,
91
+ })
92
+ }
93
+ } catch (/** @type {any} */ err) {
94
+ yield new OutputMessage(err)
95
+ }
96
+ } else {
97
+ yield new OutputMessage({
98
+ content: [` ${dep}: ${ver}`],
99
+ type: OutputMessage.TYPES.SUCCESS,
100
+ })
101
+ }
102
+ }
103
+ }
104
+
105
+ if (fails.size) {
106
+ yield new OutputMessage(
107
+ new Error(`Some dependencies are not yet published on npm: ${[...fails].join(', ')}`),
108
+ )
109
+ return
110
+ }
111
+
112
+ if ((msg.body.fix || msg.body.latest) && fixes.size) {
113
+ for (const [dep, ver] of fixes) {
114
+ // naive fix – replace in package.json
115
+ const args = ['install']
116
+ if (pkg.devDependencies?.[dep]) {
117
+ pkg.devDependencies[dep] = ver
118
+ args.push('--save-dev')
119
+ }
120
+ if (pkg.dependencies?.[dep]) pkg.dependencies[dep] = ver
121
+ args.push(dep + '@latest')
122
+ if (msg.body.latest) {
123
+ yield new OutputMessage([`Installing dependency`])
124
+ await this._run('npm', args)
125
+ }
126
+ changes.set(dep, `${dep}: ${ver}`)
127
+ yield new OutputMessage({
128
+ content: [`Fixed ${dep} → ${ver}`],
129
+ type: OutputMessage.TYPES.SUCCESS,
130
+ })
131
+ }
132
+ await this.fs.saveDocument('package.json', pkg)
133
+ yield new OutputMessage({
134
+ content: [`Fixes saved (${changes.size} changes)`],
135
+ type: OutputMessage.TYPES.SUCCESS,
136
+ })
137
+ yield new OutputMessage({ content: ['Dependencies fixed'] })
138
+ } else {
139
+ yield new OutputMessage(`Run with --fix to apply ${fixes.size} available fixes`)
140
+ yield new OutputMessage({ content: ['Dependencies check completed'] })
141
+ }
142
+ }
143
+ }