@luxkit/cli 1.0.8 → 1.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/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ### What is lux?
20
20
 
21
- `lux` is a CLI tool that initializes project formatting configs and VSCode workspace settings with a single command. It generates ESLint, Prettier, Stylelint, CSpell, EditorConfig files and VSCode settings from battle-tested presets — with smart merge and conflict resolution.
21
+ `lux` is a CLI tool that initializes project formatting configs and VSCode workspace settings with a single command. It generates ESLint, Prettier, CSpell, EditorConfig files and VSCode settings from battle-tested presets — with smart merge and conflict resolution.
22
22
 
23
23
  <div align="center">
24
24
  <img src="https://github.com/TTT1231/lux/blob/main/demo.gif?raw=true" alt="lux demo" width="640" />
@@ -26,19 +26,19 @@
26
26
 
27
27
  ### ✨ Key Highlights
28
28
 
29
- | Feature | Description |
30
- | :------------------------- | :----------------------------------------------------------------------------- |
31
- | 🎯 **One Command Setup** | `lux fmt web-vue` generates all linting & formatting configs instantly |
32
- | 🔧 **5 Fmt Presets** | `web-vue` · `electron-vue` · `uniapp` · `node` · `nest` — each with curated rules |
33
- | 🖥️ **6 VSCode Presets** | `web-vue` · `electron-vue` · `uniapp` · `node` · `nest` · `go` — settings + extensions |
34
- | 🔀 **Smart Merge** | Preset wins for linting keys; user wins for personal preferences |
35
- | 🛡️ **Conflict Resolution** | `neverOverwrite` / `forceOverwrite` lists + `--force` flag |
36
- | 📦 **Auto Install** | Detects bun / pnpm / yarn / npm and installs devDependencies |
37
- | 🔍 **Fuzzy Matching** | Typo a preset name? Levenshtein distance finds the closest match |
38
- | 🧪 **Dry Run** | Preview all changes with `--dry-run` before writing anything |
39
- | 🔗 **Script Injection** | Auto-injects `<pm> lint` / `<pm> format` scripts into package.json |
40
- | 🌐 **Proxy Management** | Persistent proxy config with `set` / `unset` — copy to CMD / PowerShell / Bash |
41
- | 🔄 **Self-Update** | `lux update` checks and installs the latest version automatically |
29
+ | Feature | Description |
30
+ | :------------------------- | :--------------------------------------------------------------------------------------------------- |
31
+ | 🎯 **One Command Setup** | `lux fmt web-vue` generates all linting & formatting configs instantly |
32
+ | 🔧 **6 Fmt Presets** | `web-vue` · `web-react` · `electron-vue` · `uniapp` · `node` · `nest` — each with curated rules |
33
+ | 🖥️ **7 VSCode Presets** | `web-vue` · `web-react` · `electron-vue` · `uniapp` · `node` · `nest` · `go` — settings + extensions |
34
+ | 🔀 **Smart Merge** | Preset wins for linting keys; user wins for personal preferences |
35
+ | 🛡️ **Conflict Resolution** | `neverOverwrite` / `forceOverwrite` lists + `--force` flag |
36
+ | 📦 **Auto Install** | Detects bun / pnpm / yarn / npm and installs devDependencies |
37
+ | 🔍 **Fuzzy Matching** | Typo a preset name? Levenshtein distance finds the closest match |
38
+ | 🧪 **Dry Run** | Preview all changes with `--dry-run` before writing anything |
39
+ | 🔗 **Script Injection** | Auto-injects `<pm> lint` / `<pm> format` scripts into package.json |
40
+ | 🌐 **Proxy Management** | Persistent proxy config with `set` / `unset` — copy to CMD / PowerShell / Bash |
41
+ | 🔄 **Self-Update** | `lux update` checks and installs the latest version automatically |
42
42
 
43
43
  <br />
44
44
 
@@ -51,11 +51,16 @@ npm install -g @luxkit/cli
51
51
  bun add -g @luxkit/cli
52
52
 
53
53
  # Initialize formatting configs
54
- lux fmt web-vue # Generate ESLint, Prettier, Stylelint, CSpell, EditorConfig
54
+ lux fmt web-vue # Generate ESLint, Prettier, CSpell
55
+ lux fmt web-vue --stylelint # Also include Stylelint
56
+ lux fmt web-vue --editorconfig # Also include EditorConfig
55
57
 
56
58
  # Initialize VSCode settings
57
59
  lux vscode web-vue # Generate .vscode/settings.json + extensions.json
58
60
 
61
+ # Initialize AI coding tool skills
62
+ lux init # Select tool interactively, copy skills to project
63
+
59
64
  # List available presets
60
65
  lux fmt list
61
66
  lux vscode list
@@ -65,33 +70,34 @@ lux vscode list
65
70
 
66
71
  ### CLI Commands
67
72
 
68
- | Command | Description |
69
- | :-------------------------- | :------------------------------------------------- |
70
- | `lux fmt <preset>` | Initialize formatting config files |
71
- | `lux fmt list` | List available fmt presets |
72
- | `lux vscode <preset>` | Initialize VSCode workspace settings |
73
- | `lux vscode list` | List available VSCode presets |
73
+ | Command | Description |
74
+ | :-------------------------- | :---------------------------------------------------------------- |
75
+ | `lux fmt <preset>` | Initialize formatting config files |
76
+ | `lux fmt list` | List available fmt presets |
77
+ | `lux vscode <preset>` | Initialize VSCode workspace settings |
78
+ | `lux vscode list` | List available VSCode presets |
79
+ | `lux init` | Initialize AI coding tool skills in current project |
74
80
  | `lux set <key=value> [...]` | Persist proxy env vars (e.g. `https_proxy=http://127.0.0.1:7890`) |
