@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,19 @@
|
|
|
1
|
+
{"instruction":"How to install with pnpm?","output":"```bash\npnpm add @nan0web/release\n```\n","context":["h1:@nan0web/release","h2:Installation"],"input":"## Installation\n\n","tags":["@nan0web/release","Installation"],"proven":"assert-in-@nan0web/release"}
|
|
2
|
+
{"instruction":"How to install with npm?","output":"```bash\nnpm install @nan0web/release\n```\n","context":["h1:@nan0web/release","h2:Installation"],"input":"## Installation\n\n","tags":["@nan0web/release","Installation"],"proven":"assert-in-@nan0web/release"}
|
|
3
|
+
{"instruction":"How to install with yarn?","output":"```bash\nyarn add @nan0web/release\n```\n","context":["h1:@nan0web/release","h2:Installation"],"input":"## Installation\n\n","tags":["@nan0web/release","Installation"],"proven":"assert-in-@nan0web/release"}
|
|
4
|
+
{"instruction":"How to initialize a new release version?","output":"```bash\nrelease init v1.0.0\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
5
|
+
{"instruction":"How to show release information?","output":"```bash\nrelease show [--full]\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\nconst cli = new ReleaseCLI()\nShow release details:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
6
|
+
{"instruction":"How to list all releases?","output":"```bash\nrelease list [--json]\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\nconst cli = new ReleaseCLI()\nShow release details:\n\nconst release = new Release({\nversion: \"v1.0.0\",\ncreatedAt: new Date(\"2025-08-20\")\n})\nconsole.info(release.version) // v1.0.0\nList all releases in the project:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
7
|
+
{"instruction":"How to write a release chat message?","output":"```bash\nrelease chat write --user alice \"Issue with the build pipeline\"\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\nconst cli = new ReleaseCLI()\nShow release details:\n\nconst release = new Release({\nversion: \"v1.0.0\",\ncreatedAt: new Date(\"2025-08-20\")\n})\nconsole.info(release.version) // v1.0.0\nList all releases in the project:\n\nconst cli = new ReleaseCLI()\nAdd a chat message to the current release:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
8
|
+
{"instruction":"How to host release UI?","output":"```bash\nrelease host [--webui] [--port 3000]\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\nconst cli = new ReleaseCLI()\nShow release details:\n\nconst release = new Release({\nversion: \"v1.0.0\",\ncreatedAt: new Date(\"2025-08-20\")\n})\nconsole.info(release.version) // v1.0.0\nList all releases in the project:\n\nconst cli = new ReleaseCLI()\nAdd a chat message to the current release:\n\nconst cli = new ReleaseCLI()\nHost a static server for viewing releases:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
9
|
+
{"instruction":"How to serve release static assets?","output":"```bash\nrelease serve [--port 8080]\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\nconst cli = new ReleaseCLI()\nShow release details:\n\nconst release = new Release({\nversion: \"v1.0.0\",\ncreatedAt: new Date(\"2025-08-20\")\n})\nconsole.info(release.version) // v1.0.0\nList all releases in the project:\n\nconst cli = new ReleaseCLI()\nAdd a chat message to the current release:\n\nconst cli = new ReleaseCLI()\nHost a static server for viewing releases:\n\nconst cli = new ReleaseCLI()\nServe all release files for local inspection:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
10
|
+
{"instruction":"How to validate release tasks?","output":"```bash\nrelease validate [--ignore-fail]\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\nconst cli = new ReleaseCLI()\nShow release details:\n\nconst release = new Release({\nversion: \"v1.0.0\",\ncreatedAt: new Date(\"2025-08-20\")\n})\nconsole.info(release.version) // v1.0.0\nList all releases in the project:\n\nconst cli = new ReleaseCLI()\nAdd a chat message to the current release:\n\nconst cli = new ReleaseCLI()\nHost a static server for viewing releases:\n\nconst cli = new ReleaseCLI()\nServe all release files for local inspection:\n\nconst cli = new ReleaseCLI()\nValidate release tasks and integrity:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
11
|
+
{"instruction":"How to seal a release?","output":"```bash\nrelease seal [--message \"All core APIs are stable and tested\"]\n```\n","context":["h1:@nan0web/release","h2:CLI Usage"],"input":"## CLI Usage\n\nStart by initializing a new release:\n\nconst cli = new ReleaseCLI()\nShow release details:\n\nconst release = new Release({\nversion: \"v1.0.0\",\ncreatedAt: new Date(\"2025-08-20\")\n})\nconsole.info(release.version) // v1.0.0\nList all releases in the project:\n\nconst cli = new ReleaseCLI()\nAdd a chat message to the current release:\n\nconst cli = new ReleaseCLI()\nHost a static server for viewing releases:\n\nconst cli = new ReleaseCLI()\nServe all release files for local inspection:\n\nconst cli = new ReleaseCLI()\nValidate release tasks and integrity:\n\nconst release = new Release({\nversion: \"v1.0.0\",\ncreatedAt: new Date(\"2025-08-20\")\n})\nSeal the release with retro reflection:\n\n","tags":["@nan0web/release","CLI Usage"],"proven":"assert-in-@nan0web/release"}
|
|
12
|
+
{"instruction":"How to instantiate a Release?","output":"```js\nimport { Release } from '@nan0web/release'\nconst release = new Release({\n\tversion: \"v1.0.0\",\n\tcreatedAt: \"2025-08-20T10:00:00Z\"\n})\nconsole.info(release.version) // ← v1.0.0\nconsole.info(release.createdAt instanceof Date) // ← true\n\n```\n","context":["h1:@nan0web/release","h2:Core Concepts","h3:1. Release as Object"],"input":"### 1. Release as Object\n\nA `Release` is a structured class containing:\n\n- `version`: release identifier (vX.Y.Z).\n- `createdAt`, `startAt`, `planAt`, `completeAt`: datetime milestones.\n- `document`: parsed markdown document with structure-aware parsing.\n\nEach property is typed and validated automatically via JSDoc and runtime parsing.\n\n","tags":["@nan0web/release","Core Concepts","1. Release as Object"],"proven":"assert-in-@nan0web/release"}
|
|
13
|
+
{"instruction":"How to parse release notes markdown into structured data?","output":"```js\nimport { ReleaseDocument } from '@nan0web/release'\nconst md = `# v1.0.0 - 2025-08-20\n\n## Overview\nRelease milestone includes UI polish and core API stabilization.\n\n### Tasks\n### Done **Implement core UI design** [ui.core-design]\n### InProgress **Fix responsive issues in mobile layout** [ui.mobile-fixes]`\n\nconst doc = ReleaseDocument.from(md)\nconsole.info(doc.version) // ← v1.0.0\nconsole.info(doc.date instanceof Date) // ← true\nconsole.info(doc.document.children instanceof Array) // ← true\n\n```\n","context":["h1:@nan0web/release","h2:Core Concepts","h3:2. Release Document Parsing"],"input":"### 2. Release Document Parsing\n\nA `ReleaseDocument` extends `@nan0web/markdown` with:\n\n- Structured parsing of markdown release notes\n- Extraction of sections and tasks\n- Team and role parsing from markdown frontmatter or code\n\n","tags":["@nan0web/release","Core Concepts","2. Release Document Parsing"],"proven":"assert-in-@nan0web/release"}
|
|
14
|
+
{"instruction":"How to create a Person with typed properties?","output":"```js\nimport { Person } from '@nan0web/release'\nconst person = new Person({\n\tname: [\"Alice\", \"Developer\"],\n\tgender: \"female\",\n\tcontacts: [\"mailto:alice@example.com\"]\n})\nconsole.info(person.name.firstName) // ← Alice\nconsole.info(person.contacts.length >= 0) // ← true\n\n```\n","context":["h1:@nan0web/release","h2:Core Concepts","h3:3. Person & Team Structures"],"input":"### 3. Person & Team Structures\n\nA `Person` instance includes:\n\n- `name`: HumanName\n- `gender`: HumanGender\n- `contacts`: array of HumanContact\n\n","tags":["@nan0web/release","Core Concepts","3. Person & Team Structures"],"proven":"assert-in-@nan0web/release"}
|
|
15
|
+
{"instruction":"How to register and validate tasks as tests?","output":"```js\nimport { ProjectManagement } from '@nan0web/release'\nconst pm = new ProjectManagement()\npm.registerTask(\"task-1\", \"./tests/task1.test.js\")\npm.registerTask(\"task-2\", \"./tests/task2.test.js\")\n\nconst mockResults = {\n\tpassed: [\"task-1\"],\n\tfailed: [],\n\tpending: [\"task-2\"]\n}\n\npm.validateProjectState = async () => mockResults\nconst results = await pm.validateProjectState()\n\nconsole.info(results.passed.includes(\"task-1\")) // ← true\nconsole.info(results.pending.includes(\"task-2\")) // ← true\n\n```\n","context":["h1:@nan0web/release","h2:Architecture: Project Management as Code","h3:Task Management"],"input":"### Task Management\n\nTasks are registered as `taskId` → `testFilePath` in `ProjectManagement`.\nTheir status is derived by running `node:test` suites.\n\n","tags":["@nan0web/release","Architecture: Project Management as Code","Task Management"],"proven":"assert-in-@nan0web/release"}
|
|
16
|
+
{"instruction":"How to execute a release after validation?","output":"```js\nimport { ReleaseManager, ProjectManagement } from '@nan0web/release'\nconst pm = new ProjectManagement()\nconst rm = new ReleaseManager(pm)\n\nrm.calculateVersion = () => \"v1.0.1\"\nrm.publish = async () => true\n\nconst result = await rm.executeRelease(\"patch\")\nconsole.info(result.version) // ← v1.0.1\nconsole.info(result.published) // ← true\n\n```\n","context":["h1:@nan0web/release","h2:Architecture: Project Management as Code","h3:Release Processing"],"input":"### Release Processing\n\nThe `ReleaseManager` coordinates releases and validates readiness.\nIt ensures:\n\n- All registered tests pass\n- Version is correctly incremented\n- Git tag is applied if all validations succeed\n\n","tags":["@nan0web/release","Architecture: Project Management as Code","Release Processing"],"proven":"assert-in-@nan0web/release"}
|
|
17
|
+
{"instruction":"How to parse changelog and extract tasks?","output":"```js\nimport { ChangelogTaskManager } from '@nan0web/release'\nconst ctm = new ChangelogTaskManager()\nconst changelog = `# Changelog\n\n## [1.0.0] - 2025-08-20\n### Added\n- Initial structure [core.init]\n- Release notes support [docs.release-notes]`\n\nconst tasks = ctm.parseChangelog(changelog)\nconsole.info(Array.isArray(tasks)) // ← true\nconsole.info(tasks.length >= 0) // ← true\n\n```\n","context":["h1:@nan0web/release","h2:Architecture: Project Management as Code","h3:Changelog Integration"],"input":"### Changelog Integration\n\nUsing `ChangelogTaskManager`, you can:\n\n- Extract task definitions from changelog entries\n- Generate corresponding test files\n- Automate task progression in CI\n\n","tags":["@nan0web/release","Architecture: Project Management as Code","Changelog Integration"],"proven":"assert-in-@nan0web/release"}
|
|
18
|
+
{"instruction":"How to run playground demo?","output":"```bash\ngit clone https://github.com/nan0web/release.git\ncd release\npnpm install\npnpm run playground\n```\n","context":["h1:@nan0web/release","h2:CLI Playground"],"input":"## CLI Playground\n\nTry the package in the terminal:\n\n","tags":["@nan0web/release","CLI Playground"],"proven":"assert-in-@nan0web/release"}
|
|
19
|
+
{"instruction":"How to check project status before publishing?","output":"```bash\nnpm test\nnpm run test:coverage\nnpm run test:status\n```\n","context":["h1:@nan0web/release","h2:Project Status"],"input":"## Project Status\n\nTo verify the project is ready, run:\n\n","tags":["@nan0web/release","Project Status"],"proven":"assert-in-@nan0web/release"}
|
package/.editorconfig
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
indent_style = tab
|
|
5
|
+
indent_size = 2
|
|
6
|
+
charset = utf-8
|
|
7
|
+
end_of_line = lf
|
|
8
|
+
trim_trailing_whitespace = true
|
|
9
|
+
insert_final_newline = true
|
|
10
|
+
|
|
11
|
+
[*.md]
|
|
12
|
+
trim_trailing_whitespace = false
|
|
13
|
+
|
|
14
|
+
[*.{jsx}]
|
|
15
|
+
indent_style = tab
|
|
16
|
+
indent_size = 2
|
|
17
|
+
|
|
18
|
+
[*.{nano,yaml}]
|
|
19
|
+
indent_style = space
|
|
20
|
+
indent_size = 2
|
package/CHANGELOG.md
ADDED
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Developer Certificate of Origin and License
|
|
4
|
+
By contributing, You accept and agree to the following terms and conditions for your present and future contributions submitted.
|
|
5
|
+
Except for the license granted herein, You reserve all right, title, and interest in and to your Contributions.
|
|
6
|
+
All contributions are subject to the Developer Certificate of Origin and license as set out in the LICENSE file.
|
|
7
|
+
|
|
8
|
+
## Code of Conduct
|
|
9
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
|
10
|
+
|
|
11
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of:
|
|
12
|
+
- Level of experience,
|
|
13
|
+
- Gender,
|
|
14
|
+
- Gender identity and expression,
|
|
15
|
+
- Sexual orientation,
|
|
16
|
+
- Disability,
|
|
17
|
+
- Personal appearance,
|
|
18
|
+
- Body size,
|
|
19
|
+
- Race,
|
|
20
|
+
- Ethnicity,
|
|
21
|
+
- Age,
|
|
22
|
+
- Religion.
|
|
23
|
+
|
|
24
|
+
> We recognize all people as the foundation of humanity, and our actions contribute to the continued existence and growth of our shared humanity.
|
|
25
|
+
|
|
26
|
+
### Release versioning
|
|
27
|
+
- Keep releases close to the minor versions and publish patches only if required.
|
|
28
|
+
|
|
29
|
+
### Examples of unacceptable behavior by participants:
|
|
30
|
+
- Use of sexual language or imagery.
|
|
31
|
+
- Derogatory comments or personal attacks.
|
|
32
|
+
- Trolling, public or private harassment.
|
|
33
|
+
- Insults or other unprofessional conduct.
|
|
34
|
+
|
|
35
|
+
### Enforcement
|
|
36
|
+
Project maintainers have the right and responsibility to:
|
|
37
|
+
- Remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions not aligned with this Code of Conduct.
|
|
38
|
+
- Remove maintainers who violate this Code of Conduct.
|
|
39
|
+
|
|
40
|
+
Instances of unacceptable behavior can be reported by contacting the project team.
|
|
41
|
+
|
|
42
|
+
This Code of Conduct is adapted from the [Contributor Covenant, version 1.1.0](https://contributor-covenant.org/version/1/1/0/).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, ЯRаСлав (YaRaSLove) <support@yaro.page>
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and distribute this software for any purpose
|
|
6
|
+
with or without fee is hereby granted, provided that the above copyright notice
|
|
7
|
+
and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# @nan0web/release
|
|
2
|
+
|
|
3
|
+
|Package name|[Status](https://github.com/nan0web/monorepo/blob/main/system.md#написання-сценаріїв)|Documentation|Test coverage|Features|Npm version|
|
|
4
|
+
|---|---|---|---|---|---|
|
|
5
|
+
|[@nan0web/release](https://github.com/nan0web/release/) |🟢 `97.1%` |🧪 [English 🏴](https://github.com/nan0web/release/blob/main/README.md)<br />[Українською 🇺🇦](https://github.com/nan0web/release/blob/main/docs/uk/README.md) |🟡 `84.2%` |✅ d.ts 📜 system.md 🕹️ playground |— |
|
|
6
|
+
|
|
7
|
+
Project management as code, Git-native, GPG-signed, and test-driven.
|
|
8
|
+
|
|
9
|
+
Unlike traditional project tools that require constant syncing or manual updates,
|
|
10
|
+
`@nan0web/release` enforces project structure where:
|
|
11
|
+
- Tasks are represented as tests in `.test.js` files.
|
|
12
|
+
- Status is tracked via test outcomes.
|
|
13
|
+
- Team roles and approvals are stored in structured files.
|
|
14
|
+
- All changes are signed, versioned, and composable.
|
|
15
|
+
|
|
16
|
+
This ensures:
|
|
17
|
+
- Clear, automatable project tracking.
|
|
18
|
+
- Immutable release notes and retro reflections.
|
|
19
|
+
- Zero ambiguity – tasks don't exist unless tested.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
How to install with pnpm?
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add @nan0web/release
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
How to install with npm?
|
|
29
|
+
```bash
|
|
30
|
+
npm install @nan0web/release
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
How to install with yarn?
|
|
34
|
+
```bash
|
|
35
|
+
yarn add @nan0web/release
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## CLI Usage
|
|
39
|
+
|
|
40
|
+
Start by initializing a new release:
|
|
41
|
+
|
|
42
|
+
How to initialize a new release version?
|
|
43
|
+
```bash
|
|
44
|
+
release init v1.0.0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
const cli = new ReleaseCLI()
|
|
48
|
+
Show release details:
|
|
49
|
+
|
|
50
|
+
How to show release information?
|
|
51
|
+
```bash
|
|
52
|
+
release show [--full]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
const release = new Release({
|
|
56
|
+
version: "v1.0.0",
|
|
57
|
+
createdAt: new Date("2025-08-20")
|
|
58
|
+
})
|
|
59
|
+
console.info(release.version) // v1.0.0
|
|
60
|
+
List all releases in the project:
|
|
61
|
+
|
|
62
|
+
How to list all releases?
|
|
63
|
+
```bash
|
|
64
|
+
release list [--json]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
const cli = new ReleaseCLI()
|
|
68
|
+
Add a chat message to the current release:
|
|
69
|
+
|
|
70
|
+
How to write a release chat message?
|
|
71
|
+
```bash
|
|
72
|
+
release chat write --user alice "Issue with the build pipeline"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
const cli = new ReleaseCLI()
|
|
76
|
+
Host a static server for viewing releases:
|
|
77
|
+
|
|
78
|
+
How to host release UI?
|
|
79
|
+
```bash
|
|
80
|
+
release host [--webui] [--port 3000]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
const cli = new ReleaseCLI()
|
|
84
|
+
Serve all release files for local inspection:
|
|
85
|
+
|
|
86
|
+
How to serve release static assets?
|
|
87
|
+
```bash
|
|
88
|
+
release serve [--port 8080]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
const cli = new ReleaseCLI()
|
|
92
|
+
Validate release tasks and integrity:
|
|
93
|
+
|
|
94
|
+
How to validate release tasks?
|
|
95
|
+
```bash
|
|
96
|
+
release validate [--ignore-fail]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
const release = new Release({
|
|
100
|
+
version: "v1.0.0",
|
|
101
|
+
createdAt: new Date("2025-08-20")
|
|
102
|
+
})
|
|
103
|
+
Seal the release with retro reflection:
|
|
104
|
+
|
|
105
|
+
How to seal a release?
|
|
106
|
+
```bash
|
|
107
|
+
release seal [--message "All core APIs are stable and tested"]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
const cli = new ReleaseCLI()
|
|
111
|
+
## Core Concepts
|
|
112
|
+
|
|
113
|
+
### 1. Release as Object
|
|
114
|
+
|
|
115
|
+
A `Release` is a structured class containing:
|
|
116
|
+
- `version`: release identifier (vX.Y.Z).
|
|
117
|
+
- `createdAt`, `startAt`, `planAt`, `completeAt`: datetime milestones.
|
|
118
|
+
- `document`: parsed markdown document with structure-aware parsing.
|
|
119
|
+
|
|
120
|
+
Each property is typed and validated automatically via JSDoc and runtime parsing.
|
|
121
|
+
|
|
122
|
+
How to instantiate a Release?
|
|
123
|
+
```js
|
|
124
|
+
import { Release } from '@nan0web/release'
|
|
125
|
+
const release = new Release({
|
|
126
|
+
version: "v1.0.0",
|
|
127
|
+
createdAt: "2025-08-20T10:00:00Z"
|
|
128
|
+
})
|
|
129
|
+
console.info(release.version) // ← v1.0.0
|
|
130
|
+
console.info(release.createdAt instanceof Date) // ← true
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
### 2. Release Document Parsing
|
|
134
|
+
|
|
135
|
+
A `ReleaseDocument` extends `@nan0web/markdown` with:
|
|
136
|
+
- Structured parsing of markdown release notes
|
|
137
|
+
- Extraction of sections and tasks
|
|
138
|
+
- Team and role parsing from markdown frontmatter or code
|
|
139
|
+
|
|
140
|
+
How to parse release notes markdown into structured data?
|
|
141
|
+
```js
|
|
142
|
+
import { ReleaseDocument } from '@nan0web/release'
|
|
143
|
+
const md = `# v1.0.0 - 2025-08-20
|
|
144
|
+
|
|
145
|
+
## Overview
|
|
146
|
+
Release milestone includes UI polish and core API stabilization.
|
|
147
|
+
|
|
148
|
+
### Tasks
|
|
149
|
+
### Done **Implement core UI design** [ui.core-design]
|
|
150
|
+
### InProgress **Fix responsive issues in mobile layout** [ui.mobile-fixes]`
|
|
151
|
+
|
|
152
|
+
const doc = ReleaseDocument.from(md)
|
|
153
|
+
console.info(doc.version) // ← v1.0.0
|
|
154
|
+
console.info(doc.date instanceof Date) // ← true
|
|
155
|
+
console.info(doc.document.children instanceof Array) // ← true
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
### 3. Person & Team Structures
|
|
159
|
+
|
|
160
|
+
A `Person` instance includes:
|
|
161
|
+
- `name`: HumanName
|
|
162
|
+
- `gender`: HumanGender
|
|
163
|
+
- `contacts`: array of HumanContact
|
|
164
|
+
|
|
165
|
+
How to create a Person with typed properties?
|
|
166
|
+
```js
|
|
167
|
+
import { Person } from '@nan0web/release'
|
|
168
|
+
const person = new Person({
|
|
169
|
+
name: ["Alice", "Developer"],
|
|
170
|
+
gender: "female",
|
|
171
|
+
contacts: ["mailto:alice@example.com"]
|
|
172
|
+
})
|
|
173
|
+
console.info(person.name.firstName) // ← Alice
|
|
174
|
+
console.info(person.contacts.length >= 0) // ← true
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
## Architecture: Project Management as Code
|
|
178
|
+
|
|
179
|
+
This architecture treats tasks as tests, roles as classes, and progress as validated outcomes.
|
|
180
|
+
|
|
181
|
+
### Task Management
|
|
182
|
+
|
|
183
|
+
Tasks are registered as `taskId` → `testFilePath` in `ProjectManagement`.
|
|
184
|
+
Their status is derived by running `node:test` suites.
|
|
185
|
+
|
|
186
|
+
How to register and validate tasks as tests?
|
|
187
|
+
```js
|
|
188
|
+
import { ProjectManagement } from '@nan0web/release'
|
|
189
|
+
const pm = new ProjectManagement()
|
|
190
|
+
pm.registerTask("task-1", "./tests/task1.test.js")
|
|
191
|
+
pm.registerTask("task-2", "./tests/task2.test.js")
|
|
192
|
+
|
|
193
|
+
const mockResults = {
|
|
194
|
+
passed: ["task-1"],
|
|
195
|
+
failed: [],
|
|
196
|
+
pending: ["task-2"]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pm.validateProjectState = async () => mockResults
|
|
200
|
+
const results = await pm.validateProjectState()
|
|
201
|
+
|
|
202
|
+
console.info(results.passed.includes("task-1")) // ← true
|
|
203
|
+
console.info(results.pending.includes("task-2")) // ← true
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
### Release Processing
|
|
207
|
+
|
|
208
|
+
The `ReleaseManager` coordinates releases and validates readiness.
|
|
209
|
+
It ensures:
|
|
210
|
+
- All registered tests pass
|
|
211
|
+
- Version is correctly incremented
|
|
212
|
+
- Git tag is applied if all validations succeed
|
|
213
|
+
|
|
214
|
+
How to execute a release after validation?
|
|
215
|
+
```js
|
|
216
|
+
import { ReleaseManager, ProjectManagement } from '@nan0web/release'
|
|
217
|
+
const pm = new ProjectManagement()
|
|
218
|
+
const rm = new ReleaseManager(pm)
|
|
219
|
+
|
|
220
|
+
rm.calculateVersion = () => "v1.0.1"
|
|
221
|
+
rm.publish = async () => true
|
|
222
|
+
|
|
223
|
+
const result = await rm.executeRelease("patch")
|
|
224
|
+
console.info(result.version) // ← v1.0.1
|
|
225
|
+
console.info(result.published) // ← true
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
### Changelog Integration
|
|
229
|
+
|
|
230
|
+
Using `ChangelogTaskManager`, you can:
|
|
231
|
+
- Extract task definitions from changelog entries
|
|
232
|
+
- Generate corresponding test files
|
|
233
|
+
- Automate task progression in CI
|
|
234
|
+
|
|
235
|
+
How to parse changelog and extract tasks?
|
|
236
|
+
```js
|
|
237
|
+
import { ChangelogTaskManager } from '@nan0web/release'
|
|
238
|
+
const ctm = new ChangelogTaskManager()
|
|
239
|
+
const changelog = `# Changelog
|
|
240
|
+
|
|
241
|
+
## [1.0.0] - 2025-08-20
|
|
242
|
+
### Added
|
|
243
|
+
- Initial structure [core.init]
|
|
244
|
+
- Release notes support [docs.release-notes]`
|
|
245
|
+
|
|
246
|
+
const tasks = ctm.parseChangelog(changelog)
|
|
247
|
+
console.info(Array.isArray(tasks)) // ← true
|
|
248
|
+
console.info(tasks.length >= 0) // ← true
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
## Java•Script Features
|
|
252
|
+
|
|
253
|
+
Uses JSDoc and .d.ts files for autocompletion
|
|
254
|
+
|
|
255
|
+
## CLI Playground
|
|
256
|
+
|
|
257
|
+
Try the package in the terminal:
|
|
258
|
+
|
|
259
|
+
How to run playground demo?
|
|
260
|
+
```bash
|
|
261
|
+
git clone https://github.com/nan0web/release.git
|
|
262
|
+
cd release
|
|
263
|
+
pnpm install
|
|
264
|
+
pnpm run playground
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Project Status
|
|
268
|
+
|
|
269
|
+
To verify the project is ready, run:
|
|
270
|
+
|
|
271
|
+
How to check project status before publishing?
|
|
272
|
+
```bash
|
|
273
|
+
npm test
|
|
274
|
+
npm run test:coverage
|
|
275
|
+
npm run test:status
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Contributing
|
|
279
|
+
|
|
280
|
+
How to contribute? - [check here](./CONTRIBUTING.md)
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
How to license ISC? - [check here](./LICENSE)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
import Logger from '@nan0web/log'
|
|
4
|
+
|
|
5
|
+
export const GREEN = '\x1b[32m'
|
|
6
|
+
export const RESET = '\x1b[0m'
|
|
7
|
+
export const OK = '✓'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect package manager used in the project
|
|
11
|
+
* @returns {string} - Detected package manager (npm, yarn, pnpm)
|
|
12
|
+
*/
|
|
13
|
+
export function detectPackageManager() {
|
|
14
|
+
if (existsSync('pnpm-lock.yaml')) return 'pnpm'
|
|
15
|
+
if (existsSync('yarn.lock')) return 'yarn'
|
|
16
|
+
if (existsSync('package-lock.json')) return 'npm'
|
|
17
|
+
|
|
18
|
+
// Default to npm if no lock file is found
|
|
19
|
+
return 'npm'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get package manager specific commands
|
|
24
|
+
* @param {string} packageManager - Package manager name
|
|
25
|
+
* @returns {object} - Commands map
|
|
26
|
+
*/
|
|
27
|
+
export function getPackageManagerCommands(packageManager) {
|
|
28
|
+
const commands = {
|
|
29
|
+
pnpm: {
|
|
30
|
+
cacheClean: 'pnpm store prune',
|
|
31
|
+
install: 'pnpm install',
|
|
32
|
+
update: 'pnpm update --latest',
|
|
33
|
+
prune: 'pnpm prune',
|
|
34
|
+
outdated: 'pnpm outdated',
|
|
35
|
+
lint: 'pnpm run lint',
|
|
36
|
+
test: 'pnpm run test',
|
|
37
|
+
build: 'pnpm run build',
|
|
38
|
+
clean: 'pnpm run clean',
|
|
39
|
+
publish: 'pnpm publish --access public',
|
|
40
|
+
pushTags: "git push origin --tags",
|
|
41
|
+
push: 'git push',
|
|
42
|
+
list: 'pnpm list',
|
|
43
|
+
storeStatus: 'pnpm store status'
|
|
44
|
+
},
|
|
45
|
+
yarn: {
|
|
46
|
+
cacheClean: 'yarn cache clean',
|
|
47
|
+
install: 'yarn install',
|
|
48
|
+
update: 'yarn upgrade',
|
|
49
|
+
prune: 'yarn autoclean --force',
|
|
50
|
+
outdated: 'yarn outdated',
|
|
51
|
+
lint: 'yarn run lint',
|
|
52
|
+
test: 'yarn run test',
|
|
53
|
+
build: 'yarn run build',
|
|
54
|
+
clean: 'yarn run clean',
|
|
55
|
+
publish: 'yarn publish --access public',
|
|
56
|
+
pushTags: 'git push origin --tags',
|
|
57
|
+
push: 'git push',
|
|
58
|
+
list: 'yarn list',
|
|
59
|
+
storeStatus: null
|
|
60
|
+
},
|
|
61
|
+
npm: {
|
|
62
|
+
cacheClean: 'npm cache clean --force',
|
|
63
|
+
installF: 'npm install',
|
|
64
|
+
update: 'npm update',
|
|
65
|
+
prune: 'npm prune',
|
|
66
|
+
outdated: 'npm outdated',
|
|
67
|
+
lint: 'npm run lint',
|
|
68
|
+
test: 'npm test',
|
|
69
|
+
build: 'npm run build',
|
|
70
|
+
clean: 'npm run clean',
|
|
71
|
+
publish: 'npm publish --access public',
|
|
72
|
+
pushTags: 'git push origin --tags',
|
|
73
|
+
push: 'git push',
|
|
74
|
+
list: 'npm list',
|
|
75
|
+
storeStatus: null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return commands[packageManager] || commands.npm
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run shell command
|
|
83
|
+
* @param {string} command - Command to execute
|
|
84
|
+
* @param {string} errorMessage - Error message to show if command fails
|
|
85
|
+
*/
|
|
86
|
+
export function runCommand(command, errorMessage) {
|
|
87
|
+
const logger = new Logger({ level: 'info' })
|
|
88
|
+
try {
|
|
89
|
+
logger.debug(`Executing command: ${command}`)
|
|
90
|
+
execSync(command, { stdio: 'inherit' })
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logger.error(errorMessage)
|
|
93
|
+
logger.debug(err.stack)
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if package.json has required scripts and dependencies
|
|
100
|
+
* @param {string} packageJsonPath - Path to package.json
|
|
101
|
+
* @param {boolean} fix - Whether to attempt fixing issues
|
|
102
|
+
* @returns {number} - Exit code (0 for success)
|
|
103
|
+
*/
|
|
104
|
+
export function checkPackageJson(packageJsonPath, fix = false) {
|
|
105
|
+
const logger = new Logger({ level: 'info' })
|
|
106
|
+
try {
|
|
107
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
108
|
+
const requiredScripts = ['dev', 'build', 'preview', 'test', 'clean']
|
|
109
|
+
const missingScripts = requiredScripts.filter(script => !pkg.scripts?.[script])
|
|
110
|
+
|
|
111
|
+
if (missingScripts.length > 0) {
|
|
112
|
+
logger.warn(`Missing scripts in package.json: ${missingScripts.join(', ')}`)
|
|
113
|
+
if (fix) {
|
|
114
|
+
// Add missing scripts with default commands
|
|
115
|
+
pkg.scripts = pkg.scripts || {}
|
|
116
|
+
missingScripts.forEach(script => {
|
|
117
|
+
switch (script) {
|
|
118
|
+
case 'dev': pkg.scripts.dev = 'vite'; break
|
|
119
|
+
case 'build': pkg.scripts.build = 'vite build'; break
|
|
120
|
+
case 'preview': pkg.scripts.preview = 'vite preview'; break
|
|
121
|
+
case 'test': pkg.scripts.test = 'vitest'; break
|
|
122
|
+
case 'clean': pkg.scripts.clean = 'rm -rf dist'; break
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2))
|
|
126
|
+
logger.info(`Fixed missing scripts in package.json`)
|
|
127
|
+
} else {
|
|
128
|
+
return 1
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for husky dependency
|
|
133
|
+
if (!pkg.devDependencies?.husky) {
|
|
134
|
+
logger.warn('Missing husky in devDependencies')
|
|
135
|
+
if (fix) {
|
|
136
|
+
pkg.devDependencies = pkg.devDependencies || {}
|
|
137
|
+
pkg.devDependencies.husky = '^8.0.0'
|
|
138
|
+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2))
|
|
139
|
+
logger.info('Added husky to devDependencies')
|
|
140
|
+
} else {
|
|
141
|
+
return 1
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
logger.error(`Failed to check package.json: ${err.message}`)
|
|
146
|
+
logger.debug(err.stack)
|
|
147
|
+
return 1
|
|
148
|
+
}
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check pre-commit hooks
|
|
154
|
+
* @param {string} packageJsonPath - Path to package.json
|
|
155
|
+
* @param {boolean} fix - Whether to attempt fixing issues
|
|
156
|
+
* @returns {number} - Exit code (0 for success)
|
|
157
|
+
*/
|
|
158
|
+
export function checkPreCommits(packageJsonPath, fix = false) {
|
|
159
|
+
const logger = new Logger({ level: 'info' })
|
|
160
|
+
try {
|
|
161
|
+
const huskyDir = resolve(dirname(packageJsonPath), '.husky')
|
|
162
|
+
const preCommitFile = resolve(huskyDir, 'pre-commit')
|
|
163
|
+
|
|
164
|
+
if (!existsSync(preCommitFile)) {
|
|
165
|
+
logger.warn('Missing pre-commit hook file')
|
|
166
|
+
if (fix) {
|
|
167
|
+
if (!existsSync(huskyDir)) {
|
|
168
|
+
mkdirSync(huskyDir)
|
|
169
|
+
}
|
|
170
|
+
writeFileSync(preCommitFile, '#!/bin/sh\n. "$(dirname "$0")/_/husky.sh"\n\npnpm run test\n')
|
|
171
|
+
execSync('chmod +x ' + preCommitFile)
|
|
172
|
+
logger.info('Created pre-commit hook file')
|
|
173
|
+
} else {
|
|
174
|
+
return 1
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
logger.error(`Failed to check pre-commit hooks: ${err.message}`)
|
|
179
|
+
logger.debug(err.stack)
|
|
180
|
+
return 1
|
|
181
|
+
}
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check for uncommitted changes
|
|
187
|
+
* @param {object} params - Parameters
|
|
188
|
+
* @param {string} params.version - Current version
|
|
189
|
+
* @param {object} params.devDependencies - Development dependencies
|
|
190
|
+
* @param {object} params.dependencies - Production dependencies
|
|
191
|
+
*/
|
|
192
|
+
export function checkUncommitted({ version, devDependencies, dependencies }) {
|
|
193
|
+
const logger = new Logger({ level: 'info' })
|
|
194
|
+
try {
|
|
195
|
+
execSync('git diff-index --quiet HEAD --', { stdio: 'ignore' })
|
|
196
|
+
execSync('git diff-files --quiet', { stdio: 'ignore' })
|
|
197
|
+
} catch (err) {
|
|
198
|
+
logger.error(`There are uncommitted changes in your repository at version ${version}`)
|
|
199
|
+
logger.debug('Dependencies:', dependencies)
|
|
200
|
+
logger.debug('Dev Dependencies:', devDependencies)
|
|
201
|
+
logger.debug(err.stack)
|
|
202
|
+
process.exit(1)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if git tag exists
|
|
208
|
+
* @param {string} tag - Tag to check
|
|
209
|
+
* @returns {boolean} - Whether tag exists
|
|
210
|
+
*/
|
|
211
|
+
export function tagExists(tag) {
|
|
212
|
+
try {
|
|
213
|
+
execSync(`git rev-parse ${tag}`, { stdio: 'ignore' })
|
|
214
|
+
return true
|
|
215
|
+
} catch {
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Tag release in git
|
|
222
|
+
* @param {string} version - Version to tag
|
|
223
|
+
*/
|
|
224
|
+
export function tagRelease(version) {
|
|
225
|
+
const tag = `v${version}`
|
|
226
|
+
const logger = new Logger({ level: 'info' })
|
|
227
|
+
|
|
228
|
+
if (tagExists(tag)) {
|
|
229
|
+
logger.warn(`Tag ${tag} already exists`)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
execSync(`git tag -a ${tag} -m "Release version ${version}"`, { stdio: 'inherit' })
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logger.error(`Failed to tag release ${tag}`)
|
|
237
|
+
logger.debug(err.stack)
|
|
238
|
+
process.exit(1)
|
|
239
|
+
}
|
|
240
|
+
}
|