@double_seven/swpm 1.0.2
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/LICENSE +21 -0
- package/README.ja.md +81 -0
- package/README.ko.md +81 -0
- package/README.md +81 -0
- package/README.zh-CN.md +81 -0
- package/README.zh-TW.md +81 -0
- package/dist/commands/export.d.ts +1 -0
- package/dist/commands/export.js +59 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +20 -0
- package/dist/commands/lang.d.ts +2 -0
- package/dist/commands/lang.js +82 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +26 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +70 -0
- package/dist/commands/uninstall.d.ts +1 -0
- package/dist/commands/uninstall.js +70 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/commands/upgrade.js +106 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +37 -0
- package/dist/i18n/en.json +81 -0
- package/dist/i18n/index.d.ts +14 -0
- package/dist/i18n/index.js +57 -0
- package/dist/i18n/ja.json +81 -0
- package/dist/i18n/ko.json +81 -0
- package/dist/i18n/zh-CN.json +81 -0
- package/dist/i18n/zh-TW.json +81 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +88 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +2 -0
- package/dist/ui/selector.d.ts +4 -0
- package/dist/ui/selector.js +86 -0
- package/dist/ui/spinner.d.ts +8 -0
- package/dist/ui/spinner.js +26 -0
- package/dist/ui/table.d.ts +2 -0
- package/dist/ui/table.js +31 -0
- package/dist/winget/client.d.ts +13 -0
- package/dist/winget/client.js +94 -0
- package/dist/winget/parser.d.ts +4 -0
- package/dist/winget/parser.js +303 -0
- package/package.json +61 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseList = parseList;
|
|
4
|
+
exports.parseSearch = parseSearch;
|
|
5
|
+
exports.parseUpgrade = parseUpgrade;
|
|
6
|
+
const HEADER_MAPS = {
|
|
7
|
+
en: { Name: "name", Id: "id", Version: "version", Available: "available", Match: "match", Source: "source" },
|
|
8
|
+
zh: { "名称": "name", "ID": "id", "版本": "version", "可用": "available", "匹配": "match", "源": "source" },
|
|
9
|
+
};
|
|
10
|
+
function detectLang(header) {
|
|
11
|
+
const checks = ["zh", "en"];
|
|
12
|
+
for (const lang of checks) {
|
|
13
|
+
const map = HEADER_MAPS[lang];
|
|
14
|
+
let hit = 0;
|
|
15
|
+
for (const key of Object.keys(map)) {
|
|
16
|
+
if (header.includes(key))
|
|
17
|
+
hit++;
|
|
18
|
+
}
|
|
19
|
+
if (hit >= 3)
|
|
20
|
+
return lang;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function cleanOutput(output) {
|
|
25
|
+
return output
|
|
26
|
+
.replace(/\r\n/g, "\n")
|
|
27
|
+
.replace(/\r/g, "")
|
|
28
|
+
.split("\n");
|
|
29
|
+
}
|
|
30
|
+
function parseList(output) {
|
|
31
|
+
return parseTableOutput(output);
|
|
32
|
+
}
|
|
33
|
+
function parseSearch(output) {
|
|
34
|
+
return parseTableOutput(output);
|
|
35
|
+
}
|
|
36
|
+
function parseUpgrade(output) {
|
|
37
|
+
return parseTableOutput(output);
|
|
38
|
+
}
|
|
39
|
+
function parseTableOutput(output) {
|
|
40
|
+
const lines = cleanOutput(output);
|
|
41
|
+
const results = [];
|
|
42
|
+
let lang = null;
|
|
43
|
+
let columns = [];
|
|
44
|
+
let positions = {};
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (!lang) {
|
|
47
|
+
lang = detectLang(line);
|
|
48
|
+
if (lang) {
|
|
49
|
+
const offset = findHeaderStart(line, lang);
|
|
50
|
+
columns = getColumnOrder(line, lang, offset);
|
|
51
|
+
positions = computePositions(line, lang, offset);
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!line.trim() || /^[\s─-]+$/.test(line))
|
|
56
|
+
continue;
|
|
57
|
+
const pkg = parseDataLine(line, columns, positions);
|
|
58
|
+
if (pkg.name && pkg.id) {
|
|
59
|
+
results.push(pkg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
function findHeaderStart(line, lang) {
|
|
65
|
+
const map = HEADER_MAPS[lang];
|
|
66
|
+
let minPos = Infinity;
|
|
67
|
+
for (const key of Object.keys(map)) {
|
|
68
|
+
const pos = line.indexOf(key);
|
|
69
|
+
if (pos !== -1 && pos < minPos)
|
|
70
|
+
minPos = pos;
|
|
71
|
+
}
|
|
72
|
+
return minPos === Infinity ? 0 : minPos;
|
|
73
|
+
}
|
|
74
|
+
function getColumnOrder(header, lang, offset) {
|
|
75
|
+
const map = HEADER_MAPS[lang];
|
|
76
|
+
return Object.entries(map)
|
|
77
|
+
.map(([col, logical]) => ({ logical, pos: header.indexOf(col) - offset }))
|
|
78
|
+
.filter((c) => c.pos >= 0)
|
|
79
|
+
.sort((a, b) => a.pos - b.pos)
|
|
80
|
+
.map((e) => e.logical);
|
|
81
|
+
}
|
|
82
|
+
function computePositions(header, lang, offset) {
|
|
83
|
+
const positions = {};
|
|
84
|
+
const map = HEADER_MAPS[lang];
|
|
85
|
+
const sorted = Object.entries(map)
|
|
86
|
+
.map(([col, logical]) => ({ logical, pos: header.indexOf(col) - offset }))
|
|
87
|
+
.filter((c) => c.pos >= 0)
|
|
88
|
+
.sort((a, b) => a.pos - b.pos);
|
|
89
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
90
|
+
const end = i < sorted.length - 1 ? sorted[i + 1].pos : undefined;
|
|
91
|
+
positions[sorted[i].logical] = {
|
|
92
|
+
start: sorted[i].pos,
|
|
93
|
+
end: end ?? Infinity,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return positions;
|
|
97
|
+
}
|
|
98
|
+
function parseDataLine(line, columns, positions) {
|
|
99
|
+
// Collect candidates from each strategy
|
|
100
|
+
const candidates = [];
|
|
101
|
+
// Strategy 1: split by 2+ spaces, with match-pattern splitting
|
|
102
|
+
const wideParts = splitWide(line);
|
|
103
|
+
if (wideParts.length === columns.length) {
|
|
104
|
+
candidates.push(buildPackage(wideParts, columns));
|
|
105
|
+
}
|
|
106
|
+
if (wideParts.length < columns.length && columns.length - wideParts.length <= 2) {
|
|
107
|
+
const filled = fillEmptyColumns(line, wideParts, columns.length);
|
|
108
|
+
if (filled.length === columns.length) {
|
|
109
|
+
candidates.push(buildPackage(filled, columns));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Strategy 2: fixed-width using header positions
|
|
113
|
+
candidates.push(extractFixedWidth(line, positions));
|
|
114
|
+
// Strategy 3: right-to-left token parsing
|
|
115
|
+
const tokens = line.trim().split(/\s+/);
|
|
116
|
+
candidates.push(parseTokensRTL(tokens, columns));
|
|
117
|
+
// Pick the best candidate
|
|
118
|
+
return bestCandidate(candidates);
|
|
119
|
+
}
|
|
120
|
+
function bestCandidate(candidates) {
|
|
121
|
+
let best = null;
|
|
122
|
+
let bestScore = -1;
|
|
123
|
+
for (const c of candidates) {
|
|
124
|
+
const score = scoreCandidate(c);
|
|
125
|
+
if (score > bestScore) {
|
|
126
|
+
bestScore = score;
|
|
127
|
+
best = c;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return best || { name: "", id: "", version: "", source: "" };
|
|
131
|
+
}
|
|
132
|
+
function scoreCandidate(pkg) {
|
|
133
|
+
let score = 0;
|
|
134
|
+
if (pkg.id && isValidId(pkg.id))
|
|
135
|
+
score += 100;
|
|
136
|
+
if (pkg.id && !isValidId(pkg.id))
|
|
137
|
+
score -= 50; // corrupt id is worse than no id
|
|
138
|
+
if (pkg.name && pkg.name.length > 0)
|
|
139
|
+
score += 10;
|
|
140
|
+
if (pkg.version && isVersion(pkg.version))
|
|
141
|
+
score += 20;
|
|
142
|
+
if (pkg.source && /^[a-zA-Z]+$/.test(pkg.source))
|
|
143
|
+
score += 10;
|
|
144
|
+
return score;
|
|
145
|
+
}
|
|
146
|
+
// Split by 2+ spaces, then split match patterns (Moniker:/Tag:) that merged with version
|
|
147
|
+
function splitWide(line) {
|
|
148
|
+
const parts = line.split(/\s{2,}/);
|
|
149
|
+
const result = [];
|
|
150
|
+
for (const part of parts) {
|
|
151
|
+
const trimmed = part.trim();
|
|
152
|
+
if (!trimmed)
|
|
153
|
+
continue;
|
|
154
|
+
const subParts = trimmed.split(/\s+(?=(?:Moniker|Tag):)/);
|
|
155
|
+
for (const sp of subParts) {
|
|
156
|
+
if (sp.trim())
|
|
157
|
+
result.push(sp.trim());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function buildPackage(parts, columns) {
|
|
163
|
+
const pkg = { name: "", id: "", version: "", source: "" };
|
|
164
|
+
for (let i = 0; i < columns.length && i < parts.length; i++) {
|
|
165
|
+
const val = parts[i].trim();
|
|
166
|
+
switch (columns[i]) {
|
|
167
|
+
case "name":
|
|
168
|
+
pkg.name = val;
|
|
169
|
+
break;
|
|
170
|
+
case "id":
|
|
171
|
+
pkg.id = val;
|
|
172
|
+
break;
|
|
173
|
+
case "version":
|
|
174
|
+
pkg.version = val;
|
|
175
|
+
break;
|
|
176
|
+
case "available":
|
|
177
|
+
pkg.available = val || undefined;
|
|
178
|
+
break;
|
|
179
|
+
case "source":
|
|
180
|
+
pkg.source = val;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return pkg;
|
|
185
|
+
}
|
|
186
|
+
// Insert empty strings at the widest gaps to handle empty columns (e.g. no update available)
|
|
187
|
+
function fillEmptyColumns(line, parts, expectedCount) {
|
|
188
|
+
const gaps = [];
|
|
189
|
+
let pos = 0;
|
|
190
|
+
for (let i = 0; i < parts.length; i++) {
|
|
191
|
+
const idx = line.indexOf(parts[i], pos);
|
|
192
|
+
if (idx < 0)
|
|
193
|
+
return parts;
|
|
194
|
+
if (i > 0)
|
|
195
|
+
gaps.push(idx - pos);
|
|
196
|
+
pos = idx + parts[i].length;
|
|
197
|
+
}
|
|
198
|
+
gaps.push(line.length - pos);
|
|
199
|
+
const result = [...parts];
|
|
200
|
+
while (result.length < expectedCount && gaps.length > 0) {
|
|
201
|
+
let maxIdx = 0;
|
|
202
|
+
for (let i = 1; i < gaps.length; i++) {
|
|
203
|
+
if (gaps[i] > gaps[maxIdx])
|
|
204
|
+
maxIdx = i;
|
|
205
|
+
}
|
|
206
|
+
result.splice(maxIdx + 1, 0, "");
|
|
207
|
+
gaps.splice(maxIdx, 1, 0, 0);
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
function extractFixedWidth(line, positions) {
|
|
212
|
+
const extract = (col) => {
|
|
213
|
+
const p = positions[col];
|
|
214
|
+
if (!p)
|
|
215
|
+
return "";
|
|
216
|
+
return line.slice(p.start, Math.min(p.end, line.length)).trim();
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
name: extract("name"),
|
|
220
|
+
id: extract("id"),
|
|
221
|
+
version: extract("version"),
|
|
222
|
+
available: extract("available") || undefined,
|
|
223
|
+
source: extract("source"),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function isValidId(id) {
|
|
227
|
+
// Valid winget IDs: publisher.package (contains dot, no spaces)
|
|
228
|
+
// ARP entries: contain backslashes
|
|
229
|
+
if (id.includes("\\") || /^[A-Za-z0-9_.+-]+\.[A-Za-z0-9_.+-]+$/.test(id)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Reject IDs with spaces (sign of corrupted column extraction)
|
|
233
|
+
if (/\s/.test(id))
|
|
234
|
+
return false;
|
|
235
|
+
return id.includes(".");
|
|
236
|
+
}
|
|
237
|
+
function parseTokensRTL(tokens, columns) {
|
|
238
|
+
const hasAvailable = columns.includes("available");
|
|
239
|
+
const remain = [...tokens];
|
|
240
|
+
const result = { name: "", id: "", version: "", source: "" };
|
|
241
|
+
// Pop source from right
|
|
242
|
+
result.source = remain.pop() || "";
|
|
243
|
+
// Find ID first (rightmost dotted/backslash token, not a version-only token)
|
|
244
|
+
let idIdx = -1;
|
|
245
|
+
for (let i = remain.length - 1; i >= 0; i--) {
|
|
246
|
+
if (isId(remain[i])) {
|
|
247
|
+
idIdx = i;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Everything to the left of ID is the name
|
|
252
|
+
if (idIdx >= 0) {
|
|
253
|
+
result.name = remain.slice(0, idIdx).join(" ");
|
|
254
|
+
result.id = remain[idIdx];
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
result.name = remain.join(" ");
|
|
258
|
+
}
|
|
259
|
+
// Find version(s) in tokens between id+1 and end
|
|
260
|
+
if (idIdx >= 0) {
|
|
261
|
+
const rightTokens = remain.slice(idIdx + 1);
|
|
262
|
+
const versionTokens = [];
|
|
263
|
+
const nonVersionTokens = [];
|
|
264
|
+
for (const t of rightTokens) {
|
|
265
|
+
if (isVersion(t))
|
|
266
|
+
versionTokens.push(t);
|
|
267
|
+
else
|
|
268
|
+
nonVersionTokens.push(t);
|
|
269
|
+
}
|
|
270
|
+
if (hasAvailable && versionTokens.length >= 2) {
|
|
271
|
+
// Rightmost two version tokens
|
|
272
|
+
result.available = versionTokens[versionTokens.length - 1];
|
|
273
|
+
result.version = versionTokens[versionTokens.length - 2];
|
|
274
|
+
}
|
|
275
|
+
else if (versionTokens.length >= 1) {
|
|
276
|
+
result.version = versionTokens[versionTokens.length - 1];
|
|
277
|
+
result.available = undefined;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
// No ID found, try old right-to-left version detection
|
|
282
|
+
const lastIsVer = remain.length > 0 && isVersion(remain[remain.length - 1]);
|
|
283
|
+
const secondLastIsVer = remain.length > 1 && isVersion(remain[remain.length - 2]);
|
|
284
|
+
if (hasAvailable && lastIsVer && secondLastIsVer) {
|
|
285
|
+
result.available = remain.pop();
|
|
286
|
+
result.version = remain.pop();
|
|
287
|
+
}
|
|
288
|
+
else if (hasAvailable && lastIsVer) {
|
|
289
|
+
result.version = remain.pop();
|
|
290
|
+
result.available = undefined;
|
|
291
|
+
}
|
|
292
|
+
else if (lastIsVer) {
|
|
293
|
+
result.version = remain.pop();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function isVersion(s) {
|
|
299
|
+
return /^\d+\.\d+/.test(s);
|
|
300
|
+
}
|
|
301
|
+
function isId(s) {
|
|
302
|
+
return (s.includes(".") || s.includes("\\")) && !/^\d+\.\d+/.test(s);
|
|
303
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@double_seven/swpm",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A winget-based Windows software package manager CLI with batch upgrade, export, and sync support",
|
|
5
|
+
"bin": {
|
|
6
|
+
"swpm": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"winget",
|
|
21
|
+
"windows",
|
|
22
|
+
"package-manager",
|
|
23
|
+
"cli",
|
|
24
|
+
"software",
|
|
25
|
+
"installer",
|
|
26
|
+
"upgrade",
|
|
27
|
+
"export",
|
|
28
|
+
"sync"
|
|
29
|
+
],
|
|
30
|
+
"author": "Dengjiansheng",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Dengjiansheng/swpm.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/Dengjiansheng/swpm#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Dengjiansheng/swpm/issues"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"os": [
|
|
44
|
+
"win32"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@inquirer/prompts": "^7.6.1",
|
|
48
|
+
"chalk": "^5.4.1",
|
|
49
|
+
"cli-table3": "^0.6.5",
|
|
50
|
+
"commander": "^13.1.0",
|
|
51
|
+
"js-yaml": "^4.1.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/js-yaml": "^4.0.9",
|
|
55
|
+
"@types/node": "^22.13.0",
|
|
56
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
57
|
+
"tsx": "^4.19.0",
|
|
58
|
+
"typescript": "^5.7.0",
|
|
59
|
+
"vitest": "^4.1.7"
|
|
60
|
+
}
|
|
61
|
+
}
|