@luxkit/cli 1.0.8 → 1.1.1

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
@@ -4,7 +4,8 @@
4
4
  import { program } from "commander";
5
5
 
6
6
  // src/commands/fmt.ts
7
- import path4 from "path";
7
+ import fs3 from "fs";
8
+ import path5 from "path";
8
9
 
9
10
  // src/presets/fmt/electron-vue.ts
10
11
  var electronVueFmt = {
@@ -114,8 +115,8 @@ trim_trailing_whitespace = false
114
115
  ]
115
116
  },
116
117
  scripts: {
117
- lint: "eslint .",
118
- "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix',
118
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
119
+ "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
119
120
  format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
120
121
  "format:check": 'prettier --check "src/**/*.{ts,js,json,vue,css,scss}"',
121
122
  stylelint: 'stylelint "src/**/*.{css,scss,vue}"',
@@ -297,8 +298,8 @@ trim_trailing_whitespace = false
297
298
  ]
298
299
  },
299
300
  scripts: {
300
- lint: "eslint .",
301
- "lint:fix": 'eslint "src/**/*.{js,ts}" --fix',
301
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
302
+ "lint:fix": 'eslint "src/**/*.{js,ts}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
302
303
  format: 'prettier --write "src/**/*.{ts,js,json}"',
303
304
  "format:check": 'prettier --check "src/**/*.{ts,js,json}"',
304
305
  cspell: 'cspell --gitignore "src/**/*"',
@@ -416,8 +417,8 @@ trim_trailing_whitespace = false
416
417
  ]
417
418
  },
418
419
  scripts: {
419
- lint: "eslint .",
420
- "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix',
420
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
421
+ "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
421
422
  format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
422
423
  "format:check": 'prettier --check "src/**/*.{ts,js,json,vue,css,scss}"',
423
424
  stylelint: 'stylelint "src/**/*.{css,scss,vue}"',
@@ -431,6 +432,134 @@ trim_trailing_whitespace = false
431
432
  }
432
433
  };
433
434
 
435
+ // src/presets/fmt/web-react.ts
436
+ var webReactFmt = {
437
+ name: "web-react",
438
+ description: "React Web frontend (Vite + React + TypeScript)",
439
+ eslint: () => `import js from '@eslint/js'
440
+ import tseslint from 'typescript-eslint'
441
+ import reactHooks from 'eslint-plugin-react-hooks'
442
+ import reactRefresh from 'eslint-plugin-react-refresh'
443
+ import globals from 'globals'
444
+ import prettierConfig from 'eslint-config-prettier'
445
+
446
+ export default tseslint.config(
447
+ { ignores: ['dist'] },
448
+ {
449
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
450
+ files: ['**/*.{ts,tsx}'],
451
+ languageOptions: {
452
+ ecmaVersion: 2020,
453
+ globals: globals.browser,
454
+ },
455
+ plugins: {
456
+ 'react-hooks': reactHooks,
457
+ 'react-refresh': reactRefresh,
458
+ },
459
+ rules: {
460
+ ...reactHooks.configs.recommended.rules,
461
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
462
+ '@typescript-eslint/no-explicit-any': 'warn',
463
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
464
+ },
465
+ },
466
+ prettierConfig,
467
+ )
468
+ `,
469
+ prettier: () => JSON.stringify(
470
+ {
471
+ semi: false,
472
+ singleQuote: true,
473
+ tabWidth: 2,
474
+ trailingComma: "all",
475
+ printWidth: 100,
476
+ endOfLine: "lf"
477
+ },
478
+ null,
479
+ 2
480
+ ) + "\n",
481
+ prettierIgnore: () => `node_modules/
482
+ <lockfile>
483
+ dist/
484
+ coverage/
485
+ `,
486
+ stylelint: () => `export default {
487
+ plugins: ['stylelint-order', '@stylistic/stylelint-plugin'],
488
+ extends: [
489
+ 'stylelint-config-standard-scss',
490
+ 'stylelint-config-recess-order',
491
+ ],
492
+ rules: {
493
+ 'selector-class-pattern': null,
494
+ 'scss/dollar-variable-pattern': null,
495
+ 'scss/percent-placeholder-pattern': null,
496
+ 'scss/at-mixin-pattern': null,
497
+ 'order/properties-order': null,
498
+ },
499
+ }
500
+ `,
501
+ stylelintIgnore: () => `node_modules/
502
+ dist/
503
+ `,
504
+ cspell: () => JSON.stringify(
505
+ {
506
+ $schema: "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
507
+ version: "0.2",
508
+ language: "en,en-US",
509
+ allowCompoundWords: true,
510
+ words: ["vite", "react", "zustand", "tanstack"]
511
+ },
512
+ null,
513
+ 2
514
+ ) + "\n",
515
+ editorconfig: () => `root = true
516
+
517
+ [*]
518
+ charset = utf-8
519
+ indent_style = space
520
+ indent_size = 2
521
+ end_of_line = lf
522
+ insert_final_newline = true
523
+ trim_trailing_whitespace = true
524
+
525
+ [*.md]
526
+ trim_trailing_whitespace = false
527
+ `,
528
+ dependencies: {
529
+ dev: [
530
+ "eslint",
531
+ "@eslint/js",
532
+ "typescript-eslint",
533
+ "eslint-plugin-react-hooks",
534
+ "eslint-plugin-react-refresh",
535
+ "eslint-config-prettier",
536
+ "globals",
537
+ "prettier",
538
+ "stylelint",
539
+ "stylelint-config-standard-scss",
540
+ "stylelint-order",
541
+ "stylelint-scss",
542
+ "@stylistic/stylelint-plugin",
543
+ "postcss-scss",
544
+ "cspell"
545
+ ]
546
+ },
547
+ scripts: {
548
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
549
+ "lint:fix": 'eslint "src/**/*.{js,ts,jsx,tsx}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
550
+ format: 'prettier --write "src/**/*.{ts,js,json,jsx,tsx,css,scss}"',
551
+ "format:check": 'prettier --check "src/**/*.{ts,js,json,jsx,tsx,css,scss}"',
552
+ stylelint: 'stylelint "src/**/*.{css,scss}"',
553
+ "stylelint:fix": 'stylelint "src/**/*.{css,scss}" --fix',
554
+ cspell: 'cspell --gitignore "src/**/*"',
555
+ "type:check": "tsc --noEmit",
556
+ "code:check": "<pm> lint && <pm> format:check",
557
+ "code:fix": "<pm> lint:fix && <pm> format",
558
+ "code:check:all": "<pm> lint && <pm> format:check && <pm> stylelint && <pm> cspell",
559
+ "code:fix:all": "<pm> lint:fix && <pm> format && <pm> stylelint:fix"
560
+ }
561
+ };
562
+
434
563
  // src/presets/fmt/web-vue.ts
435
564
  var webVueFmt = {
436
565
  name: "web-vue",
@@ -535,8 +664,8 @@ trim_trailing_whitespace = false
535
664
  ]
536
665
  },
537
666
  scripts: {
538
- lint: "eslint .",
539
- "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix',
667
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
668
+ "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
540
669
  format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
541
670
  "format:check": 'prettier --check "src/**/*.{ts,js,json,vue,css,scss}"',
542
671
  stylelint: 'stylelint "src/**/*.{css,scss,vue}"',
@@ -551,7 +680,14 @@ trim_trailing_whitespace = false
551
680
  };
552
681
 
553
682
  // src/presets/fmt/index.ts
554
- var FMT_PRESETS = [webVueFmt, electronVueFmt, uniappFmt, nodeFmt, nestFmt];
683
+ var FMT_PRESETS = [
684
+ webVueFmt,
685
+ webReactFmt,
686
+ electronVueFmt,
687
+ uniappFmt,
688
+ nodeFmt,
689
+ nestFmt
690
+ ];
555
691
 
556
692
  // src/utils/logger.ts
557
693
  import chalk from "chalk";
@@ -667,7 +803,10 @@ function readJson(filePath) {
667
803
  try {
668
804
  const raw = fs.readFileSync(filePath, "utf-8");
669
805
  return JSON.parse(raw);
670
- } catch {
806
+ } catch (error) {
807
+ if (error.code === "ENOENT") return null;
808
+ const message = error instanceof Error ? error.message : String(error);
809
+ logger.error(`Failed to read or parse ${path.basename(filePath)}: ${message}`);
671
810
  return null;
672
811
  }
673
812
  }
