@vsceasy/cli 0.1.9 → 0.1.10

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/dist/index.js CHANGED
@@ -134,15 +134,26 @@ async function scaffold(opts) {
134
134
  throw new Error(`Target directory not empty: ${opts.targetDir}`);
135
135
  }
136
136
  fs2.mkdirSync(opts.targetDir, { recursive: true });
137
+ const type = opts.type ?? "ui";
137
138
  const vars = buildVars(opts);
138
139
  await copyTree(src, opts.targetDir, vars);
139
- applyPreset(opts.targetDir, opts.preset ?? "full");
140
+ applyType(opts.targetDir, type, opts.preset ?? "full", opts.templatesRoot, vars);
140
141
  writeConfig(opts.targetDir, {
141
142
  publisher: opts.publisher,
142
143
  commandPrefix: vars.commandPrefix,
143
- ui: opts.ui
144
+ type,
145
+ ...type === "ui" ? { ui: opts.ui } : {}
144
146
  });
145
147
  }
148
+ function applyType(targetDir, type, preset, templatesRoot, vars) {
149
+ if (type === "ui") {
150
+ applyPreset(targetDir, preset);
151
+ return;
152
+ }
153
+ stripWebview(targetDir);
154
+ if (type === "language")
155
+ applyLanguage(targetDir, templatesRoot, vars);
156
+ }
146
157
  function applyPreset(targetDir, preset) {
147
158
  if (preset === "full")
148
159
  return;
@@ -165,15 +176,104 @@ function applyPreset(targetDir, preset) {
165
176
  `);
166
177
  }
167
178
  }
179
+ function stripWebview(targetDir) {
180
+ const removals = [
181
+ "src/panels",
182
+ "src/webview",
183
+ "src/commands/hello.ts",
184
+ "vite.config.ts"
185
+ ];
186
+ for (const rel of removals) {
187
+ const abs = path2.join(targetDir, rel);
188
+ if (fs2.existsSync(abs))
189
+ fs2.rmSync(abs, { recursive: true, force: true });
190
+ }
191
+ const apiPath = path2.join(targetDir, "src", "shared", "api.ts");
192
+ if (fs2.existsSync(apiPath)) {
193
+ fs2.writeFileSync(apiPath, `// RPC contracts go here (add a panel with \`vsceasy panel add\`).
194
+ `);
195
+ }
196
+ trimReactFromPackageJson(targetDir);
197
+ }
198
+ function trimReactFromPackageJson(targetDir) {
199
+ const pkgPath = path2.join(targetDir, "package.json");
200
+ if (!fs2.existsSync(pkgPath))
201
+ return;
202
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
203
+ for (const section of ["dependencies", "devDependencies"]) {
204
+ if (!pkg[section])
205
+ continue;
206
+ for (const dep of Object.keys(pkg[section])) {
207
+ if (REACT_DEPS.has(dep))
208
+ delete pkg[section][dep];
209
+ }
210
+ if (Object.keys(pkg[section]).length === 0)
211
+ delete pkg[section];
212
+ }
213
+ if (pkg.scripts) {
214
+ for (const key of Object.keys(pkg.scripts)) {
215
+ if (UI_SCRIPT_KEYS.has(key))
216
+ delete pkg.scripts[key];
217
+ }
218
+ if (pkg.scripts.dev) {
219
+ pkg.scripts.dev = "bun run gen:scan && bun run dev:ext";
220
+ }
221
+ for (const [k, v] of Object.entries(pkg.scripts)) {
222
+ pkg.scripts[k] = v.replace(/\s*&&\s*bun run build:ui/g, "");
223
+ }
224
+ }
225
+ fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
226
+ `);
227
+ }
228
+ function applyLanguage(targetDir, templatesRoot, vars) {
229
+ const assetsDir = path2.join(templatesRoot, "_assets", "language");
230
+ if (!fs2.existsSync(assetsDir)) {
231
+ throw new Error(`Language assets not found: ${assetsDir}`);
232
+ }
233
+ copyAssetsTree(assetsDir, targetDir, vars);
234
+ const langReadme = path2.join(targetDir, "README.language.md");
235
+ if (fs2.existsSync(langReadme)) {
236
+ fs2.renameSync(langReadme, path2.join(targetDir, "README.md"));
237
+ }
238
+ const pkgPath = path2.join(targetDir, "package.json");
239
+ if (fs2.existsSync(pkgPath)) {
240
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
241
+ pkg.activationEvents = [`onLanguage:${vars.langId}`];
242
+ fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
243
+ `);
244
+ }
245
+ }
246
+ function copyAssetsTree(srcDir, destDir, vars) {
247
+ for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
248
+ if (SKIP_NAMES.has(entry.name))
249
+ continue;
250
+ const srcPath = path2.join(srcDir, entry.name);
251
+ const destName = substitute(entry.name, vars);
252
+ const destPath = path2.join(destDir, destName);
253
+ if (entry.isDirectory()) {
254
+ fs2.mkdirSync(destPath, { recursive: true });
255
+ copyAssetsTree(srcPath, destPath, vars);
256
+ } else if (entry.isFile()) {
257
+ fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
258
+ fs2.writeFileSync(destPath, substitute(fs2.readFileSync(srcPath, "utf8"), vars));
259
+ }
260
+ }
261
+ }
168
262
  function buildVars(opts) {
169
263
  const simpleName = opts.name.replace(/^@[^/]+\//, "");
170
264
  const commandPrefix = simpleName.replace(/[^a-zA-Z0-9]+/g, "");
265
+ const langId = simpleName.toLowerCase().replace(/[^a-z0-9]+/g, "");
266
+ const scopeName = `source.${langId}`;
267
+ const langExt = langId.slice(0, 4).toUpperCase();
171
268
  return {
172
269
  name: opts.name,
173
270
  displayName: opts.displayName,
174
271
  description: opts.description,
175
272
  publisher: opts.publisher,
176
- commandPrefix
273
+ commandPrefix,
274
+ langId,
275
+ scopeName,
276
+ langExt
177
277
  };
178
278
  }
179
279
  async function copyTree(srcDir, destDir, vars) {
@@ -199,7 +299,7 @@ async function copyTree(srcDir, destDir, vars) {
199
299
  function substitute(input, vars) {
200
300
  return input.replace(/\{\{(\w+)\}\}/g, (_m, key) => vars[key] ?? `{{${key}}}`);
201
301
  }
202
- var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES;
302
+ var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES, REACT_DEPS, UI_SCRIPT_KEYS;
203
303
  var init_scaffold = __esm(() => {
204
304
  init_config();
205
305
  fs2 = __toESM(require("fs"));
@@ -219,6 +319,15 @@ var init_scaffold = __esm(() => {
219
319
  ".yaml"
220
320
  ]);
221
321
  SKIP_NAMES = new Set(["node_modules", "dist", ".DS_Store"]);
322
+ REACT_DEPS = new Set([
323
+ "react",
324
+ "react-dom",
325
+ "@types/react",
326
+ "@types/react-dom",
327
+ "@vitejs/plugin-react",
328
+ "vite"
329
+ ]);
330
+ UI_SCRIPT_KEYS = new Set(["dev:ui", "build:ui"]);
222
331
  });
223
332
 
224
333
  // src/lib/validate.ts
@@ -1580,6 +1689,9 @@ function runDoctor(opts) {
1580
1689
  results.push(...checkStatusBars(root));
1581
1690
  results.push(...checkSubpanels(root));
1582
1691
  results.push(checkContributesSync(root, pkg));
1692
+ const langCheck = checkLanguageAssets(root, pkg);
1693
+ if (langCheck)
1694
+ results.push(langCheck);
1583
1695
  results.push(checkActivationEvents(pkg));
1584
1696
  results.push(checkMarketplaceIcon(root, pkg));
1585
1697
  results.push(checkGenScript(root));
@@ -1842,6 +1954,50 @@ function checkContributesSync(root, pkg) {
1842
1954
  details: stale
1843
1955
  };
1844
1956
  }
1957
+ function checkLanguageAssets(root, pkg) {
1958
+ const sources = [pkg?.contributes];
1959
+ const extraPath = path12.join(root, "contributes.extra.json");
1960
+ if (fs12.existsSync(extraPath)) {
1961
+ try {
1962
+ sources.push(JSON.parse(fs12.readFileSync(extraPath, "utf8")));
1963
+ } catch {
1964
+ return {
1965
+ id: "language",
1966
+ level: "error",
1967
+ message: "contributes.extra.json is not valid JSON"
1968
+ };
1969
+ }
1970
+ }
1971
+ const refs = new Set;
1972
+ for (const c of sources) {
1973
+ if (!c)
1974
+ continue;
1975
+ for (const l of c.languages ?? [])
1976
+ if (l.configuration)
1977
+ refs.add(l.configuration);
1978
+ for (const g of c.grammars ?? [])
1979
+ if (g.path)
1980
+ refs.add(g.path);
1981
+ for (const s of c.snippets ?? [])
1982
+ if (s.path)
1983
+ refs.add(s.path);
1984
+ for (const t of c.iconThemes ?? [])
1985
+ if (t.path)
1986
+ refs.add(t.path);
1987
+ }
1988
+ if (refs.size === 0)
1989
+ return null;
1990
+ const missing = [...refs].filter((rel) => !fs12.existsSync(path12.join(root, rel)));
1991
+ if (missing.length === 0) {
1992
+ return { id: "language", level: "ok", message: `language ${refs.size} asset(s) present` };
1993
+ }
1994
+ return {
1995
+ id: "language",
1996
+ level: "error",
1997
+ message: `language: ${missing.length} referenced asset(s) missing`,
1998
+ details: missing
1999
+ };
2000
+ }
1845
2001
  function checkGitignore(root) {
1846
2002
  const file = path12.join(root, ".gitignore");
1847
2003
  const required = ["dist", "node_modules"];
@@ -2238,9 +2394,314 @@ var init_upgrade = __esm(() => {
2238
2394
  });
2239
2395
 
2240
2396
  // src/lib/templatesData.ts
2241
- var TEMPLATES_VERSION = "0.1.9", TEMPLATE_FILES;
2397
+ var TEMPLATES_VERSION = "0.1.10", TEMPLATE_FILES;
2242
2398
  var init_templatesData = __esm(() => {
2243
2399
  TEMPLATE_FILES = {
2400
+ "_assets/language/README.language.md": "## {{displayName}} language support\n\nThis extension was scaffolded with `vsceasy create --type language`. It provides\neditor support for `.{{langId}}` files:\n\n- **Syntax highlighting** — TextMate grammar in `syntaxes/{{langId}}.tmLanguage.json`\n- **Language configuration** — comments, brackets, auto-closing pairs, folding in\n `language-configuration.json`\n- **Snippets** — `snippets/{{langId}}.json`\n- **File icon** (opt-in) — `fileicons/{{langId}}-icon-theme.json`\n\n### How contributions are wired\n\n`vsceasy`'s `scripts/gen.ts` regenerates the generated parts of\n`package.json#contributes` (commands, views…) on every build. Language\ncontributions are **not** generated — they live in **`contributes.extra.json`**\nat the project root and are deep-merged into `package.json` by `gen.ts`. Edit\n`contributes.extra.json` to change languages / grammars / snippets / iconThemes,\nthen run `bun run gen`.\n\n### File icon is opt-in\n\nVS Code file icons are provided by an **icon theme**, which is global: activating\nit replaces *all* file icons in the workbench, not just `.{{langId}}`. This\nextension ships a `{{displayName}} Icons` theme but does **not** force it. To use\nit: `Preferences: File Icon Theme` → pick `{{displayName}} Icons`. If you don't\nwant to override every icon, leave it unselected — highlighting, config and\nsnippets work regardless.\n\n### Develop\n\n```sh\nbun install\nbun run gen # sync package.json#contributes\nbun run package # build a .vsix\n```\n\nPress `F5` (or run `bun run launch`) to open an Extension Development Host and\nopen a `.{{langId}}` file to see highlighting.\n",
2401
+ "_assets/language/contributes.extra.json": `{
2402
+ "languages": [
2403
+ {
2404
+ "id": "{{langId}}",
2405
+ "aliases": ["{{displayName}}", "{{langId}}"],
2406
+ "extensions": [".{{langId}}"],
2407
+ "configuration": "./language-configuration.json"
2408
+ }
2409
+ ],
2410
+ "grammars": [
2411
+ {
2412
+ "language": "{{langId}}",
2413
+ "scopeName": "{{scopeName}}",
2414
+ "path": "./syntaxes/{{langId}}.tmLanguage.json"
2415
+ }
2416
+ ],
2417
+ "snippets": [
2418
+ {
2419
+ "language": "{{langId}}",
2420
+ "path": "./snippets/{{langId}}.json"
2421
+ }
2422
+ ],
2423
+ "iconThemes": [
2424
+ {
2425
+ "id": "{{langId}}-icons",
2426
+ "label": "{{displayName}} Icons",
2427
+ "path": "./fileicons/{{langId}}-icon-theme.json"
2428
+ }
2429
+ ],
2430
+ "configuration": {
2431
+ "title": "{{displayName}}",
2432
+ "properties": {
2433
+ "{{commandPrefix}}.colorize": {
2434
+ "type": "boolean",
2435
+ "default": true,
2436
+ "markdownDescription": "Automatically apply distinct section colors to \`.{{langId}}\` files (scoped to \`{{scopeName}}\`, so other languages are unaffected). Turn off to keep your theme's default colors."
2437
+ }
2438
+ }
2439
+ }
2440
+ }
2441
+ `,
2442
+ "_assets/language/fileicons/{{langId}}-icon-theme.json": `{
2443
+ "iconDefinitions": {
2444
+ "{{langId}}_file": {
2445
+ "iconPath": "../icons/{{langId}}.svg"
2446
+ }
2447
+ },
2448
+ "languageIds": {
2449
+ "{{langId}}": "{{langId}}_file"
2450
+ },
2451
+ "fileExtensions": {
2452
+ "{{langId}}": "{{langId}}_file"
2453
+ }
2454
+ }
2455
+ `,
2456
+ "_assets/language/icons/{{langId}}.svg": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2457
+ <path fill="#8a8a8a" d="M6 3h14l6 6v20a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/>
2458
+ <path fill="#5a5a5a" d="M20 3l6 6h-6z"/>
2459
+ <text x="16" y="22" font-family="monospace" font-size="9" font-weight="bold" fill="#fff" text-anchor="middle">{{langExt}}</text>
2460
+ </svg>
2461
+ `,
2462
+ "_assets/language/language-configuration.json": `{
2463
+ "comments": {
2464
+ "lineComment": "#"
2465
+ },
2466
+ "brackets": [
2467
+ ["{", "}"],
2468
+ ["[", "]"],
2469
+ ["(", ")"]
2470
+ ],
2471
+ "autoClosingPairs": [
2472
+ { "open": "{", "close": "}" },
2473
+ { "open": "[", "close": "]" },
2474
+ { "open": "(", "close": ")" },
2475
+ { "open": "\\"", "close": "\\"", "notIn": ["string"] },
2476
+ { "open": "'", "close": "'", "notIn": ["string"] }
2477
+ ],
2478
+ "surroundingPairs": [
2479
+ ["{", "}"],
2480
+ ["[", "]"],
2481
+ ["(", ")"],
2482
+ ["\\"", "\\""],
2483
+ ["'", "'"]
2484
+ ],
2485
+ "folding": {
2486
+ "markers": {
2487
+ "start": "^\\\\s*#\\\\s*region\\\\b",
2488
+ "end": "^\\\\s*#\\\\s*endregion\\\\b"
2489
+ }
2490
+ }
2491
+ }
2492
+ `,
2493
+ "_assets/language/snippets/{{langId}}.json": `{
2494
+ "Section": {
2495
+ "prefix": "section",
2496
+ "body": ["[\${1:name}]", "$0"],
2497
+ "description": "A {{displayName}} section"
2498
+ },
2499
+ "Key/Value": {
2500
+ "prefix": "kv",
2501
+ "body": ["\${1:key} = \${2:value}"],
2502
+ "description": "A key/value pair"
2503
+ }
2504
+ }
2505
+ `,
2506
+ "_assets/language/src/colorize.ts": `import type * as vscode from 'vscode';
2507
+ import { applyTokenColors, removeTokenColors, type TokenColorRule } from './helpers/colorize';
2508
+
2509
+ /** Root TextMate scope of this language — rules are applied only to these files. */
2510
+ export const SCOPE = '{{scopeName}}';
2511
+
2512
+ /**
2513
+ * Default token colors for {{displayName}}, emphasizing each construct. Edit
2514
+ * freely — they are applied to \`[{{scopeName}}]\` only, so other languages keep
2515
+ * the user's theme. Scope names must match your grammar
2516
+ * (syntaxes/{{langId}}.tmLanguage.json).
2517
+ */
2518
+ export const RULES: TokenColorRule[] = [
2519
+ { scope: 'comment.line.number-sign.{{langId}}', settings: { foreground: '#6b7a6e', fontStyle: 'italic' } },
2520
+ { scope: 'string.quoted.double.basic.{{langId}}', settings: { foreground: '#98c379' } },
2521
+ { scope: 'string.quoted.single.literal.{{langId}}', settings: { foreground: '#98c379' } },
2522
+ { scope: 'constant.numeric.{{langId}}', settings: { foreground: '#d19a66' } },
2523
+ ];
2524
+
2525
+ export async function applyColors(vscodeNs: typeof vscode): Promise<void> {
2526
+ await applyTokenColors(SCOPE, RULES);
2527
+ }
2528
+
2529
+ export async function removeColors(vscodeNs: typeof vscode): Promise<void> {
2530
+ await removeTokenColors(SCOPE);
2531
+ }
2532
+
2533
+ /** True when the user has opted in (default) to automatic coloring. */
2534
+ export function colorizeEnabled(vscodeNs: typeof vscode): boolean {
2535
+ return vscodeNs.workspace.getConfiguration('{{commandPrefix}}').get<boolean>('colorize', true);
2536
+ }
2537
+ `,
2538
+ "_assets/language/src/commands/applyColors.ts": `import { defineCommand } from '../shared/vsceasy';
2539
+ import { applyColors } from '../colorize';
2540
+
2541
+ export default defineCommand({
2542
+ id: 'applyColors',
2543
+ title: '{{displayName}}: Apply Colors',
2544
+ run: async (vscode) => {
2545
+ await applyColors(vscode);
2546
+ vscode.window.showInformationMessage('{{displayName}} colors applied.');
2547
+ },
2548
+ });
2549
+ `,
2550
+ "_assets/language/src/commands/removeColors.ts": `import { defineCommand } from '../shared/vsceasy';
2551
+ import { removeColors } from '../colorize';
2552
+
2553
+ export default defineCommand({
2554
+ id: 'removeColors',
2555
+ title: '{{displayName}}: Remove Colors',
2556
+ run: async (vscode) => {
2557
+ await removeColors(vscode);
2558
+ vscode.window.showInformationMessage('{{displayName}} colors removed.');
2559
+ },
2560
+ });
2561
+ `,
2562
+ "_assets/language/src/extension/extension.ts": `import { bootstrap } from '../shared/vsceasy';
2563
+ import { registry } from './_registry';
2564
+ import { applyColors, removeColors, colorizeEnabled } from '../colorize';
2565
+
2566
+ export const activate = bootstrap(registry, {
2567
+ onActivate: [
2568
+ async (context, vscode) => {
2569
+ // Auto-apply scoped token colors on activate when opted in (default).
2570
+ // Scoped to {{scopeName}} only — other languages are untouched.
2571
+ if (colorizeEnabled(vscode)) {
2572
+ await applyColors(vscode);
2573
+ }
2574
+ // React to the user toggling \`{{commandPrefix}}.colorize\` at runtime.
2575
+ context.subscriptions.push(
2576
+ vscode.workspace.onDidChangeConfiguration(async (e) => {
2577
+ if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
2578
+ if (colorizeEnabled(vscode)) await applyColors(vscode);
2579
+ else await removeColors(vscode);
2580
+ }),
2581
+ );
2582
+ },
2583
+ ],
2584
+ });
2585
+
2586
+ export function deactivate() {}
2587
+ `,
2588
+ "_assets/language/src/helpers/colorize.ts": `import * as vscode from 'vscode';
2589
+
2590
+ /**
2591
+ * Apply theme-independent token colors to a single TextMate scope (e.g. a
2592
+ * language's root scope like \`{{scopeName}}\`), written to the user's
2593
+ * \`editor.tokenColorCustomizations\`. Because the rules are keyed by
2594
+ * \`[<scope>]\`, only files in that scope are recolored — every other language
2595
+ * keeps the active theme's colors.
2596
+ *
2597
+ * Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
2598
+ * the ones this extension added, preserving any the user wrote by hand.
2599
+ */
2600
+
2601
+ export interface TokenColorRule {
2602
+ /** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
2603
+ scope: string;
2604
+ settings: { foreground?: string; background?: string; fontStyle?: string };
2605
+ }
2606
+
2607
+ type TaggedRule = TokenColorRule & { [MARK]?: true };
2608
+
2609
+ /** Marker key identifying rules this extension wrote (vs. the user's own). */
2610
+ const MARK = '{{commandPrefix}}Colorize';
2611
+ const SECTION = 'editor.tokenColorCustomizations';
2612
+
2613
+ const blockKey = (scope: string) => \`[\${scope}]\`;
2614
+
2615
+ /**
2616
+ * Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
2617
+ * preserving the user's own rules and other scope keys. Idempotent — re-applying
2618
+ * replaces only previously-applied rules from this extension.
2619
+ */
2620
+ export async function applyTokenColors(
2621
+ scope: string,
2622
+ rules: TokenColorRule[],
2623
+ target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
2624
+ ): Promise<void> {
2625
+ const cfg = vscode.workspace.getConfiguration();
2626
+ const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
2627
+ const key = blockKey(scope);
2628
+ const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
2629
+ const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
2630
+ const userRules = existing.filter((r) => !r[MARK]);
2631
+ const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
2632
+ const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
2633
+ await cfg.update(SECTION, next, target);
2634
+ }
2635
+
2636
+ /** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
2637
+ export async function removeTokenColors(
2638
+ scope: string,
2639
+ target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
2640
+ ): Promise<void> {
2641
+ const cfg = vscode.workspace.getConfiguration();
2642
+ const current = cfg.get<Record<string, any>>(SECTION);
2643
+ const key = blockKey(scope);
2644
+ if (!current || !current[key]) return;
2645
+ const block = current[key] as { textMateRules?: TaggedRule[] };
2646
+ const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
2647
+
2648
+ const nextBlock: Record<string, unknown> = { ...block };
2649
+ if (userRules.length) nextBlock.textMateRules = userRules;
2650
+ else delete nextBlock.textMateRules;
2651
+
2652
+ const next = { ...current };
2653
+ if (Object.keys(nextBlock).length) next[key] = nextBlock;
2654
+ else delete next[key];
2655
+
2656
+ await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
2657
+ }
2658
+ `,
2659
+ "_assets/language/syntaxes/{{langId}}.tmLanguage.json": `{
2660
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
2661
+ "name": "{{displayName}}",
2662
+ "scopeName": "{{scopeName}}",
2663
+ "patterns": [
2664
+ { "include": "#comments" },
2665
+ { "include": "#strings" },
2666
+ { "include": "#numbers" }
2667
+ ],
2668
+ "repository": {
2669
+ "comments": {
2670
+ "patterns": [
2671
+ {
2672
+ "name": "comment.line.number-sign.{{langId}}",
2673
+ "match": "#.*$"
2674
+ }
2675
+ ]
2676
+ },
2677
+ "strings": {
2678
+ "patterns": [
2679
+ {
2680
+ "name": "string.quoted.double.{{langId}}",
2681
+ "begin": "\\"",
2682
+ "end": "\\"",
2683
+ "patterns": [
2684
+ { "name": "constant.character.escape.{{langId}}", "match": "\\\\\\\\." }
2685
+ ]
2686
+ },
2687
+ {
2688
+ "name": "string.quoted.single.{{langId}}",
2689
+ "begin": "'",
2690
+ "end": "'"
2691
+ }
2692
+ ]
2693
+ },
2694
+ "numbers": {
2695
+ "patterns": [
2696
+ {
2697
+ "name": "constant.numeric.{{langId}}",
2698
+ "match": "\\\\b[0-9]+(\\\\.[0-9]+)?\\\\b"
2699
+ }
2700
+ ]
2701
+ }
2702
+ }
2703
+ }
2704
+ `,
2244
2705
  "_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
2245
2706
 
2246
2707
  export default defineCommand({
@@ -2848,6 +3309,92 @@ export function createCache<V = unknown>(opts: CacheOptions = {}): Cache<V> {
2848
3309
 
2849
3310
  return cache;
2850
3311
  }
3312
+ `,
3313
+ "_generators/helper/colorize.ts.tpl": `import * as vscode from 'vscode';
3314
+
3315
+ /**
3316
+ * Apply theme-independent token colors to a single TextMate scope (e.g. a
3317
+ * language's root scope like \`source.toml\`), written to the user's
3318
+ * \`editor.tokenColorCustomizations\`. Because the rules are keyed by
3319
+ * \`[<scope>]\`, only files in that scope are recolored — every other language
3320
+ * keeps the active theme's colors.
3321
+ *
3322
+ * Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
3323
+ * the ones this extension added, preserving any the user wrote by hand.
3324
+ *
3325
+ * Typical use — auto-apply on activate behind an opt-out setting:
3326
+ *
3327
+ * // extension.ts (onActivate hook)
3328
+ * if (config.get<boolean>('colorize', true)) {
3329
+ * await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
3330
+ * }
3331
+ * vscode.workspace.onDidChangeConfiguration(async (e) => {
3332
+ * if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
3333
+ * if (config.get<boolean>('colorize', true)) await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
3334
+ * else await removeTokenColors('source.{{commandPrefix}}');
3335
+ * });
3336
+ *
3337
+ * Declare the opt-out in package.json#contributes.configuration:
3338
+ * "{{commandPrefix}}.colorize": { "type": "boolean", "default": true }
3339
+ */
3340
+
3341
+ export interface TokenColorRule {
3342
+ /** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
3343
+ scope: string;
3344
+ settings: { foreground?: string; background?: string; fontStyle?: string };
3345
+ }
3346
+
3347
+ type TaggedRule = TokenColorRule & { [MARK]?: true };
3348
+
3349
+ /** Marker key identifying rules this extension wrote (vs. the user's own). */
3350
+ const MARK = '{{commandPrefix}}Colorize';
3351
+ const SECTION = 'editor.tokenColorCustomizations';
3352
+
3353
+ const blockKey = (scope: string) => \`[\${scope}]\`;
3354
+
3355
+ /**
3356
+ * Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
3357
+ * preserving the user's own rules and other scope keys. Idempotent — re-applying
3358
+ * replaces only previously-applied rules from this extension.
3359
+ */
3360
+ export async function applyTokenColors(
3361
+ scope: string,
3362
+ rules: TokenColorRule[],
3363
+ target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
3364
+ ): Promise<void> {
3365
+ const cfg = vscode.workspace.getConfiguration();
3366
+ const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
3367
+ const key = blockKey(scope);
3368
+ const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
3369
+ const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
3370
+ const userRules = existing.filter((r) => !r[MARK]);
3371
+ const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
3372
+ const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
3373
+ await cfg.update(SECTION, next, target);
3374
+ }
3375
+
3376
+ /** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
3377
+ export async function removeTokenColors(
3378
+ scope: string,
3379
+ target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
3380
+ ): Promise<void> {
3381
+ const cfg = vscode.workspace.getConfiguration();
3382
+ const current = cfg.get<Record<string, any>>(SECTION);
3383
+ const key = blockKey(scope);
3384
+ if (!current || !current[key]) return;
3385
+ const block = current[key] as { textMateRules?: TaggedRule[] };
3386
+ const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
3387
+
3388
+ const nextBlock: Record<string, unknown> = { ...block };
3389
+ if (userRules.length) nextBlock.textMateRules = userRules;
3390
+ else delete nextBlock.textMateRules;
3391
+
3392
+ const next = { ...current };
3393
+ if (Object.keys(nextBlock).length) next[key] = nextBlock;
3394
+ else delete next[key];
3395
+
3396
+ await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
3397
+ }
2851
3398
  `,
2852
3399
  "_generators/helper/config.ts.tpl": `import * as vscode from 'vscode';
2853
3400
 
@@ -4166,6 +4713,12 @@ bun run package # → {{name}}-0.0.1.vsix
4166
4713
  "react/scripts/gen.ts": `#!/usr/bin/env bun
4167
4714
  // Scans src/panels, src/commands, and src/menus; writes src/extension/_registry.ts
4168
4715
  // and syncs package.json#contributes (commands, viewsContainers, views).
4716
+ //
4717
+ // Non-generated contributes (e.g. languages, grammars, snippets, themes,
4718
+ // iconThemes, walkthroughs) go in an optional \`contributes.extra.json\` at the
4719
+ // project root. It is deep-merged into package.json#contributes on every run —
4720
+ // the keys gen.ts owns (commands, keybindings, menus.commandPalette,
4721
+ // viewsContainers, views) always win, everything else from extra is preserved.
4169
4722
 
4170
4723
  import * as fs from 'fs';
4171
4724
  import * as path from 'path';
@@ -4181,6 +4734,10 @@ const TREE_VIEWS_DIR = path.join(SRC, 'treeViews');
4181
4734
  const JOBS_DIR = path.join(SRC, 'jobs');
4182
4735
  const OUT = path.join(SRC, 'extension', '_registry.ts');
4183
4736
  const PKG_PATH = path.join(ROOT, 'package.json');
4737
+ const EXTRA_PATH = path.join(ROOT, 'contributes.extra.json');
4738
+
4739
+ /** Keys gen.ts owns — never overridden by contributes.extra.json. */
4740
+ const GEN_OWNED_KEYS = new Set(['commands', 'keybindings', 'viewsContainers', 'views']);
4184
4741
 
4185
4742
  interface Discovered {
4186
4743
  id: string;
@@ -4369,9 +4926,54 @@ function syncPackageJson(
4369
4926
  delete contributes.views;
4370
4927
  }
4371
4928
 
4929
+ mergeExtraContributes(contributes);
4930
+
4372
4931
  fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\\n');
4373
4932
  }
4374
4933
 
4934
+ /**
4935
+ * Deep-merge the optional \`contributes.extra.json\` (project root) into the
4936
+ * package's \`contributes\`. Use for any contribution point gen.ts doesn't
4937
+ * generate — languages, grammars, snippets, themes, iconThemes, walkthroughs…
4938
+ *
4939
+ * Rules:
4940
+ * - gen-owned keys (commands, keybindings, viewsContainers, views) are ignored
4941
+ * if present in extra — gen.ts stays authoritative for those.
4942
+ * - plain objects merge recursively; arrays and primitives from extra replace.
4943
+ *
4944
+ * NOTE: this is an inline copy of src/lib/contributesMerge.ts in the vsceasy
4945
+ * source (the script must run standalone). Keep the two in sync.
4946
+ */
4947
+ function mergeExtraContributes(contributes: Record<string, any>) {
4948
+ if (!fs.existsSync(EXTRA_PATH)) return;
4949
+ let extra: Record<string, any>;
4950
+ try {
4951
+ extra = JSON.parse(fs.readFileSync(EXTRA_PATH, 'utf8'));
4952
+ } catch (err) {
4953
+ console.warn(\`! Skipping contributes.extra.json — invalid JSON: \${(err as Error).message}\`);
4954
+ return;
4955
+ }
4956
+ if (!extra || typeof extra !== 'object') return;
4957
+ for (const [key, value] of Object.entries(extra)) {
4958
+ if (GEN_OWNED_KEYS.has(key)) continue;
4959
+ contributes[key] = deepMerge(contributes[key], value);
4960
+ }
4961
+ }
4962
+
4963
+ function isPlainObject(v: unknown): v is Record<string, any> {
4964
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
4965
+ }
4966
+
4967
+ function deepMerge(base: any, override: any): any {
4968
+ if (isPlainObject(base) && isPlainObject(override)) {
4969
+ const out: Record<string, any> = { ...base };
4970
+ for (const [k, v] of Object.entries(override)) out[k] = deepMerge(base[k], v);
4971
+ return out;
4972
+ }
4973
+ // arrays and primitives: override wins
4974
+ return override;
4975
+ }
4976
+
4375
4977
  function loadDef(file: string): {
4376
4978
  id?: string;
4377
4979
  title?: string;
@@ -6537,7 +7139,7 @@ var init_add9 = __esm(() => {
6537
7139
  init_config();
6538
7140
  fs18 = __toESM(require("fs"));
6539
7141
  path18 = __toESM(require("path"));
6540
- HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache"];
7142
+ HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache", "colorize"];
6541
7143
  });
6542
7144
 
6543
7145
  // src/lib/job/add.ts