icuewidget-cli 0.4.47 → 0.4.49

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/README.md CHANGED
@@ -4,9 +4,9 @@ This repository contains the `icuewidget` command-line tool for creating, valida
4
4
 
5
5
  The CLI uses:
6
6
 
7
- - Node.js so it can run through `npx` without a native installer
7
+ - Node.js 18 or newer, so it runs through `npx` or a global npm install
8
8
  - built-in Node APIs for prompts, validation, and `.icuewidget` packaging
9
- - optional native installer builders for Windows `.exe` and macOS `.pkg`
9
+ - no runtime dependencies
10
10
 
11
11
  ## Commands
12
12
 
@@ -21,18 +21,16 @@ The CLI uses:
21
21
 
22
22
  ```text
23
23
  .github/workflows/
24
- installer/
25
24
  node-bin/
26
- scripts/
27
25
  src-node/
28
26
  test-node/
29
27
  package-lock.json
30
28
  package.json
31
29
  ```
32
30
 
33
- ## Run with npx
31
+ ## Install
34
32
 
35
- After publishing to npm:
33
+ Run without installing:
36
34
 
37
35
  ```bash
38
36
  npx icuewidget-cli --help
@@ -42,6 +40,13 @@ npx icuewidget-cli update ./MyWidget --min-app-version 5.45
42
40
  npx icuewidget-cli package ./MyWidget
43
41
  ```
44
42
 
43
+ Or install globally:
44
+
45
+ ```bash
46
+ npm install -g icuewidget-cli
47
+ icuewidget --help
48
+ ```
49
+
45
50
  ## Run locally without publishing
46
51
 