75
- | `lux unset` | Clear all stored proxy configuration |
76
- | `lux show env` | Display stored proxy environment variables |
77
- | `lux vpn cmd` | Copy CMD proxy commands to clipboard |
78
- | `lux vpn pw` | Copy PowerShell proxy commands to clipboard |
79
- | `lux vpn bash` | Copy Bash proxy commands to clipboard |
80
- | `lux update` | Update `@luxkit/cli` to the latest version |
81
- | `lux update --check` | Check for available updates without installing |
81
+ | `lux unset` | Clear all stored proxy configuration |
82
+ | `lux show env` | Display stored proxy environment variables |
83
+ | `lux vpn cmd` | Copy CMD proxy commands to clipboard |
84
+ | `lux vpn pw` | Copy PowerShell proxy commands to clipboard |
85
+ | `lux vpn bash` | Copy Bash proxy commands to clipboard |
86
+ | `lux update` | Update `@luxkit/cli` to the latest version |
87
+ | `lux update --check` | Check for available updates without installing |
82
88
 
83
89
  <br />
84
90
 
85
91
  ### Available Presets
86
92
 
87
- | Preset | Fmt | VSCode | Stack |
88
- | :--------- | :-: | :----: | :--------------------------- |
89
- | `web-vue` | ✅ | ✅ | Vue / React / TS / CSS |
90
- | `electron-vue` | ✅ | ✅ | Electron + Web stack |
91
- | `uniapp` | ✅ | ✅ | UniApp / WeChat Mini Program |
92
- | `node` | ✅ | ✅ | Node.js backend |
93
- | `nest` | ✅ | ✅ | NestJS backend |
94
- | `go` | — | ✅ | Go backend |
93
+ | Preset | Fmt | VSCode | Stack |
94
+ | :------------- | :-: | :----: | :------------------------- |
95
+ | `web-vue` | ✅ | ✅ | Vue 3 / Vite / TS / CSS |
96
+ | `web-react` | ✅ | ✅ | React / Vite / TS / CSS |
97
+ | `electron-vue` | ✅ | ✅ | Electron + Vue / Web stack |
98
+ | `node` | ✅ | ✅ | Node.js backend |
99
+ | `nest` | ✅ | ✅ | NestJS backend |
100
+ | `go` | — | ✅ | Go backend |
95
101
 
96
102
  <br />
97
103
 
@@ -100,9 +106,11 @@ lux vscode list
100
106
  ```bash
101
107
  lux fmt <preset> [options]
102
108
 
103
- --force Force overwrite existing files
104
- --no-install Skip dependency installation
105
- --dry-run Preview without writing files
109
+ --force Force overwrite existing files
110
+ --no-install Skip dependency installation
111
+ --dry-run Preview without writing files
112
+ --stylelint Include Stylelint config generation (opt-in)
113
+ --editorconfig Include EditorConfig config generation (opt-in)
106
114
  ```
107
115
 
108
116
  <br />
@@ -142,7 +150,7 @@ lux fmt web-vue
142
150
  | Test | Vitest (unit + acceptance) |
143
151
  | CLI | Commander.js |
144
152
  | Output | Chalk |
145
- | Bundle | Zero runtime deps (chalk + commander only) |
153
+ | Bundle | Minimal runtime deps (chalk + commander + @clack/prompts) |
146
154
 
147
155
  <br />
148
156
 
package/dist/index.js CHANGED
@@ -114,8 +114,8 @@ trim_trailing_whitespace = false
114
114
  ]
115
115
  },
116
116
  scripts: {
117
- lint: "eslint .",
118
- "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix',
117
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
118
+ "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
119
119
  format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
120
120
  "format:check": 'prettier --check "src/**/*.{ts,js,json,vue,css,scss}"',
121
121
  stylelint: 'stylelint "src/**/*.{css,scss,vue}"',
@@ -297,8 +297,8 @@ trim_trailing_whitespace = false
297
297
  ]
298
298
  },
299
299
  scripts: {
300
- lint: "eslint .",
301
- "lint:fix": 'eslint "src/**/*.{js,ts}" --fix',
300
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
301
+ "lint:fix": 'eslint "src/**/*.{js,ts}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
302
302
  format: 'prettier --write "src/**/*.{ts,js,json}"',
303
303
  "format:check": 'prettier --check "src/**/*.{ts,js,json}"',
304
304
  cspell: 'cspell --gitignore "src/**/*"',
@@ -416,8 +416,8 @@ trim_trailing_whitespace = false
416
416
  ]
417
417
  },
418
418
  scripts: {
419
- lint: "eslint .",
420
- "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix',
419
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
420
+ "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
421
421
  format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
422
422
  "format:check": 'prettier --check "src/**/*.{ts,js,json,vue,css,scss}"',
423
423
  stylelint: 'stylelint "src/**/*.{css,scss,vue}"',
@@ -431,6 +431,134 @@ trim_trailing_whitespace = false
431
431
  }
432
432
  };
433
433
 
