@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +81 -0
  3. package/README.ko.md +81 -0
  4. package/README.md +81 -0
  5. package/README.zh-CN.md +81 -0
  6. package/README.zh-TW.md +81 -0
  7. package/dist/commands/export.d.ts +1 -0
  8. package/dist/commands/export.js +59 -0
  9. package/dist/commands/install.d.ts +1 -0
  10. package/dist/commands/install.js +20 -0
  11. package/dist/commands/lang.d.ts +2 -0
  12. package/dist/commands/lang.js +82 -0
  13. package/dist/commands/list.d.ts +1 -0
  14. package/dist/commands/list.js +26 -0
  15. package/dist/commands/search.d.ts +1 -0
  16. package/dist/commands/search.js +26 -0
  17. package/dist/commands/sync.d.ts +1 -0
  18. package/dist/commands/sync.js +70 -0
  19. package/dist/commands/uninstall.d.ts +1 -0
  20. package/dist/commands/uninstall.js +70 -0
  21. package/dist/commands/upgrade.d.ts +1 -0
  22. package/dist/commands/upgrade.js +106 -0
  23. package/dist/config.d.ts +6 -0
  24. package/dist/config.js +37 -0
  25. package/dist/i18n/en.json +81 -0
  26. package/dist/i18n/index.d.ts +14 -0
  27. package/dist/i18n/index.js +57 -0
  28. package/dist/i18n/ja.json +81 -0
  29. package/dist/i18n/ko.json +81 -0
  30. package/dist/i18n/zh-CN.json +81 -0
  31. package/dist/i18n/zh-TW.json +81 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +88 -0
  34. package/dist/types.d.ts +20 -0
  35. package/dist/types.js +2 -0
  36. package/dist/ui/selector.d.ts +4 -0
  37. package/dist/ui/selector.js +86 -0
  38. package/dist/ui/spinner.d.ts +8 -0
  39. package/dist/ui/spinner.js +26 -0
  40. package/dist/ui/table.d.ts +2 -0
  41. package/dist/ui/table.js +31 -0
  42. package/dist/winget/client.d.ts +13 -0
  43. package/dist/winget/client.js +94 -0
  44. package/dist/winget/parser.d.ts +4 -0
  45. package/dist/winget/parser.js +303 -0
  46. 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
+ }