47
52
  ```bash
@@ -92,72 +97,5 @@ icuewidget init ./HelloWidget
92
97
 
93
98
  ## Versioning
94
99
 
95
- The source package uses the base version `0.4.0`. Release automation can set the
96
- third digit at build time with one of these environment variables:
97
-
98
- ```bash
99
- CLI_BUILD_PATCH=123 npm run build
100
- CLI_BUILD_VERSION=0.4.123 npm run build
101
- ```
102
-
103
- `CLI_BUILD_PATCH`, `BUILD_NUMBER`, or `GITHUB_RUN_NUMBER` produces
104
- `0.4.<patch>` for installer filenames and the bundled CLI `--version` output.
105
-
106
- For npm publishing, publish a full semantic version, for example:
107
-
108
- ```bash
109
- npm version 0.4.123
110
- npm publish --access public
111
- ```
112
-
113
- ## npm publishing
114
-
115
- Before publishing, verify the package contents and tests:
116
-
117
- ```bash
118
- npm test
119
- npm pack --dry-run
120
- ```
121
-
122
- Then publish from this package directory:
123
-
124
- ```bash
125
- npm publish --access public
126
- ```
127
-
128
- Only the CLI runtime files listed in `package.json` are published to npm. The
129
- installer assets and build scripts stay in the repository and are excluded from
130
- the npm package.
131
-
132
- ## Native installers
133
-
134
- The repository can also build native installers for users who do not want to use
135
- `npx` or `npm install -g`.
136
-
137
- Build standalone binaries first, then the platform installer:
138
-
139
- ```bash
140
- npm run build
141
- ```
142
-
143
- Outputs are written to:
144
-
145
- ```text
146
- release/
147
- ```
148
-
149
- Platform requirements:
150
-
151
- - Windows `.exe`: build on Windows with NSIS `makensis` on `PATH`
152
- - macOS `.pkg`: build on macOS with `pkgbuild` and `productbuild`
153
-
154
- By default, installer builds package the checked-out local CLI source. To build
155
- from a published npm package or tarball, set `CLI_PACKAGE_SPEC`:
156
-
157
- ```bash
158
- CLI_PACKAGE_SPEC=icuewidget-cli@latest npm run build
159
- CLI_PACKAGE_SPEC=./corsair-icuewidget-cli-1.0.0.tgz npm run build
160
- ```
161
-
162
- The installers include best-effort cleanup for older Python-distributed versions
163
- of `icuewidget-cli` before installing the standalone Node CLI binary.
100
+ The CLI follows semantic versioning. The version lives only in `package.json`,
101
+ and `icuewidget --version` reads it from there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icuewidget-cli",
3
- "version": "0.4.47",
3
+ "version": "0.4.49",
4
4
  "description": "Create, validate, update, and package CORSAIR iCUE widgets.",
5
5
  "keywords": [
6
6
  "corsair",
@@ -8,38 +8,33 @@
8
8
  "widget",
9
9
  "cli"
10
10
  ],
11
- "homepage": "https://gitlab.com/corsair-gaming/various-projects/icuewidgets-cli#readme",
11
+ "homepage": "https://github.com/Corsair-Labs/icuewidget-cli#readme",
12
12
  "bugs": {
13
- "url": "https://gitlab.com/corsair-gaming/various-projects/icuewidgets-cli/issues"
13
+ "url": "https://github.com/Corsair-Labs/icuewidget-cli/issues"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://gitlab.com/corsair-gaming/various-projects/icuewidgets-cli.git"
17
+ "url": "git+https://github.com/Corsair-Labs/icuewidget-cli.git"
18
18
  },
19
19
  "license": "Apache-2.0",
20
20
  "author": "CORSAIR",
21
21
  "type": "module",
22
- "main": "index.js",
23
22
  "bin": {
24
23
  "icuewidget": "node-bin/icuewidget.js"
25
24
  },
26
25
  "files": [
27
26
  "node-bin/",
28
- "src-node/",
29
- "README.md"
27
+ "src-node/"
30
28
  ],
31
29
  "scripts": {
32
30
  "test": "node --test test-node/*.test.js",
33
- "build:binaries": "node scripts/build-binaries.mjs",
34
- "build:installers": "node scripts/build-installers.mjs",
35
- "build": "npm run build:binaries && npm run build:installers",
36
- "check": "node --check scripts/build-binaries.mjs && node --check scripts/build-installers.mjs"
37
- },
38
- "dependencies": {},
39
- "devDependencies": {
40
- "@yao-pkg/pkg": "^6.5.1"
31
+ "pack:check": "npm pack --dry-run",
32
+ "prepublishOnly": "npm test"
41
33
  },
42
34
  "engines": {
43
35
  "node": ">=18.0.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
44
39
  }
45
40
  }
package/src-node/cli.js CHANGED
@@ -19,6 +19,8 @@ import { notifyAboutUpdate, runUpdateCheckWorker } from "./update-check.js";
19
19
  import { ValidationError, validateWidget } from "./validation.js";
20
20
  import { createPackage } from "./zip.js";
21
21
 
22
+ const COMMANDS = ["init", "validate", "update", "package"];
23
+
22
24
  export async function main(argv = process.argv) {
23
25
  try {
24
26
  const args = argv.slice(2);
@@ -26,11 +28,18 @@ export async function main(argv = process.argv) {
26
28
  await runUpdateCheckWorker();
27
29
  return;
28
30
  }
29
- if (args.length === 0 || has(args, "--help") || has(args, "-h")) {
31
+ if (args.length === 0) {
30
32
  printHelp();
31
33
  return;
32
34
  }
33
- if (has(args, "--version") || has(args, "-v")) {
35
+ if (has(args, "--help") || has(args, "-h")) {
36
+ if (COMMANDS.includes(args[0])) printCommandHelp(args[0]);
37
+ else printHelp();
38
+ return;
39
+ }
40
+ // `update <widget> --version <semver>` uses --version as a subcommand option, so only
41
+ // treat it as the global flag when no subcommand leads the argument list.
42
+ if (!COMMANDS.includes(args[0]) && (has(args, "--version") || has(args, "-v"))) {
34
43
  console.log(CLI_VERSION);
35
44
  return;
36
45
  }
@@ -165,9 +174,11 @@ function printHelp() {
165
174
  panel("Commands", [
166
175
  `${color.cyan("init")} Creates a new widget from the starter template.`,
167
176
  `${color.cyan("validate")} Validates widget files and manifest requirements.`,
168
- `${color.cyan("update")} Updates manifest requirements, bumps widget version, and regenerates package.`,
177
+ `${color.cyan("update")} Sets the minimum iCUE version and/or widget version, then regenerates the package.`,
169
178
  `${color.cyan("package")} Validates widget and creates an .icuewidget package.`,
170
179
  ]);
180
+ console.log(`Run ${color.bold(`${APP_NAME} <command> --help`)} for command-specific options.`);
181
+ console.log();
171
182
  panel("Examples", [
172
183
  `${APP_NAME} init HelloWidget`,
173
184
  `${APP_NAME} validate HelloWidget`,
@@ -177,6 +188,49 @@ function printHelp() {
177
188
  ], color.magenta);
178
189
  }
179
190
 
191
+ const COMMAND_HELP = {
192
+ init: {
193
+ usage: "init [<dir>]",
194
+ summary: "Creates a new widget from the starter template, prompting for its details.",
195
+ options: [],
196
+ examples: ["init HelloWidget"],
197
+ },
198
+ validate: {
199
+ usage: "validate [<widget-dir>]",
200
+ summary: "Validates widget files and manifest requirements.",
201
+ options: [],
202
+ examples: ["validate HelloWidget"],
203
+ },
204
+ update: {
205
+ usage: "update [<widget-dir>] [--min-app-version <ver>] [--version <semver>] [-o <file>]",
206
+ summary: "Updates the manifest and regenerates the package. Prompts for min_app_version if it is missing.",
207
+ options: [
208
+ ["--min-app-version <ver>", "Set manifest.min_app_version (e.g. 5.45 or 5.45.104). Bumps the patch version unless --version is given."],
209
+ ["--version <semver>", "Set manifest.version (e.g. 1.0.1)."],
210
+ ["-o, --output <file>", "Write the package to this path."],
211
+ ],
212
+ examples: ["update HelloWidget --min-app-version 5.45", "update HelloWidget --version 1.0.1"],
213
+ },
214
+ package: {
215
+ usage: "package [<widget-dir>] [-o <file>]",
216
+ summary: "Validates the widget and creates an .icuewidget package.",
217
+ options: [["-o, --output <file>", "Write the package to this path."]],
218
+ examples: ["package HelloWidget", "package HelloWidget -o dist/hello.icuewidget"],
219
+ },
220
+ };
221
+
222
+ function printCommandHelp(command) {
223
+ const { usage, summary, options, examples } = COMMAND_HELP[command];
224
+ header(APP_NAME, CLI_VERSION);
225
+ console.log();
226
+ console.log(summary);
227
+ console.log();
228
+ console.log(`${color.yellow("Usage:")} ${color.bold(APP_NAME)} ${usage}`);
229
+ console.log();
230
+ if (options.length) panel("Options", options.map(([flag, text]) => `${color.cyan(flag)} ${text}`));
231
+ panel("Examples", examples.map((example) => `${APP_NAME} ${example}`), color.magenta);
232
+ }
233
+
180
234
  function parseArgs(args, specs = {}) {
181
235
  const options = {};
182
236
  const positionals = [];
@@ -1,8 +1,12 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
4
+
1
5
  export const APP_NAME = "icuewidget";
2
- export const NPM_PACKAGE_NAME = "icuewidget-cli";
3
- export const CLI_VERSION = "0.4.47";
6
+ export const NPM_PACKAGE_NAME = packageJson.name;
7
+ export const CLI_VERSION = packageJson.version;
4
8
  export const DEFAULT_APP_VERSION = "5.45";
5
- export const DEFAULT_FRAMEWORK_VERSION = "1.0.0";
9
+ export const DEFAULT_FRAMEWORK_VERSION = "1.4.0";
6
10
  export const DEFAULT_PLATFORM = "windows";
7
11
  export const DEFAULT_DEVICE_TYPE = "dashboard_lcd";
8
12
 
@@ -14,20 +14,23 @@ export async function updateManifestToLatest(widgetDir, options = {}) {
14
14
  const changes = [];
15
15
  const requirementChanges = [];
16
16
 
17
- if (typeof manifest.min_app_version !== "string" || !APP_VERSION_RE.test(manifest.min_app_version.trim())) {
18
- let value = options.minAppVersion;
19
- if (value !== undefined) {
20
- value = value.trim();
21
- if (!APP_VERSION_RE.test(value)) throw new Error("--min-app-version must use iCUE version format like 5.45 or 5.45.104.");
22
- } else {
23
- value = await promptValidated(
24
- "Minimum iCUE version required",
25
- DEFAULT_APP_VERSION,
26
- (answer) => APP_VERSION_RE.test(answer),
27
- "Use iCUE version format like 5.45 or 5.45.104.",
28
- );
29
- }
30
- manifest.min_app_version = value;
17
+ const currentMinAppVersion = typeof manifest.min_app_version === "string" ? manifest.min_app_version.trim() : "";
18
+ const currentIsValid = APP_VERSION_RE.test(currentMinAppVersion);
19
+ let minAppVersion;
20
+ if (options.minAppVersion !== undefined) {
21
+ // An explicit flag is a setter: validate it and apply it whenever it differs from the manifest.
22
+ minAppVersion = options.minAppVersion.trim();
23
+ if (!APP_VERSION_RE.test(minAppVersion)) throw new Error("--min-app-version must use iCUE version format like 5.45 or 5.45.104.");
24
+ } else if (!currentIsValid) {
25
+ minAppVersion = await promptValidated(
26
+ "Minimum iCUE version required",
27
+ DEFAULT_APP_VERSION,
28
+ (answer) => APP_VERSION_RE.test(answer),
29
+ "Use iCUE version format like 5.45 or 5.45.104.",
30
+ );
31
+ }
32
+ if (minAppVersion !== undefined && minAppVersion !== manifest.min_app_version) {
33
+ manifest.min_app_version = minAppVersion;
31
34
  requirementChanges.push("manifest.min_app_version");
32
35
  }
33
36
 
@@ -10,10 +10,30 @@ function reader() {
10
10
 
11
11
  export async function prompt(label, defaultValue = "") {
12
12
  const suffix = defaultValue ? ` (${defaultValue})` : "";
13
- const answer = (await reader().question(`${label}${suffix}: `)).trim();
13
+ const answer = (await ask(`${label}${suffix}: `, label)).trim();
14
14
  return answer || defaultValue;
15
15
  }
16
16
 
17
+ // readline never settles a pending question when stdin ends (closed pipe, CI, /dev/null), so
18
+ // Node would exit 0 with the command half-run. Reject instead so the failure is visible.
19
+ function ask(question, label) {
20
+ const rl = reader();
21
+ return new Promise((resolve, reject) => {
22
+ const onClose = () => reject(new Error(`Input ended while waiting for "${label}". Pass the value as a flag or run interactively.`));
23
+ rl.once("close", onClose);
24
+ rl.question(question).then(
25
+ (answer) => {
26
+ rl.off("close", onClose);
27
+ resolve(answer);
28
+ },
29
+ (error) => {
30
+ rl.off("close", onClose);
31
+ reject(error);
32
+ },
33
+ );
34
+ });
35
+ }
36
+
17
37
  export async function promptValidated(label, defaultValue, validator, errorMessage) {
18
38
  while (true) {
19
39
  const answer = await prompt(label, defaultValue);
@@ -23,7 +23,7 @@ export async function validateWidget(widgetDir) {
23
23
  }
24
24
  }
25
25
 
26
- const manifest = await validateManifest(root);
26
+ const manifest = await validateManifest(root, warnings);
27
27
  if (await pathExists(path.join(root, "translation.json"))) {
28
28
  await readJson(path.join(root, "translation.json"));
29
29
  } else {
@@ -34,7 +34,7 @@ export async function validateWidget(widgetDir) {
34
34
  return { widgetDir: root, manifest, warnings };
35
35
  }
36
36
 
37
- async function validateManifest(root) {
37
+ async function validateManifest(root, warnings) {
38
38
  const manifest = await readJson(path.join(root, "manifest.json"));
39
39
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
40
40
  throw new ValidationError("manifest.json must contain a JSON object");
@@ -78,6 +78,18 @@ async function validateManifest(root) {
78
78
  if (manifest.interactive !== undefined && typeof manifest.interactive !== "boolean") {
79
79
  throw new ValidationError("manifest.interactive must be a boolean when provided");
80
80
  }
81
+ if (manifest.interactive === true) {
82
+ const nonDashboardTypes = manifest.supported_devices
83
+ .map((d) => d.type)
84
+ .filter((t) => t !== "dashboard_lcd");
85
+ if (nonDashboardTypes.length > 0) {
86
+ warnings.push(
87
+ `manifest.interactive is true, but supported_devices includes ${nonDashboardTypes.join(", ")}. ` +
88
+ `iCUE only recognizes interactive widgets on dashboard_lcd (Xeneon Edge). ` +
89
+ `The widget will not appear for ${nonDashboardTypes.join(" or ")} devices.`
90
+ );
91
+ }
92
+ }
81
93
  if (manifest.required_plugins !== undefined) validateStringArray(manifest.required_plugins, "manifest.required_plugins");
82
94
  if (manifest.modules !== undefined) {
83
95
  validateStringArray(manifest.modules, "manifest.modules");
@@ -91,19 +103,23 @@ async function validateIndex(root, warnings) {
91
103
  const html = await fs.readFile(path.join(root, "index.html"), "utf8");
92
104
  if (!/<title>[^<]+<\/title>/i.test(html)) warnings.push("index.html does not define a <title>.");
93
105
  if (!/rel=["'][^"']*icon/i.test(html)) warnings.push("index.html does not reference a favicon/icon.");
94
- if (!html.includes("icueEvents")) warnings.push("index.html or scripts do not reference icueEvents.");
95
106
 
96
107
  validateHeadWellFormed(html);
97
108
  validatePropertyTypes(html);
98
109
  validatePropertyGroups(html);
99
110
 
111
+ let referencesIcueEvents = html.includes("icueEvents");
100
112
  const assetRefs = [...extractAttributes(html, "href"), ...extractAttributes(html, "src")];
101
113
  for (const ref of assetRefs) {
102
114
  if (isRemote(ref) || ref.startsWith("data:") || ref.startsWith("#")) continue;
103
115
  await validateRelativeFile(root, stripQuery(ref), `index.html asset reference ${ref}`);
104
116
  if (ref.endsWith(".css")) await validateCss(path.join(root, stripQuery(ref)));
105
- if (ref.endsWith(".js")) await validateJs(path.join(root, stripQuery(ref)));
117
+ if (ref.endsWith(".js")) {
118
+ const code = await validateJs(path.join(root, stripQuery(ref)));
119
+ if (code.includes("icueEvents")) referencesIcueEvents = true;
120
+ }
106
121
  }
122
+ if (!referencesIcueEvents) warnings.push("index.html or scripts do not reference icueEvents.");
107
123
  }
108
124
 
109
125
  // iCUE imports a widget by parsing <head> with a strict XML parser (Qt QDomDocument).
@@ -215,6 +231,7 @@ async function validateJs(filePath) {
215
231
  } catch (error) {
216
232
  throw new ValidationError(`${path.basename(filePath)} has invalid JavaScript: ${error.message}`);
217
233
  }
234
+ return code;
218
235
  }
219
236
 
220
237
  async function validateRelativeFile(root, relativePath, label) {