@@ -693,13 +832,20 @@ function generateConfigFile(preset, filename, content, opts) {
693
832
  if (action === "skip") return "skipped";
694
833
  if (opts.dryRun) return exists ? "overwritten" : "created";
695
834
  const resolved = opts.lockfile ? content.replace(/<lockfile>/g, opts.lockfile) : content.replace(/<lockfile>\n?/g, "");
696
- writeFile(filepath, resolved);
835
+ try {
836
+ writeFile(filepath, resolved);
837
+ } catch (error) {
838
+ const message = error instanceof Error ? error.message : String(error);
839
+ logger.error(`Failed to write ${filename}: ${message}`);
840
+ return null;
841
+ }
697
842
  return exists ? "overwritten" : "created";
698
843
  }
699
844
  function generateAllFmt(preset, opts) {
700
845
  const result = { created: [], overwritten: [], skipped: [] };
701
846
  for (const { filename, getContent } of CONFIG_FILES) {
702
847
  if (opts.noStylelint && filename.includes("stylelint")) continue;
848
+ if (opts.noEditorconfig && filename === ".editorconfig") continue;
703
849
  const content = getContent(preset);
704
850
  if (content === void 0) continue;
705
851
  const action = generateConfigFile(preset, filename, content, opts);
@@ -713,6 +859,29 @@ function generateAllFmt(preset, opts) {
713
859
  // src/utils/deps.ts
714
860
  import path3 from "path";
715
861
  import { spawn } from "child_process";
862
+
863
+ // src/utils/execFileNoThrow.ts
864
+ import { exec } from "child_process";
865
+ import { promisify } from "util";
866
+ var execAsync = promisify(exec);
867
+ async function execFileNoThrow(command, args, options) {
868
+ const cmdStr = args.length > 0 ? `${command} ${args.join(" ")}` : command;
869
+ try {
870
+ const { stdout, stderr } = await execAsync(cmdStr, {
871
+ cwd: options?.cwd
872
+ });
873
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
874
+ } catch (err) {
875
+ const error = err;
876
+ return {
877
+ stdout: (error.stdout ?? "").trim(),
878
+ stderr: (error.stderr ?? "").trim(),
879
+ exitCode: error.code === "ENOENT" ? null : 1
880
+ };
881
+ }
882
+ }
883
+
884
+ // src/utils/deps.ts
716
885
  function detectPackageManager(cwd) {
717
886
  if (fileExists(`${cwd}/bun.lockb`) || fileExists(`${cwd}/bun.lock`)) return "bun";
718
887
  if (fileExists(`${cwd}/pnpm-lock.yaml`)) return "pnpm";
@@ -743,6 +912,37 @@ function getRunPrefix(pm) {
743
912
  return "npm run";
744
913
  }
745
914
  }
915
+ async function fetchPackageVersion(pkg) {
916
+ const { stdout, exitCode } = await execFileNoThrow("npm", ["view", pkg, "version"]);
917
+ if (exitCode !== 0 || !stdout) {
918
+ throw new Error(`Failed to fetch version for "${pkg}" from npm registry.`);
919
+ }
920
+ const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
921
+ return lines[lines.length - 1].trim();
922
+ }
923
+ async function addDepsToManifest(packages, cwd) {
924
+ const pkgPath = path3.join(cwd, "package.json");
925
+ const pkg = readJson(pkgPath);
926
+ if (!pkg) {
927
+ throw new Error("package.json not found");
928
+ }
929
+ const devDeps = pkg.devDependencies ?? {};
930
+ const missing = packages.filter((p) => !devDeps[p]);
931
+ if (missing.length === 0) return [];
932
+ const results = await Promise.all(
933
+ missing.map(async (pkgName) => {
934
+ const version = await fetchPackageVersion(pkgName);
935
+ return { pkgName, version };
936
+ })
937
+ );
938
+ const updatedDevDeps = { ...devDeps };
939
+ for (const { pkgName, version } of results) {
940
+ updatedDevDeps[pkgName] = `^${version}`;
941
+ }
942
+ pkg.devDependencies = updatedDevDeps;
943
+ writeJson(pkgPath, pkg);
944
+ return results.map((r) => r.pkgName);
945
+ }
746
946
  async function installDevDeps(packages, cwd, pm) {
747
947
  const manager = pm ?? detectPackageManager(cwd);
748
948
  const pkg = readJson(path3.join(cwd, "package.json"));
@@ -780,6 +980,347 @@ async function installDevDeps(packages, cwd, pm) {
780
980
  }
781
981
  }
782
982
 
983
+ // src/core/local-preset.ts
984
+ import fs2 from "fs";
985
+ import os from "os";
986
+ import path4 from "path";
987
+
988
+ // src/core/merge-settings.ts
989
+ var USER_PRIORITY_KEYS = /* @__PURE__ */ new Set([
990
+ // Cursor/animation
991
+ "editor.cursorBlinking",
992
+ "editor.cursorSmoothCaretAnimation",
993
+ "editor.renderWhitespace",
994
+ "editor.guides.indentation",
995
+ "editor.largeFileOptimizations",
996
+ // Theme/appearance
997
+ "workbench.iconTheme",
998
+ "workbench.colorTheme",
999
+ // Suggestions
1000
+ "editor.inlineSuggest.enabled",
1001
+ "editor.suggestSelection",
1002
+ "editor.acceptSuggestionOnEnter",
1003
+ "editor.bracketPairColorization.enabled",
1004
+ "editor.autoClosingBrackets",
1005
+ "editor.autoClosingOvertype"
1006
+ ]);
1007
+ function mergeVscodeSettings(preset, existing) {
1008
+ const result = { ...existing };
1009
+ for (const [key, presetVal] of Object.entries(preset)) {
1010
+ const existingVal = existing[key];
1011
+ if (existingVal === void 0) {
1012
+ result[key] = presetVal;
1013
+ continue;
1014
+ }
1015
+ if (USER_PRIORITY_KEYS.has(key)) {
1016
+ continue;
1017
+ }
1018
+ if (isPlainObject(presetVal) && isPlainObject(existingVal)) {
1019
+ result[key] = mergeVscodeSettings(
1020
+ presetVal,
1021
+ existingVal
1022
+ );
1023
+ continue;
1024
+ }
1025
+ result[key] = presetVal;
1026
+ }
1027
+ return result;
1028
+ }
1029
+ function isPlainObject(val) {
1030
+ return typeof val === "object" && val !== null && !Array.isArray(val);
1031
+ }
1032
+
1033
+ // src/core/local-preset.ts
1034
+ var CONFIG_GETTERS = [
1035
+ { filename: "eslint.config.mjs", getContent: (p) => p.eslint?.() },
1036
+ { filename: ".prettierrc", getContent: (p) => p.prettier?.() },
1037
+ { filename: ".prettierignore", getContent: (p) => p.prettierIgnore?.() },
1038
+ { filename: "stylelint.config.mjs", getContent: (p) => p.stylelint?.() },
1039
+ { filename: ".stylelintignore", getContent: (p) => p.stylelintIgnore?.() },
1040
+ { filename: "cspell.json", getContent: (p) => p.cspell?.() },
1041
+ { filename: ".editorconfig", getContent: (p) => p.editorconfig?.() }
1042
+ ];
1043
+ var STYLELINT_FILES = /* @__PURE__ */ new Set(["stylelint.config.mjs", ".stylelintignore"]);
1044
+ var EDITORCONFIG_FILE = ".editorconfig";
1045
+ var STYLELINT_SETTINGS_PREFIXES = [
1046
+ "stylelint.",
1047
+ "css.validate",
1048
+ "less.validate",
1049
+ "scss.validate"
1050
+ ];
1051
+ var STYLELINT_DEPS = /* @__PURE__ */ new Set([
1052
+ "stylelint",
1053
+ "stylelint-config-standard-scss",
1054
+ "stylelint-order",
1055
+ "stylelint-scss",
1056
+ "@stylistic/stylelint-plugin",
1057
+ "postcss-html",
1058
+ "postcss-scss"
1059
+ ]);
1060
+ var STYLELINT_EXTENSION = "stylelint.vscode-stylelint";
1061
+ function getLuxDir() {
1062
+ return process.env.LUX_HOME || path4.join(os.homedir(), ".lux");
1063
+ }
1064
+ function getLocalPresetDir(type, presetName) {
1065
+ if (!isValidPresetName(presetName)) {
1066
+ throw new Error(`Invalid preset name: "${presetName}"`);
1067
+ }
1068
+ return path4.join(getLuxDir(), "preset", type, presetName);
1069
+ }
1070
+ function isValidPresetName(name) {
1071
+ return name.length > 0 && !name.includes("/") && !name.includes("\\") && !name.includes("..");
1072
+ }
1073
+ function localPresetExists(type, presetName) {
1074
+ const dir = getLocalPresetDir(type, presetName);
1075
+ return fs2.existsSync(dir);
1076
+ }
1077
+ function resetLocalPreset(type, presetName) {
1078
+ const dir = getLocalPresetDir(type, presetName);
1079
+ if (fs2.existsSync(dir)) {
1080
+ fs2.rmSync(dir, { recursive: true, force: true });
1081
+ logger.log(`Reset local preset: ${dir}`);
1082
+ }
1083
+ }
1084
+ function materializeFmtPreset(presetName, preset, opts) {
1085
+ if (opts.dryRun) {
1086
+ logger.log("[dry-run] Would materialize local preset to ~/.lux/preset/fmt/" + presetName);
1087
+ return;
1088
+ }
1089
+ const presetDir = getLocalPresetDir("fmt", presetName);
1090
+ ensureDir(presetDir);
1091
+ for (const { filename, getContent } of CONFIG_GETTERS) {
1092
+ const content = getContent(preset);
1093
+ if (content === void 0) continue;
1094
+ const resolved = opts.lockfile ? content.replace(/<lockfile>/g, opts.lockfile) : content.replace(/<lockfile>\n?/g, "");
1095
+ writeFile(path4.join(presetDir, filename), resolved);
1096
+ }
1097
+ const templatePkg = buildTemplatePackageJson(preset);
1098
+ writeJson(path4.join(presetDir, "package.json"), templatePkg);
1099
+ logger.log(`Local preset created at ${presetDir}`);
1100
+ }
1101
+ function materializeVscodePreset(cwd, presetName) {
1102
+ const presetDir = getLocalPresetDir("vscode", presetName);
1103
+ ensureDir(presetDir);
1104
+ const settingsSrc = path4.join(cwd, ".vscode", "settings.json");
1105
+ if (fileExists(settingsSrc)) {
1106
+ const content = fs2.readFileSync(settingsSrc, "utf-8");
1107
+ writeFile(path4.join(presetDir, "settings.json"), content);
1108
+ }
1109
+ const extensionsSrc = path4.join(cwd, ".vscode", "extensions.json");
1110
+ if (fileExists(extensionsSrc)) {
1111
+ const content = fs2.readFileSync(extensionsSrc, "utf-8");
1112
+ writeFile(path4.join(presetDir, "extensions.json"), content);
1113
+ }
1114
+ logger.log(`Local preset created at ${presetDir}`);
1115
+ }
1116
+ var InvalidPackageJsonError = class extends Error {
1117
+ constructor(filePath) {
1118
+ super(`package.json exists but is not valid JSON: ${filePath}`);
1119
+ this.filePath = filePath;
1120
+ }
1121
+ filePath;
1122
+ };
1123
+ function applyLocalFmtPreset(cwd, presetName, opts) {
1124
+ const result = {
1125
+ created: [],
1126
+ overwritten: [],
1127
+ skipped: [],
1128
+ scriptsAdded: 0,
1129
+ scriptsSkipped: 0
1130
+ };
1131
+ const presetDir = getLocalPresetDir("fmt", presetName);
1132
+ if (!fs2.existsSync(presetDir)) {
1133
+ logger.warn(`Local preset not found at ${presetDir}`);
1134
+ return result;
1135
+ }
1136
+ const projectPkgPath = path4.join(cwd, "package.json");
1137
+ if (fileExists(projectPkgPath)) {
1138
+ try {
1139
+ JSON.parse(fs2.readFileSync(projectPkgPath, "utf-8"));
1140
+ } catch {
1141
+ throw new InvalidPackageJsonError(projectPkgPath);
1142
+ }
1143
+ }
1144
+ const entries = fs2.readdirSync(presetDir).filter((name) => name !== "package.json" && fs2.statSync(path4.join(presetDir, name)).isFile());
1145
+ for (const filename of entries) {
1146
+ if (opts.noStylelint && STYLELINT_FILES.has(filename)) continue;
1147
+ if (opts.noEditorconfig && filename === EDITORCONFIG_FILE) continue;
1148
+ const destPath = path4.join(cwd, filename);
1149
+ const exists = fileExists(destPath);
1150
+ if (exists && !opts.force) {
1151
+ result.skipped.push(filename);
1152
+ if (opts.dryRun) {
1153
+ logger.log(`[dry-run] Skipped ${filename} (already exists)`);
1154
+ }
1155
+ continue;
1156
+ }
1157
+ if (opts.dryRun) {
1158
+ (exists ? result.overwritten : result.created).push(filename);
1159
+ logger.log(`[dry-run] Would copy ${filename} from local preset`);
1160
+ continue;
1161
+ }
1162
+ const content = fs2.readFileSync(path4.join(presetDir, filename), "utf-8");
1163
+ writeFile(destPath, content);
1164
+ (exists ? result.overwritten : result.created).push(filename);
1165
+ }
1166
+ const templatePkg = readJson(path4.join(presetDir, "package.json"));
1167
+ const projectPkg = readJson(projectPkgPath);
1168
+ if (templatePkg && projectPkg) {
1169
+ const pm = fileExists(path4.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
1170
+ const merged = mergeTemplateIntoProject(templatePkg, projectPkg, pm, opts, result);
1171
+ if (!opts.dryRun) {
1172
+ writeJson(projectPkgPath, merged);
1173
+ }
1174
+ }
1175
+ return result;
1176
+ }
1177
+ function applyLocalVscodePreset(cwd, presetName, opts) {
1178
+ const result = {
1179
+ created: [],
1180
+ overwritten: [],
1181
+ skipped: [],
1182
+ scriptsAdded: 0,
1183
+ scriptsSkipped: 0
1184
+ };
1185
+ const presetDir = getLocalPresetDir("vscode", presetName);
1186
+ if (!fs2.existsSync(presetDir)) {
1187
+ logger.warn(`Local preset not found at ${presetDir}`);
1188
+ return result;
1189
+ }
1190
+ const settingsSrc = path4.join(presetDir, "settings.json");
1191
+ if (fileExists(settingsSrc)) {
1192
+ const presetSettings = readJson(settingsSrc);
1193
+ const filteredSettings = opts.noStylelint ? filterStylelintSettings(presetSettings ?? {}) : presetSettings;
1194
+ if (filteredSettings) {
1195
+ const settingsDest = path4.join(cwd, ".vscode", "settings.json");
1196
+ const existingSettings = readJson(settingsDest);
1197
+ if (existingSettings) {
1198
+ if (opts.dryRun) {
1199
+ result.overwritten.push(".vscode/settings.json");
1200
+ logger.log("[dry-run] Would merge .vscode/settings.json from local preset");
1201
+ } else {
1202
+ const merged = mergeVscodeSettings(filteredSettings, existingSettings);
1203
+ writeJson(settingsDest, merged);
1204
+ result.overwritten.push(".vscode/settings.json");
1205
+ }
1206
+ } else {
1207
+ if (opts.dryRun) {
1208
+ result.created.push(".vscode/settings.json");
1209
+ logger.log("[dry-run] Would create .vscode/settings.json from local preset");
1210
+ } else {
1211
+ writeJson(settingsDest, filteredSettings);
1212
+ result.created.push(".vscode/settings.json");
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+ const extensionsSrc = path4.join(presetDir, "extensions.json");
1218
+ if (fileExists(extensionsSrc)) {
1219
+ const extensionsData = readJson(extensionsSrc);
1220
+ if (extensionsData) {
1221
+ let presetRecommendations = extensionsData.recommendations ?? [];
1222
+ if (opts.noStylelint) {
1223
+ presetRecommendations = presetRecommendations.filter((ext) => ext !== STYLELINT_EXTENSION);
1224
+ }
1225
+ if (opts.dryRun) {
1226
+ result.created.push(".vscode/extensions.json");
1227
+ logger.log("[dry-run] Would create .vscode/extensions.json from local preset");
1228
+ } else {
1229
+ const extensionsDest = path4.join(cwd, ".vscode", "extensions.json");
1230
+ const existingExtensions = readJson(extensionsDest);
1231
+ const existingRecommendations = existingExtensions?.recommendations ?? [];
1232
+ const merged = [.../* @__PURE__ */ new Set([...existingRecommendations, ...presetRecommendations])];
1233
+ writeJson(extensionsDest, { recommendations: merged });
1234
+ result.created.push(".vscode/extensions.json");
1235
+ }
1236
+ }
1237
+ }
1238
+ return result;
1239
+ }
1240
+ function buildTemplatePackageJson(preset) {
1241
+ const deps = {};
1242
+ if (preset.dependencies?.dev) {
1243
+ for (const dep of preset.dependencies.dev) {
1244
+ deps[dep] = "<latest>";
1245
+ }
1246
+ }
1247
+ const scripts = preset.scripts ? { ...preset.scripts } : void 0;
1248
+ const result = {};
1249
+ if (Object.keys(deps).length > 0) {
1250
+ result.devDependencies = deps;
1251
+ }
1252
+ if (scripts && Object.keys(scripts).length > 0) {
1253
+ result.scripts = scripts;
1254
+ }
1255
+ return result;
1256
+ }
1257
+ function mergeTemplateIntoProject(templatePkg, projectPkg, pm, opts, result) {
1258
+ const merged = { ...projectPkg };
1259
+ const prefix = pm ? getRunPrefix(pm) : "";
1260
+ if (templatePkg.devDependencies) {
1261
+ const existingDeps = merged.devDependencies ?? {};
1262
+ const newDeps = { ...existingDeps };
1263
+ for (const [dep, version] of Object.entries(templatePkg.devDependencies)) {
1264
+ if (opts.noStylelint && STYLELINT_DEPS.has(dep)) continue;
1265
+ if (opts.noEditorconfig && dep.includes("editorconfig")) continue;
1266
+ if (existingDeps[dep] === void 0 && version !== "<latest>") {
1267
+ newDeps[dep] = version;
1268
+ }
1269
+ }
1270
+ merged.devDependencies = newDeps;
1271
+ }
1272
+ if (templatePkg.scripts) {
1273
+ const existingScripts = merged.scripts ?? {};
1274
+ const newScripts = { ...existingScripts };
1275
+ for (const [key, value] of Object.entries(templatePkg.scripts)) {
1276
+ if (opts.noStylelint && key.startsWith("stylelint")) continue;
1277
+ const resolved = value.replace(/<pm>/g, prefix);
1278
+ if (existingScripts[key] !== void 0 && !opts.force) {
1279
+ result.scriptsSkipped++;
1280
+ if (opts.dryRun) {
1281
+ logger.log(`[dry-run] Skipped script "${key}" (already exists)`);
1282
+ } else {
1283
+ logger.log(`Skipped script "${key}" (already exists)`);
1284
+ }
1285
+ continue;
1286
+ }
1287
+ if (opts.dryRun) {
1288
+ result.scriptsAdded++;
1289
+ logger.log(`[dry-run] Would add script "${key}"`);
1290
+ continue;
1291
+ }
1292
+ newScripts[key] = resolved;
1293
+ result.scriptsAdded++;
1294
+ }
1295
+ merged.scripts = newScripts;
1296
+ }
1297
+ return merged;
1298
+ }
1299
+ function filterStylelintSettings(settings) {
1300
+ const filtered = Object.fromEntries(
1301
+ Object.entries(settings).filter(
1302
+ ([key]) => !STYLELINT_SETTINGS_PREFIXES.some((prefix) => key.startsWith(prefix))
1303
+ )
1304
+ );
1305
+ if (typeof filtered["editor.codeActionsOnSave"] === "object" && filtered["editor.codeActionsOnSave"] !== null) {
1306
+ const actions = { ...filtered["editor.codeActionsOnSave"] };
1307
+ delete actions["source.fixAll.stylelint"];
1308
+ filtered["editor.codeActionsOnSave"] = actions;
1309
+ }
1310
+ return filtered;
1311
+ }
1312
+ function resolveLocalDeps(deps) {
1313
+ const packages = [];
1314
+ for (const [name, version] of Object.entries(deps)) {
1315
+ if (version === "<latest>") {
1316
+ packages.push(name);
1317
+ } else {
1318
+ packages.push(`${name}@${version}`);
1319
+ }
1320
+ }
1321
+ return packages;
1322
+ }
1323
+
783
1324
  // src/commands/fmt.ts
784
1325
  function filterStylelintScripts(scripts) {
785
1326
  const filtered = {};
@@ -794,52 +1335,35 @@ function isNotStylelintDep(dep) {
794
1335
  if (dep === "postcss-html" || dep === "postcss-scss") return false;
795
1336
  return true;
796
1337
  }
1338
+ function isNotEditorconfigDep(dep) {
1339
+ return !dep.includes("editorconfig");
1340
+ }
797
1341
  function registerFmtCommand(program2) {
798
1342
  const fmt = program2.command("fmt").description("Initialize formatting config with preset");
799
- fmt.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--no-install", "Skip dependency installation").option("--dry-run", "Preview without writing files").option("--no-stylelint", "Skip Stylelint config generation").action(
1343
+ fmt.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--no-install", "Skip dependency installation").option("--dry-run", "Preview without writing files").option("--stylelint", "Include Stylelint config generation").option("--editorconfig", "Include EditorConfig config generation").option("--reset", "Reset local preset and re-materialize from built-in").action(
800
1344
  async (presetName, options) => {
801
1345
  const preset = resolvePreset(FMT_PRESETS, presetName);
802
1346
  if (!preset) return;
803
1347
  const cwd = process.cwd();
804
- const pm = fileExists(path4.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
805
- const opts = {
806
- cwd,
807
- force: options.force ?? false,
808
- dryRun: options.dryRun ?? false,
809
- noStylelint: options.stylelint === false,
810
- lockfile: pm ? getLockfileName(pm) : void 0
811
- };
812
- const result = generateAllFmt(preset, opts);
813
- const allFiles = [...result.created, ...result.overwritten];
814
- if (allFiles.length === 0 && result.skipped.length === 0) {
815
- logger.warn("No files to generate for this preset");
816
- return;
1348
+ const pkgPath = path5.join(cwd, "package.json");
1349
+ if (fileExists(pkgPath)) {
1350
+ try {
1351
+ JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1352
+ } catch {
1353
+ logger.error(
1354
+ "package.json exists but is not valid JSON. Fix it first, then re-run this command."
1355
+ );
1356
+ return;
1357
+ }
817
1358
  }
818
- logGenerationResult(result, opts.dryRun);
819
- if (!pm) {
820
- warnMissingPackageJson(preset, options.install !== false);
821
- return;
1359
+ if (options.reset) {
1360
+ resetLocalPreset("fmt", presetName);
822
1361
  }
823
- const scripts = opts.noStylelint && preset.scripts ? filterStylelintScripts(preset.scripts) : preset.scripts;
824
- if (scripts) {
825
- await injectScripts(scripts, opts, pm);
826
- }
827
- if (!preset.dependencies?.dev) return;
828
- const devDeps = opts.noStylelint ? preset.dependencies.dev.filter(isNotStylelintDep) : preset.dependencies.dev;
829
- if (options.install === false) {
830
- logger.log(`Dependencies: ${devDeps.join(", ")}`);
831
- return;
832
- }
833
- if (opts.dryRun) {
834
- logger.log(`[dry-run] Would install: ${devDeps.join(", ")}`);
835
- return;
836
- }
837
- try {
838
- logger.log(`Installing dependencies with ${pm}...`);
839
- await installDevDeps(devDeps, cwd, pm);
840
- logger.success("Dependencies installed successfully");
841
- } catch {
842
- logger.warn("Dependency installation failed. You can install manually.");
1362
+ const useLocal = localPresetExists("fmt", presetName);
1363
+ if (useLocal) {
1364
+ await executeLocalPath(cwd, presetName, options);
1365
+ } else {
1366
+ await executeBuiltinPath(cwd, presetName, preset, options);
843
1367
  }
844
1368
  }
845
1369
  );
@@ -849,6 +1373,139 @@ function registerFmtCommand(program2) {
849
1373
  }
850
1374
  });
851
1375
  }
1376
+ async function executeLocalPath(cwd, presetName, options) {
1377
+ logger.log("Using local custom preset");
1378
+ const opts = {
1379
+ cwd,
1380
+ force: options.force ?? false,
1381
+ dryRun: options.dryRun ?? false,
1382
+ noStylelint: options.stylelint !== true,
1383
+ noEditorconfig: options.editorconfig !== true
1384
+ };
1385
+ let result;
1386
+ try {
1387
+ result = applyLocalFmtPreset(cwd, presetName, opts);
1388
+ } catch (error) {
1389
+ if (error instanceof InvalidPackageJsonError) {
1390
+ logger.error(
1391
+ "package.json exists but is not valid JSON. Fix it first, then re-run this command."
1392
+ );
1393
+ return;
1394
+ }
1395
+ throw error;
1396
+ }
1397
+ const allFiles = [...result.created, ...result.overwritten];
1398
+ if (allFiles.length > 0 || result.skipped.length > 0) {
1399
+ logApplyResult(result);
1400
+ }
1401
+ if (result.scriptsAdded > 0 || result.scriptsSkipped > 0) {
1402
+ logger.log(
1403
+ `Added ${result.scriptsAdded} script${result.scriptsAdded > 1 ? "s" : ""} to package.json${result.scriptsSkipped > 0 ? ` (${result.scriptsSkipped} skipped)` : ""}`
1404
+ );
1405
+ }
1406
+ const pm = fileExists(path5.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
1407
+ if (!pm) return;
1408
+ const templatePkgPath = path5.join(getLocalPresetDir("fmt", presetName), "package.json");
1409
+ const templatePkg = readJson(templatePkgPath);
1410
+ if (!templatePkg?.devDependencies) return;
1411
+ const depsToInstall = filterDeps(
1412
+ Object.keys(templatePkg.devDependencies),
1413
+ opts.noStylelint,
1414
+ opts.noEditorconfig
1415
+ );
1416
+ const projectPkgPath = path5.join(cwd, "package.json");
1417
+ const projectPkg = readJson(projectPkgPath);
1418
+ if (!projectPkg) return;
1419
+ const existingDeps = projectPkg.devDependencies ?? {};
1420
+ const missing = depsToInstall.filter((dep) => !existingDeps[dep]);
1421
+ if (missing.length === 0) return;
1422
+ if (options.install === false) {
1423
+ const resolved = resolveLocalDeps(templatePkg.devDependencies);
1424
+ const added = await addDepsToManifest(resolved, cwd);
1425
+ if (added.length > 0) {
1426
+ logger.success(`Added to package.json (skipped install): ${added.join(", ")}`);
1427
+ } else {
1428
+ logger.log("All dependencies already in package.json");
1429
+ }
1430
+ return;
1431
+ }
1432
+ if (opts.dryRun) {
1433
+ logger.log(`[dry-run] Would install: ${missing.join(", ")}`);
1434
+ return;
1435
+ }
1436
+ try {
1437
+ logger.log(`Installing dependencies with ${pm}...`);
1438
+ const resolved = resolveLocalDeps(
1439
+ Object.fromEntries(
1440
+ Object.entries(templatePkg.devDependencies).filter(([k]) => missing.includes(k))
1441
+ )
1442
+ );
1443
+ await installDevDeps(resolved, cwd, pm);
1444
+ logger.success("Dependencies installed successfully");
1445
+ } catch (error) {
1446
+ const message = error instanceof Error ? error.message : String(error);
1447
+ logger.warn(`Dependency installation failed: ${message}. You can install manually.`);
1448
+ }
1449
+ }
1450
+ async function executeBuiltinPath(cwd, presetName, preset, options) {
1451
+ const pm = fileExists(path5.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
1452
+ const opts = {
1453
+ cwd,
1454
+ force: options.force ?? false,
1455
+ dryRun: options.dryRun ?? false,
1456
+ noStylelint: options.stylelint !== true,
1457
+ noEditorconfig: options.editorconfig !== true,
1458
+ lockfile: pm ? getLockfileName(pm) : void 0
1459
+ };
1460
+ const result = generateAllFmt(preset, opts);
1461
+ const allFiles = [...result.created, ...result.overwritten];
1462
+ if (allFiles.length === 0 && result.skipped.length === 0) {
1463
+ logger.warn("No files to generate for this preset");
1464
+ return;
1465
+ }
1466
+ logGenerationResult(result, opts.dryRun);
1467
+ if (!opts.dryRun) {
1468
+ materializeFmtPreset(presetName, preset, opts);
1469
+ }
1470
+ if (!pm) {
1471
+ warnMissingPackageJson(preset, options.install !== false);
1472
+ return;
1473
+ }
1474
+ const scripts = opts.noStylelint && preset.scripts ? filterStylelintScripts(preset.scripts) : preset.scripts;
1475
+ if (scripts) {
1476
+ await injectScripts(scripts, opts, pm);
1477
+ }
1478
+ if (!preset.dependencies?.dev) return;
1479
+ const devDeps = opts.noStylelint ? preset.dependencies.dev.filter(isNotStylelintDep) : preset.dependencies.dev;
1480
+ const finalDeps = opts.noEditorconfig ? devDeps.filter(isNotEditorconfigDep) : devDeps;
1481
+ if (options.install === false) {
1482
+ const added = await addDepsToManifest(finalDeps, cwd);
1483
+ if (added.length > 0) {
1484
+ logger.success(`Added to package.json (skipped install): ${added.join(", ")}`);
1485
+ } else {
1486
+ logger.log("All dependencies already in package.json");
1487
+ }
1488
+ return;
1489
+ }
1490
+ if (opts.dryRun) {
1491
+ logger.log(`[dry-run] Would install: ${finalDeps.join(", ")}`);
1492
+ return;
1493
+ }
1494
+ try {
1495
+ logger.log(`Installing dependencies with ${pm}...`);
1496
+ await installDevDeps(finalDeps, cwd, pm);
1497
+ logger.success("Dependencies installed successfully");
1498
+ } catch (error) {
1499
+ const message = error instanceof Error ? error.message : String(error);
1500
+ logger.warn(`Dependency installation failed: ${message}. You can install manually.`);
1501
+ }
1502
+ }
1503
+ function filterDeps(deps, noStylelint, noEditorconfig) {
1504
+ let filtered = deps;
1505
+ if (noStylelint) filtered = filtered.filter(isNotStylelintDep);
1506
+ if (noEditorconfig) filtered = filtered.filter(isNotEditorconfigDep);
1507
+ return filtered;
1508
+ }
852
1509
  function logGenerationResult(result, dryRun) {
853
1510
  const files = [...result.created, ...result.overwritten];
854
1511
  if (dryRun) {
@@ -862,12 +1519,29 @@ function logGenerationResult(result, dryRun) {
862
1519
  }
863
1520
  if (result.created.length > 0) {
864
1521
  logger.log(
865
- `Created ${summarizeFiles(result.created)} config ${result.created.length} file${result.created.length > 1 ? "s" : ""}`
1522
+ `Created ${summarizeFiles(result.created)} config ${result.created.length} file${result.created.length > 1 ? "s" : ""}`
1523
+ );
1524
+ }
1525
+ if (result.overwritten.length > 0) {
1526
+ logger.log(
1527
+ `Overwritten ${summarizeFiles(result.overwritten)} config ${result.overwritten.length} file${result.overwritten.length > 1 ? "s" : ""}`
1528
+ );
1529
+ }
1530
+ if (result.skipped.length > 0) {
1531
+ logger.log(
1532
+ `Skipped ${result.skipped.length} file${result.skipped.length > 1 ? "s" : ""} (already exists)`
1533
+ );
1534
+ }
1535
+ }
1536
+ function logApplyResult(result) {
1537
+ if (result.created.length > 0) {
1538
+ logger.log(
1539
+ `Created ${summarizeFiles(result.created)} config ${result.created.length} file${result.created.length > 1 ? "s" : ""} from local preset`
866
1540
  );
867
1541
  }
868
1542
  if (result.overwritten.length > 0) {
869
1543
  logger.log(
870
- `Overwritten ${summarizeFiles(result.overwritten)} config ${result.overwritten.length} file${result.overwritten.length > 1 ? "s" : ""}`
1544
+ `Overwritten ${summarizeFiles(result.overwritten)} config ${result.overwritten.length} file${result.overwritten.length > 1 ? "s" : ""} from local preset`
871
1545
  );
872
1546
  }
873
1547
  if (result.skipped.length > 0) {
@@ -896,7 +1570,7 @@ function summarizeFiles(filenames) {
896
1570
  return [...categories].join(", ");
897
1571
  }
898
1572
  async function injectScripts(scripts, opts, pm) {
899
- const pkgPath = path4.join(opts.cwd, "package.json");
1573
+ const pkgPath = path5.join(opts.cwd, "package.json");
900
1574
  const pkg = readJson(pkgPath);
901
1575
  if (!pkg) {
902
1576
  logger.warn("package.json not found, skipping script injection");
@@ -931,19 +1605,109 @@ async function injectScripts(scripts, opts, pm) {
931
1605
  }
932
1606
  }
933
1607
 
1608
+ // src/commands/init.ts
1609
+ import { select, isCancel, cancel, outro } from "@clack/prompts";
1610
+
1611
+ // src/presets/init.ts
1612
+ var INIT_TOOLS = [
1613
+ {
1614
+ name: "claude",
1615
+ label: "Claude Code",
1616
+ targetDir: ".claude/skills"
1617
+ },
1618
+ {
1619
+ name: "opencode",
1620
+ label: "OpenCode",
1621
+ targetDir: ".opencode/skills"
1622
+ }
1623
+ ];
1624
+
1625
+ // src/generators/init.ts
1626
+ import fs4 from "fs";
1627
+ import path6 from "path";
1628
+ function resolveSkillsDir() {
1629
+ const entryDir = path6.dirname(process.argv[1] ?? "");
1630
+ return path6.resolve(entryDir, "skills");
1631
+ }
1632
+ function listFilesRecursive(dir, base) {
1633
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
1634
+ const files = [];
1635
+ for (const entry of entries) {
1636
+ const childBase = `${base}/${entry.name}`;
1637
+ const fullPath = path6.join(dir, entry.name);
1638
+ if (entry.isDirectory()) {
1639
+ files.push(...listFilesRecursive(fullPath, childBase));
1640
+ } else {
1641
+ files.push(childBase);
1642
+ }
1643
+ }
1644
+ return files;
1645
+ }
1646
+ function generateInitSkills(targetBaseDir, cwd) {
1647
+ const skillsDir = resolveSkillsDir();
1648
+ if (!fs4.existsSync(skillsDir)) {
1649
+ logger.error(`Bundled skills directory not found: ${skillsDir}`);
1650
+ logger.error('Please run "lux build" or reinstall lux.');
1651
+ return { copiedFiles: [], targetDir: targetBaseDir };
1652
+ }
1653
+ const targetPath = path6.resolve(cwd, targetBaseDir);
1654
+ try {
1655
+ fs4.cpSync(skillsDir, targetPath, { recursive: true, force: true });
1656
+ } catch (error) {
1657
+ const message = error instanceof Error ? error.message : String(error);
1658
+ logger.error(`Failed to copy skills to ${targetPath}: ${message}`);
1659
+ return { copiedFiles: [], targetDir: targetBaseDir };
1660
+ }
1661
+ const copiedFiles = fs4.existsSync(targetPath) ? listFilesRecursive(targetPath, targetBaseDir) : [];
1662
+ return { copiedFiles, targetDir: targetBaseDir };
1663
+ }
1664
+
1665
+ // src/commands/init.ts
1666
+ function registerInitCommand(program2) {
1667
+ program2.command("init").description("Initialize AI coding tool skills in current project").action(async () => {
1668
+ const toolOptions = INIT_TOOLS.map((tool2) => ({
1669
+ value: tool2.name,
1670
+ label: tool2.label
1671
+ }));
1672
+ const selected = await select({
1673
+ message: "Which AI coding tool do you use?",
1674
+ options: toolOptions
1675
+ });
1676
+ if (isCancel(selected)) {
1677
+ cancel("Operation cancelled.");
1678
+ return;
1679
+ }
1680
+ const tool = INIT_TOOLS.find((t) => t.name === selected);
1681
+ if (!tool) {
1682
+ logger.error(`Unknown tool: ${String(selected)}`);
1683
+ return;
1684
+ }
1685
+ const cwd = process.cwd();
1686
+ const result = generateInitSkills(tool.targetDir, cwd);
1687
+ if (result.copiedFiles.length === 0) {
1688
+ logger.warn("No skill files were copied.");
1689
+ return;
1690
+ }
1691
+ for (const file of result.copiedFiles) {
1692
+ logger.log(` ${file}`);
1693
+ }
1694
+ outro(`Skills installed to ${tool.targetDir}/`);
1695
+ });
1696
+ }
1697
+
934
1698
  // src/utils/config.ts
935
- import fs2 from "fs";
936
- import os from "os";
937
- import path5 from "path";
1699
+ import fs5 from "fs";
1700
+ import os2 from "os";
1701
+ import path7 from "path";
938
1702
  var CONFIG_DIR = ".lux";
939
1703
  var ENV_FILE = "env.txt";
940
1704
  function getEnvConfigPath() {
941
- return path5.join(os.homedir(), CONFIG_DIR, ENV_FILE);
1705
+ return path7.join(os2.homedir(), CONFIG_DIR, ENV_FILE);
942
1706
  }
943
1707
  function getEnvConfig() {
944
1708
  let content;
945
1709
  try {
946
- content = fs2.readFileSync(getEnvConfigPath(), "utf-8");
1710
+ content = fs5.readFileSync(getEnvConfigPath(), "utf-8");
947
1711
  } catch {
948
1712
  return {};
949
1713
  }
@@ -966,7 +1730,7 @@ function setEnvConfig(data) {
966
1730
  }
967
1731
  function clearEnvConfig() {
968
1732
  try {
969
- fs2.unlinkSync(getEnvConfigPath());
1733
+ fs5.unlinkSync(getEnvConfigPath());
970
1734
  } catch {
971
1735
  }
972
1736
  }
@@ -988,27 +1752,6 @@ function registerShowCommand(program2) {
988
1752
  show.command("env").description("Display stored proxy environment variables").action(() => handleShowEnv());
989
1753
  }
990
1754
 
991
- // src/utils/execFileNoThrow.ts
992
- import { exec } from "child_process";
993
- import { promisify } from "util";
994
- var execAsync = promisify(exec);
995
- async function execFileNoThrow(command, args, options) {
996
- const cmdStr = args.length > 0 ? `${command} ${args.join(" ")}` : command;
997
- try {
998
- const { stdout, stderr } = await execAsync(cmdStr, {
999
- cwd: options?.cwd
1000
- });
1001
- return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
1002
- } catch (err) {
1003
- const error = err;
1004
- return {
1005
- stdout: (error.stdout ?? "").trim(),
1006
- stderr: (error.stderr ?? "").trim(),
1007
- exitCode: error.code === "ENOENT" ? null : 1
1008
- };
1009
- }
1010
- }
1011
-
1012
1755
  // src/utils/version.ts
1013
1756
  import { existsSync, readFileSync } from "fs";
1014
1757
  import { dirname, join } from "path";
@@ -1206,6 +1949,138 @@ var webVueVscode = {
1206
1949
  ]
1207
1950
  };
1208
1951
 
1952
+ // src/presets/vscode/web-react.ts
1953
+ var webReactVscode = {
1954
+ name: "web-react",
1955
+ description: "VSCode config for React Web",
1956
+ settings: () => ({
1957
+ // ===== Editor Preferences =====
1958
+ "editor.tabSize": 2,
1959
+ "editor.detectIndentation": false,
1960
+ "editor.insertSpaces": true,
1961
+ "editor.renderWhitespace": "selection",
1962
+ "editor.guides.indentation": true,
1963
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1964
+ "editor.formatOnSave": true,
1965
+ "editor.codeActionsOnSave": {
1966
+ "source.fixAll.eslint": "explicit",
1967
+ "source.fixAll.stylelint": "explicit",
1968
+ "source.organizeImports": "never"
1969
+ },
1970
+ // Cursor & Animation
1971
+ "editor.cursorBlinking": "expand",
1972
+ "editor.cursorSmoothCaretAnimation": "on",
1973
+ "editor.largeFileOptimizations": true,
1974
+ // Code Assistance
1975
+ "editor.inlineSuggest.enabled": true,
1976
+ "editor.suggestSelection": "recentlyUsedByPrefix",
1977
+ "editor.acceptSuggestionOnEnter": "smart",
1978
+ "editor.bracketPairColorization.enabled": true,
1979
+ "editor.autoClosingBrackets": "beforeWhitespace",
1980
+ "editor.autoClosingOvertype": "always",
1981
+ // ===== TypeScript =====
1982
+ "js/ts.inlayHints.enumMemberValues.enabled": true,
1983
+ "js/ts.preferences.preferTypeOnlyAutoImports": true,
1984
+ "js/ts.preferences.includePackageJsonAutoImports": "on",
1985
+ "js/ts.preferences.importModuleSpecifier": "relative",
1986
+ "js/ts.suggest.autoImports": true,
1987
+ "js/ts.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
1988
+ // ===== Language-specific Formatting =====
1989
+ "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1990
+ "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1991
+ "[scss]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1992
+ "[typescript]": {
1993
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1994
+ "editor.formatOnSave": true
1995
+ },
1996
+ "[javascript]": {
1997
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1998
+ "editor.formatOnSave": true
1999
+ },
2000
+ "[typescriptreact]": {
2001
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
2002
+ "editor.formatOnSave": true
2003
+ },
2004
+ "[javascriptreact]": {
2005
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
2006
+ "editor.formatOnSave": true
2007
+ },
2008
+ "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
2009
+ "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
2010
+ // ===== Terminal =====
2011
+ "terminal.integrated.cursorBlinking": true,
2012
+ "terminal.integrated.tabs.enabled": true,
2013
+ "terminal.integrated.scrollback": 1e4,
2014
+ // ===== File Exclusion =====
2015
+ "files.watcherExclude": {
2016
+ "**/.git/objects/**": true,
2017
+ "**/.git/subtree-cache/**": true,
2018
+ "**/.vscode/**": true,
2019
+ "**/node_modules/**": true,
2020
+ "**/tmp/**": true,
2021
+ "**/dist/**": true,
2022
+ "**/pnpm-lock.yaml": true,
2023
+ "**/package-lock.json": true,
2024
+ "**/bun.lock": true,
2025
+ "**/yarn.lock": true
2026
+ },
2027
+ "search.exclude": {
2028
+ "**/node_modules": true,
2029
+ "**/*.log": true,
2030
+ "**/*.log*": true,
2031
+ "**/dist": true,
2032
+ "**/.git": true,
2033
+ "**/.vscode": false,
2034
+ "**/tmp": true,
2035
+ node_modules: true,
2036
+ "**/pnpm-lock.yaml": true,
2037
+ "**/package-lock.json": true,
2038
+ "**/bun.lock": true,
2039
+ "**/yarn.lock": true
2040
+ },
2041
+ // ===== File Nesting =====
2042
+ "explorer.fileNesting.enabled": true,
2043
+ "explorer.fileNesting.expand": false,
2044
+ "explorer.fileNesting.patterns": {
2045
+ "package.json": "pnpm-lock.yaml,yarn.lock,bun.lock, .gitignore, .browserslistrc, .npmrc, cspell.json,README.md, LICENSE*,.editorconfig",
2046
+ "eslint.config.mjs": ".prettierignore, .prettierrc, .prettierrc.json, .editorconfig",
2047
+ "tsconfig.json": "tsconfig.*.json",
2048
+ "tailwind.config.js": "postcss.config.js",
2049
+ "vite.config.{js,ts}": "vite.*.{js,ts}",
2050
+ ".env": ".env.*"
2051
+ },
2052
+ // ===== ESLint =====
2053
+ "eslint.validate": [
2054
+ "javascript",
2055
+ "typescript",
2056
+ "javascriptreact",
2057
+ "typescriptreact",
2058
+ "html",
2059
+ "markdown",
2060
+ "json",
2061
+ "jsonc",
2062
+ "json5"
2063
+ ],
2064
+ // ===== Stylelint =====
2065
+ "stylelint.enable": true,
2066
+ "stylelint.validate": ["css", "scss"],
2067
+ "stylelint.snippet": ["css", "scss"],
2068
+ "css.validate": false,
2069
+ "less.validate": false,
2070
+ "scss.validate": false,
2071
+ // ===== CSpell =====
2072
+ "cSpell.language": "en"
2073
+ }),
2074
+ extensions: () => [
2075
+ "dbaeumer.vscode-eslint",
2076
+ "esbenp.prettier-vscode",
2077
+ "stylelint.vscode-stylelint",
2078
+ "mrmlnc.vscode-scss",
2079
+ "streetsidesoftware.code-spell-checker",
2080
+ "editorconfig.editorconfig"
2081
+ ]
2082
+ };
2083
+
1209
2084
  // src/presets/vscode/electron.ts
1210
2085
  var electronVueVscode = {
1211
2086
  name: "electron-vue",
@@ -1697,6 +2572,7 @@ var goVscode = {
1697
2572
  // src/presets/vscode/index.ts
1698
2573
  var VSCODE_PRESETS = [
1699
2574
  webVueVscode,
2575
+ webReactVscode,
1700
2576
  electronVueVscode,
1701
2577
  uniappVscode,
1702
2578
  nodeVscode,
@@ -1704,59 +2580,14 @@ var VSCODE_PRESETS = [
1704
2580
  goVscode
1705
2581
  ];
1706
2582
 
1707
- // src/core/merge-settings.ts
1708
- var USER_PRIORITY_KEYS = /* @__PURE__ */ new Set([
1709
- // Cursor/animation
1710
- "editor.cursorBlinking",
1711
- "editor.cursorSmoothCaretAnimation",
1712
- "editor.renderWhitespace",
1713
- "editor.guides.indentation",
1714
- "editor.largeFileOptimizations",
1715
- // Theme/appearance
1716
- "workbench.iconTheme",
1717
- "workbench.colorTheme",
1718
- // Suggestions
1719
- "editor.inlineSuggest.enabled",
1720
- "editor.suggestSelection",
1721
- "editor.acceptSuggestionOnEnter",
1722
- "editor.bracketPairColorization.enabled",
1723
- "editor.autoClosingBrackets",
1724
- "editor.autoClosingOvertype"
1725
- ]);
1726
- function mergeVscodeSettings(preset, existing) {
1727
- const result = { ...existing };
1728
- for (const [key, presetVal] of Object.entries(preset)) {
1729
- const existingVal = existing[key];
1730
- if (existingVal === void 0) {
1731
- result[key] = presetVal;
1732
- continue;
1733
- }
1734
- if (USER_PRIORITY_KEYS.has(key)) {
1735
- continue;
1736
- }
1737
- if (isPlainObject(presetVal) && isPlainObject(existingVal)) {
1738
- result[key] = mergeVscodeSettings(
1739
- presetVal,
1740
- existingVal
1741
- );
1742
- continue;
1743
- }
1744
- result[key] = presetVal;
1745
- }
1746
- return result;
1747
- }
1748
- function isPlainObject(val) {
1749
- return typeof val === "object" && val !== null && !Array.isArray(val);
1750
- }
1751
-
1752
2583
  // src/generators/vscode.ts
1753
- var STYLELINT_SETTINGS_PREFIXES = [
2584
+ var STYLELINT_SETTINGS_PREFIXES2 = [
1754
2585
  "stylelint.",
1755
2586
  "css.validate",
1756
2587
  "less.validate",
1757
2588
  "scss.validate"
1758
2589
  ];
1759
- var STYLELINT_EXTENSION = "stylelint.vscode-stylelint";
2590
+ var STYLELINT_EXTENSION2 = "stylelint.vscode-stylelint";
1760
2591
  function generateVscodeSettings(preset, opts) {
1761
2592
  const settingsPath = `${opts.cwd}/.vscode/settings.json`;
1762
2593
  if (opts.dryRun) {
@@ -1764,25 +2595,50 @@ function generateVscodeSettings(preset, opts) {
1764
2595
  return existingSettings2 ? "overwritten" : "created";
1765
2596
  }
1766
2597
  const rawSettings = preset.settings();
1767
- const presetSettings = opts.noStylelint ? filterStylelintSettings(rawSettings) : rawSettings;
2598
+ const presetSettings = opts.noStylelint ? filterStylelintSettings2(rawSettings) : rawSettings;
1768
2599
  const existingSettings = readJson(settingsPath);
1769
2600
  if (existingSettings) {
1770
2601
  const backupPath = `${settingsPath}.bak`;
1771
2602
  if (!fileExists(backupPath)) {
1772
- writeFile(backupPath, JSON.stringify(existingSettings, null, 2) + "\n");
1773
- logger.log(`Backed up .vscode/settings.json \u2192 settings.json.bak`);
2603
+ try {
2604
+ writeFile(backupPath, JSON.stringify(existingSettings, null, 2) + "\n");
2605
+ logger.log("Backed up .vscode/settings.json \u2192 settings.json.bak");
2606
+ } catch (error) {
2607
+ const message = error instanceof Error ? error.message : String(error);
2608
+ logger.warn(
2609
+ `Failed to backup .vscode/settings.json: ${message}. Continuing without backup.`
2610
+ );
2611
+ }
2612
+ }
2613
+ try {
2614
+ const merged = mergeVscodeSettings(presetSettings, existingSettings);
2615
+ writeJson(settingsPath, merged);
2616
+ } catch (error) {
2617
+ const msg = error instanceof Error ? error.message : String(error);
2618
+ logger.error(`Failed to write .vscode/settings.json: ${msg}`);
2619
+ return null;
1774
2620
  }
1775
- const merged = mergeVscodeSettings(presetSettings, existingSettings);
1776
- writeJson(settingsPath, merged);
1777
2621
  return "overwritten";
1778
2622
  }
1779
- writeJson(settingsPath, presetSettings);
2623
+ try {
2624
+ writeJson(settingsPath, presetSettings);
2625
+ } catch (error) {
2626
+ const msg = error instanceof Error ? error.message : String(error);
2627
+ logger.error(`Failed to write .vscode/settings.json: ${msg}`);
2628
+ return null;
2629
+ }
1780
2630
  return "created";
1781
2631
  }
1782
2632
  function generateVscodeExtensions(preset, opts) {
1783
2633
  if (opts.dryRun) return "created";
1784
- const extensions = opts.noStylelint ? preset.extensions().filter((ext) => ext !== STYLELINT_EXTENSION) : preset.extensions();
1785
- writeJson(`${opts.cwd}/.vscode/extensions.json`, { recommendations: extensions });
2634
+ const extensions = opts.noStylelint ? preset.extensions().filter((ext) => ext !== STYLELINT_EXTENSION2) : preset.extensions();
2635
+ try {
2636
+ writeJson(`${opts.cwd}/.vscode/extensions.json`, { recommendations: extensions });
2637
+ } catch (error) {
2638
+ const msg = error instanceof Error ? error.message : String(error);
2639
+ logger.error(`Failed to write .vscode/extensions.json: ${msg}`);
2640
+ return null;
2641
+ }
1786
2642
  return "created";
1787
2643
  }
1788
2644
  function generateAllVscode(preset, opts) {
@@ -1794,10 +2650,10 @@ function generateAllVscode(preset, opts) {
1794
2650
  if (extAction === "created") result.created.push(".vscode/extensions.json");
1795
2651
  return result;
1796
2652
  }
1797
- function filterStylelintSettings(settings) {
2653
+ function filterStylelintSettings2(settings) {
1798
2654
  const filtered = Object.fromEntries(
1799
2655
  Object.entries(settings).filter(
1800
- ([key]) => !STYLELINT_SETTINGS_PREFIXES.some((prefix) => key.startsWith(prefix))
2656
+ ([key]) => !STYLELINT_SETTINGS_PREFIXES2.some((prefix) => key.startsWith(prefix))
1801
2657
  )
1802
2658
  );
1803
2659
  if (typeof filtered["editor.codeActionsOnSave"] === "object" && filtered["editor.codeActionsOnSave"] !== null) {
@@ -1811,28 +2667,20 @@ function filterStylelintSettings(settings) {
1811
2667
  // src/commands/vscode.ts
1812
2668
  function registerVscodeCommand(program2) {
1813
2669
  const vscode = program2.command("vscode").description("Initialize VSCode config with preset");
1814
- vscode.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--dry-run", "Preview without writing files").option("--no-stylelint", "Skip Stylelint settings and extension").action(
2670
+ vscode.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--dry-run", "Preview without writing files").option("--stylelint", "Include Stylelint settings and extension").option("--reset", "Reset local preset and re-materialize from built-in").action(
1815
2671
  async (presetName, options) => {
1816
2672
  const preset = resolvePreset(VSCODE_PRESETS, presetName);
1817
2673
  if (!preset) return;
1818
2674
  const cwd = process.cwd();
1819
- const opts = {
1820
- cwd,
1821
- force: options.force ?? false,
1822
- dryRun: options.dryRun ?? false,
1823
- noStylelint: options.stylelint === false
1824
- };
1825
- const result = generateAllVscode(preset, opts);
1826
- const files = [...result.created, ...result.overwritten];
1827
- if (files.length === 0) {
1828
- logger.warn("No files generated");
1829
- return;
2675
+ if (options.reset) {
2676
+ resetLocalPreset("vscode", presetName);
1830
2677
  }
1831
- if (opts.dryRun) {
1832
- logger.log(`[dry-run] Would create ${files.join(", ")}`);
1833
- return;
2678
+ const useLocal = localPresetExists("vscode", presetName);
2679
+ if (useLocal) {
2680
+ executeVscodeLocalPath(cwd, presetName, options);
2681
+ } else {
2682
+ executeVscodeBuiltinPath(cwd, presetName, preset, options);
1834
2683
  }
1835
- logger.log(`Created ${files.join(", ")}`);
1836
2684
  }
1837
2685
  );
1838
2686
  vscode.command("list").description("List available vscode presets").action(() => {
@@ -1841,6 +2689,48 @@ function registerVscodeCommand(program2) {
1841
2689
  }
1842
2690
  });
1843
2691
  }
2692
+ function executeVscodeLocalPath(cwd, presetName, options) {
2693
+ logger.log("Using local custom preset");
2694
+ const opts = {
2695
+ cwd,
2696
+ force: options.force ?? false,
2697
+ dryRun: options.dryRun ?? false,
2698
+ noStylelint: options.stylelint !== true,
2699
+ noEditorconfig: false
2700
+ };
2701
+ const result = applyLocalVscodePreset(cwd, presetName, opts);
2702
+ const files = [...result.created, ...result.overwritten];
2703
+ if (files.length === 0) {
2704
+ logger.warn("No files generated");
2705
+ return;
2706
+ }
2707
+ if (opts.dryRun) {
2708
+ logger.log(`[dry-run] Would create ${files.join(", ")} from local preset`);
2709
+ return;
2710
+ }
2711
+ logger.log(`Created ${files.join(", ")} from local preset`);
2712
+ }
2713
+ function executeVscodeBuiltinPath(cwd, presetName, preset, options) {
2714
+ const opts = {
2715
+ cwd,
2716
+ force: options.force ?? false,
2717
+ dryRun: options.dryRun ?? false,
2718
+ noStylelint: options.stylelint !== true,
2719
+ noEditorconfig: false
2720
+ };
2721
+ const result = generateAllVscode(preset, opts);
2722
+ const files = [...result.created, ...result.overwritten];
2723
+ if (files.length === 0) {
2724
+ logger.warn("No files generated");
2725
+ return;
2726
+ }
2727
+ if (opts.dryRun) {
2728
+ logger.log(`[dry-run] Would create ${files.join(", ")}`);
2729
+ return;
2730
+ }
2731
+ logger.log(`Created ${files.join(", ")}`);
2732
+ materializeVscodePreset(cwd, presetName);
2733
+ }
1844
2734
 
1845
2735
  // src/commands/vpn.ts
1846
2736
  import { spawnSync } from "child_process";
@@ -1921,6 +2811,7 @@ function registerVpnCommand(program2) {
1921
2811
  // src/index.ts
1922
2812
  program.name("lux").description("One-click project formatting & VSCode config CLI").version(getCurrentVersion());
1923
2813
  registerFmtCommand(program);
2814
+ registerInitCommand(program);
1924
2815
  registerVscodeCommand(program);
1925
2816
  registerVpnCommand(program);
1926
2817
  registerShowCommand(program);