434
+ // src/presets/fmt/web-react.ts
435
+ var webReactFmt = {
436
+ name: "web-react",
437
+ description: "React Web frontend (Vite + React + TypeScript)",
438
+ eslint: () => `import js from '@eslint/js'
439
+ import tseslint from 'typescript-eslint'
440
+ import reactHooks from 'eslint-plugin-react-hooks'
441
+ import reactRefresh from 'eslint-plugin-react-refresh'
442
+ import globals from 'globals'
443
+ import prettierConfig from 'eslint-config-prettier'
444
+
445
+ export default tseslint.config(
446
+ { ignores: ['dist'] },
447
+ {
448
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
449
+ files: ['**/*.{ts,tsx}'],
450
+ languageOptions: {
451
+ ecmaVersion: 2020,
452
+ globals: globals.browser,
453
+ },
454
+ plugins: {
455
+ 'react-hooks': reactHooks,
456
+ 'react-refresh': reactRefresh,
457
+ },
458
+ rules: {
459
+ ...reactHooks.configs.recommended.rules,
460
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
461
+ '@typescript-eslint/no-explicit-any': 'warn',
462
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
463
+ },
464
+ },
465
+ prettierConfig,
466
+ )
467
+ `,
468
+ prettier: () => JSON.stringify(
469
+ {
470
+ semi: false,
471
+ singleQuote: true,
472
+ tabWidth: 2,
473
+ trailingComma: "all",
474
+ printWidth: 100,
475
+ endOfLine: "lf"
476
+ },
477
+ null,
478
+ 2
479
+ ) + "\n",
480
+ prettierIgnore: () => `node_modules/
481
+ <lockfile>
482
+ dist/
483
+ coverage/
484
+ `,
485
+ stylelint: () => `export default {
486
+ plugins: ['stylelint-order', '@stylistic/stylelint-plugin'],
487
+ extends: [
488
+ 'stylelint-config-standard-scss',
489
+ 'stylelint-config-recess-order',
490
+ ],
491
+ rules: {
492
+ 'selector-class-pattern': null,
493
+ 'scss/dollar-variable-pattern': null,
494
+ 'scss/percent-placeholder-pattern': null,
495
+ 'scss/at-mixin-pattern': null,
496
+ 'order/properties-order': null,
497
+ },
498
+ }
499
+ `,
500
+ stylelintIgnore: () => `node_modules/
501
+ dist/
502
+ `,
503
+ cspell: () => JSON.stringify(
504
+ {
505
+ $schema: "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
506
+ version: "0.2",
507
+ language: "en,en-US",
508
+ allowCompoundWords: true,
509
+ words: ["vite", "react", "zustand", "tanstack"]
510
+ },
511
+ null,
512
+ 2
513
+ ) + "\n",
514
+ editorconfig: () => `root = true
515
+
516
+ [*]
517
+ charset = utf-8
518
+ indent_style = space
519
+ indent_size = 2
520
+ end_of_line = lf
521
+ insert_final_newline = true
522
+ trim_trailing_whitespace = true
523
+
524
+ [*.md]
525
+ trim_trailing_whitespace = false
526
+ `,
527
+ dependencies: {
528
+ dev: [
529
+ "eslint",
530
+ "@eslint/js",
531
+ "typescript-eslint",
532
+ "eslint-plugin-react-hooks",
533
+ "eslint-plugin-react-refresh",
534
+ "eslint-config-prettier",
535
+ "globals",
536
+ "prettier",
537
+ "stylelint",
538
+ "stylelint-config-standard-scss",
539
+ "stylelint-order",
540
+ "stylelint-scss",
541
+ "@stylistic/stylelint-plugin",
542
+ "postcss-scss",
543
+ "cspell"
544
+ ]
545
+ },
546
+ scripts: {
547
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
548
+ "lint:fix": 'eslint "src/**/*.{js,ts,jsx,tsx}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
549
+ format: 'prettier --write "src/**/*.{ts,js,json,jsx,tsx,css,scss}"',
550
+ "format:check": 'prettier --check "src/**/*.{ts,js,json,jsx,tsx,css,scss}"',
551
+ stylelint: 'stylelint "src/**/*.{css,scss}"',
552
+ "stylelint:fix": 'stylelint "src/**/*.{css,scss}" --fix',
553
+ cspell: 'cspell --gitignore "src/**/*"',
554
+ "type:check": "tsc --noEmit",
555
+ "code:check": "<pm> lint && <pm> format:check",
556
+ "code:fix": "<pm> lint:fix && <pm> format",
557
+ "code:check:all": "<pm> lint && <pm> format:check && <pm> stylelint && <pm> cspell",
558
+ "code:fix:all": "<pm> lint:fix && <pm> format && <pm> stylelint:fix"
559
+ }
560
+ };
561
+
434
562
  // src/presets/fmt/web-vue.ts
435
563
  var webVueFmt = {
436
564
  name: "web-vue",
@@ -535,8 +663,8 @@ trim_trailing_whitespace = false
535
663
  ]
536
664
  },
537
665
  scripts: {
538
- lint: "eslint .",
539
- "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix',
666
+ lint: "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
667
+ "lint:fix": 'eslint "src/**/*.{js,ts,vue}" --fix --cache --cache-location node_modules/.cache/.eslintcache',
540
668
  format: 'prettier --write "src/**/*.{ts,js,json,vue,css,scss}"',
541
669
  "format:check": 'prettier --check "src/**/*.{ts,js,json,vue,css,scss}"',
542
670
  stylelint: 'stylelint "src/**/*.{css,scss,vue}"',
@@ -551,7 +679,14 @@ trim_trailing_whitespace = false
551
679
  };
552
680
 
553
681
  // src/presets/fmt/index.ts
554
- var FMT_PRESETS = [webVueFmt, electronVueFmt, uniappFmt, nodeFmt, nestFmt];
682
+ var FMT_PRESETS = [
683
+ webVueFmt,
684
+ webReactFmt,
685
+ electronVueFmt,
686
+ uniappFmt,
687
+ nodeFmt,
688
+ nestFmt
689
+ ];
555
690
 
556
691
  // src/utils/logger.ts
