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.
- package/.github/workflows/lint-and-format.yml +2 -2
- package/.github/workflows/publish.yml +3 -3
- package/README.md +16 -0
- package/package.json +1 -1
- package/pics/example.jpg +0 -0
- package/src/index.js +6 -5
- package/src/lib/menu.js +5 -3
- package/src/lib/system.js +28 -0
- package/src/lib/version.js +1 -1
- package/src/lib/wgu_i18n.js +43 -0
- package/src/lib/winget.js +31 -54
|
@@ -16,9 +16,9 @@ jobs:
|
|
|
16
16
|
runs-on: ubuntu-latest
|
|
17
17
|
steps:
|
|
18
18
|
- name: Checkout code
|
|
19
|
-
uses: actions/checkout@
|
|
19
|
+
uses: actions/checkout@v6
|
|
20
20
|
- name: Set up Node.js
|
|
21
|
-
uses: actions/setup-node@
|
|
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@
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
16
|
|
|
17
|
-
- uses: actions/setup-node@
|
|
17
|
+
- uses: actions/setup-node@v6
|
|
18
18
|
with:
|
|
19
|
-
node-version: '
|
|
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
package/pics/example.jpg
ADDED
|
Binary file
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
|
37
|
+
const candidates = getUpdateCandidates();
|
|
37
38
|
|
|
38
|
-
if (
|
|
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(
|
|
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 (
|
|
22
|
-
|
|
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
|
+
}
|
package/src/lib/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const WGU_VERSION = '0.0.
|
|
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 {
|
|
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
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
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
|
-
|
|
33
|
-
const
|
|
34
|
-
const idPos =
|
|
35
|
-
const versionPos =
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
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
|
|
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
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
],
|
|
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
|
-
}
|