vscode-autoconfig 0.1.0
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.md +73 -0
- package/dist/cli.js +873 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryo Kasai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# vscode-autoconfig
|
|
2
|
+
|
|
3
|
+
[](https://github.com/RKasai127/vscode-autoconfig/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/vscode-autoconfig)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Analyze your project's manifest files (`package.json`, `requirements.txt`, `pyproject.toml`, ...) and generate `.vscode/settings.json` + `.vscode/extensions.json` recommendations — using a plain rule table.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx vscode-autoconfig # dry-run: shows what would change, writes nothing
|
|
13
|
+
npx vscode-autoconfig --write # applies the changes shown by the dry-run above
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The dry-run is always the default. Nothing is ever written to disk unless you pass `--write` (or `-w`).
|
|
17
|
+
|
|
18
|
+
### Example dry-run output
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
vscode-autoconfig — dry run (no files written)
|
|
22
|
+
|
|
23
|
+
Detected
|
|
24
|
+
node typescript (devDependency)
|
|
25
|
+
node react (dependency)
|
|
26
|
+
|
|
27
|
+
.vscode/settings.json (will be created)
|
|
28
|
+
+ typescript.tsdk: "node_modules/typescript/lib"
|
|
29
|
+
|
|
30
|
+
.vscode/extensions.json (will be created)
|
|
31
|
+
+ dbaeumer.vscode-eslint
|
|
32
|
+
+ dsznajder.es7-react-js-snippets
|
|
33
|
+
|
|
34
|
+
2 settings, 2 extensions would be added.
|
|
35
|
+
Run again with --write to apply.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Flags
|
|
39
|
+
|
|
40
|
+
| Flag | Description |
|
|
41
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `-w, --write` | Apply changes. Without it, the command only previews changes (dry-run). |
|
|
43
|
+
| `--ecosystems <list>` | Comma-separated list of ecosystems to detect, e.g. `--ecosystems node`. Defaults to all supported ecosystems. |
|
|
44
|
+
| `--rules <path>` | Path to an external rule file (JSON or YAML). Merged with the built-in rules by default. |
|
|
45
|
+
| `--no-builtin-rules` | Ignore the built-in rules; use only the file passed via `--rules`. |
|
|
46
|
+
| `--json` | Print machine-readable JSON instead of the terminal report. |
|
|
47
|
+
|
|
48
|
+
There is intentionally no `--cwd` flag: the tool always operates on the current directory. `cd` into the project you want to configure before running it.
|
|
49
|
+
|
|
50
|
+
## How it works
|
|
51
|
+
|
|
52
|
+
1. **Detectors** scan the current directory (non-recursively — files inside `node_modules/`, `.venv/`, etc. are never inspected) and produce a flat list of _findings_: which manifest files exist, which dependencies/devDependencies are declared, which config files are present.
|
|
53
|
+
2. **Rules** (plain JSON, see `src/rules/*.json`) declare a condition over findings and, when satisfied, contribute VS Code settings and/or extension recommendations.
|
|
54
|
+
3. The matched rules are aggregated into one settings patch and one extension list, then merged into your existing `.vscode/settings.json` / `.vscode/extensions.json` — additively, never overwriting a value you already set.
|
|
55
|
+
|
|
56
|
+
### Supported ecosystems (v1)
|
|
57
|
+
|
|
58
|
+
- **Node.js** — `package.json` dependencies/devDependencies, plus common config files (`tsconfig.json`, ESLint/Prettier configs).
|
|
59
|
+
- **Python** — `requirements.txt` and `pyproject.toml` (both PEP 621 `project.dependencies` and Poetry's `tool.poetry.dependencies`).
|
|
60
|
+
|
|
61
|
+
## Merging behavior
|
|
62
|
+
|
|
63
|
+
- Settings/extensions you already have are **never overwritten**. Only missing keys/extensions are added.
|
|
64
|
+
- For `.vscode/extensions.json`, if you manually remove an ID from `recommendations`, it will simply be re-added the next time you run `--write` (deletions are not tracked). If you don't want an extension recommended again, add it to `unwantedRecommendations` yourself.
|
|
65
|
+
- Rules are merged in a fixed order (built-in rules, then any `--rules` file). If two rules propose conflicting values for the same settings key, the first one wins and the rest are reported as skipped — nothing is silently overwritten.
|
|
66
|
+
|
|
67
|
+
## External rule files (`--rules`)
|
|
68
|
+
|
|
69
|
+
External rule files (JSON or YAML) let you extend or fully replace the built-in rule table without forking this tool. **Only use rule files from sources you trust** — treat them like any other executable configuration you'd run on your machine, and never run one handed to you by someone you don't trust.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/args.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program.name("vscode-autoconfig").description(
|
|
8
|
+
"Analyze your project's manifests and generate .vscode/settings.json + extensions.json recommendations"
|
|
9
|
+
).option("-w, --write", "apply changes (default: dry-run only, writes nothing)", false).option("--ecosystems <list>", "comma-separated list of ecosystems to detect (node,python)").option(
|
|
10
|
+
"--rules <path>",
|
|
11
|
+
"path to an external rule file (JSON or YAML), merged with the built-in rules"
|
|
12
|
+
).option("--no-builtin-rules", "ignore the built-in rules; use only the file passed via --rules").option("--json", "output machine-readable JSON instead of a terminal report", false).version("0.1.0");
|
|
13
|
+
program.parse(argv);
|
|
14
|
+
const opts = program.opts();
|
|
15
|
+
return {
|
|
16
|
+
write: opts.write,
|
|
17
|
+
ecosystems: opts.ecosystems ? opts.ecosystems.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
18
|
+
rules: opts.rules,
|
|
19
|
+
builtinRules: opts.builtinRules,
|
|
20
|
+
json: opts.json
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/cli/run.ts
|
|
25
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
26
|
+
|
|
27
|
+
// src/core/aggregate.ts
|
|
28
|
+
function deepEqual(a, b) {
|
|
29
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
30
|
+
}
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function isLanguageOverrideKey(key) {
|
|
35
|
+
return /^\[.+]$/.test(key);
|
|
36
|
+
}
|
|
37
|
+
function mergeKey(target, key, patchValue, depthRemaining, pathLabel, ruleId, owners, conflicts) {
|
|
38
|
+
if (!(key in target)) {
|
|
39
|
+
target[key] = structuredClone(patchValue);
|
|
40
|
+
owners.set(pathLabel, ruleId);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const currentValue = target[key];
|
|
44
|
+
if (deepEqual(currentValue, patchValue)) return;
|
|
45
|
+
if (depthRemaining > 0 && isPlainObject(currentValue) && isPlainObject(patchValue)) {
|
|
46
|
+
for (const [subKey, subValue] of Object.entries(patchValue)) {
|
|
47
|
+
mergeKey(
|
|
48
|
+
currentValue,
|
|
49
|
+
subKey,
|
|
50
|
+
subValue,
|
|
51
|
+
depthRemaining - 1,
|
|
52
|
+
`${pathLabel}.${subKey}`,
|
|
53
|
+
ruleId,
|
|
54
|
+
owners,
|
|
55
|
+
conflicts
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const existingConflict = conflicts.get(pathLabel);
|
|
61
|
+
if (existingConflict) {
|
|
62
|
+
existingConflict.ignoredRuleIds.push(ruleId);
|
|
63
|
+
} else {
|
|
64
|
+
conflicts.set(pathLabel, {
|
|
65
|
+
key: pathLabel,
|
|
66
|
+
winningRuleId: owners.get(pathLabel) ?? ruleId,
|
|
67
|
+
ignoredRuleIds: [ruleId]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function aggregate(matchedRules) {
|
|
72
|
+
const settings = {};
|
|
73
|
+
const owners = /* @__PURE__ */ new Map();
|
|
74
|
+
const conflictsByKey = /* @__PURE__ */ new Map();
|
|
75
|
+
const extensionIds = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const rule of matchedRules) {
|
|
77
|
+
for (const [key, value] of Object.entries(rule.settings ?? {})) {
|
|
78
|
+
const maxDepth = isLanguageOverrideKey(key) ? 2 : 1;
|
|
79
|
+
mergeKey(settings, key, value, maxDepth, key, rule.id, owners, conflictsByKey);
|
|
80
|
+
}
|
|
81
|
+
for (const extensionId of rule.extensions ?? []) {
|
|
82
|
+
const lower = extensionId.toLowerCase();
|
|
83
|
+
if (!extensionIds.has(lower)) {
|
|
84
|
+
extensionIds.set(lower, extensionId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
matchedRuleIds: matchedRules.map((rule) => rule.id),
|
|
90
|
+
settings,
|
|
91
|
+
extensions: [...extensionIds.values()].sort((a, b) => a.localeCompare(b)),
|
|
92
|
+
conflicts: [...conflictsByKey.values()]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/core/engine.ts
|
|
97
|
+
function isFindingMatcher(condition) {
|
|
98
|
+
return !("all" in condition) && !("any" in condition) && !("not" in condition);
|
|
99
|
+
}
|
|
100
|
+
function matchesFinding(matcher, finding) {
|
|
101
|
+
if (matcher.ecosystem !== void 0 && matcher.ecosystem !== finding.ecosystem) return false;
|
|
102
|
+
if (matcher.kind !== void 0 && matcher.kind !== finding.kind) return false;
|
|
103
|
+
if (matcher.name !== void 0 && matcher.name !== finding.name) return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function matchesCondition(condition, findings) {
|
|
107
|
+
if ("all" in condition) {
|
|
108
|
+
return condition.all.every((sub) => matchesCondition(sub, findings));
|
|
109
|
+
}
|
|
110
|
+
if ("any" in condition) {
|
|
111
|
+
return condition.any.some((sub) => matchesCondition(sub, findings));
|
|
112
|
+
}
|
|
113
|
+
if ("not" in condition) {
|
|
114
|
+
return !matchesCondition(condition.not, findings);
|
|
115
|
+
}
|
|
116
|
+
if (isFindingMatcher(condition)) {
|
|
117
|
+
return findings.some((finding) => matchesFinding(condition, finding));
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
function evaluateRules(findings, rules) {
|
|
122
|
+
return rules.filter((rule) => matchesCondition(rule.when, findings));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/detectors/node.ts
|
|
126
|
+
import { existsSync, readFileSync } from "fs";
|
|
127
|
+
import { join } from "path";
|
|
128
|
+
|
|
129
|
+
// src/detectors/types.ts
|
|
130
|
+
var DetectorError = class extends Error {
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/detectors/node.ts
|
|
134
|
+
var NODE_CONFIG_FILENAMES = [
|
|
135
|
+
"tsconfig.json",
|
|
136
|
+
".eslintrc",
|
|
137
|
+
".eslintrc.json",
|
|
138
|
+
".eslintrc.js",
|
|
139
|
+
".eslintrc.cjs",
|
|
140
|
+
".eslintrc.yml",
|
|
141
|
+
".eslintrc.yaml",
|
|
142
|
+
"eslint.config.js",
|
|
143
|
+
"eslint.config.mjs",
|
|
144
|
+
"eslint.config.cjs",
|
|
145
|
+
"eslint.config.ts",
|
|
146
|
+
".prettierrc",
|
|
147
|
+
".prettierrc.json",
|
|
148
|
+
".prettierrc.js",
|
|
149
|
+
".prettierrc.cjs",
|
|
150
|
+
".prettierrc.yml",
|
|
151
|
+
".prettierrc.yaml",
|
|
152
|
+
"prettier.config.js",
|
|
153
|
+
"prettier.config.cjs"
|
|
154
|
+
];
|
|
155
|
+
var nodeDetector = {
|
|
156
|
+
ecosystem: "node",
|
|
157
|
+
detect(ctx) {
|
|
158
|
+
const findings = [];
|
|
159
|
+
const pkgPath = join(ctx.cwd, "package.json");
|
|
160
|
+
if (existsSync(pkgPath)) {
|
|
161
|
+
findings.push({ ecosystem: "node", kind: "manifestPresence", name: "package.json" });
|
|
162
|
+
let pkg;
|
|
163
|
+
try {
|
|
164
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new DetectorError(`Failed to parse package.json: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
for (const name of Object.keys(pkg.dependencies ?? {})) {
|
|
169
|
+
findings.push({ ecosystem: "node", kind: "dependency", name });
|
|
170
|
+
}
|
|
171
|
+
for (const name of Object.keys(pkg.devDependencies ?? {})) {
|
|
172
|
+
findings.push({ ecosystem: "node", kind: "devDependency", name });
|
|
173
|
+
}
|
|
174
|
+
for (const name of Object.keys(pkg.scripts ?? {})) {
|
|
175
|
+
findings.push({ ecosystem: "node", kind: "scriptPresence", name });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const filename of NODE_CONFIG_FILENAMES) {
|
|
179
|
+
if (existsSync(join(ctx.cwd, filename))) {
|
|
180
|
+
findings.push({ ecosystem: "node", kind: "configFilePresence", name: filename });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/detectors/python.ts
|
|
188
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
189
|
+
import { join as join2 } from "path";
|
|
190
|
+
import { parse as parseToml } from "smol-toml";
|
|
191
|
+
function parsePackageName(spec) {
|
|
192
|
+
const match = spec.trim().match(/^([A-Za-z0-9_.-]+)/);
|
|
193
|
+
return match?.[1] ?? null;
|
|
194
|
+
}
|
|
195
|
+
function parseRequirementsTxt(content) {
|
|
196
|
+
const names = [];
|
|
197
|
+
for (const rawLine of content.split("\n")) {
|
|
198
|
+
const line = rawLine.split("#")[0]?.trim() ?? "";
|
|
199
|
+
if (!line || line.startsWith("-")) continue;
|
|
200
|
+
const name = parsePackageName(line);
|
|
201
|
+
if (name) names.push(name);
|
|
202
|
+
}
|
|
203
|
+
return names;
|
|
204
|
+
}
|
|
205
|
+
function parsePyprojectToml(content) {
|
|
206
|
+
const doc = parseToml(content);
|
|
207
|
+
const dependencies = [];
|
|
208
|
+
const devDependencies = [];
|
|
209
|
+
const pep621Deps = doc.project?.dependencies;
|
|
210
|
+
if (Array.isArray(pep621Deps)) {
|
|
211
|
+
for (const spec of pep621Deps) {
|
|
212
|
+
if (typeof spec !== "string") continue;
|
|
213
|
+
const name = parsePackageName(spec);
|
|
214
|
+
if (name) dependencies.push(name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const poetryDeps = doc.tool?.poetry?.dependencies ?? {};
|
|
218
|
+
for (const name of Object.keys(poetryDeps)) {
|
|
219
|
+
if (name === "python") continue;
|
|
220
|
+
dependencies.push(name);
|
|
221
|
+
}
|
|
222
|
+
const poetryDevDeps = doc.tool?.poetry?.group?.dev?.dependencies ?? doc.tool?.poetry?.["dev-dependencies"] ?? {};
|
|
223
|
+
for (const name of Object.keys(poetryDevDeps)) {
|
|
224
|
+
devDependencies.push(name);
|
|
225
|
+
}
|
|
226
|
+
return { dependencies, devDependencies };
|
|
227
|
+
}
|
|
228
|
+
var pythonDetector = {
|
|
229
|
+
ecosystem: "python",
|
|
230
|
+
detect(ctx) {
|
|
231
|
+
const findings = [];
|
|
232
|
+
const requirementsPath = join2(ctx.cwd, "requirements.txt");
|
|
233
|
+
if (existsSync2(requirementsPath)) {
|
|
234
|
+
findings.push({ ecosystem: "python", kind: "manifestPresence", name: "requirements.txt" });
|
|
235
|
+
const names = parseRequirementsTxt(readFileSync2(requirementsPath, "utf-8"));
|
|
236
|
+
for (const name of names) {
|
|
237
|
+
findings.push({ ecosystem: "python", kind: "dependency", name });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const pyprojectPath = join2(ctx.cwd, "pyproject.toml");
|
|
241
|
+
if (existsSync2(pyprojectPath)) {
|
|
242
|
+
findings.push({ ecosystem: "python", kind: "manifestPresence", name: "pyproject.toml" });
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = parsePyprojectToml(readFileSync2(pyprojectPath, "utf-8"));
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throw new DetectorError(`Failed to parse pyproject.toml: ${error.message}`);
|
|
248
|
+
}
|
|
249
|
+
for (const name of parsed.dependencies) {
|
|
250
|
+
findings.push({ ecosystem: "python", kind: "dependency", name });
|
|
251
|
+
}
|
|
252
|
+
for (const name of parsed.devDependencies) {
|
|
253
|
+
findings.push({ ecosystem: "python", kind: "devDependency", name });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return findings;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/detectors/registry.ts
|
|
261
|
+
var detectorRegistry = [nodeDetector, pythonDetector];
|
|
262
|
+
|
|
263
|
+
// src/detectors/collect.ts
|
|
264
|
+
function collectFindings(ctx, options = {}, detectors = detectorRegistry) {
|
|
265
|
+
const findings = [];
|
|
266
|
+
const errors = [];
|
|
267
|
+
const activeDetectors = options.ecosystems ? detectors.filter((detector) => options.ecosystems.includes(detector.ecosystem)) : detectors;
|
|
268
|
+
for (const detector of activeDetectors) {
|
|
269
|
+
try {
|
|
270
|
+
findings.push(...detector.detect(ctx));
|
|
271
|
+
} catch (error) {
|
|
272
|
+
errors.push(`[${detector.ecosystem}] ${error.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { findings, errors };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/vscode/jsonc.ts
|
|
279
|
+
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
|
280
|
+
function lineFromOffset(text, offset) {
|
|
281
|
+
let line = 1;
|
|
282
|
+
for (let i = 0; i < offset && i < text.length; i++) {
|
|
283
|
+
if (text[i] === "\n") line++;
|
|
284
|
+
}
|
|
285
|
+
return line;
|
|
286
|
+
}
|
|
287
|
+
function parseJsonc(text) {
|
|
288
|
+
const rawErrors = [];
|
|
289
|
+
const value = parse(text, rawErrors, { allowTrailingComma: true });
|
|
290
|
+
const errors = rawErrors.map((error) => ({
|
|
291
|
+
message: printParseErrorCode(error.error),
|
|
292
|
+
line: lineFromOffset(text, error.offset)
|
|
293
|
+
}));
|
|
294
|
+
return { value: errors.length > 0 ? void 0 : value, errors };
|
|
295
|
+
}
|
|
296
|
+
var FORMATTING_OPTIONS = { insertSpaces: true, tabSize: 2, eol: "\n" };
|
|
297
|
+
function setJsoncValue(text, path, value) {
|
|
298
|
+
const edits = modify(text, path, value, { formattingOptions: FORMATTING_OPTIONS });
|
|
299
|
+
return applyEdits(text, edits);
|
|
300
|
+
}
|
|
301
|
+
function insertJsoncArrayItem(text, path, index, value) {
|
|
302
|
+
const edits = modify(text, [...path, index], value, {
|
|
303
|
+
isArrayInsertion: true,
|
|
304
|
+
formattingOptions: FORMATTING_OPTIONS
|
|
305
|
+
});
|
|
306
|
+
return applyEdits(text, edits);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/vscode/extensions-merge.ts
|
|
310
|
+
function readRecommendations(text) {
|
|
311
|
+
const parsed = parseJsonc(text);
|
|
312
|
+
return parsed.value?.recommendations ?? [];
|
|
313
|
+
}
|
|
314
|
+
function mergeExtensions(existingText, ids) {
|
|
315
|
+
const baseText = existingText && existingText.trim().length > 0 ? existingText : "{}";
|
|
316
|
+
const parsed = parseJsonc(baseText);
|
|
317
|
+
const unwanted = new Set(
|
|
318
|
+
(parsed.value?.unwantedRecommendations ?? []).map((id) => id.toLowerCase())
|
|
319
|
+
);
|
|
320
|
+
const seen = new Set((parsed.value?.recommendations ?? []).map((id) => id.toLowerCase()));
|
|
321
|
+
let text = baseText;
|
|
322
|
+
const added = [];
|
|
323
|
+
const alreadyPresent = [];
|
|
324
|
+
const skippedUnwanted = [];
|
|
325
|
+
for (const id of ids) {
|
|
326
|
+
const lower = id.toLowerCase();
|
|
327
|
+
if (seen.has(lower)) {
|
|
328
|
+
alreadyPresent.push(id);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (unwanted.has(lower)) {
|
|
332
|
+
skippedUnwanted.push(id);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const currentLength = readRecommendations(text).length;
|
|
336
|
+
text = insertJsoncArrayItem(text, ["recommendations"], currentLength, id);
|
|
337
|
+
seen.add(lower);
|
|
338
|
+
added.push(id);
|
|
339
|
+
}
|
|
340
|
+
return { nextText: text, added, alreadyPresent, skippedUnwanted };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/vscode/settings-merge.ts
|
|
344
|
+
function isPlainObject2(value) {
|
|
345
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
346
|
+
}
|
|
347
|
+
function isLanguageOverrideKey2(key) {
|
|
348
|
+
return /^\[.+]$/.test(key);
|
|
349
|
+
}
|
|
350
|
+
function planNested(path, currentValue, patchValue, depthRemaining, additions, skipped) {
|
|
351
|
+
if (depthRemaining <= 0 || !isPlainObject2(currentValue) || !isPlainObject2(patchValue)) {
|
|
352
|
+
skipped[path.join(".")] = "existing";
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const [subKey, subValue] of Object.entries(patchValue)) {
|
|
356
|
+
const subPath = [...path, subKey];
|
|
357
|
+
if (!(subKey in currentValue)) {
|
|
358
|
+
additions.push({ path: subPath, value: subValue });
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
planNested(subPath, currentValue[subKey], subValue, depthRemaining - 1, additions, skipped);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function planSettingsPatch(current, patch) {
|
|
365
|
+
const additions = [];
|
|
366
|
+
const skipped = {};
|
|
367
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
368
|
+
if (!(key in current)) {
|
|
369
|
+
additions.push({ path: [key], value });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const maxDepth = isLanguageOverrideKey2(key) ? 2 : 1;
|
|
373
|
+
planNested([key], current[key], value, maxDepth, additions, skipped);
|
|
374
|
+
}
|
|
375
|
+
return { additions, skipped };
|
|
376
|
+
}
|
|
377
|
+
function mergeSettings(existingText, patch) {
|
|
378
|
+
const baseText = existingText && existingText.trim().length > 0 ? existingText : "{}";
|
|
379
|
+
const parsed = parseJsonc(baseText);
|
|
380
|
+
const current = parsed.value ?? {};
|
|
381
|
+
const { additions, skipped } = planSettingsPatch(current, patch);
|
|
382
|
+
let text = baseText;
|
|
383
|
+
const added = {};
|
|
384
|
+
for (const addition of additions) {
|
|
385
|
+
text = setJsoncValue(text, addition.path, addition.value);
|
|
386
|
+
added[addition.path.join(".")] = addition.value;
|
|
387
|
+
}
|
|
388
|
+
return { nextText: text, added, skipped };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/diff/preview.ts
|
|
392
|
+
function checkParseError(text) {
|
|
393
|
+
if (text === void 0) return void 0;
|
|
394
|
+
const parsed = parseJsonc(text);
|
|
395
|
+
if (parsed.errors.length === 0) return void 0;
|
|
396
|
+
const first = parsed.errors[0];
|
|
397
|
+
return { line: first.line, message: first.message };
|
|
398
|
+
}
|
|
399
|
+
function buildPreview(params) {
|
|
400
|
+
const {
|
|
401
|
+
mode,
|
|
402
|
+
findings,
|
|
403
|
+
ecosystems,
|
|
404
|
+
aggregated,
|
|
405
|
+
settingsPath,
|
|
406
|
+
extensionsPath,
|
|
407
|
+
settingsText,
|
|
408
|
+
extensionsText
|
|
409
|
+
} = params;
|
|
410
|
+
const files = [];
|
|
411
|
+
const skippedFiles = [];
|
|
412
|
+
let settingsAdded = {};
|
|
413
|
+
let settingsSkipped = {};
|
|
414
|
+
const settingsError = checkParseError(settingsText);
|
|
415
|
+
if (settingsError) {
|
|
416
|
+
skippedFiles.push({
|
|
417
|
+
file: settingsPath,
|
|
418
|
+
reason: `Syntax error near line ${settingsError.line} of settings.json (${settingsError.message}). This file was skipped.`,
|
|
419
|
+
line: settingsError.line
|
|
420
|
+
});
|
|
421
|
+
} else {
|
|
422
|
+
const result = mergeSettings(settingsText, aggregated.settings);
|
|
423
|
+
settingsAdded = result.added;
|
|
424
|
+
settingsSkipped = result.skipped;
|
|
425
|
+
files.push({
|
|
426
|
+
path: settingsPath,
|
|
427
|
+
label: "settings.json",
|
|
428
|
+
existed: settingsText !== void 0,
|
|
429
|
+
before: settingsText ?? "",
|
|
430
|
+
after: result.nextText
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
let extensionsAdded = [];
|
|
434
|
+
let extensionsAlreadyPresent = [];
|
|
435
|
+
let extensionsUnwanted = [];
|
|
436
|
+
const extensionsError = checkParseError(extensionsText);
|
|
437
|
+
if (extensionsError) {
|
|
438
|
+
skippedFiles.push({
|
|
439
|
+
file: extensionsPath,
|
|
440
|
+
reason: `Syntax error near line ${extensionsError.line} of extensions.json (${extensionsError.message}). This file was skipped.`,
|
|
441
|
+
line: extensionsError.line
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
const result = mergeExtensions(extensionsText, aggregated.extensions);
|
|
445
|
+
extensionsAdded = result.added;
|
|
446
|
+
extensionsAlreadyPresent = result.alreadyPresent;
|
|
447
|
+
extensionsUnwanted = result.skippedUnwanted;
|
|
448
|
+
files.push({
|
|
449
|
+
path: extensionsPath,
|
|
450
|
+
label: "extensions.json",
|
|
451
|
+
existed: extensionsText !== void 0,
|
|
452
|
+
before: extensionsText ?? "",
|
|
453
|
+
after: result.nextText
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
const json = {
|
|
457
|
+
mode,
|
|
458
|
+
detected: { ecosystems, findings },
|
|
459
|
+
matchedRules: aggregated.matchedRuleIds,
|
|
460
|
+
settings: { added: settingsAdded, skipped: settingsSkipped },
|
|
461
|
+
extensions: {
|
|
462
|
+
added: extensionsAdded,
|
|
463
|
+
alreadyPresent: extensionsAlreadyPresent,
|
|
464
|
+
unwanted: extensionsUnwanted
|
|
465
|
+
},
|
|
466
|
+
conflicts: aggregated.conflicts,
|
|
467
|
+
skippedFiles
|
|
468
|
+
};
|
|
469
|
+
return { json, files };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/diff/render.ts
|
|
473
|
+
import { diffLines } from "diff";
|
|
474
|
+
import pc from "picocolors";
|
|
475
|
+
function renderJson(json) {
|
|
476
|
+
return JSON.stringify(json, null, 2);
|
|
477
|
+
}
|
|
478
|
+
function renderTerminal(json, files) {
|
|
479
|
+
const lines = [];
|
|
480
|
+
const title = json.mode === "dry-run" ? "vscode-autoconfig \u2014 dry run (no files written)" : "vscode-autoconfig \u2014 applying changes";
|
|
481
|
+
lines.push(pc.bold(title));
|
|
482
|
+
lines.push("");
|
|
483
|
+
if (json.detected.findings.length > 0) {
|
|
484
|
+
lines.push(pc.bold("Detected"));
|
|
485
|
+
for (const finding of json.detected.findings) {
|
|
486
|
+
lines.push(` ${finding.ecosystem.padEnd(7)} ${finding.name} (${finding.kind})`);
|
|
487
|
+
}
|
|
488
|
+
lines.push("");
|
|
489
|
+
}
|
|
490
|
+
for (const file of files) {
|
|
491
|
+
if (file.before === file.after) continue;
|
|
492
|
+
lines.push(pc.bold(`${file.path}${file.existed ? "" : " (will be created)"}`));
|
|
493
|
+
for (const part of diffLines(file.before, file.after)) {
|
|
494
|
+
if (part.removed) continue;
|
|
495
|
+
const prefix = part.added ? "+" : " ";
|
|
496
|
+
const colorize = part.added ? pc.green : (s) => s;
|
|
497
|
+
for (const line of part.value.split("\n")) {
|
|
498
|
+
if (line.length === 0) continue;
|
|
499
|
+
lines.push(colorize(` ${prefix} ${line}`));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
lines.push("");
|
|
503
|
+
}
|
|
504
|
+
if (json.conflicts.length > 0) {
|
|
505
|
+
lines.push(pc.yellow("Conflicts (first matching rule wins, no overwrite):"));
|
|
506
|
+
for (const conflict of json.conflicts) {
|
|
507
|
+
lines.push(
|
|
508
|
+
` ${conflict.key}: "${conflict.winningRuleId}" wins over ${conflict.ignoredRuleIds.join(", ")}`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
lines.push("");
|
|
512
|
+
}
|
|
513
|
+
if (json.skippedFiles.length > 0) {
|
|
514
|
+
lines.push(pc.red("Skipped files:"));
|
|
515
|
+
for (const skipped of json.skippedFiles) {
|
|
516
|
+
lines.push(` ${skipped.file}: ${skipped.reason}`);
|
|
517
|
+
}
|
|
518
|
+
lines.push("");
|
|
519
|
+
}
|
|
520
|
+
const settingsAddedCount = Object.keys(json.settings.added).length;
|
|
521
|
+
const extensionsAddedCount = json.extensions.added.length;
|
|
522
|
+
lines.push(`${settingsAddedCount} settings, ${extensionsAddedCount} extensions would be added.`);
|
|
523
|
+
if (json.mode === "dry-run") {
|
|
524
|
+
lines.push("Run again with --write to apply.");
|
|
525
|
+
}
|
|
526
|
+
return lines.join("\n");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/rules/loader.ts
|
|
530
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
531
|
+
import { extname } from "path";
|
|
532
|
+
import { parse as parseYaml } from "yaml";
|
|
533
|
+
|
|
534
|
+
// src/rules/schema.ts
|
|
535
|
+
import { z } from "zod";
|
|
536
|
+
|
|
537
|
+
// src/core/types.ts
|
|
538
|
+
var RULE_SCHEMA_VERSION = 1;
|
|
539
|
+
|
|
540
|
+
// src/rules/schema.ts
|
|
541
|
+
var ecosystemSchema = z.enum(["node", "python"]);
|
|
542
|
+
var findingKindSchema = z.enum([
|
|
543
|
+
"dependency",
|
|
544
|
+
"devDependency",
|
|
545
|
+
"manifestPresence",
|
|
546
|
+
"configFilePresence",
|
|
547
|
+
"scriptPresence"
|
|
548
|
+
]);
|
|
549
|
+
var findingMatcherSchema = z.object({
|
|
550
|
+
ecosystem: ecosystemSchema.optional(),
|
|
551
|
+
kind: findingKindSchema.optional(),
|
|
552
|
+
name: z.string().optional()
|
|
553
|
+
}).strict();
|
|
554
|
+
var jsonValueSchema = z.lazy(
|
|
555
|
+
() => z.union([
|
|
556
|
+
z.string(),
|
|
557
|
+
z.number(),
|
|
558
|
+
z.boolean(),
|
|
559
|
+
z.null(),
|
|
560
|
+
z.array(jsonValueSchema),
|
|
561
|
+
z.record(jsonValueSchema)
|
|
562
|
+
])
|
|
563
|
+
);
|
|
564
|
+
var ruleConditionSchema = z.lazy(
|
|
565
|
+
() => z.union([
|
|
566
|
+
z.object({ all: z.array(ruleConditionSchema) }).strict(),
|
|
567
|
+
z.object({ any: z.array(ruleConditionSchema) }).strict(),
|
|
568
|
+
z.object({ not: ruleConditionSchema }).strict(),
|
|
569
|
+
findingMatcherSchema
|
|
570
|
+
])
|
|
571
|
+
);
|
|
572
|
+
var ruleSchema = z.object({
|
|
573
|
+
id: z.string().min(1),
|
|
574
|
+
description: z.string().optional(),
|
|
575
|
+
when: ruleConditionSchema,
|
|
576
|
+
settings: z.record(jsonValueSchema).optional(),
|
|
577
|
+
extensions: z.array(z.string()).optional()
|
|
578
|
+
});
|
|
579
|
+
var ruleFileSchema = z.object({
|
|
580
|
+
schemaVersion: z.number().int(),
|
|
581
|
+
rules: z.array(ruleSchema)
|
|
582
|
+
}).superRefine((file, ctx) => {
|
|
583
|
+
if (file.schemaVersion !== RULE_SCHEMA_VERSION) {
|
|
584
|
+
ctx.addIssue({
|
|
585
|
+
code: z.ZodIssueCode.custom,
|
|
586
|
+
path: ["schemaVersion"],
|
|
587
|
+
message: `This rule file has schemaVersion ${file.schemaVersion}, but this tool only supports version ${RULE_SCHEMA_VERSION}.`
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
591
|
+
for (const [index, rule] of file.rules.entries()) {
|
|
592
|
+
if (seenIds.has(rule.id)) {
|
|
593
|
+
ctx.addIssue({
|
|
594
|
+
code: z.ZodIssueCode.custom,
|
|
595
|
+
path: ["rules", index, "id"],
|
|
596
|
+
message: `Rule id "${rule.id}" is duplicated. Rule ids must be unique.`
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
seenIds.add(rule.id);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// src/rules/node.json
|
|
604
|
+
var node_default = {
|
|
605
|
+
schemaVersion: 1,
|
|
606
|
+
rules: [
|
|
607
|
+
{
|
|
608
|
+
id: "node-typescript-presence",
|
|
609
|
+
description: "TypeScript project detected via tsconfig.json or a typescript devDependency",
|
|
610
|
+
when: {
|
|
611
|
+
any: [
|
|
612
|
+
{ ecosystem: "node", kind: "configFilePresence", name: "tsconfig.json" },
|
|
613
|
+
{ ecosystem: "node", kind: "devDependency", name: "typescript" }
|
|
614
|
+
]
|
|
615
|
+
},
|
|
616
|
+
settings: {
|
|
617
|
+
"js/ts.tsdk.path": "./node_modules/typescript/lib"
|
|
618
|
+
},
|
|
619
|
+
extensions: ["dbaeumer.vscode-eslint"]
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: "node-eslint-config-presence",
|
|
623
|
+
description: "ESLint config file or eslint devDependency detected",
|
|
624
|
+
when: {
|
|
625
|
+
any: [
|
|
626
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".eslintrc" },
|
|
627
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".eslintrc.json" },
|
|
628
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".eslintrc.js" },
|
|
629
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".eslintrc.cjs" },
|
|
630
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".eslintrc.yml" },
|
|
631
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".eslintrc.yaml" },
|
|
632
|
+
{ ecosystem: "node", kind: "configFilePresence", name: "eslint.config.js" },
|
|
633
|
+
{ ecosystem: "node", kind: "configFilePresence", name: "eslint.config.mjs" },
|
|
634
|
+
{ ecosystem: "node", kind: "configFilePresence", name: "eslint.config.cjs" },
|
|
635
|
+
{ ecosystem: "node", kind: "configFilePresence", name: "eslint.config.ts" },
|
|
636
|
+
{ ecosystem: "node", kind: "devDependency", name: "eslint" }
|
|
637
|
+
]
|
|
638
|
+
},
|
|
639
|
+
settings: {
|
|
640
|
+
"eslint.enable": true,
|
|
641
|
+
"editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }
|
|
642
|
+
},
|
|
643
|
+
extensions: ["dbaeumer.vscode-eslint"]
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: "node-prettier-config-presence",
|
|
647
|
+
description: "Prettier config file or prettier devDependency detected",
|
|
648
|
+
when: {
|
|
649
|
+
any: [
|
|
650
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".prettierrc" },
|
|
651
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".prettierrc.json" },
|
|
652
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".prettierrc.js" },
|
|
653
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".prettierrc.cjs" },
|
|
654
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".prettierrc.yml" },
|
|
655
|
+
{ ecosystem: "node", kind: "configFilePresence", name: ".prettierrc.yaml" },
|
|
656
|
+
{ ecosystem: "node", kind: "configFilePresence", name: "prettier.config.js" },
|
|
657
|
+
{ ecosystem: "node", kind: "configFilePresence", name: "prettier.config.cjs" },
|
|
658
|
+
{ ecosystem: "node", kind: "devDependency", name: "prettier" }
|
|
659
|
+
]
|
|
660
|
+
},
|
|
661
|
+
settings: {
|
|
662
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
663
|
+
},
|
|
664
|
+
extensions: ["esbenp.prettier-vscode"]
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
id: "node-react-dependency",
|
|
668
|
+
description: "React dependency detected",
|
|
669
|
+
when: { ecosystem: "node", kind: "dependency", name: "react" },
|
|
670
|
+
extensions: ["dsznajder.es7-react-js-snippets"]
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
id: "node-vue-dependency",
|
|
674
|
+
description: "Vue dependency detected",
|
|
675
|
+
when: { ecosystem: "node", kind: "dependency", name: "vue" },
|
|
676
|
+
extensions: ["Vue.volar"]
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
id: "node-next-dependency",
|
|
680
|
+
description: "Next.js dependency detected",
|
|
681
|
+
when: { ecosystem: "node", kind: "dependency", name: "next" },
|
|
682
|
+
extensions: ["dsznajder.es7-react-js-snippets"]
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: "node-tailwind-dependency",
|
|
686
|
+
description: "Tailwind CSS dependency detected",
|
|
687
|
+
when: { ecosystem: "node", kind: "dependency", name: "tailwindcss" },
|
|
688
|
+
extensions: ["bradlc.vscode-tailwindcss"]
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/rules/python.json
|
|
694
|
+
var python_default = {
|
|
695
|
+
schemaVersion: 1,
|
|
696
|
+
rules: [
|
|
697
|
+
{
|
|
698
|
+
id: "python-manifest-presence",
|
|
699
|
+
description: "requirements.txt or pyproject.toml detected",
|
|
700
|
+
when: {
|
|
701
|
+
any: [
|
|
702
|
+
{ ecosystem: "python", kind: "manifestPresence", name: "requirements.txt" },
|
|
703
|
+
{ ecosystem: "python", kind: "manifestPresence", name: "pyproject.toml" }
|
|
704
|
+
]
|
|
705
|
+
},
|
|
706
|
+
extensions: ["ms-python.python", "ms-python.vscode-pylance"]
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: "python-black-dependency",
|
|
710
|
+
description: "black dependency detected",
|
|
711
|
+
when: {
|
|
712
|
+
any: [
|
|
713
|
+
{ ecosystem: "python", kind: "dependency", name: "black" },
|
|
714
|
+
{ ecosystem: "python", kind: "devDependency", name: "black" }
|
|
715
|
+
]
|
|
716
|
+
},
|
|
717
|
+
settings: {
|
|
718
|
+
"[python]": {
|
|
719
|
+
"editor.defaultFormatter": "ms-python.black-formatter",
|
|
720
|
+
"editor.formatOnSave": true
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
extensions: ["ms-python.black-formatter"]
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
id: "python-ruff-dependency",
|
|
727
|
+
description: "ruff dependency detected",
|
|
728
|
+
when: {
|
|
729
|
+
any: [
|
|
730
|
+
{ ecosystem: "python", kind: "dependency", name: "ruff" },
|
|
731
|
+
{ ecosystem: "python", kind: "devDependency", name: "ruff" }
|
|
732
|
+
]
|
|
733
|
+
},
|
|
734
|
+
settings: {
|
|
735
|
+
"[python]": {
|
|
736
|
+
"editor.codeActionsOnSave": { "source.fixAll.ruff": true }
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
extensions: ["charliermarsh.ruff"]
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
id: "python-mypy-dependency",
|
|
743
|
+
description: "mypy dependency detected",
|
|
744
|
+
when: {
|
|
745
|
+
any: [
|
|
746
|
+
{ ecosystem: "python", kind: "dependency", name: "mypy" },
|
|
747
|
+
{ ecosystem: "python", kind: "devDependency", name: "mypy" }
|
|
748
|
+
]
|
|
749
|
+
},
|
|
750
|
+
extensions: ["ms-python.mypy-type-checker"]
|
|
751
|
+
}
|
|
752
|
+
]
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/rules/loader.ts
|
|
756
|
+
var RuleFileValidationError = class extends Error {
|
|
757
|
+
};
|
|
758
|
+
function validate(source, raw) {
|
|
759
|
+
const result = ruleFileSchema.safeParse(raw);
|
|
760
|
+
if (!result.success) {
|
|
761
|
+
const messages = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
762
|
+
throw new RuleFileValidationError(`Rule file "${source}" failed validation:
|
|
763
|
+
${messages}`);
|
|
764
|
+
}
|
|
765
|
+
return result.data.rules;
|
|
766
|
+
}
|
|
767
|
+
function loadBuiltinRules() {
|
|
768
|
+
return [...validate("node.json", node_default), ...validate("python.json", python_default)];
|
|
769
|
+
}
|
|
770
|
+
function loadExternalRules(path) {
|
|
771
|
+
const raw = readFileSync3(path, "utf-8");
|
|
772
|
+
const ext = extname(path).toLowerCase();
|
|
773
|
+
const parsed = ext === ".yaml" || ext === ".yml" ? parseYaml(raw) : JSON.parse(raw);
|
|
774
|
+
return validate(path, parsed);
|
|
775
|
+
}
|
|
776
|
+
function checkForDuplicateIds(rules) {
|
|
777
|
+
const ruleIds = /* @__PURE__ */ new Set();
|
|
778
|
+
for (const rule of rules) {
|
|
779
|
+
if (ruleIds.has(rule.id)) {
|
|
780
|
+
throw new RuleFileValidationError(
|
|
781
|
+
`Rule id "${rule.id}" is duplicated across the built-in and external rule sets. Rule ids must be unique.`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
ruleIds.add(rule.id);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function loadRules({ externalPath, useBuiltinRules = true } = {}) {
|
|
788
|
+
const externalRules = externalPath ? loadExternalRules(externalPath) : [];
|
|
789
|
+
const rules = useBuiltinRules ? [...loadBuiltinRules(), ...externalRules] : externalRules;
|
|
790
|
+
checkForDuplicateIds(rules);
|
|
791
|
+
return rules;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/vscode/paths.ts
|
|
795
|
+
import { join as join3 } from "path";
|
|
796
|
+
function vscodeDir(cwd) {
|
|
797
|
+
return join3(cwd, ".vscode");
|
|
798
|
+
}
|
|
799
|
+
function settingsJsonPath(cwd) {
|
|
800
|
+
return join3(cwd, ".vscode", "settings.json");
|
|
801
|
+
}
|
|
802
|
+
function extensionsJsonPath(cwd) {
|
|
803
|
+
return join3(cwd, ".vscode", "extensions.json");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/cli/run.ts
|
|
807
|
+
function run(options) {
|
|
808
|
+
try {
|
|
809
|
+
const rules = loadRules({
|
|
810
|
+
externalPath: options.rulesPath,
|
|
811
|
+
useBuiltinRules: options.builtinRules
|
|
812
|
+
});
|
|
813
|
+
const { findings, errors } = collectFindings(
|
|
814
|
+
{ cwd: options.cwd },
|
|
815
|
+
{ ecosystems: options.ecosystems }
|
|
816
|
+
);
|
|
817
|
+
const matchedRules = evaluateRules(findings, rules);
|
|
818
|
+
const aggregated = aggregate(matchedRules);
|
|
819
|
+
const settingsPath = settingsJsonPath(options.cwd);
|
|
820
|
+
const extensionsPath = extensionsJsonPath(options.cwd);
|
|
821
|
+
const settingsText = existsSync3(settingsPath) ? readFileSync4(settingsPath, "utf-8") : void 0;
|
|
822
|
+
const extensionsText = existsSync3(extensionsPath) ? readFileSync4(extensionsPath, "utf-8") : void 0;
|
|
823
|
+
const ecosystemsDetected = [...new Set(findings.map((s) => s.ecosystem))];
|
|
824
|
+
const preview = buildPreview({
|
|
825
|
+
mode: options.write ? "write" : "dry-run",
|
|
826
|
+
findings,
|
|
827
|
+
ecosystems: ecosystemsDetected,
|
|
828
|
+
aggregated,
|
|
829
|
+
settingsPath,
|
|
830
|
+
extensionsPath,
|
|
831
|
+
settingsText,
|
|
832
|
+
extensionsText
|
|
833
|
+
});
|
|
834
|
+
const hasDiff = Object.keys(preview.json.settings.added).length > 0 || preview.json.extensions.added.length > 0;
|
|
835
|
+
const hasErrors = errors.length > 0 || preview.json.skippedFiles.length > 0;
|
|
836
|
+
const reportLines = errors.map((error) => `Error: ${error}`);
|
|
837
|
+
const report = options.json ? renderJson(preview.json) : renderTerminal(preview.json, preview.files);
|
|
838
|
+
const outputLines = [...reportLines, report];
|
|
839
|
+
if (options.write && hasDiff) {
|
|
840
|
+
mkdirSync(vscodeDir(options.cwd), { recursive: true });
|
|
841
|
+
for (const file of preview.files) {
|
|
842
|
+
if (file.before !== file.after) {
|
|
843
|
+
writeFileSync(file.path, file.after, "utf-8");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return { exitCode: hasErrors ? 2 : hasDiff ? 1 : 0, output: outputLines.join("\n") };
|
|
848
|
+
} catch (error) {
|
|
849
|
+
if (error instanceof RuleFileValidationError) {
|
|
850
|
+
return { exitCode: 2, output: `Error: ${error.message}` };
|
|
851
|
+
}
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/cli.ts
|
|
857
|
+
async function main() {
|
|
858
|
+
const cliArgs = parseArgs(process.argv);
|
|
859
|
+
const runResutl = run({
|
|
860
|
+
cwd: process.cwd(),
|
|
861
|
+
write: cliArgs.write,
|
|
862
|
+
ecosystems: cliArgs.ecosystems,
|
|
863
|
+
rulesPath: cliArgs.rules,
|
|
864
|
+
builtinRules: cliArgs.builtinRules,
|
|
865
|
+
json: cliArgs.json
|
|
866
|
+
});
|
|
867
|
+
console.log(runResutl.output);
|
|
868
|
+
process.exitCode = runResutl.exitCode;
|
|
869
|
+
}
|
|
870
|
+
main().catch((error) => {
|
|
871
|
+
console.error(error instanceof Error ? error.message : error);
|
|
872
|
+
process.exitCode = 2;
|
|
873
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vscode-autoconfig",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Analyze your project's manifests and auto-generate .vscode/settings.json and .vscode/extensions.json via rule-based detection (no AI/LLM).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vscode-autoconfig": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"lint": "eslint .",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"format": "prettier --write .",
|
|
26
|
+
"format:check": "prettier --check .",
|
|
27
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"diff": "^5.2.0",
|
|
32
|
+
"jsonc-parser": "^3.3.1",
|
|
33
|
+
"picocolors": "^1.0.1",
|
|
34
|
+
"smol-toml": "^1.3.0",
|
|
35
|
+
"yaml": "^2.5.0",
|
|
36
|
+
"zod": "^3.23.8"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@eslint/js": "^9.7.0",
|
|
40
|
+
"@types/diff": "^5.2.1",
|
|
41
|
+
"@types/node": "^20.14.0",
|
|
42
|
+
"eslint": "^9.7.0",
|
|
43
|
+
"eslint-config-prettier": "^9.1.0",
|
|
44
|
+
"prettier": "^3.3.3",
|
|
45
|
+
"tsup": "^8.1.0",
|
|
46
|
+
"typescript": "^5.5.3",
|
|
47
|
+
"typescript-eslint": "^8.0.0",
|
|
48
|
+
"vitest": "^2.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|