557
692
  import chalk from "chalk";
@@ -667,7 +802,10 @@ function readJson(filePath) {
667
802
  try {
668
803
  const raw = fs.readFileSync(filePath, "utf-8");
669
804
  return JSON.parse(raw);
670
- } catch {
805
+ } catch (error) {
806
+ if (error.code === "ENOENT") return null;
807
+ const message = error instanceof Error ? error.message : String(error);
808
+ logger.error(`Failed to read or parse ${path.basename(filePath)}: ${message}`);
671
809
  return null;
672
810
  }
673
811
  }
@@ -693,13 +831,20 @@ function generateConfigFile(preset, filename, content, opts) {
693
831
  if (action === "skip") return "skipped";
694
832
  if (opts.dryRun) return exists ? "overwritten" : "created";
695
833
  const resolved = opts.lockfile ? content.replace(/<lockfile>/g, opts.lockfile) : content.replace(/<lockfile>\n?/g, "");
696
- writeFile(filepath, resolved);
834
+ try {
835
+ writeFile(filepath, resolved);
836
+ } catch (error) {
837
+ const message = error instanceof Error ? error.message : String(error);
838
+ logger.error(`Failed to write ${filename}: ${message}`);
839
+ return null;
840
+ }
697
841
  return exists ? "overwritten" : "created";
698
842
  }
