win-get-updates 0.0.2 → 0.0.4

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.
@@ -16,9 +16,9 @@ jobs:
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
18
  - name: Checkout code
19
- uses: actions/checkout@v4
19
+ uses: actions/checkout@v6
20
20
  - name: Set up Node.js
21
- uses: actions/setup-node@v4
21
+ uses: actions/setup-node@v6
22
22
  with:
23
23
  node-version: 'lts/*'
24
24
  - name: Install dependencies
@@ -12,11 +12,11 @@ jobs:
12
12
  publish:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
15
+ - uses: actions/checkout@v6
16
16
 
17
- - uses: actions/setup-node@v4
17
+ - uses: actions/setup-node@v6
18
18
  with:
19
- node-version: '24'
19
+ node-version: 'lts/*'
20
20
  registry-url: 'https://registry.npmjs.org'
21
21
  - run: npm ci
22
22
  - run: npm publish
package/README.md CHANGED
@@ -2,12 +2,28 @@
2
2
 
3
3
  Third party CLI frontend for [winget](https://en.wikipedia.org/wiki/Windows_Package_Manager) update runs. It lets you interactively choose which package to install.
4
4
 
5
+ <img src="./pics/example.jpg" alt="Example screenshot of win-get-updates in action" width="650px" />
6
+
5
7
  ## Requirements
6
8
 
7
9
  Windows with [winget](https://en.wikipedia.org/wiki/Windows_Package_Manager) installed. Supposed to run on cmd.exe.
8
10
 
11
+ ## Install
12
+
13
+ ```
14
+ npm install -g win-get-updates
15
+ ```
16
+
9
17
  ## Run
10
18
 
19
+ ### Global
20
+
21
+ ```
22
+ wgu
23
+ ```
24
+
25
+ ### Local development
26
+
11
27
  ```
12
28
  node src\cli.js
13
29
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "win-get-updates",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Third party CLI frontend for winget update runs.",
5
5
  "repository": {
6
6
  "type": "git",
Binary file
package/src/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { getUpdateCandidateIds, runUpdates } from './lib/winget.js';
1
+ import { getUpdateCandidates, runUpdates } from './lib/winget.js';
2
2
  import { interactiveSelect } from './lib/menu.js';
3
3
  import WGU_VERSION from './lib/version.js';
4
4
  import { askPermissionToContinue } from './lib/console_commons.js';
5
5
  import { assertWindows, assertWingetAvailable } from './lib/os.js';
6
-
6
+ import { getWindowsUserLang } from './lib/wgu_i18n.js';
7
7
  /**
8
8
  * Main application logic
9
9
  * @param {Object} options - Configuration options
@@ -30,17 +30,18 @@ export async function main({ stdout = process.stdout, stderr = process.stderr }
30
30
  });
31
31
 
32
32
  console.log(`This is WGU v${WGU_VERSION}`);
33
+ console.log('Detected Windows User language:', getWindowsUserLang());
33
34
  console.log('');
34
35
 
35
36
  console.log('Retrieving list of updatable packages..');
36
- const candidateIds = getUpdateCandidateIds();
37
+ const candidates = getUpdateCandidates();
37
38
 
38
- if (candidateIds.length === 0) {
39
+ if (candidates.length === 0) {
39
40
  console.log('No packages available to update.');
40
41
  return 0;
41
42
  }
42
43
 
43
- const selectedIds = await interactiveSelect(candidateIds);
44
+ const selectedIds = await interactiveSelect(candidates);
44
45
 
45
46
  if (selectedIds.length === 0) {
46
47
  console.log('Exiting without performing updates.');
package/src/lib/menu.js CHANGED
@@ -18,8 +18,10 @@ export async function interactiveSelect(items) {
18
18
  let activeLine = 0;
19
19
 
20
20
  // Display initial menu
21
- for (const item of items) {
22
- console.log(`[ ] ${item}`);
21
+ for (let i = 0; i < items.length; i++) {
22
+ const item = items[i];
23
+ console.log(`[x] ${item.id} ${item.currentVersion} -> ${item.availableVersion}`);
24
+ selectedLines.set(i, true);
23
25
  }
24
26
  console.log('');
25
27
  console.log("Use Up/Down arrows to navigate, Space to toggle selection, 'y' to confirm, 'q' to quit.");
@@ -78,7 +80,7 @@ export async function interactiveSelect(items) {
78
80
 
79
81
  // Sort selected line numbers and get corresponding items
80
82
  const selectedIndices = Array.from(selectedLines.keys()).sort((a, b) => a - b);
81
- const selectedItems = selectedIndices.map((idx) => items[idx]);
83
+ const selectedItems = selectedIndices.map((idx) => items[idx].id);
82
84
  resolve(selectedItems);
83
85
  } else if (str === 'q' || str === 'Q' || (key && key.name === 'escape')) {
84
86
  // Quit without selection
@@ -0,0 +1,28 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ /**
4
+ * Spawns a synchronous child process to run a command with the given arguments.
5
+ *
6
+ * @param {string} cmd - The command to run.
7
+ * @param {string[]} args - The list of string arguments.
8
+ * @param {boolean} [inheritStdio=false] - Whether to inherit stdio from the parent process.
9
+ * @returns {string} The standard output from the command.
10
+ * @throws {Error} If the process fails to start or exits with a non-zero status code.
11
+ */
12
+ export function spawnSyncProcess(cmd, args, inheritStdio = false) {
13
+ const stdio = inheritStdio ? 'inherit' : 'pipe';
14
+ const child = spawnSync(cmd, args, { shell: false, stdio, encoding: 'utf8' });
15
+
16
+ const stdout = child.stdout || '';
17
+ const stderr = child.stderr || '';
18
+
19
+ if (child.error) {
20
+ throw new Error(`Cmd failed with: ${child.error.message}`);
21
+ }
22
+
23
+ if (child.status !== 0) {
24
+ throw new Error(`Cmd exited with code ${child.status}${stderr ? `: ${stderr}` : ''}`);
25
+ }
26
+
27
+ return stdout;
28
+ }
@@ -1,2 +1,2 @@
1
- const WGU_VERSION = '0.0.2';
1
+ const WGU_VERSION = '0.0.4';
2
2
  export default WGU_VERSION;
@@ -0,0 +1,43 @@
1
+ import { spawnSyncProcess } from './system.js';
2
+
3
+ export const WINGET_COLS = Object.freeze({
4
+ NAME: 'name_col',
5
+ ID: 'id_col',
6
+ VERSION: 'version_col',
7
+ AVAILABLE: 'available_col',
8
+ SOURCE: 'source_col',
9
+ });
10
+
11
+ // Winget table column names
12
+ const col_names = {
13
+ name_col: { en: 'Name', de: 'Name' },
14
+ id_col: { en: 'ID', de: 'ID' },
15
+ version_col: { en: 'Version', de: 'Version' },
16
+ available_col: { en: 'Available', de: 'Verfügbar' },
17
+ source_col: { en: 'Source', de: 'Quelle' },
18
+ };
19
+
20
+ /**
21
+ * Returns the localized string for the given name and locale.
22
+ * Falls back to 'en' if not found, or returns the key if missing.
23
+ * @param {string} key - The string key
24
+ * @param {string} locale - The locale code (e.g. 'en', 'de')
25
+ * @returns {string}
26
+ */
27
+ export function getColName(key, locale) {
28
+ if (col_names[key]) {
29
+ return col_names[key][locale] || col_names[key].en;
30
+ }
31
+ throw new Error(`Missing localization for key: ${key}`);
32
+ }
33
+
34
+ export function getWindowsUserLang() {
35
+ let windowsUserLangTag = 'en-US';
36
+ try {
37
+ windowsUserLangTag = spawnSyncProcess('powershell', ['-c', '(Get-WinUserLanguageList)[0].LanguageTag'])?.trim();
38
+ } catch (err) {
39
+ console.debug(`Could not determine Windows user language, defaulting to ${windowsUserLangTag}:`, err.message);
40
+ }
41
+
42
+ return new Intl.Locale(windowsUserLangTag).language;
43
+ }
package/src/lib/winget.js CHANGED
@@ -1,19 +1,21 @@
1
- import { spawnSync } from 'node:child_process';
1
+ import { getColName, getWindowsUserLang, WINGET_COLS } from './wgu_i18n.js';
2
+ import { spawnSyncProcess } from './system.js';
2
3
 
3
4
  /**
4
5
  * Retrieves a list of package IDs that can be updated via winget
5
6
  * @returns {string[]} Array of package IDs
6
7
  * @throws {Error} If winget command fails or returns empty output
7
8
  */
8
- export function getUpdateCandidateIds() {
9
- const NAME_COL = 'Name';
10
- const ID_COL = 'ID';
11
- const VERSION_COL = 'Version';
12
- const tableHeaderRegex = new RegExp(`.*${NAME_COL}\\s+${ID_COL}\\s+${VERSION_COL}.*`);
13
-
14
- // Get winget upgrade list
15
- const wgOutput = execWinget(['upgrade', '--include-unknown']);
16
-
9
+ export function getUpdateCandidates() {
10
+ const locale = getWindowsUserLang();
11
+ const NAME_COL_NAME = getColName(WINGET_COLS.NAME, locale);
12
+ const ID_COL_NAME = getColName(WINGET_COLS.ID, locale);
13
+ const VERSION_COL_NAME = getColName(WINGET_COLS.VERSION, locale);
14
+ const AVAILABLE_COL_NAME = getColName(WINGET_COLS.AVAILABLE, locale);
15
+ const SOURCE_COL_NAME = getColName(WINGET_COLS.SOURCE, locale);
16
+ const tableHeaderRegex = new RegExp(`.*${NAME_COL_NAME}\\s+${ID_COL_NAME}\\s+${VERSION_COL_NAME}\\s+${AVAILABLE_COL_NAME}\\s+${SOURCE_COL_NAME}.*`);
17
+
18
+ const wgOutput = spawnSyncProcess('winget', ['upgrade', '--include-unknown']);
17
19
  if (!wgOutput || wgOutput.trim().length === 0) {
18
20
  throw new Error('winget update command failed or returned empty output');
19
21
  }
@@ -29,36 +31,37 @@ export function getUpdateCandidateIds() {
29
31
  if (tableHeaderIndex === -1) {
30
32
  throw new Error('Could not find table header in winget output');
31
33
  }
32
- // Find column positions
33
- const namePos = lines[tableHeaderIndex].indexOf(NAME_COL);
34
- const idPos = lines[tableHeaderIndex].indexOf(ID_COL);
35
- const versionPos = lines[tableHeaderIndex].indexOf(VERSION_COL);
36
- if (namePos === -1 || idPos === -1 || versionPos === -1) {
34
+ const headerLine = lines[tableHeaderIndex].trim();
35
+ const posOffset = headerLine.indexOf(NAME_COL_NAME);
36
+ const idPos = headerLine.indexOf(ID_COL_NAME) - posOffset;
37
+ const versionPos = headerLine.indexOf(VERSION_COL_NAME) - posOffset;
38
+ const availablePos = headerLine.indexOf(AVAILABLE_COL_NAME) - posOffset;
39
+ const sourcePos = headerLine.indexOf(SOURCE_COL_NAME) - posOffset;
40
+ if (posOffset === -1 || idPos === -1 || versionPos === -1 || availablePos === -1 || sourcePos === -1) {
37
41
  throw new Error('Could not find expected column headers in winget output');
38
42
  }
39
43
 
40
- // Calculate column positions relative to start of each line
41
- const packageIdStartPos = idPos - namePos;
42
- const packageIdColLength = versionPos - idPos;
44
+ const idColLength = versionPos - idPos;
45
+ const versionColLength = availablePos - versionPos;
46
+ const availableColLength = sourcePos - availablePos;
43
47
 
44
48
  const packageListStartLineIndex = tableHeaderIndex + 2; // Skip header and separator line
45
- const packageIds = [];
49
+ const candidates = [];
46
50
 
47
- // Extract package IDs from each line
48
51
  for (let i = packageListStartLineIndex; i < outputLineCount; i++) {
49
52
  const line = lines[i];
50
53
  if (!line || line.trim().length === 0) {
51
54
  continue;
52
55
  }
53
-
54
- const packageId = line.substring(packageIdStartPos, packageIdStartPos + packageIdColLength).trim();
55
-
56
- if (packageId) {
57
- packageIds.push(packageId);
56
+ const id = line.substring(idPos, idPos + idColLength).trim();
57
+ const currentVersion = line.substring(versionPos, versionPos + versionColLength).trim();
58
+ const availableVersion = line.substring(availablePos, availablePos + availableColLength).trim();
59
+ if (id) {
60
+ candidates.push({ id, currentVersion, availableVersion });
58
61
  }
59
62
  }
60
63
 
61
- return packageIds;
64
+ return candidates;
62
65
  }
63
66
 
64
67
  /**
@@ -71,14 +74,14 @@ export async function runUpdates(ids, errorHandler = async (_id, _err) => false)
71
74
  console.log(`Updating package: ${id}`);
72
75
  // prettier-ignore
73
76
  try {
74
- execWinget([
77
+ spawnSyncProcess('winget', [
75
78
  'upgrade',
76
79
  '-i',
77
80
  '--id',
78
81
  id,
79
82
  '--accept-source-agreements',
80
83
  '--accept-package-agreements'
81
- ], { inheritStdio: true });
84
+ ], true);
82
85
  console.log(`Package ${id} updated successfully.`);
83
86
  } catch (err) {
84
87
  // eslint-disable-next-line no-await-in-loop
@@ -88,29 +91,3 @@ export async function runUpdates(ids, errorHandler = async (_id, _err) => false)
88
91
  }
89
92
  }
90
93
  }
91
-
92
- /**
93
- * Synchronously executes winget command and returns stdout
94
- * @param {string[]} args - Command arguments
95
- * @param {Object} options - Execution options
96
- * @param {boolean} options.inheritStdio - Whether to inherit stdio
97
- * @returns {string} Command stdout
98
- * @throws {Error} If command fails
99
- */
100
- function execWinget(args, { inheritStdio = false } = {}) {
101
- const stdio = inheritStdio ? 'inherit' : 'pipe';
102
- const child = spawnSync('winget', args, { shell: false, stdio, encoding: 'utf8' });
103
-
104
- const stdout = child.stdout || '';
105
- const stderr = child.stderr || '';
106
-
107
- if (child.error) {
108
- throw new Error(`Failed to execute winget: ${child.error.message}`);
109
- }
110
-
111
- if (child.status !== 0) {
112
- throw new Error(`winget exited with code ${child.status}${stderr ? `: ${stderr}` : ''}`);
113
- }
114
-
115
- return stdout;
116
- }