├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── eslint.config.js
├── package-lock.json
├── package.json
├── src
├── bin
│ └── nodecg.ts
├── commands
│ ├── defaultconfig.ts
│ ├── index.ts
│ ├── install.ts
│ ├── schema-types.ts
│ ├── setup.ts
│ ├── start.ts
│ └── uninstall.ts
├── index.ts
└── lib
│ ├── fetch-tags.ts
│ ├── install-bundle-deps.ts
│ ├── sample
│ ├── npm-release.json
│ └── npm-release.ts
│ └── util.ts
├── test
├── commands
│ ├── defaultconfig.spec.ts
│ ├── install.spec.ts
│ ├── schema-types.spec.ts
│ ├── setup.spec.ts
│ ├── tmp-dir.ts
│ └── uninstall.spec.ts
├── fixtures
│ └── bundles
│ │ ├── config-schema
│ │ ├── configschema.json
│ │ └── package.json
│ │ ├── schema-types
│ │ ├── package.json
│ │ └── schemas
│ │ │ └── example.json
│ │ └── uninstall-test
│ │ └── package.json
└── mocks
│ └── program.ts
├── tsconfig.build.json
├── tsconfig.json
└── vitest.config.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{package.json,*.yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # Don't remove trailing whitespace from Markdown
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | strategy:
14 | matrix:
15 | node-version:
16 | - "18"
17 | - "20"
18 | - "22"
19 | os:
20 | - ubuntu-latest
21 | - windows-latest
22 | runs-on: ${{ matrix.os }}
23 | steps:
24 | - uses: actions/checkout@v4
25 | - uses: actions/setup-node@v4
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: npm
29 | - run: npm i -g bower
30 | - run: npm ci
31 | - run: npm run static
32 | - run: npm test
33 | - run: npm run build
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release-please
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 | packages: write
12 |
13 | jobs:
14 | release-please:
15 | runs-on: ubuntu-latest
16 | outputs:
17 | release_created: ${{ steps.release-please.outputs.release_created }}
18 | release_name: ${{ steps.release-please.outputs.name }}
19 | steps:
20 | - uses: googleapis/release-please-action@v4
21 | id: release-please
22 | with:
23 | release-type: node
24 |
25 | publish:
26 | runs-on: ubuntu-latest
27 | needs: release-please
28 | steps:
29 | - uses: actions/checkout@v4
30 | - uses: actions/setup-node@v4
31 | with:
32 | node-version: "22"
33 | registry-url: "https://registry.npmjs.org"
34 | cache: npm
35 | - run: npm ci
36 | - run: npm run build
37 |
38 | - if: ${{ needs.release-please.outputs.release_created }}
39 | run: npm publish --access public
40 | env:
41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
42 |
43 | - if: ${{ !needs.release-please.outputs.release_created }}
44 | run: |
45 | npm version 0.0.0-canary.${{ github.sha }} --no-git-tag-version
46 | npm publish --tag canary
47 | env:
48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist/
2 | tsconfig.vitest-temp.json
3 |
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 | .pnpm-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # Snowpack dependency directory (https://snowpack.dev/)
50 | web_modules/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional stylelint cache
62 | .stylelintcache
63 |
64 | # Microbundle cache
65 | .rpt2_cache/
66 | .rts2_cache_cjs/
67 | .rts2_cache_es/
68 | .rts2_cache_umd/
69 |
70 | # Optional REPL history
71 | .node_repl_history
72 |
73 | # Output of 'npm pack'
74 | *.tgz
75 |
76 | # Yarn Integrity file
77 | .yarn-integrity
78 |
79 | # dotenv environment variable files
80 | .env
81 | .env.development.local
82 | .env.test.local
83 | .env.production.local
84 | .env.local
85 |
86 | # parcel-bundler cache (https://parceljs.org/)
87 | .cache
88 | .parcel-cache
89 |
90 | # Next.js build output
91 | .next
92 | out
93 |
94 | # Nuxt.js build / generate output
95 | .nuxt
96 | dist
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and not Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # vuepress v2.x temp and cache directory
108 | .temp
109 | .cache
110 |
111 | # Docusaurus cache and generated files
112 | .docusaurus
113 |
114 | # Serverless directories
115 | .serverless/
116 |
117 | # FuseBox cache
118 | .fusebox/
119 |
120 | # DynamoDB Local files
121 | .dynamodb/
122 |
123 | # TernJS port file
124 | .tern-port
125 |
126 | # Stores VSCode versions used for testing VSCode extensions
127 | .vscode-test
128 |
129 | # yarn v2
130 | .yarn/cache
131 | .yarn/unplugged
132 | .yarn/build-state.yml
133 | .yarn/install-state.gz
134 | .pnp.*
135 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /test/fixtures
2 | CHANGELOG.md
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [9.0.2](https://github.com/nodecg/nodecg-cli/compare/v9.0.1...v9.0.2) (2025-01-05)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * windows npm install ([#132](https://github.com/nodecg/nodecg-cli/issues/132)) ([d042394](https://github.com/nodecg/nodecg-cli/commit/d0423942b11bfced0c5d94a44f909cac92d53eb3))
11 |
12 | ## [9.0.1](https://github.com/nodecg/nodecg-cli/compare/v9.0.0...v9.0.1) (2025-01-03)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * release CI bug ([4bb377a](https://github.com/nodecg/nodecg-cli/commit/4bb377a7540f4774edb75643d9694c21338de3a2))
18 |
19 | ## [9.0.0](https://github.com/nodecg/nodecg-cli/compare/v8.7.0...v9.0.0) (2025-01-01)
20 |
21 |
22 | ### ⚠ BREAKING CHANGES
23 |
24 | * **setup:** Drop support for nodecg 0.x.x and 1.x.x.
25 | * **schema-types:** schema-types command no longer outputs index file
26 | * `defaultconfig` command now uses ajv to generate default value. A config schema that are not object will throw an error. A config schema with top level default will now throw an error.
27 |
28 | ### Features
29 |
30 | * deprecated package ([#128](https://github.com/nodecg/nodecg-cli/issues/128)) ([1726529](https://github.com/nodecg/nodecg-cli/commit/17265294e94cb93a0b88d19e31135cba6441ca12))
31 | * use ajv for defaultconfig command ([#117](https://github.com/nodecg/nodecg-cli/issues/117)) ([6f2c19d](https://github.com/nodecg/nodecg-cli/commit/6f2c19d8a7a99f9ca90c5cfff5cd636f5804333b))
32 |
33 |
34 | ### Bug Fixes
35 |
36 | * **defaultconfig:** correct styling of log output ([#120](https://github.com/nodecg/nodecg-cli/issues/120)) ([7cf54f3](https://github.com/nodecg/nodecg-cli/commit/7cf54f3efeae36ad8008777e321128f5970ab939))
37 | * **schema-types:** don't output index file ([#119](https://github.com/nodecg/nodecg-cli/issues/119)) ([4ca2931](https://github.com/nodecg/nodecg-cli/commit/4ca29311b5220fdc53357c54dff41a3f1dc20686))
38 |
39 |
40 | ### Code Refactoring
41 |
42 | * **setup:** remove support for nodecg less than 2.0.0 ([#124](https://github.com/nodecg/nodecg-cli/issues/124)) ([4536527](https://github.com/nodecg/nodecg-cli/commit/4536527088ccfb320cb6989a00bc9bf04b3a0266))
43 |
44 | ## [8.7.0](https://github.com/nodecg/nodecg-cli/compare/v8.6.8...v8.7.0) (2024-12-26)
45 |
46 |
47 | ### Features
48 |
49 | * update commander and inquirer ([#109](https://github.com/nodecg/nodecg-cli/issues/109)) ([579b79e](https://github.com/nodecg/nodecg-cli/commit/579b79ed255875e76cb06b453a54f150f6f76172))
50 | * update node version to 18/20/22 ([#98](https://github.com/nodecg/nodecg-cli/issues/98)) ([9990349](https://github.com/nodecg/nodecg-cli/commit/999034977350695e4c09fbf12446e900743f81db))
51 | * use esm and update packages ([#108](https://github.com/nodecg/nodecg-cli/issues/108)) ([058be35](https://github.com/nodecg/nodecg-cli/commit/058be35204e48ee989161b96f7cc36b7b5eeb904))
52 |
53 |
54 | ### Bug Fixes
55 |
56 | * remove __dirname ([#113](https://github.com/nodecg/nodecg-cli/issues/113)) ([8c9a033](https://github.com/nodecg/nodecg-cli/commit/8c9a033dce5630f6ef0edf5515ffdc8f1f673f6d))
57 | * remove node-fetch ([#102](https://github.com/nodecg/nodecg-cli/issues/102)) ([50a6323](https://github.com/nodecg/nodecg-cli/commit/50a632382fba9a05f087042d78dde09ae3c96997))
58 |
59 | ## [8.6.8](https://github.com/nodecg/nodecg-cli/compare/v8.6.7...v8.6.8) (2023-06-20)
60 |
61 | ### Bug Fixes
62 |
63 | - try prepending "v" to the checkout tag when installing a bundle ([58daa03](https://github.com/nodecg/nodecg-cli/commit/58daa0336319624f5ec783806d0e8a00f4aefb24))
64 |
65 | ## [8.6.7](https://github.com/nodecg/nodecg-cli/compare/v8.6.6...v8.6.7) (2023-06-20)
66 |
67 | ### Bug Fixes
68 |
69 | - better semver parsing ([61a0e8c](https://github.com/nodecg/nodecg-cli/commit/61a0e8cdc704bff5c081beffd3f30d76dbf59cbb))
70 |
71 | ## [8.6.6](https://github.com/nodecg/nodecg-cli/compare/v8.6.5...v8.6.6) (2023-06-11)
72 |
73 | ### Bug Fixes
74 |
75 | - force build to try to fix release-please ([e2ae645](https://github.com/nodecg/nodecg-cli/commit/e2ae6451be408d821d3211fea82ed1a95cf6db89))
76 |
77 | ## [8.6.5](https://github.com/nodecg/nodecg-cli/compare/v8.6.4...v8.6.5) (2023-06-11)
78 |
79 | ### Bug Fixes
80 |
81 | - don't return a promise from the compile method, it isn't used ([2903e50](https://github.com/nodecg/nodecg-cli/commit/2903e5016a9daa410f3972b7d94873fd9f41adee))
82 | - prevent eslint and typescript from being overly worried about replicant schemas ([3d2dd82](https://github.com/nodecg/nodecg-cli/commit/3d2dd82ea642e5a6e596ff2191577f0dd8424f42))
83 |
84 | ## [6.1.0](https://github.com/nodecg/nodecg-cli/compare/v6.0.4-alpha.2...v6.1.0) (2019-08-06)
85 |
86 | ### Features
87 |
88 | - make installation of Bower deps optional ([#66](https://github.com/nodecg/nodecg-cli/issues/66)) ([2e16c1b](https://github.com/nodecg/nodecg-cli/commit/2e16c1b))
89 |
90 | ## [6.0.4-alpha.2](https://github.com/nodecg/nodecg-cli/compare/v6.0.4-alpha.1...v6.0.4-alpha.2) (2019-01-27)
91 |
92 | ### Bug Fixes
93 |
94 | - **modules:** use commonjs export for dynamic modules ([523a6a6](https://github.com/nodecg/nodecg-cli/commit/523a6a6))
95 |
96 |
97 |
98 | ## [6.0.4-alpha.1](https://github.com/nodecg/nodecg-cli/compare/v6.0.4-alpha.0...v6.0.4-alpha.1) (2019-01-27)
99 |
100 | ### Bug Fixes
101 |
102 | - correct require path to package.json ([035e71e](https://github.com/nodecg/nodecg-cli/commit/035e71e))
103 |
104 |
105 |
106 | ## [6.0.4-alpha.0](https://github.com/nodecg/nodecg-cli/compare/v6.0.3...v6.0.4-alpha.0) (2019-01-27)
107 |
108 |
109 |
110 | ## [6.0.3](https://github.com/nodecg/nodecg-cli/compare/v6.0.2...v6.0.3) (2018-12-03)
111 |
112 | ### Bug Fixes
113 |
114 | - **package:** make fs-extra a prod dep, not a devDep ([76ab59d](https://github.com/nodecg/nodecg-cli/commit/76ab59d))
115 |
116 |
117 |
118 | ## [6.0.2](https://github.com/nodecg/nodecg-cli/compare/v6.0.1...v6.0.2) (2018-12-03)
119 |
120 |
121 |
122 | ## [6.0.1](https://github.com/nodecg/nodecg-cli/compare/v6.0.0...v6.0.1) (2018-12-03)
123 |
124 |
125 |
126 | # [6.0.0](https://github.com/nodecg/nodecg-cli/compare/v5.0.1...v6.0.0) (2018-12-03)
127 |
128 | ### Bug Fixes
129 |
130 | - **package:** update chalk to version 2.0.0 ([#44](https://github.com/nodecg/nodecg-cli/issues/44)) ([b19ddc1](https://github.com/nodecg/nodecg-cli/commit/b19ddc1))
131 | - **package:** update inquirer to version 4.0.0 ([#52](https://github.com/nodecg/nodecg-cli/issues/52)) ([ef4560f](https://github.com/nodecg/nodecg-cli/commit/ef4560f))
132 |
133 | ### Code Refactoring
134 |
135 | - port to ES6 ([b373fff](https://github.com/nodecg/nodecg-cli/commit/b373fff))
136 |
137 | ### Features
138 |
139 | - add schema-types command ([#62](https://github.com/nodecg/nodecg-cli/issues/62)) ([237d734](https://github.com/nodecg/nodecg-cli/commit/237d734))
140 |
141 | ### BREAKING CHANGES
142 |
143 | - drop support for Node 6
144 | - requires Node 6+
145 |
146 |
147 |
148 | ## [5.0.1](https://github.com/nodecg/nodecg-cli/compare/v5.0.0...v5.0.1) (2016-03-06)
149 |
150 | ### Bug Fixes
151 |
152 | - **setup:** run `git fetch` before attempting to check out an updated release ([aa352c2](https://github.com/nodecg/nodecg-cli/commit/aa352c2))
153 |
154 |
155 |
156 | # [5.0.0](https://github.com/nodecg/nodecg-cli/compare/v4.1.0...v5.0.0) (2016-03-02)
157 |
158 | ### Bug Fixes
159 |
160 | - **install:** I'm the worst, go back to using httpsUrl ([501264d](https://github.com/nodecg/nodecg-cli/commit/501264d))
161 | - **install:** use gitUrl instead of httpsUrl ([8c483a3](https://github.com/nodecg/nodecg-cli/commit/8c483a3))
162 | - **install:** use ssh instead of giturl ([7c25f0f](https://github.com/nodecg/nodecg-cli/commit/7c25f0f))
163 | - **update:** fix error when installing bundle deps ([20ccda4](https://github.com/nodecg/nodecg-cli/commit/20ccda4))
164 |
165 | ### Code Refactoring
166 |
167 | - **install:** use system bower ([1109d82](https://github.com/nodecg/nodecg-cli/commit/1109d82))
168 |
169 | ### Features
170 |
171 | - **install:** install command now respects semver ranges, if supplied ([3be0c6a](https://github.com/nodecg/nodecg-cli/commit/3be0c6a))
172 | - **setup:** add `-k` alias for `--skip-dependencies` ([fcd841a](https://github.com/nodecg/nodecg-cli/commit/fcd841a))
173 | - **update:** remove update command while its functionality is re-evaluated ([52fbe07](https://github.com/nodecg/nodecg-cli/commit/52fbe07))
174 |
175 | ### BREAKING CHANGES
176 |
177 | - update: remove update command
178 | - install: requires bower to be globally installed
179 |
180 |
181 |
182 | # [4.1.0](https://github.com/nodecg/nodecg-cli/compare/v4.0.0...v4.1.0) (2016-02-18)
183 |
184 | ### Features
185 |
186 | - **command:** Add defaultconfig command ([e247110](https://github.com/nodecg/nodecg-cli/commit/e247110))
187 |
188 |
189 |
190 | # [4.0.0](https://github.com/nodecg/nodecg-cli/compare/v3.0.1...v4.0.0) (2016-02-07)
191 |
192 |
193 |
194 | ## [3.0.1](https://github.com/nodecg/nodecg-cli/compare/v3.0.0...v3.0.1) (2016-02-02)
195 |
196 |
197 |
198 | # [3.0.0](https://github.com/nodecg/nodecg-cli/compare/v2.2.4...v3.0.0) (2016-02-02)
199 |
200 |
201 |
202 | ## [2.2.4](https://github.com/nodecg/nodecg-cli/compare/v2.2.3...v2.2.4) (2015-04-23)
203 |
204 |
205 |
206 | ## [2.2.3](https://github.com/nodecg/nodecg-cli/compare/v2.2.1...v2.2.3) (2015-04-10)
207 |
208 |
209 |
210 | ## [2.2.1](https://github.com/nodecg/nodecg-cli/compare/v2.2.0...v2.2.1) (2015-02-20)
211 |
212 |
213 |
214 | # [2.2.0](https://github.com/nodecg/nodecg-cli/compare/v2.1.1...v2.2.0) (2015-02-20)
215 |
216 |
217 |
218 | ## [2.1.1](https://github.com/nodecg/nodecg-cli/compare/v2.1.0...v2.1.1) (2015-02-19)
219 |
220 |
221 |
222 | ## [2.0.1](https://github.com/nodecg/nodecg-cli/compare/v2.0.0...v2.0.1) (2015-02-18)
223 |
224 |
225 |
226 | # [2.0.0](https://github.com/nodecg/nodecg-cli/compare/v1.0.3...v2.0.0) (2015-02-18)
227 |
228 |
229 |
230 | ## [1.0.3](https://github.com/nodecg/nodecg-cli/compare/v1.0.1...v1.0.3) (2015-02-14)
231 |
232 |
233 |
234 | ## [1.0.1](https://github.com/nodecg/nodecg-cli/compare/v1.0.0...v1.0.1) (2015-01-24)
235 |
236 |
237 |
238 | # [1.0.0](https://github.com/nodecg/nodecg-cli/compare/v0.0.2...v1.0.0) (2015-01-18)
239 |
240 |
241 |
242 | ## [0.0.2](https://github.com/nodecg/nodecg-cli/compare/v0.0.1...v0.0.2) (2015-01-17)
243 |
244 |
245 |
246 | ## 0.0.1 (2015-01-16)
247 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 The NodeCG Project (https://nodecg.dev)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nodecg-cli [](https://github.com/nodecg/nodecg-cli/actions/workflows/ci.yml)
2 |
3 | [NodeCG](https://github.com/nodecg/nodecg)'s command line interface.
4 |
5 | ## Deprecation Notice
6 |
7 | `nodecg-cli` has been migrated to [`nodecg`](github.com/nodecg/nodecg). This repository is now deprecated and will no longer be maintained. Please uninstall `nodecg-cli` and install `nodecg` instead.
8 |
9 | ```sh
10 | npm un -g nodecg-cli
11 | npm i -g nodecg
12 | ```
13 |
14 | The `nodecg` includes the CLI from v2.4.0, which is equivalent to `nodecg-cli@9.0.1`.
15 |
16 | ## Compatibility
17 |
18 | - `nodecg-cli` version earlier than 8.6.1 is not compatible with NodeCG 2.x.x.
19 | - `nodecg-cli` version 9.0.0 and later are not compatible with NodeCG 0.x.x and 1.x.x.
20 |
21 | | NodeCG | nodecg-cli |
22 | | ------ | ---------- |
23 | | 0.x.x | < 9.0.0 |
24 | | 1.x.x | < 9.0.0 |
25 | | 2.x.x | >= 8.6.1 |
26 |
27 | ## Installation
28 |
29 | First, make sure you have [git](http://git-scm.com/) installed, and that it is in your PATH.
30 |
31 | Once those are installed, you may install nodecg-cli via npm:
32 |
33 | ```sh
34 | npm install -g nodecg-cli
35 | ```
36 |
37 | Installing `nodecg-cli` does not install `NodeCG`.
38 | To install an instance of `NodeCG`, use the `setup` command in an empty directory:
39 |
40 | ```sh
41 | mkdir nodecg
42 | cd nodecg
43 | nodecg setup
44 | ```
45 |
46 | ## Usage
47 |
48 | - `nodecg setup [version] [--update]`, install a new instance of NodeCG. `version` is a semver range.
49 | If `version` is not supplied, the latest release will be installed.
50 | Enable `--update` flag to install over an existing copy of NodeCG.
51 | - `nodecg start`, start the NodeCG instance in this directory path
52 | - `nodecg install [repo] [--dev]`, install a bundle by cloning a git repo.
53 | Can be a GitHub owner/repo pair (`supportclass/lfg-sublistener`) or https git url (`https://github.com/SupportClass/lfg-sublistener.git`).
54 | If run in a bundle directory with no arguments, installs that bundle's dependencies.
55 | Enable `--dev` flag to install the bundle's `devDependencies`.
56 | - `nodecg uninstall `, uninstall a bundle
57 | - `nodecg defaultconfig`, If a bundle has a `configschema.json` present in its root, this command will create a default
58 | config file at `nodecg/cfg/:bundleName.json` with defaults based on that schema.
59 | - `nodecg schema-types [dir]`, Generate d.ts TypeScript typedef files from Replicant schemas and configschema.json (if present)
60 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import eslint from "@eslint/js";
4 | import tseslint from "typescript-eslint";
5 | import prettier from "eslint-config-prettier";
6 | import simpleImportSort from "eslint-plugin-simple-import-sort";
7 |
8 | export default tseslint.config(
9 | eslint.configs.recommended,
10 | tseslint.configs.strictTypeChecked,
11 | tseslint.configs.stylisticTypeChecked,
12 | {
13 | languageOptions: {
14 | parserOptions: {
15 | projectService: true,
16 | tsconfigRootDir: import.meta.dirname,
17 | },
18 | },
19 | },
20 | {
21 | rules: {
22 | "@typescript-eslint/no-explicit-any": "off",
23 | "@typescript-eslint/no-unused-vars": "off",
24 | "@typescript-eslint/no-unsafe-return": "off",
25 | "@typescript-eslint/no-unsafe-argument": "off",
26 | "@typescript-eslint/no-unsafe-assignment": "off",
27 | "@typescript-eslint/no-unsafe-member-access": "off",
28 | "@typescript-eslint/no-unsafe-call": "off",
29 | "@typescript-eslint/restrict-template-expressions": [
30 | "error",
31 | { allowNumber: true },
32 | ],
33 | },
34 | },
35 | {
36 | plugins: {
37 | "simple-import-sort": simpleImportSort,
38 | },
39 | rules: {
40 | "simple-import-sort/imports": "error",
41 | "simple-import-sort/exports": "error",
42 | },
43 | },
44 | {
45 | files: ["test/**/*.ts"],
46 | rules: {
47 | "@typescript-eslint/no-non-null-assertion": "off",
48 | },
49 | },
50 | prettier,
51 | {
52 | ignores: [
53 | "node_modules",
54 | "coverage",
55 | "dist",
56 | "test/fixtures",
57 | "eslint.config.js",
58 | ],
59 | },
60 | );
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodecg-cli",
3 | "version": "9.0.2",
4 | "description": "The NodeCG command line interface.",
5 | "bugs": {
6 | "url": "http://github.com/nodecg/nodecg-cli/issues"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git://github.com/nodecg/nodecg-cli.git"
11 | },
12 | "license": "MIT",
13 | "type": "module",
14 | "bin": {
15 | "nodecg": "dist/bin/nodecg.js"
16 | },
17 | "files": [
18 | "AUTHORS",
19 | "LICENSE",
20 | "README.md",
21 | "dist"
22 | ],
23 | "contributors": [
24 | {
25 | "name": "Alex Van Camp",
26 | "email": "email@alexvan.camp",
27 | "url": "https://alexvan.camp/"
28 | },
29 | {
30 | "name": "Matthew McNamara",
31 | "email": "matt@mattmcn.com",
32 | "url": "http://mattmcn.com/"
33 | },
34 | {
35 | "name": "Keiichiro Amemiya",
36 | "email": "kei@hoishin.dev"
37 | }
38 | ],
39 | "scripts": {
40 | "build": "del-cli dist && tsc --build tsconfig.build.json",
41 | "dev": "tsc --build tsconfig.build.json --watch",
42 | "format": "prettier --write \"**/*.ts\"",
43 | "static": "run-s static:*",
44 | "static:prettier": "prettier --check \"**/*.ts\"",
45 | "static:eslint": "eslint --cache",
46 | "fix": "run-s fix:*",
47 | "fix:prettier": "prettier --write \"**/*.ts\"",
48 | "fix:eslint": "eslint --fix",
49 | "test": "vitest"
50 | },
51 | "prettier": {},
52 | "dependencies": {
53 | "@inquirer/prompts": "^7.2.1",
54 | "ajv": "^8.17.1",
55 | "chalk": "^5.4.1",
56 | "commander": "^12.1.0",
57 | "hosted-git-info": "^8.0.2",
58 | "json-schema-to-typescript": "^15.0.3",
59 | "npm-package-arg": "^12.0.1",
60 | "semver": "^7.6.3",
61 | "tar": "^7.4.3",
62 | "update-notifier": "^7.3.1"
63 | },
64 | "devDependencies": {
65 | "@eslint/js": "^9.17.0",
66 | "@types/hosted-git-info": "^3.0.5",
67 | "@types/node": "18",
68 | "@types/npm-package-arg": "^6.1.4",
69 | "@types/semver": "^7.5.8",
70 | "@types/update-notifier": "^6.0.8",
71 | "del-cli": "^6.0.0",
72 | "eslint": "^9.17.0",
73 | "eslint-config-prettier": "^9.1.0",
74 | "eslint-plugin-simple-import-sort": "^12.1.1",
75 | "npm-run-all2": "^7.0.2",
76 | "prettier": "^3.4.2",
77 | "type-fest": "^4.31.0",
78 | "typescript": "~5.7.2",
79 | "typescript-eslint": "^8.18.2",
80 | "vitest": "^2.1.8"
81 | },
82 | "engines": {
83 | "node": "^18.17.0 || ^20.9.0 || ^22.11.0"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/bin/nodecg.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { execSync } from "node:child_process";
4 |
5 | import chalk from "chalk";
6 | import updateNotifier from "update-notifier";
7 |
8 | import packageJson from "../../package.json" with { type: "json" };
9 |
10 | console.warn(
11 | "`nodecg-cli` package is deprecated. Please uninstall `nodecg-cli` and install `nodecg` instead.",
12 | );
13 |
14 | updateNotifier({ pkg: packageJson }).notify();
15 |
16 | try {
17 | execSync("git --version");
18 | } catch {
19 | console.error(
20 | `nodecg-cli requires that ${chalk.cyan("git")} be available in your PATH.`,
21 | );
22 | process.exit(1);
23 | }
24 |
25 | await import("../index.js");
26 |
--------------------------------------------------------------------------------
/src/commands/defaultconfig.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 |
4 | import { Ajv, type JSONSchemaType } from "ajv";
5 | import chalk from "chalk";
6 | import { Command } from "commander";
7 |
8 | import { getNodeCGPath, isBundleFolder } from "../lib/util.js";
9 |
10 | const ajv = new Ajv({ useDefaults: true, strict: true });
11 |
12 | export function defaultconfigCommand(program: Command) {
13 | program
14 | .command("defaultconfig [bundle]")
15 | .description("Generate default config from configschema.json")
16 | .action(action);
17 | }
18 |
19 | function action(bundleName?: string) {
20 | const cwd = process.cwd();
21 | const nodecgPath = getNodeCGPath();
22 |
23 | if (!bundleName) {
24 | if (isBundleFolder(cwd)) {
25 | bundleName = bundleName ?? path.basename(cwd);
26 | } else {
27 | console.error(
28 | `${chalk.red("Error:")} No bundle found in the current directory!`,
29 | );
30 | return;
31 | }
32 | }
33 |
34 | const bundlePath = path.join(nodecgPath, "bundles/", bundleName);
35 | const schemaPath = path.join(
36 | nodecgPath,
37 | "bundles/",
38 | bundleName,
39 | "/configschema.json",
40 | );
41 | const cfgPath = path.join(nodecgPath, "cfg/");
42 |
43 | if (!fs.existsSync(bundlePath)) {
44 | console.error(`${chalk.red("Error:")} Bundle ${bundleName} does not exist`);
45 | return;
46 | }
47 |
48 | if (!fs.existsSync(schemaPath)) {
49 | console.error(
50 | `${chalk.red("Error:")} Bundle ${bundleName} does not have a configschema.json`,
51 | );
52 | return;
53 | }
54 |
55 | if (!fs.existsSync(cfgPath)) {
56 | fs.mkdirSync(cfgPath);
57 | }
58 |
59 | const schema: JSONSchemaType = JSON.parse(
60 | fs.readFileSync(schemaPath, "utf8"),
61 | );
62 | const configPath = path.join(nodecgPath, "cfg/", `${bundleName}.json`);
63 | if (fs.existsSync(configPath)) {
64 | console.error(
65 | `${chalk.red("Error:")} Bundle ${bundleName} already has a config file`,
66 | );
67 | } else {
68 | try {
69 | const validate = ajv.compile(schema);
70 | const data = {};
71 | validate(data);
72 |
73 | fs.writeFileSync(configPath, JSON.stringify(data, null, 2));
74 | console.log(
75 | `${chalk.green("Success:")} Created ${chalk.bold(bundleName)}'s default config from schema\n`,
76 | );
77 | } catch (error) {
78 | console.error(chalk.red("Error:"), error);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/commands/index.ts:
--------------------------------------------------------------------------------
1 | import type { Command } from "commander";
2 |
3 | import { defaultconfigCommand } from "./defaultconfig.js";
4 | import { installCommand } from "./install.js";
5 | import { schemaTypesCommand } from "./schema-types.js";
6 | import { setupCommand } from "./setup.js";
7 | import { startCommand } from "./start.js";
8 | import { uninstallCommand } from "./uninstall.js";
9 |
10 | export function setupCommands(program: Command) {
11 | defaultconfigCommand(program);
12 | installCommand(program);
13 | schemaTypesCommand(program);
14 | setupCommand(program);
15 | startCommand(program);
16 | uninstallCommand(program);
17 | }
18 |
--------------------------------------------------------------------------------
/src/commands/install.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 | import fs from "node:fs";
3 | import os from "node:os";
4 | import path from "node:path";
5 |
6 | import chalk from "chalk";
7 | import { Command } from "commander";
8 | import HostedGitInfo from "hosted-git-info";
9 | import npa from "npm-package-arg";
10 | import semver, { SemVer } from "semver";
11 |
12 | import { fetchTags } from "../lib/fetch-tags.js";
13 | import { installBundleDeps } from "../lib/install-bundle-deps.js";
14 | import { getNodeCGPath } from "../lib/util.js";
15 |
16 | export function installCommand(program: Command) {
17 | program
18 | .command("install [repo]")
19 | .description(
20 | "Install a bundle by cloning a git repo. Can be a GitHub owner/repo pair or a git url." +
21 | "\n\t\t If run in a bundle directory with no arguments, installs that bundle's dependencies.",
22 | )
23 | .option("-d, --dev", "install development npm & bower dependencies")
24 | .action(action);
25 | }
26 |
27 | function action(repo: string, options: { dev: boolean }) {
28 | const dev = options.dev || false;
29 |
30 | // If no args are supplied, assume the user is intending to operate on the bundle in the current dir
31 | if (!repo) {
32 | installBundleDeps(process.cwd(), dev);
33 | return;
34 | }
35 |
36 | let range = "";
37 | if (repo.indexOf("#") > 0) {
38 | const repoParts = repo.split("#");
39 | range = repoParts[1] ?? "";
40 | repo = repoParts[0] ?? "";
41 | }
42 |
43 | const nodecgPath = getNodeCGPath();
44 | const parsed = npa(repo);
45 | if (!parsed.hosted) {
46 | console.error(
47 | "Please enter a valid git repository URL or GitHub username/repo pair.",
48 | );
49 | return;
50 | }
51 |
52 | const hostedInfo = parsed.hosted as unknown as HostedGitInfo;
53 | const repoUrl = hostedInfo.https();
54 | if (!repoUrl) {
55 | console.error(
56 | "Please enter a valid git repository URL or GitHub username/repo pair.",
57 | );
58 | return;
59 | }
60 |
61 | // Check that `bundles` exists
62 | const bundlesPath = path.join(nodecgPath, "bundles");
63 | /* istanbul ignore next: Simple directory creation, not necessary to test */
64 | if (!fs.existsSync(bundlesPath)) {
65 | fs.mkdirSync(bundlesPath);
66 | }
67 |
68 | // Extract repo name from git url
69 | const temp = repoUrl.split("/").pop() ?? "";
70 | const bundleName = temp.slice(0, temp.length - 4);
71 | const bundlePath = path.join(nodecgPath, "bundles/", bundleName);
72 |
73 | // Figure out what version to checkout
74 | process.stdout.write(`Fetching ${bundleName} release list... `);
75 | let tags;
76 | let target;
77 | try {
78 | tags = fetchTags(repoUrl);
79 | target = semver.maxSatisfying(
80 | tags
81 | .map((tag) => semver.coerce(tag))
82 | .filter((coercedTag): coercedTag is SemVer => Boolean(coercedTag)),
83 | range,
84 | );
85 | process.stdout.write(chalk.green("done!") + os.EOL);
86 | } catch (e: any) {
87 | /* istanbul ignore next */
88 | process.stdout.write(chalk.red("failed!") + os.EOL);
89 | /* istanbul ignore next */
90 | console.error(e.stack);
91 | /* istanbul ignore next */
92 | return;
93 | }
94 |
95 | // Clone from github
96 | process.stdout.write(`Installing ${bundleName}... `);
97 | try {
98 | execSync(`git clone ${repoUrl} "${bundlePath}"`, {
99 | stdio: ["pipe", "pipe", "pipe"],
100 | });
101 | process.stdout.write(chalk.green("done!") + os.EOL);
102 | } catch (e: any) {
103 | /* istanbul ignore next */
104 | process.stdout.write(chalk.red("failed!") + os.EOL);
105 | /* istanbul ignore next */
106 | console.error(e.stack);
107 | /* istanbul ignore next */
108 | return;
109 | }
110 |
111 | // If a bundle has no git tags, target will be null.
112 | if (target) {
113 | process.stdout.write(`Checking out version ${target.version}... `);
114 | try {
115 | // First try the target as-is.
116 | execSync(`git checkout ${target.version}`, {
117 | cwd: bundlePath,
118 | stdio: ["pipe", "pipe", "pipe"],
119 | });
120 | process.stdout.write(chalk.green("done!") + os.EOL);
121 | } catch (_) {
122 | try {
123 | // Next try prepending `v` to the target, which may have been stripped by `semver.coerce`.
124 | execSync(`git checkout v${target.version}`, {
125 | cwd: bundlePath,
126 | stdio: ["pipe", "pipe", "pipe"],
127 | });
128 | process.stdout.write(chalk.green("done!") + os.EOL);
129 | } catch (e: any) {
130 | /* istanbul ignore next */
131 | process.stdout.write(chalk.red("failed!") + os.EOL);
132 | /* istanbul ignore next */
133 | console.error(e.stack);
134 | /* istanbul ignore next */
135 | return;
136 | }
137 | }
138 | }
139 |
140 | // After installing the bundle, install its npm dependencies
141 | installBundleDeps(bundlePath, dev);
142 | }
143 |
--------------------------------------------------------------------------------
/src/commands/schema-types.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 |
4 | import chalk from "chalk";
5 | import { Command } from "commander";
6 | import { compileFromFile } from "json-schema-to-typescript";
7 |
8 | export function schemaTypesCommand(program: Command) {
9 | program
10 | .command("schema-types [dir]")
11 | .option(
12 | "-o, --out-dir [path]",
13 | "Where to put the generated d.ts files",
14 | "src/types/schemas",
15 | )
16 | .option(
17 | "--no-config-schema",
18 | "Don't generate a typedef from configschema.json",
19 | )
20 | .description(
21 | "Generate d.ts TypeScript typedef files from Replicant schemas and configschema.json (if present)",
22 | )
23 | .action(action);
24 | }
25 |
26 | function action(inDir: string, cmd: { outDir: string; configSchema: boolean }) {
27 | const processCwd = process.cwd();
28 | const schemasDir = path.resolve(processCwd, inDir || "schemas");
29 | if (!fs.existsSync(schemasDir)) {
30 | console.error(`${chalk.red("Error:")} Input directory does not exist`);
31 | return;
32 | }
33 |
34 | const outDir = path.resolve(processCwd, cmd.outDir);
35 | if (!fs.existsSync(outDir)) {
36 | fs.mkdirSync(outDir, { recursive: true });
37 | }
38 |
39 | const configSchemaPath = path.join(processCwd, "configschema.json");
40 | const schemas = fs.readdirSync(schemasDir).filter((f) => f.endsWith(".json"));
41 |
42 | const style = {
43 | singleQuote: true,
44 | useTabs: true,
45 | };
46 |
47 | const compilePromises: Promise[] = [];
48 | const compile = (input: string, output: string, cwd = processCwd) => {
49 | const promise = compileFromFile(input, {
50 | cwd,
51 | declareExternallyReferenced: true,
52 | enableConstEnums: true,
53 | style,
54 | })
55 | .then((ts) =>
56 | fs.promises.writeFile(output, "/* prettier-ignore */\n" + ts),
57 | )
58 | .then(() => {
59 | console.log(output);
60 | })
61 | .catch((err: unknown) => {
62 | console.error(err);
63 | });
64 | compilePromises.push(promise);
65 | };
66 |
67 | if (fs.existsSync(configSchemaPath) && cmd.configSchema) {
68 | compile(configSchemaPath, path.resolve(outDir, "configschema.d.ts"));
69 | }
70 |
71 | for (const schema of schemas) {
72 | compile(
73 | path.resolve(schemasDir, schema),
74 | path.resolve(outDir, schema.replace(/\.json$/i, ".d.ts")),
75 | schemasDir,
76 | );
77 | }
78 |
79 | return Promise.all(compilePromises).then(() => {
80 | (process.emit as any)("schema-types-done");
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/src/commands/setup.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 | import fs from "node:fs";
3 | import os from "node:os";
4 | import stream from "node:stream/promises";
5 |
6 | import { confirm } from "@inquirer/prompts";
7 | import chalk from "chalk";
8 | import { Command } from "commander";
9 | import semver from "semver";
10 | import * as tar from "tar";
11 |
12 | import { fetchTags } from "../lib/fetch-tags.js";
13 | import type { NpmRelease } from "../lib/sample/npm-release.js";
14 | import { getCurrentNodeCGVersion, pathContainsNodeCG } from "../lib/util.js";
15 |
16 | const NODECG_GIT_URL = "https://github.com/nodecg/nodecg.git";
17 |
18 | export function setupCommand(program: Command) {
19 | program
20 | .command("setup [version]")
21 | .option("-u, --update", "Update the local NodeCG installation")
22 | .option(
23 | "-k, --skip-dependencies",
24 | "Skip installing npm & bower dependencies",
25 | )
26 | .description("Install a new NodeCG instance")
27 | .action(decideActionVersion);
28 | }
29 |
30 | async function decideActionVersion(
31 | version: string,
32 | options: { update: boolean; skipDependencies: boolean },
33 | ) {
34 | // If NodeCG is already installed but the `-u` flag was not supplied, display an error and return.
35 | let isUpdate = false;
36 |
37 | // If NodeCG exists in the cwd, but the `-u` flag was not supplied, display an error and return.
38 | // If it was supplied, fetch the latest tags and set the `isUpdate` flag to true for later use.
39 | // Else, if this is a clean, empty directory, then we need to clone a fresh copy of NodeCG into the cwd.
40 | if (pathContainsNodeCG(process.cwd())) {
41 | if (!options.update) {
42 | console.error("NodeCG is already installed in this directory.");
43 | console.error(
44 | `Use ${chalk.cyan("nodecg setup [version] -u")} if you want update your existing install.`,
45 | );
46 | return;
47 | }
48 |
49 | isUpdate = true;
50 | }
51 |
52 | if (version) {
53 | process.stdout.write(
54 | `Finding latest release that satisfies semver range ${chalk.magenta(version)}... `,
55 | );
56 | } else if (isUpdate) {
57 | process.stdout.write("Checking against local install for updates... ");
58 | } else {
59 | process.stdout.write("Finding latest release... ");
60 | }
61 |
62 | let tags;
63 | try {
64 | tags = fetchTags(NODECG_GIT_URL);
65 | } catch (error) {
66 | process.stdout.write(chalk.red("failed!") + os.EOL);
67 | console.error(error instanceof Error ? error.message : error);
68 | return;
69 | }
70 |
71 | let target: string;
72 |
73 | // If a version (or semver range) was supplied, find the latest release that satisfies the range.
74 | // Else, make the target the newest version.
75 | if (version) {
76 | const maxSatisfying = semver.maxSatisfying(tags, version);
77 | if (!maxSatisfying) {
78 | process.stdout.write(chalk.red("failed!") + os.EOL);
79 | console.error(
80 | `No releases match the supplied semver range (${chalk.magenta(version)})`,
81 | );
82 | return;
83 | }
84 |
85 | target = maxSatisfying;
86 | } else {
87 | target = semver.maxSatisfying(tags, "") ?? "";
88 | }
89 |
90 | process.stdout.write(chalk.green("done!") + os.EOL);
91 |
92 | let current: string | undefined;
93 | let downgrade = false;
94 |
95 | if (isUpdate) {
96 | current = getCurrentNodeCGVersion();
97 |
98 | if (semver.eq(target, current)) {
99 | console.log(
100 | `The target version (${chalk.magenta(target)}) is equal to the current version (${chalk.magenta(current)}). No action will be taken.`,
101 | );
102 | return;
103 | }
104 |
105 | if (semver.lt(target, current)) {
106 | console.log(
107 | `${chalk.red("WARNING:")} The target version (${chalk.magenta(target)}) is older than the current version (${chalk.magenta(current)})`,
108 | );
109 |
110 | const answer = await confirm({
111 | message: "Are you sure you wish to continue?",
112 | });
113 |
114 | if (!answer) {
115 | console.log("Setup cancelled.");
116 | return;
117 | }
118 |
119 | downgrade = true;
120 | }
121 | }
122 |
123 | if (semver.lt(target, "v2.0.0")) {
124 | console.error(
125 | "nodecg-cli does not support NodeCG versions older than v2.0.0.",
126 | );
127 | return;
128 | }
129 |
130 | await installNodecg(current, target, isUpdate);
131 |
132 | // Install NodeCG's dependencies
133 | // This operation takes a very long time, so we don't test it.
134 | if (!options.skipDependencies) {
135 | installDependencies();
136 | }
137 |
138 | if (isUpdate) {
139 | const verb = downgrade ? "downgraded" : "upgraded";
140 | console.log(`NodeCG ${verb} to ${chalk.magenta(target)}`);
141 | } else {
142 | console.log(`NodeCG (${target}) installed to ${process.cwd()}`);
143 | }
144 | }
145 |
146 | async function installNodecg(
147 | current: string | undefined,
148 | target: string,
149 | isUpdate: boolean,
150 | ) {
151 | if (isUpdate) {
152 | const deletingDirectories = [".git", "build", "scripts", "schemas"];
153 | await Promise.all(
154 | deletingDirectories.map((dir) =>
155 | fs.promises.rm(dir, { recursive: true, force: true }),
156 | ),
157 | );
158 | }
159 |
160 | process.stdout.write(`Downloading ${target} from npm... `);
161 |
162 | const targetVersion = semver.coerce(target)?.version;
163 | if (!targetVersion) {
164 | throw new Error(`Failed to determine target NodeCG version`);
165 | }
166 | const releaseResponse = await fetch(
167 | `http://registry.npmjs.org/nodecg/${targetVersion}`,
168 | );
169 | if (!releaseResponse.ok) {
170 | throw new Error(
171 | `Failed to fetch NodeCG release information from npm, status code ${releaseResponse.status}`,
172 | );
173 | }
174 | const release = (await releaseResponse.json()) as NpmRelease;
175 |
176 | process.stdout.write(chalk.green("done!") + os.EOL);
177 |
178 | if (current) {
179 | const verb = semver.lt(target, current) ? "Downgrading" : "Upgrading";
180 | process.stdout.write(
181 | `${verb} from ${chalk.magenta(current)} to ${chalk.magenta(target)}... `,
182 | );
183 | }
184 |
185 | const tarballResponse = await fetch(release.dist.tarball);
186 | if (!tarballResponse.ok || !tarballResponse.body) {
187 | throw new Error(
188 | `Failed to fetch release tarball from ${release.dist.tarball}, status code ${tarballResponse.status}`,
189 | );
190 | }
191 | await stream.pipeline(tarballResponse.body, tar.x({ strip: 1 }));
192 | }
193 |
194 | function installDependencies() {
195 | try {
196 | process.stdout.write("Installing production npm dependencies... ");
197 | execSync("npm install --production");
198 |
199 | process.stdout.write(chalk.green("done!") + os.EOL);
200 | } catch (e: any) {
201 | process.stdout.write(chalk.red("failed!") + os.EOL);
202 | console.error(e);
203 | return;
204 | }
205 |
206 | if (fs.existsSync("./bower.json")) {
207 | process.stdout.write("Installing production bower dependencies... ");
208 | try {
209 | execSync("bower install --production", {
210 | stdio: ["pipe", "pipe", "pipe"],
211 | });
212 | process.stdout.write(chalk.green("done!") + os.EOL);
213 | } catch (e: any) {
214 | process.stdout.write(chalk.red("failed!") + os.EOL);
215 | console.error(e.stack);
216 | }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/commands/start.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 |
3 | import { Command } from "commander";
4 |
5 | import { pathContainsNodeCG } from "../lib/util.js";
6 |
7 | export function startCommand(program: Command) {
8 | program
9 | .command("start")
10 | .option("-d, --disable-source-maps", "Disable source map support")
11 | .description("Start NodeCG")
12 | .action((options: { disableSourceMaps: boolean }) => {
13 | // Check if nodecg is already installed
14 | if (pathContainsNodeCG(process.cwd())) {
15 | if (options.disableSourceMaps) {
16 | execSync("node index.js", { stdio: "inherit" });
17 | } else {
18 | execSync("node --enable-source-maps index.js", { stdio: "inherit" });
19 | }
20 | } else {
21 | console.warn("No NodeCG installation found in this folder.");
22 | }
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/commands/uninstall.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import os from "node:os";
3 | import path from "node:path";
4 |
5 | import { confirm } from "@inquirer/prompts";
6 | import chalk from "chalk";
7 | import { Command } from "commander";
8 |
9 | import { getNodeCGPath } from "../lib/util.js";
10 |
11 | export function uninstallCommand(program: Command) {
12 | program
13 | .command("uninstall ")
14 | .description("Uninstalls a bundle.")
15 | .option("-f, --force", "ignore warnings")
16 | .action(action);
17 | }
18 |
19 | function action(bundleName: string, options: { force: boolean }) {
20 | const nodecgPath = getNodeCGPath();
21 | const bundlePath = path.join(nodecgPath, "bundles/", bundleName);
22 |
23 | if (!fs.existsSync(bundlePath)) {
24 | console.error(
25 | `Cannot uninstall ${chalk.magenta(bundleName)}: bundle is not installed.`,
26 | );
27 | return;
28 | }
29 |
30 | /* istanbul ignore if: deleteBundle() is tested in the else path */
31 | if (options.force) {
32 | deleteBundle(bundleName, bundlePath);
33 | } else {
34 | void confirm({
35 | message: `Are you sure you wish to uninstall ${chalk.magenta(bundleName)}?`,
36 | }).then((answer) => {
37 | if (answer) {
38 | deleteBundle(bundleName, bundlePath);
39 | }
40 | });
41 | }
42 | }
43 |
44 | function deleteBundle(name: string, path: string) {
45 | if (!fs.existsSync(path)) {
46 | console.log("Nothing to uninstall.");
47 | return;
48 | }
49 |
50 | process.stdout.write(`Uninstalling ${chalk.magenta(name)}... `);
51 | try {
52 | fs.rmSync(path, { recursive: true, force: true });
53 | } catch (e: any) {
54 | /* istanbul ignore next */
55 | process.stdout.write(chalk.red("failed!") + os.EOL);
56 | /* istanbul ignore next */
57 | console.error(e.stack);
58 | /* istanbul ignore next */
59 | return;
60 | }
61 |
62 | process.stdout.write(chalk.green("done!") + os.EOL);
63 | }
64 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | process.title = "nodecg";
2 |
3 | import { Command } from "commander";
4 |
5 | import packageJson from "../package.json" with { type: "json" };
6 | import { setupCommands } from "./commands/index.js";
7 |
8 | const program = new Command("nodecg");
9 |
10 | // Initialise CLI
11 | program.version(packageJson.version).usage(" [options]");
12 |
13 | // Initialise commands
14 | setupCommands(program);
15 |
16 | // Handle unknown commands
17 | program.on("*", () => {
18 | console.log("Unknown command:", program.args.join(" "));
19 | program.help();
20 | });
21 |
22 | // Print help if no commands were given
23 | if (!process.argv.slice(2).length) {
24 | program.help();
25 | }
26 |
27 | // Process commands
28 | program.parse(process.argv);
29 |
--------------------------------------------------------------------------------
/src/lib/fetch-tags.ts:
--------------------------------------------------------------------------------
1 | import { spawnSync } from "node:child_process";
2 |
3 | export function fetchTags(repoUrl: string) {
4 | return spawnSync("git", ["ls-remote", "--refs", "--tags", repoUrl])
5 | .stdout.toString("utf-8")
6 | .trim()
7 | .split("\n")
8 | .map((rawTag) => rawTag.split("refs/tags/").at(-1))
9 | .filter((t) => typeof t === "string");
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/install-bundle-deps.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 | import fs from "node:fs";
3 | import os from "node:os";
4 | import path from "node:path";
5 |
6 | import chalk from "chalk";
7 |
8 | import { isBundleFolder } from "./util.js";
9 |
10 | /**
11 | * Installs npm and bower dependencies for the NodeCG bundle present at the given path.
12 | * @param bundlePath - The path of the NodeCG bundle to install dependencies for.
13 | * @param installDev - Whether to install devDependencies.
14 | */
15 | export function installBundleDeps(bundlePath: string, installDev = false) {
16 | if (!isBundleFolder(bundlePath)) {
17 | console.error(
18 | `${chalk.red("Error:")} There doesn't seem to be a valid NodeCG bundle in this folder:\n\t${chalk.magenta(bundlePath)}`,
19 | );
20 | process.exit(1);
21 | }
22 |
23 | let cmdline;
24 |
25 | const cachedCwd = process.cwd();
26 | if (fs.existsSync(path.join(bundlePath, "package.json"))) {
27 | process.chdir(bundlePath);
28 | let cmdline: string;
29 | if (fs.existsSync(path.join(bundlePath, "yarn.lock"))) {
30 | cmdline = installDev ? "yarn" : "yarn --production";
31 | process.stdout.write(
32 | `Installling npm dependencies with yarn (dev: ${installDev})... `,
33 | );
34 | } else {
35 | cmdline = installDev ? "npm install" : "npm install --production";
36 | process.stdout.write(
37 | `Installing npm dependencies (dev: ${installDev})... `,
38 | );
39 | }
40 |
41 | try {
42 | execSync(cmdline, {
43 | cwd: bundlePath,
44 | stdio: ["pipe", "pipe", "pipe"],
45 | });
46 | process.stdout.write(chalk.green("done!") + os.EOL);
47 | } catch (e: any) {
48 | /* istanbul ignore next */
49 | process.stdout.write(chalk.red("failed!") + os.EOL);
50 | /* istanbul ignore next */
51 | console.error(e.stack);
52 | /* istanbul ignore next */
53 | return;
54 | }
55 |
56 | process.chdir(cachedCwd);
57 | }
58 |
59 | if (fs.existsSync(path.join(bundlePath, "bower.json"))) {
60 | cmdline = installDev ? "bower install" : "bower install --production";
61 | process.stdout.write(
62 | `Installing bower dependencies (dev: ${installDev})... `,
63 | );
64 | try {
65 | execSync(cmdline, {
66 | cwd: bundlePath,
67 | stdio: ["pipe", "pipe", "pipe"],
68 | });
69 | process.stdout.write(chalk.green("done!") + os.EOL);
70 | } catch (e: any) {
71 | /* istanbul ignore next */
72 | process.stdout.write(chalk.red("failed!") + os.EOL);
73 | /* istanbul ignore next */
74 | console.error(e.stack);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/lib/sample/npm-release.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodecg-cli",
3 | "version": "8.7.0",
4 | "license": "MIT",
5 | "_id": "nodecg-cli@8.7.0",
6 | "maintainers": [
7 | { "name": "mattmcnam", "email": "matt@mattmcn.com" },
8 | { "name": "hoishin", "email": "hoishinxii@gmail.com" }
9 | ],
10 | "contributors": [
11 | {
12 | "url": "https://alexvan.camp/",
13 | "name": "Alex Van Camp",
14 | "email": "email@alexvan.camp"
15 | },
16 | {
17 | "url": "http://mattmcn.com/",
18 | "name": "Matthew McNamara",
19 | "email": "matt@mattmcn.com"
20 | },
21 | { "name": "Keiichiro Amemiya", "email": "kei@hoishin.dev" }
22 | ],
23 | "homepage": "https://github.com/nodecg/nodecg-cli#readme",
24 | "bugs": { "url": "http://github.com/nodecg/nodecg-cli/issues" },
25 | "bin": { "nodecg": "dist/bin/nodecg.js" },
26 | "dist": {
27 | "shasum": "bd59db2d98c2077bc03623cb9be59ff45fb511a2",
28 | "tarball": "https://registry.npmjs.org/nodecg-cli/-/nodecg-cli-8.7.0.tgz",
29 | "fileCount": 28,
30 | "integrity": "sha512-RcoE8PhtivBz1dtKDmgBh3MTozckwymcPr9UC1oiagYMlDLoTvLC1Bqssef5h+rNNK/aBeVB33leySFEQWNbjQ==",
31 | "signatures": [
32 | {
33 | "sig": "MEUCIQDuXO/x48us+Bt7A9SskHcLDaAmDFhEDCgbMcTfolZHUQIgNLF0GtmhOF5COKdZue+HwYIRAeO8KbScnZeE/U6PQe4=",
34 | "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"
35 | }
36 | ],
37 | "unpackedSize": 155193
38 | },
39 | "type": "module",
40 | "engines": { "node": "^18.17.0 || ^20.9.0 || ^22.11.0" },
41 | "gitHead": "15d3992ec6d6eeb874d9c20473a63435689acbc9",
42 | "scripts": {
43 | "dev": "tsc --build tsconfig.build.json --watch",
44 | "fix": "run-s fix:*",
45 | "test": "vitest",
46 | "build": "del-cli dist && tsc --build tsconfig.build.json",
47 | "format": "prettier --write \"**/*.ts\"",
48 | "static": "run-s static:*",
49 | "fix:eslint": "eslint --fix",
50 | "fix:prettier": "prettier --write \"**/*.ts\"",
51 | "static:eslint": "eslint --cache",
52 | "static:prettier": "prettier --check \"**/*.ts\""
53 | },
54 | "_npmUser": { "name": "hoishin", "email": "hoishinxii@gmail.com" },
55 | "prettier": {},
56 | "repository": {
57 | "url": "git://github.com/nodecg/nodecg-cli.git",
58 | "type": "git"
59 | },
60 | "_npmVersion": "10.9.0",
61 | "description": "The NodeCG command line interface.",
62 | "directories": {},
63 | "_nodeVersion": "22.12.0",
64 | "dependencies": {
65 | "tar": "^7.4.3",
66 | "chalk": "^5.4.1",
67 | "semver": "^7.6.3",
68 | "inquirer": "^12.3.0",
69 | "commander": "^12.1.0",
70 | "hosted-git-info": "^8.0.2",
71 | "npm-package-arg": "^12.0.1",
72 | "json-schema-defaults": "0.4.0",
73 | "json-schema-to-typescript": "^15.0.3"
74 | },
75 | "_hasShrinkwrap": false,
76 | "devDependencies": {
77 | "eslint": "^9.17.0",
78 | "vitest": "^2.1.8",
79 | "del-cli": "^6.0.0",
80 | "prettier": "^3.4.2",
81 | "type-fest": "^4.31.0",
82 | "@eslint/js": "^9.17.0",
83 | "typescript": "~5.7.2",
84 | "@types/node": "18",
85 | "npm-run-all2": "^7.0.2",
86 | "@types/semver": "^7.5.8",
87 | "typescript-eslint": "^8.18.2",
88 | "@vitest/coverage-v8": "^2.1.8",
89 | "@types/hosted-git-info": "^3.0.5",
90 | "@types/npm-package-arg": "^6.1.4",
91 | "eslint-config-prettier": "^9.1.0",
92 | "eslint-plugin-simple-import-sort": "^12.1.1"
93 | },
94 | "_npmOperationalInternal": {
95 | "tmp": "tmp/nodecg-cli_8.7.0_1735252303987_0.756945223884677",
96 | "host": "s3://npm-registry-packages-npm-production"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/lib/sample/npm-release.ts:
--------------------------------------------------------------------------------
1 | import npmRelease from "./npm-release.json" with { type: "json" };
2 |
3 | export type NpmRelease = typeof npmRelease;
4 |
--------------------------------------------------------------------------------
/src/lib/util.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 |
4 | /**
5 | * Checks if the given directory contains a NodeCG installation.
6 | * @param pathToCheck
7 | */
8 | export function pathContainsNodeCG(pathToCheck: string): boolean {
9 | const pjsonPath = path.join(pathToCheck, "package.json");
10 | try {
11 | const pjson = JSON.parse(fs.readFileSync(pjsonPath, "utf-8"));
12 | return pjson.name.toLowerCase() === "nodecg";
13 | } catch {
14 | return false;
15 | }
16 | }
17 |
18 | /**
19 | * Gets the nearest NodeCG installation folder. First looks in process.cwd(), then looks
20 | * in every parent folder until reaching the root. Throws an error if no NodeCG installation
21 | * could be found.
22 | */
23 | export function getNodeCGPath() {
24 | let curr = process.cwd();
25 | do {
26 | if (pathContainsNodeCG(curr)) {
27 | return curr;
28 | }
29 |
30 | const nextCurr = path.resolve(curr, "..");
31 | if (nextCurr === curr) {
32 | throw new Error(
33 | "NodeCG installation could not be found in this directory or any parent directory.",
34 | );
35 | }
36 |
37 | curr = nextCurr;
38 | } while (fs.lstatSync(curr).isDirectory());
39 |
40 | throw new Error(
41 | "NodeCG installation could not be found in this directory or any parent directory.",
42 | );
43 | }
44 |
45 | /**
46 | * Checks if the given directory is a NodeCG bundle.
47 | */
48 | export function isBundleFolder(pathToCheck: string) {
49 | const pjsonPath = path.join(pathToCheck, "package.json");
50 | if (fs.existsSync(pjsonPath)) {
51 | const pjson = JSON.parse(fs.readFileSync(pjsonPath, "utf8"));
52 | return typeof pjson.nodecg === "object";
53 | }
54 |
55 | return false;
56 | }
57 |
58 | /**
59 | * Gets the currently-installed NodeCG version string, in the format "vX.Y.Z"
60 | */
61 | export function getCurrentNodeCGVersion(): string {
62 | const nodecgPath = getNodeCGPath();
63 | return JSON.parse(fs.readFileSync(`${nodecgPath}/package.json`, "utf8"))
64 | .version;
65 | }
66 |
--------------------------------------------------------------------------------
/test/commands/defaultconfig.spec.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 | import { fileURLToPath } from "node:url";
4 |
5 | import { Command } from "commander";
6 | import { beforeEach, describe, expect, it, vi } from "vitest";
7 |
8 | import { defaultconfigCommand } from "../../src/commands/defaultconfig.js";
9 | import { createMockProgram, MockCommand } from "../mocks/program.js";
10 | import { setupTmpDir } from "./tmp-dir.js";
11 |
12 | const dirname = path.dirname(fileURLToPath(import.meta.url));
13 |
14 | let program: MockCommand;
15 |
16 | beforeEach(() => {
17 | // Set up environment.
18 | const tempFolder = setupTmpDir();
19 | process.chdir(tempFolder);
20 | fs.writeFileSync("package.json", JSON.stringify({ name: "nodecg" }));
21 |
22 | // Copy fixtures.
23 | fs.cpSync(path.resolve(dirname, "../fixtures/"), "./", { recursive: true });
24 |
25 | // Build program.
26 | program = createMockProgram();
27 | defaultconfigCommand(program as unknown as Command);
28 | });
29 |
30 | describe("when run with a bundle argument", () => {
31 | it("should successfully create a bundle config file when bundle has configschema.json", async () => {
32 | await program.runWith("defaultconfig config-schema");
33 | const config = JSON.parse(
34 | fs.readFileSync("./cfg/config-schema.json", { encoding: "utf8" }),
35 | );
36 | expect(config.username).toBe("user");
37 | expect(config.value).toBe(5);
38 | expect(config.nodefault).toBeUndefined();
39 | });
40 |
41 | it("should print an error when the target bundle does not have a configschema.json", async () => {
42 | const spy = vi.spyOn(console, "error");
43 | fs.mkdirSync(
44 | path.resolve(process.cwd(), "./bundles/missing-schema-bundle"),
45 | { recursive: true },
46 | );
47 | await program.runWith("defaultconfig missing-schema-bundle");
48 | expect(spy.mock.calls[0]).toMatchInlineSnapshot(
49 | `
50 | [
51 | "Error: Bundle missing-schema-bundle does not have a configschema.json",
52 | ]
53 | `,
54 | );
55 | spy.mockRestore();
56 | });
57 |
58 | it("should print an error when the target bundle does not exist", async () => {
59 | const spy = vi.spyOn(console, "error");
60 | await program.runWith("defaultconfig not-installed");
61 | expect(spy.mock.calls[0]).toMatchInlineSnapshot(
62 | `
63 | [
64 | "Error: Bundle not-installed does not exist",
65 | ]
66 | `,
67 | );
68 | spy.mockRestore();
69 | });
70 |
71 | it("should print an error when the target bundle already has a config", async () => {
72 | const spy = vi.spyOn(console, "error");
73 | fs.mkdirSync("./cfg");
74 | fs.writeFileSync(
75 | "./cfg/config-schema.json",
76 | JSON.stringify({ fake: "data" }),
77 | );
78 | await program.runWith("defaultconfig config-schema");
79 | expect(spy.mock.calls[0]).toMatchInlineSnapshot(
80 | `
81 | [
82 | "Error: Bundle config-schema already has a config file",
83 | ]
84 | `,
85 | );
86 | spy.mockRestore();
87 | });
88 | });
89 |
90 | describe("when run with no arguments", () => {
91 | it("should successfully create a bundle config file when run from inside bundle directory", async () => {
92 | process.chdir("./bundles/config-schema");
93 | await program.runWith("defaultconfig");
94 | expect(fs.existsSync("../../cfg/config-schema.json")).toBe(true);
95 | });
96 |
97 | it("should print an error when in a folder with no package.json", async () => {
98 | fs.mkdirSync(path.resolve(process.cwd(), "./bundles/not-a-bundle"), {
99 | recursive: true,
100 | });
101 | process.chdir("./bundles/not-a-bundle");
102 |
103 | const spy = vi.spyOn(console, "error");
104 | await program.runWith("defaultconfig");
105 | expect(spy.mock.calls[0]).toMatchInlineSnapshot(
106 | `
107 | [
108 | "Error: No bundle found in the current directory!",
109 | ]
110 | `,
111 | );
112 | spy.mockRestore();
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/test/commands/install.spec.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 |
3 | import { Command } from "commander";
4 | import semver from "semver";
5 | import { beforeEach, expect, it, vi } from "vitest";
6 |
7 | import { installCommand } from "../../src/commands/install.js";
8 | import { createMockProgram, MockCommand } from "../mocks/program.js";
9 | import { setupTmpDir } from "./tmp-dir.js";
10 |
11 | let program: MockCommand;
12 | const tempFolder = setupTmpDir();
13 | process.chdir(tempFolder);
14 | fs.writeFileSync("package.json", JSON.stringify({ name: "nodecg" }));
15 |
16 | beforeEach(() => {
17 | program = createMockProgram();
18 | installCommand(program as unknown as Command);
19 | });
20 |
21 | it("should install a bundle and its dependencies", async () => {
22 | await program.runWith("install supportclass/lfg-streamtip");
23 | expect(fs.existsSync("./bundles/lfg-streamtip/package.json")).toBe(true);
24 | expect(
25 | fs.readdirSync("./bundles/lfg-streamtip/node_modules").length,
26 | ).toBeGreaterThan(0);
27 | expect(
28 | fs.readdirSync("./bundles/lfg-streamtip/bower_components").length,
29 | ).toBeGreaterThan(0);
30 | });
31 |
32 | it("should install a version that satisfies a provided semver range", async () => {
33 | await program.runWith("install supportclass/lfg-nucleus#^1.1.0");
34 | expect(fs.existsSync("./bundles/lfg-nucleus/package.json")).toBe(true);
35 |
36 | const pjson = JSON.parse(
37 | fs.readFileSync("./bundles/lfg-nucleus/package.json", {
38 | encoding: "utf8",
39 | }),
40 | );
41 | expect(semver.satisfies(pjson.version, "^1.1.0")).toBe(true);
42 | });
43 |
44 | it("should install bower & npm dependencies when run with no arguments in a bundle directory", async () => {
45 | fs.rmSync("./bundles/lfg-streamtip/node_modules", {
46 | recursive: true,
47 | force: true,
48 | });
49 | fs.rmSync("./bundles/lfg-streamtip/bower_components", {
50 | recursive: true,
51 | force: true,
52 | });
53 |
54 | process.chdir("./bundles/lfg-streamtip");
55 | await program.runWith("install");
56 | expect(fs.readdirSync("./node_modules").length).toBeGreaterThan(0);
57 | expect(fs.readdirSync("./bower_components").length).toBeGreaterThan(0);
58 | });
59 |
60 | it("should print an error when no valid git repo is provided", async () => {
61 | const spy = vi.spyOn(console, "error");
62 | await program.runWith("install 123");
63 | expect(spy).toBeCalledWith(
64 | "Please enter a valid git repository URL or GitHub username/repo pair.",
65 | );
66 | spy.mockRestore();
67 | });
68 |
--------------------------------------------------------------------------------
/test/commands/schema-types.spec.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "node:events";
2 | import fs from "node:fs";
3 | import path from "node:path";
4 | import { fileURLToPath } from "node:url";
5 |
6 | import { beforeEach, expect, it, vi } from "vitest";
7 |
8 | import { schemaTypesCommand } from "../../src/commands/schema-types.js";
9 | import { createMockProgram, MockCommand } from "../mocks/program.js";
10 | import { setupTmpDir } from "./tmp-dir.js";
11 |
12 | const dirname = path.dirname(fileURLToPath(import.meta.url));
13 |
14 | let program: MockCommand;
15 |
16 | beforeEach(() => {
17 | // Set up environment.
18 | const tempFolder = setupTmpDir();
19 | process.chdir(tempFolder);
20 | fs.writeFileSync("package.json", JSON.stringify({ name: "nodecg" }));
21 |
22 | // Copy fixtures.
23 | fs.cpSync(path.resolve(dirname, "../fixtures/"), "./", { recursive: true });
24 |
25 | // Build program.
26 | program = createMockProgram();
27 | schemaTypesCommand(program as any);
28 | });
29 |
30 | it("should successfully create d.ts files from the replicant schemas and create an index.d.ts file", async () => {
31 | process.chdir("bundles/schema-types");
32 |
33 | /*
34 | * Commander has no return values for command invocations.
35 | * This means that if your command returns a promise (or is otherwise async),
36 | * there is no way to get a reference to that promise to await it.
37 | * The command is just invoked by a dispatched event, with no
38 | * way to access the return value of your command's action.
39 | *
40 | * This makes testing async actions very challenging.
41 | *
42 | * Our current solution is to hack custom events onto the process global.
43 | * It's gross, but whatever. It works for now.
44 | */
45 | await Promise.all([
46 | program.runWith("schema-types"),
47 | waitForEvent(process, "schema-types-done"),
48 | ]);
49 |
50 | const outputPath = "./src/types/schemas/example.d.ts";
51 | expect(fs.existsSync(outputPath)).toBe(true);
52 |
53 | expect(fs.readFileSync(outputPath, "utf8")).toMatchInlineSnapshot(`
54 | "/* prettier-ignore */
55 | /* eslint-disable */
56 | /**
57 | * This file was automatically generated by json-schema-to-typescript.
58 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
59 | * and run json-schema-to-typescript to regenerate this file.
60 | */
61 |
62 | export interface Example {
63 | ip: string;
64 | port: number;
65 | password: string;
66 | status: 'connected' | 'connecting' | 'disconnected' | 'error';
67 | }
68 | "
69 | `);
70 | });
71 |
72 | it("should print an error when the target bundle does not have a schemas dir", async () => {
73 | process.chdir("bundles/uninstall-test");
74 | const spy = vi.spyOn(console, "error");
75 | await program.runWith("schema-types");
76 | expect(spy.mock.calls[0]).toMatchInlineSnapshot(
77 | `
78 | [
79 | "Error: Input directory does not exist",
80 | ]
81 | `,
82 | );
83 | spy.mockRestore();
84 | });
85 |
86 | it("should successfully compile the config schema", async () => {
87 | process.chdir("bundles/config-schema");
88 | fs.mkdirSync("empty-dir");
89 |
90 | await Promise.all([
91 | program.runWith("schema-types empty-dir"),
92 | waitForEvent(process, "schema-types-done"),
93 | ]);
94 |
95 | const outputPath = "./src/types/schemas/configschema.d.ts";
96 | expect(fs.existsSync(outputPath)).toBe(true);
97 |
98 | expect(fs.readFileSync(outputPath, "utf8")).toMatchInlineSnapshot(`
99 | "/* prettier-ignore */
100 | /* eslint-disable */
101 | /**
102 | * This file was automatically generated by json-schema-to-typescript.
103 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
104 | * and run json-schema-to-typescript to regenerate this file.
105 | */
106 |
107 | export interface Configschema {
108 | username?: string;
109 | value?: number;
110 | nodefault?: string;
111 | [k: string]: unknown;
112 | }
113 | "
114 | `);
115 | });
116 |
117 | async function waitForEvent(emitter: EventEmitter, eventName: string) {
118 | return new Promise((resolve) => {
119 | emitter.on(eventName, () => {
120 | resolve();
121 | });
122 | });
123 | }
124 |
--------------------------------------------------------------------------------
/test/commands/setup.spec.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 |
3 | import { Command } from "commander";
4 | import type { PackageJson } from "type-fest";
5 | import { beforeEach, expect, test, vi } from "vitest";
6 |
7 | import { setupCommand } from "../../src/commands/setup.js";
8 | import { createMockProgram, MockCommand } from "../mocks/program.js";
9 | import { setupTmpDir } from "./tmp-dir.js";
10 |
11 | vi.mock("@inquirer/prompts", () => ({ confirm: () => Promise.resolve(true) }));
12 |
13 | let program: MockCommand;
14 | let currentDir = setupTmpDir();
15 | const chdir = (keepCurrentDir = false) => {
16 | if (!keepCurrentDir) {
17 | currentDir = setupTmpDir();
18 | }
19 |
20 | process.chdir(currentDir);
21 | };
22 |
23 | const readPackageJson = (): PackageJson => {
24 | return JSON.parse(fs.readFileSync("./package.json", { encoding: "utf8" }));
25 | };
26 |
27 | beforeEach(() => {
28 | chdir(true);
29 | program = createMockProgram();
30 | setupCommand(program as unknown as Command);
31 | });
32 |
33 | test("should install the latest NodeCG when no version is specified", async () => {
34 | chdir();
35 | await program.runWith("setup --skip-dependencies");
36 | expect(readPackageJson().name).toBe("nodecg");
37 | });
38 |
39 | test("should install v2 NodeCG when specified", async () => {
40 | chdir();
41 | await program.runWith("setup 2.0.0 --skip-dependencies");
42 | expect(readPackageJson().name).toBe("nodecg");
43 | expect(readPackageJson().version).toBe("2.0.0");
44 |
45 | await program.runWith("setup 2.1.0 -u --skip-dependencies");
46 | expect(readPackageJson().version).toBe("2.1.0");
47 |
48 | await program.runWith("setup 2.0.0 -u --skip-dependencies");
49 | expect(readPackageJson().version).toBe("2.0.0");
50 | });
51 |
52 | test("install NodeCG with dependencies", async () => {
53 | chdir();
54 | await program.runWith("setup 2.4.0");
55 | expect(readPackageJson().name).toBe("nodecg");
56 | expect(readPackageJson().version).toBe("2.4.0");
57 | expect(fs.readdirSync(".")).toContain("node_modules");
58 | });
59 |
60 | test("should throw when trying to install v1 NodeCG", async () => {
61 | chdir();
62 | const consoleError = vi.spyOn(console, "error");
63 | await program.runWith("setup 1.9.0 -u --skip-dependencies");
64 | expect(consoleError.mock.calls[0]).toMatchInlineSnapshot(`
65 | [
66 | "nodecg-cli does not support NodeCG versions older than v2.0.0.",
67 | ]
68 | `);
69 | });
70 |
71 | test("should print an error when the target version is the same as current", async () => {
72 | chdir();
73 | const spy = vi.spyOn(console, "log");
74 | await program.runWith("setup 2.1.0 --skip-dependencies");
75 | await program.runWith("setup 2.1.0 -u --skip-dependencies");
76 | expect(spy.mock.calls[1]).toMatchInlineSnapshot(`
77 | [
78 | "The target version (v2.1.0) is equal to the current version (2.1.0). No action will be taken.",
79 | ]
80 | `);
81 | spy.mockRestore();
82 | });
83 |
84 | test("should print an error when the target version doesn't exist", async () => {
85 | chdir();
86 | const spy = vi.spyOn(console, "error");
87 | await program.runWith("setup 999.999.999 --skip-dependencies");
88 | expect(spy.mock.calls[0]).toMatchInlineSnapshot(`
89 | [
90 | "No releases match the supplied semver range (999.999.999)",
91 | ]
92 | `);
93 | spy.mockRestore();
94 | });
95 |
96 | test("should print an error and exit, when nodecg is already installed in the current directory ", async () => {
97 | chdir();
98 | const spy = vi.spyOn(console, "error");
99 | await program.runWith("setup 2.0.0 --skip-dependencies");
100 | await program.runWith("setup 2.0.0 --skip-dependencies");
101 | expect(spy).toBeCalledWith("NodeCG is already installed in this directory.");
102 | spy.mockRestore();
103 | });
104 |
--------------------------------------------------------------------------------
/test/commands/tmp-dir.ts:
--------------------------------------------------------------------------------
1 | import { randomUUID } from "node:crypto";
2 | import { mkdirSync } from "node:fs";
3 | import { tmpdir } from "node:os";
4 | import path from "node:path";
5 |
6 | export function setupTmpDir() {
7 | const dir = path.join(tmpdir(), randomUUID());
8 | mkdirSync(dir);
9 | return dir;
10 | }
11 |
--------------------------------------------------------------------------------
/test/commands/uninstall.spec.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 | import { fileURLToPath } from "node:url";
4 |
5 | import { Command } from "commander";
6 | import { beforeEach, expect, it, vi } from "vitest";
7 |
8 | import { uninstallCommand } from "../../src/commands/uninstall.js";
9 | import { createMockProgram, MockCommand } from "../mocks/program.js";
10 | import { setupTmpDir } from "./tmp-dir.js";
11 |
12 | vi.mock("@inquirer/prompts", () => ({ confirm: () => Promise.resolve(true) }));
13 |
14 | const dirname = path.dirname(fileURLToPath(import.meta.url));
15 |
16 | let program: MockCommand;
17 |
18 | beforeEach(() => {
19 | // Set up environment.
20 | const tempFolder = setupTmpDir();
21 | process.chdir(tempFolder);
22 | fs.writeFileSync("package.json", JSON.stringify({ name: "nodecg" }));
23 |
24 | // Copy fixtures.
25 | fs.cpSync(path.resolve(dirname, "../fixtures/"), "./", { recursive: true });
26 |
27 | // Build program.
28 | program = createMockProgram();
29 | uninstallCommand(program as unknown as Command);
30 | });
31 |
32 | it("should delete the bundle's folder after prompting for confirmation", async () => {
33 | await program.runWith("uninstall uninstall-test");
34 | expect(fs.existsSync("./bundles/uninstall-test")).toBe(false);
35 | });
36 |
37 | it("should print an error when the target bundle is not installed", async () => {
38 | const spy = vi.spyOn(console, "error");
39 | await program.runWith("uninstall not-installed");
40 | expect(spy.mock.calls[0]).toMatchInlineSnapshot(`
41 | [
42 | "Cannot uninstall not-installed: bundle is not installed.",
43 | ]
44 | `);
45 | spy.mockRestore();
46 | });
47 |
--------------------------------------------------------------------------------
/test/fixtures/bundles/config-schema/configschema.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "properties": {
4 | "username": {
5 | "type": "string",
6 | "default": "user"
7 | },
8 | "value": {
9 | "type": "integer",
10 | "default": 5
11 | },
12 | "nodefault": {
13 | "type": "string"
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/fixtures/bundles/config-schema/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "config-schema",
3 | "nodecg": {
4 | "compatibleRange": "^1.3.0"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/bundles/schema-types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "schema-types",
3 | "nodecg": {
4 | "compatibleRange": "^1.3.0"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/bundles/schema-types/schemas/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "additionalProperties": false,
4 | "properties": {
5 | "ip": {
6 | "type": "string",
7 | "default": "localhost"
8 | },
9 | "port": {
10 | "type": "number",
11 | "default": 4444
12 | },
13 | "password": {
14 | "type": "string",
15 | "default": ""
16 | },
17 | "status": {
18 | "type": "string",
19 | "enum": ["connected", "connecting", "disconnected", "error"],
20 | "default": "disconnected"
21 | }
22 | },
23 | "required": [
24 | "ip",
25 | "port",
26 | "password",
27 | "status"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/test/fixtures/bundles/uninstall-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uninstall-test",
3 | "nodecg": {
4 | "compatibleRange": "^1.3.0"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/mocks/program.ts:
--------------------------------------------------------------------------------
1 | import { Command } from "commander";
2 | import { vi } from "vitest";
3 |
4 | export class MockCommand extends Command {
5 | log() {
6 | // To be mocked later
7 | }
8 |
9 | request(opts: any) {
10 | throw new Error(`Unexpected request: ${JSON.stringify(opts, null, 2)}`);
11 | }
12 |
13 | runWith(argString: string) {
14 | return this.parseAsync(["node", "./", ...argString.split(" ")]);
15 | }
16 | }
17 |
18 | export const createMockProgram = () => {
19 | const program = new MockCommand();
20 |
21 | vi.spyOn(program, "log").mockImplementation(() => void 0);
22 |
23 | return program;
24 | };
25 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "sourceMap": true,
6 | "rootDir": "./src"
7 | },
8 | "include": ["./src/**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 |
5 | "target": "ES2020",
6 | "lib": ["ES2020"],
7 |
8 | "module": "NodeNext",
9 | "types": ["node"],
10 |
11 | "isolatedModules": true,
12 | "verbatimModuleSyntax": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "resolveJsonModule": true,
15 |
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noImplicitReturns": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedIndexedAccess": true,
22 | "noImplicitOverride": true,
23 | "noPropertyAccessFromIndexSignature": true,
24 |
25 | "skipLibCheck": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | testTimeout: 30_000,
6 | },
7 | });
8 |
--------------------------------------------------------------------------------