699
843
  function generateAllFmt(preset, opts) {
700
844
  const result = { created: [], overwritten: [], skipped: [] };
701
845
  for (const { filename, getContent } of CONFIG_FILES) {
702
846
  if (opts.noStylelint && filename.includes("stylelint")) continue;
847
+ if (opts.noEditorconfig && filename === ".editorconfig") continue;
703
848
  const content = getContent(preset);
704
849
  if (content === void 0) continue;
705
850
  const action = generateConfigFile(preset, filename, content, opts);
@@ -713,6 +858,29 @@ function generateAllFmt(preset, opts) {
713
858
  // src/utils/deps.ts
714
859
  import path3 from "path";
715
860
  import { spawn } from "child_process";
861
+
862
+ // src/utils/execFileNoThrow.ts
863
+ import { exec } from "child_process";
864
+ import { promisify } from "util";
865
+ var execAsync = promisify(exec);
866
+ async function execFileNoThrow(command, args, options) {
867
+ const cmdStr = args.length > 0 ? `${command} ${args.join(" ")}` : command;
868
+ try {
869
+ const { stdout, stderr } = await execAsync(cmdStr, {
870
+ cwd: options?.cwd
871
+ });
872
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
873
+ } catch (err) {
874
+ const error = err;
875
+ return {
876
+ stdout: (error.stdout ?? "").trim(),
877
+ stderr: (error.stderr ?? "").trim(),
878
+ exitCode: error.code === "ENOENT" ? null : 1
879
+ };
880
+ }
881
+ }
882
+
883
+ // src/utils/deps.ts
716
884
  function detectPackageManager(cwd) {
717
885
  if (fileExists(`${cwd}/bun.lockb`) || fileExists(`${cwd}/bun.lock`)) return "bun";
718
886
  if (fileExists(`${cwd}/pnpm-lock.yaml`)) return "pnpm";
@@ -743,6 +911,37 @@ function getRunPrefix(pm) {
743
911
  return "npm run";
744
912
  }
745
913
  }
914
+ async function fetchPackageVersion(pkg) {
915
+ const { stdout, exitCode } = await execFileNoThrow("npm", ["view", pkg, "version"]);
916
+ if (exitCode !== 0 || !stdout) {
917
+ throw new Error(`Failed to fetch version for "${pkg}" from npm registry.`);
918
+ }
919
+ const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
920
+ return lines[lines.length - 1].trim();
921
+ }
922
+ async function addDepsToManifest(packages, cwd) {
923
+ const pkgPath = path3.join(cwd, "package.json");
924
+ const pkg = readJson(pkgPath);
925
+ if (!pkg) {
926
+ throw new Error("package.json not found");
927
+ }
928
+ const devDeps = pkg.devDependencies ?? {};
929
+ const missing = packages.filter((p) => !devDeps[p]);
930
+ if (missing.length === 0) return [];
931
+ const results = await Promise.all(
932
+ missing.map(async (pkgName) => {
933
+ const version = await fetchPackageVersion(pkgName);
934
+ return { pkgName, version };
935
+ })
936
+ );
937
+ const updatedDevDeps = { ...devDeps };
938
+ for (const { pkgName, version } of results) {
939
+ updatedDevDeps[pkgName] = `^${version}`;
940
+ }
941
+ pkg.devDependencies = updatedDevDeps;
942
+ writeJson(pkgPath, pkg);
943
+ return results.map((r) => r.pkgName);
944
+ }
746
945
  async function installDevDeps(packages, cwd, pm) {
747
946
  const manager = pm ?? detectPackageManager(cwd);
748
947
  const pkg = readJson(path3.join(cwd, "package.json"));
@@ -794,9 +993,12 @@ function isNotStylelintDep(dep) {
794
993
  if (dep === "postcss-html" || dep === "postcss-scss") return false;
795
994
  return true;
796
995
  }
996
+ function isNotEditorconfigDep(dep) {
997
+ return !dep.includes("editorconfig");
998
+ }
797
999
  function registerFmtCommand(program2) {
798
1000
  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(
1001
+ 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").action(
800
1002
  async (presetName, options) => {
801
1003
  const preset = resolvePreset(FMT_PRESETS, presetName);
802
1004
  if (!preset) return;
@@ -806,7 +1008,8 @@ function registerFmtCommand(program2) {
806
1008
  cwd,
807
1009
  force: options.force ?? false,
808
1010
  dryRun: options.dryRun ?? false,
809
- noStylelint: options.stylelint === false,
1011
+ noStylelint: options.stylelint !== true,
1012
+ noEditorconfig: options.editorconfig !== true,
810
1013
  lockfile: pm ? getLockfileName(pm) : void 0
811
1014
  };
812
1015
  const result = generateAllFmt(preset, opts);
@@ -826,20 +1029,27 @@ function registerFmtCommand(program2) {
826
1029
  }
827
1030
  if (!preset.dependencies?.dev) return;
828
1031
  const devDeps = opts.noStylelint ? preset.dependencies.dev.filter(isNotStylelintDep) : preset.dependencies.dev;
1032
+ const finalDeps = opts.noEditorconfig ? devDeps.filter(isNotEditorconfigDep) : devDeps;
829
1033
  if (options.install === false) {
830
- logger.log(`Dependencies: ${devDeps.join(", ")}`);
1034
+ const added = await addDepsToManifest(finalDeps, cwd);
1035
+ if (added.length > 0) {
1036
+ logger.success(`Added to package.json (skipped install): ${added.join(", ")}`);
1037
+ } else {
1038
+ logger.log("All dependencies already in package.json");
1039
+ }
831
1040
  return;
832
1041
  }
833
1042
  if (opts.dryRun) {
834
- logger.log(`[dry-run] Would install: ${devDeps.join(", ")}`);
1043
+ logger.log(`[dry-run] Would install: ${finalDeps.join(", ")}`);
835
1044
  return;
836
1045
  }
837
1046
  try {
838
1047
  logger.log(`Installing dependencies with ${pm}...`);
839
- await installDevDeps(devDeps, cwd, pm);
1048
+ await installDevDeps(finalDeps, cwd, pm);
840
1049
  logger.success("Dependencies installed successfully");
841
- } catch {
842
- logger.warn("Dependency installation failed. You can install manually.");
1050
+ } catch (error) {
1051
+ const message = error instanceof Error ? error.message : String(error);
1052
+ logger.warn(`Dependency installation failed: ${message}. You can install manually.`);
843
1053
  }
844
1054
  }
845
1055
  );
@@ -931,19 +1141,109 @@ async function injectScripts(scripts, opts, pm) {
931
1141
  }
932
1142
  }
933
1143
 
934
- // src/utils/config.ts
1144
+ // src/commands/init.ts
1145
+ import { select, isCancel, cancel, outro } from "@clack/prompts";
1146
+
1147
+ // src/presets/init.ts
1148
+ var INIT_TOOLS = [
1149
+ {
1150
+ name: "claude",
1151
+ label: "Claude Code",
1152
+ targetDir: ".claude/skills"
1153
+ },
1154
+ {
1155
+ name: "opencode",
1156
+ label: "OpenCode",
1157
+ targetDir: ".opencode/skills"
1158
+ }
1159
+ ];
1160
+
1161
+ // src/generators/init.ts
935
1162
  import fs2 from "fs";
936
- import os from "os";
937
1163
  import path5 from "path";
1164
+ function resolveSkillsDir() {
1165
+ const entryDir = path5.dirname(process.argv[1] ?? "");
1166
+ return path5.resolve(entryDir, "skills");
1167
+ }
1168
+ function listFilesRecursive(dir, base) {
1169
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
1170
+ const files = [];
1171
+ for (const entry of entries) {
1172
+ const childBase = `${base}/${entry.name}`;
1173
+ const fullPath = path5.join(dir, entry.name);
1174
+ if (entry.isDirectory()) {
1175
+ files.push(...listFilesRecursive(fullPath, childBase));
1176
+ } else {
1177
+ files.push(childBase);
1178
+ }
1179
+ }
1180
+ return files;
1181
+ }
1182
+ function generateInitSkills(targetBaseDir, cwd) {
1183
+ const skillsDir = resolveSkillsDir();
1184
+ if (!fs2.existsSync(skillsDir)) {
1185
+ logger.error(`Bundled skills directory not found: ${skillsDir}`);
1186
+ logger.error('Please run "lux build" or reinstall lux.');
1187
+ return { copiedFiles: [], targetDir: targetBaseDir };
1188
+ }
1189
+ const targetPath = path5.resolve(cwd, targetBaseDir);
1190
+ try {
1191
+ fs2.cpSync(skillsDir, targetPath, { recursive: true, force: true });
1192
+ } catch (error) {
1193
+ const message = error instanceof Error ? error.message : String(error);
1194
+ logger.error(`Failed to copy skills to ${targetPath}: ${message}`);
1195
+ return { copiedFiles: [], targetDir: targetBaseDir };
1196
+ }
1197
+ const copiedFiles = fs2.existsSync(targetPath) ? listFilesRecursive(targetPath, targetBaseDir) : [];
1198
+ return { copiedFiles, targetDir: targetBaseDir };
1199
+ }
1200
+
1201
+ // src/commands/init.ts
1202
+ function registerInitCommand(program2) {
1203
+ program2.command("init").description("Initialize AI coding tool skills in current project").action(async () => {
1204
+ const toolOptions = INIT_TOOLS.map((tool2) => ({
1205
+ value: tool2.name,
1206
+ label: tool2.label
1207
+ }));
1208
+ const selected = await select({
1209
+ message: "Which AI coding tool do you use?",
1210
+ options: toolOptions
1211
+ });
1212
+ if (isCancel(selected)) {
1213
+ cancel("Operation cancelled.");
1214
+ return;
1215
+ }
1216
+ const tool = INIT_TOOLS.find((t) => t.name === selected);
1217
+ if (!tool) {
1218
+ logger.error(`Unknown tool: ${String(selected)}`);
1219
+ return;
1220
+ }
1221
+ const cwd = process.cwd();
1222
+ const result = generateInitSkills(tool.targetDir, cwd);
1223
+ if (result.copiedFiles.length === 0) {
1224
+ logger.warn("No skill files were copied.");
1225
+ return;
1226
+ }
1227
+ for (const file of result.copiedFiles) {
1228
+ logger.log(` ${file}`);
1229
+ }
1230
+ outro(`Skills installed to ${tool.targetDir}/`);
1231
+ });
1232
+ }
1233
+
1234
+ // src/utils/config.ts
1235
+ import fs3 from "fs";
1236
+ import os from "os";
1237
+ import path6 from "path";
938
1238
  var CONFIG_DIR = ".lux";
939
1239
  var ENV_FILE = "env.txt";
940
1240
  function getEnvConfigPath() {
941
- return path5.join(os.homedir(), CONFIG_DIR, ENV_FILE);
1241
+ return path6.join(os.homedir(), CONFIG_DIR, ENV_FILE);
942
1242
  }
943
1243
  function getEnvConfig() {
944
1244
  let content;
945
1245
  try {
946
- content = fs2.readFileSync(getEnvConfigPath(), "utf-8");
1246
+ content = fs3.readFileSync(getEnvConfigPath(), "utf-8");
947
1247
  } catch {
948
1248
  return {};
949
1249
  }
@@ -966,7 +1266,7 @@ function setEnvConfig(data) {
966
1266
  }
967
1267
  function clearEnvConfig() {
968
1268
  try {
969
- fs2.unlinkSync(getEnvConfigPath());
1269
+ fs3.unlinkSync(getEnvConfigPath());
970
1270
  } catch {
971
1271
  }
972
1272
  }
@@ -988,27 +1288,6 @@ function registerShowCommand(program2) {
988
1288
  show.command("env").description("Display stored proxy environment variables").action(() => handleShowEnv());
989
1289
  }
990
1290
 
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
1291
  // src/utils/version.ts
1013
1292
  import { existsSync, readFileSync } from "fs";
1014
1293
  import { dirname, join } from "path";
@@ -1206,6 +1485,138 @@ var webVueVscode = {
1206
1485
  ]
1207
1486
  };
1208
1487
 
1488
+ // src/presets/vscode/web-react.ts
1489
+ var webReactVscode = {
1490
+ name: "web-react",
1491
+ description: "VSCode config for React Web",
1492
+ settings: () => ({
1493
+ // ===== Editor Preferences =====
1494
+ "editor.tabSize": 2,
1495
+ "editor.detectIndentation": false,
1496
+ "editor.insertSpaces": true,
1497
+ "editor.renderWhitespace": "selection",
1498
+ "editor.guides.indentation": true,
1499
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1500
+ "editor.formatOnSave": true,
1501
+ "editor.codeActionsOnSave": {
1502
+ "source.fixAll.eslint": "explicit",
1503
+ "source.fixAll.stylelint": "explicit",
1504
+ "source.organizeImports": "never"
1505
+ },
1506
+ // Cursor & Animation
1507
+ "editor.cursorBlinking": "expand",
1508
+ "editor.cursorSmoothCaretAnimation": "on",
1509
+ "editor.largeFileOptimizations": true,
1510
+ // Code Assistance
1511
+ "editor.inlineSuggest.enabled": true,
1512
+ "editor.suggestSelection": "recentlyUsedByPrefix",
1513
+ "editor.acceptSuggestionOnEnter": "smart",
1514
+ "editor.bracketPairColorization.enabled": true,
1515
+ "editor.autoClosingBrackets": "beforeWhitespace",
1516
+ "editor.autoClosingOvertype": "always",
1517
+ // ===== TypeScript =====
1518
+ "js/ts.inlayHints.enumMemberValues.enabled": true,
1519
+ "js/ts.preferences.preferTypeOnlyAutoImports": true,
1520
+ "js/ts.preferences.includePackageJsonAutoImports": "on",
1521
+ "js/ts.preferences.importModuleSpecifier": "relative",
1522
+ "js/ts.suggest.autoImports": true,
1523
+ "js/ts.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
1524
+ // ===== Language-specific Formatting =====
1525
+ "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1526
+ "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1527
+ "[scss]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1528
+ "[typescript]": {
1529
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1530
+ "editor.formatOnSave": true
1531
+ },
1532
+ "[javascript]": {
1533
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1534
+ "editor.formatOnSave": true
1535
+ },
1536
+ "[typescriptreact]": {
1537
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1538
+ "editor.formatOnSave": true
1539
+ },
1540
+ "[javascriptreact]": {
1541
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
1542
+ "editor.formatOnSave": true
1543
+ },
1544
+ "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1545
+ "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
1546
+ // ===== Terminal =====
1547
+ "terminal.integrated.cursorBlinking": true,
1548
+ "terminal.integrated.tabs.enabled": true,
1549
+ "terminal.integrated.scrollback": 1e4,
1550
+ // ===== File Exclusion =====
1551
+ "files.watcherExclude": {
1552
+ "**/.git/objects/**": true,
1553
+ "**/.git/subtree-cache/**": true,
1554
+ "**/.vscode/**": true,
1555
+ "**/node_modules/**": true,
1556
+ "**/tmp/**": true,
1557
+ "**/dist/**": true,
1558
+ "**/pnpm-lock.yaml": true,
1559
+ "**/package-lock.json": true,
1560
+ "**/bun.lock": true,
1561
+ "**/yarn.lock": true
1562
+ },
1563
+ "search.exclude": {
1564
+ "**/node_modules": true,
1565
+ "**/*.log": true,
1566
+ "**/*.log*": true,
1567
+ "**/dist": true,
1568
+ "**/.git": true,
1569
+ "**/.vscode": false,
1570
+ "**/tmp": true,
1571
+ node_modules: true,
1572
+ "**/pnpm-lock.yaml": true,
1573
+ "**/package-lock.json": true,
1574
+ "**/bun.lock": true,
1575
+ "**/yarn.lock": true
1576
+ },
1577
+ // ===== File Nesting =====
1578
+ "explorer.fileNesting.enabled": true,
1579
+ "explorer.fileNesting.expand": false,
1580
+ "explorer.fileNesting.patterns": {
1581
+ "package.json": "pnpm-lock.yaml,yarn.lock,bun.lock, .gitignore, .browserslistrc, .npmrc, cspell.json,README.md, LICENSE*,.editorconfig",
1582
+ "eslint.config.mjs": ".prettierignore, .prettierrc, .prettierrc.json, .editorconfig",
1583
+ "tsconfig.json": "tsconfig.*.json",
1584
+ "tailwind.config.js": "postcss.config.js",
1585
+ "vite.config.{js,ts}": "vite.*.{js,ts}",
1586
+ ".env": ".env.*"
1587
+ },
1588
+ // ===== ESLint =====
1589
+ "eslint.validate": [
1590
+ "javascript",
1591
+ "typescript",
1592
+ "javascriptreact",
1593
+ "typescriptreact",
1594
+ "html",
1595
+ "markdown",
1596
+ "json",
1597
+ "jsonc",
1598
+ "json5"
1599
+ ],
1600
+ // ===== Stylelint =====
1601
+ "stylelint.enable": true,
1602
+ "stylelint.validate": ["css", "scss"],
1603
+ "stylelint.snippet": ["css", "scss"],
1604
+ "css.validate": false,
1605
+ "less.validate": false,
1606
+ "scss.validate": false,
1607
+ // ===== CSpell =====
1608
+ "cSpell.language": "en"
1609
+ }),
1610
+ extensions: () => [
1611
+ "dbaeumer.vscode-eslint",
1612
+ "esbenp.prettier-vscode",
1613
+ "stylelint.vscode-stylelint",
1614
+ "mrmlnc.vscode-scss",
1615
+ "streetsidesoftware.code-spell-checker",
1616
+ "editorconfig.editorconfig"
1617
+ ]
1618
+ };
1619
+
1209
1620
  // src/presets/vscode/electron.ts
1210
1621
  var electronVueVscode = {
1211
1622
  name: "electron-vue",
@@ -1697,6 +2108,7 @@ var goVscode = {
1697
2108
  // src/presets/vscode/index.ts
1698
2109
  var VSCODE_PRESETS = [
1699
2110
  webVueVscode,
2111
+ webReactVscode,
1700
2112
  electronVueVscode,
1701
2113
  uniappVscode,
1702
2114
  nodeVscode,
@@ -1769,20 +2181,45 @@ function generateVscodeSettings(preset, opts) {
1769
2181
  if (existingSettings) {
1770
2182
  const backupPath = `${settingsPath}.bak`;
1771
2183
  if (!fileExists(backupPath)) {
1772
- writeFile(backupPath, JSON.stringify(existingSettings, null, 2) + "\n");
1773
- logger.log(`Backed up .vscode/settings.json \u2192 settings.json.bak`);
2184
+ try {
2185
+ writeFile(backupPath, JSON.stringify(existingSettings, null, 2) + "\n");
2186
+ logger.log("Backed up .vscode/settings.json \u2192 settings.json.bak");
2187
+ } catch (error) {
2188
+ const message = error instanceof Error ? error.message : String(error);
2189
+ logger.warn(
2190
+ `Failed to backup .vscode/settings.json: ${message}. Continuing without backup.`
2191
+ );
2192
+ }
2193
+ }
2194
+ try {
2195
+ const merged = mergeVscodeSettings(presetSettings, existingSettings);
2196
+ writeJson(settingsPath, merged);
2197
+ } catch (error) {
2198
+ const msg = error instanceof Error ? error.message : String(error);
2199
+ logger.error(`Failed to write .vscode/settings.json: ${msg}`);
2200
+ return null;
1774
2201
  }
1775
- const merged = mergeVscodeSettings(presetSettings, existingSettings);
1776
- writeJson(settingsPath, merged);
1777
2202
  return "overwritten";
1778
2203
  }
1779
- writeJson(settingsPath, presetSettings);
2204
+ try {
2205
+ writeJson(settingsPath, presetSettings);
2206
+ } catch (error) {
2207
+ const msg = error instanceof Error ? error.message : String(error);
2208
+ logger.error(`Failed to write .vscode/settings.json: ${msg}`);
2209
+ return null;
2210
+ }
1780
2211
  return "created";
1781
2212
  }
1782
2213
  function generateVscodeExtensions(preset, opts) {
1783
2214
  if (opts.dryRun) return "created";
1784
2215
  const extensions = opts.noStylelint ? preset.extensions().filter((ext) => ext !== STYLELINT_EXTENSION) : preset.extensions();
1785
- writeJson(`${opts.cwd}/.vscode/extensions.json`, { recommendations: extensions });
2216
+ try {
2217
+ writeJson(`${opts.cwd}/.vscode/extensions.json`, { recommendations: extensions });
2218
+ } catch (error) {
2219
+ const msg = error instanceof Error ? error.message : String(error);
2220
+ logger.error(`Failed to write .vscode/extensions.json: ${msg}`);
2221
+ return null;
2222
+ }
1786
2223
  return "created";
1787
2224
  }
1788
2225
  function generateAllVscode(preset, opts) {
@@ -1811,7 +2248,7 @@ function filterStylelintSettings(settings) {
1811
2248
  // src/commands/vscode.ts
1812
2249
  function registerVscodeCommand(program2) {
1813
2250
  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(
2251
+ vscode.argument("<preset>").option("-F, --force", "Force overwrite existing files").option("--dry-run", "Preview without writing files").option("--stylelint", "Include Stylelint settings and extension").action(
1815
2252
  async (presetName, options) => {
1816
2253
  const preset = resolvePreset(VSCODE_PRESETS, presetName);
1817
2254
  if (!preset) return;
@@ -1820,7 +2257,8 @@ function registerVscodeCommand(program2) {
1820
2257
  cwd,
1821
2258
  force: options.force ?? false,
1822
2259
  dryRun: options.dryRun ?? false,
1823
- noStylelint: options.stylelint === false
2260
+ noStylelint: options.stylelint !== true,
2261
+ noEditorconfig: false
1824
2262
  };
1825
2263
  const result = generateAllVscode(preset, opts);
1826
2264
  const files = [...result.created, ...result.overwritten];
@@ -1921,6 +2359,7 @@ function registerVpnCommand(program2) {
1921
2359
  // src/index.ts
1922
2360
  program.name("lux").description("One-click project formatting & VSCode config CLI").version(getCurrentVersion());
1923
2361
  registerFmtCommand(program);
2362
+ registerInitCommand(program);
1924
2363
  registerVscodeCommand(program);
1925
2364
  registerVpnCommand(program);
1926
2365
  registerShowCommand(program);
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: lux
3
+ description: Use when setting up ESLint, Prettier, CSpell, Stylelint, EditorConfig, VSCode workspace settings, proxy env
4
+ ---
5
+
6
+ ## fmt — generate lint/format configs
7
+
8
+ ```bash
9
+ lux fmt <preset> [--stylelint] [--editorconfig]
10
+ lux fmt list
11
+ ```
12
+
13
+ - `--force` — overwrite existing config files (default: skip)
14
+ - `--dry-run` — preview what would be generated, write nothing
15
+ - `--no-install` — write deps to package.json but skip install
16
+
17
+ Presets: `web-vue` `web-react` `electron-vue` `uniapp` `node` `nest`
18
+
19
+ ## vscode — generate editor settings
20
+
21
+ ```bash
22
+ lux vscode <preset> [--dry-run]
23
+ lux vscode list
24
+ ```
25
+
26
+ Presets: `web-vue` `web-react` `electron-vue` `uniapp` `node` `nest` `go`
27
+
28
+ ## init — lux skill(human interaction)
29
+
30
+ ```bash
31
+ lux init
32
+ ```
33
+
34
+ ## vpn — proxy clipboard helper
35
+
36
+ ```bash
37
+ lux vpn cmd # copy CMD proxy commands
38
+ lux vpn pw # copy PowerShell proxy commands
39
+ lux vpn bash # copy Bash proxy commands
40
+ ```
41
+
42
+ ## env — proxy env management
43
+
44
+ ```bash
45
+ lux set https_proxy=http://127.0.0.1:7890
46
+ lux unset # clear all proxy config
47
+ lux show env # show stored proxy env
48
+ ```
49
+
50
+ ## update — self-update
51
+
52
+ ```bash
53
+ lux update # update to latest
54
+ lux update --check
55
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxkit/cli",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "One-click project formatting & VSCode config CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,27 +13,39 @@
13
13
  "access": "public"
14
14
  },
15
15
  "scripts": {
16
- "build": "tsup",
16
+ "build": "tsup && node scripts/copy-assets.mjs",
17
17
  "dev": "tsup --watch",
18
18
  "lint": "eslint .",
19
- "lint:fix": "eslint \"src/**/*.{js,ts}\" --fix",
20
19
  "format": "prettier --write \"src/**/*.{ts,js,json}\"",
21
- "format:check": "prettier --check \"src/**/*.{ts,js,json}\"",
22
20
  "cspell": "cspell --gitignore \"src/**/*\"",
21
+ "format:check": "prettier --check \"src/**/*.{ts,js,json}\"",
23
22
  "type:check": "tsc --noEmit",
24
23
  "code:check": "bun run lint && bun run format:check",
25
- "code:fix": "bun run lint:fix && bun run format",
26
24
  "code:check:all": "bun run lint && bun run format:check && bun run cspell",
25
+ "lint:fix": "eslint \"src/**/*.{js,ts}\" --fix",
26
+ "code:fix": "bun run lint:fix && bun run format",
27
27
  "code:fix:all": "bun run lint:fix && bun run format",
28
28
  "prepublishOnly": "cross-env NODE_ENV=production bun run build",
29
29
  "test": "vitest run",
30
30
  "test:watch": "vitest",
31
31
  "test:coverage": "vitest run --coverage"
32
32
  },
33
- "keywords": [],
34
- "author": "",
33
+ "keywords": [
34
+ "cli",
35
+ "eslint",
36
+ "prettier",
37
+ "stylelint",
38
+ "cspell",
39
+ "editorconfig",
40
+ "vscode",
41
+ "formatting",
42
+ "config-generator",
43
+ "preset"
44
+ ],
45
+ "author": "TTT1231",
35
46
  "license": "ISC",
36
47
  "dependencies": {
48
+ "@clack/prompts": "^1.4.0",
37
49
  "chalk": "5",
38
50
  "commander": "^14.0.3"
39
51
  },