@luxkit/cli 1.0.7 → 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"));
@@ -781,9 +980,25 @@ async function installDevDeps(packages, cwd, pm) {
781
980
  }
782
981
 
783
982
  // src/commands/fmt.ts
983
+ function filterStylelintScripts(scripts) {
984
+ const filtered = {};
985
+ for (const [key, value] of Object.entries(scripts)) {
986
+ if (key.startsWith("stylelint")) continue;
987
+ filtered[key] = value.replace(/\s*&&\s*<pm>\s+stylelint\S*/g, "");
988
+ }
989
+ return filtered;
990
+ }
991
+ function isNotStylelintDep(dep) {
992
+ if (dep.includes("stylelint")) return false;
993
+ if (dep === "postcss-html" || dep === "postcss-scss") return false;
994
+ return true;
995
+ }
996
+ function isNotEditorconfigDep(dep) {
997
+ return !dep.includes("editorconfig");
998
+ }
784
999
  function registerFmtCommand(program2) {
785
1000
  const fmt = program2.command("fmt").description("Initialize formatting config with preset");
786
- 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(
787
1002
  async (presetName, options) => {
788
1003
  const preset = resolvePreset(FMT_PRESETS, presetName);
789
1004
  if (!preset) return;
@@ -793,7 +1008,8 @@ function registerFmtCommand(program2) {
793
1008
  cwd,
794
1009
  force: options.force ?? false,
795
1010
  dryRun: options.dryRun ?? false,
796
- noStylelint: options.stylelint === false,
1011
+ noStylelint: options.stylelint !== true,
1012
+ noEditorconfig: options.editorconfig !== true,
797
1013
  lockfile: pm ? getLockfileName(pm) : void 0
798
1014
  };
799
1015
  const result = generateAllFmt(preset, opts);
@@ -807,24 +1023,33 @@ function registerFmtCommand(program2) {
807
1023
  warnMissingPackageJson(preset, options.install !== false);
808
1024
  return;
809
1025
  }
810
- if (preset.scripts) {
811
- await injectScripts(preset.scripts, opts, pm);
1026
+ const scripts = opts.noStylelint && preset.scripts ? filterStylelintScripts(preset.scripts) : preset.scripts;
1027
+ if (scripts) {
1028
+ await injectScripts(scripts, opts, pm);
812
1029
  }
813
1030
  if (!preset.dependencies?.dev) return;
1031
+ const devDeps = opts.noStylelint ? preset.dependencies.dev.filter(isNotStylelintDep) : preset.dependencies.dev;
1032
+ const finalDeps = opts.noEditorconfig ? devDeps.filter(isNotEditorconfigDep) : devDeps;
814
1033
  if (options.install === false) {
815
- logger.log(`Dependencies: ${preset.dependencies.dev.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
+ }
816
1040
  return;
817
1041
  }
818
1042
  if (opts.dryRun) {
819
- logger.log(`[dry-run] Would install: ${preset.dependencies.dev.join(", ")}`);
1043
+ logger.log(`[dry-run] Would install: ${finalDeps.join(", ")}`);
820
1044
  return;
821
1045
  }
822
1046
  try {
823
1047
  logger.log(`Installing dependencies with ${pm}...`);
824
- await installDevDeps(preset.dependencies.dev, cwd, pm);
1048
+ await installDevDeps(finalDeps, cwd, pm);
825
1049
  logger.success("Dependencies installed successfully");
826
- } catch {
827
- 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.`);
828
1053
  }
829
1054
  }
830
1055
  );
@@ -916,19 +1141,109 @@ async function injectScripts(scripts, opts, pm) {
916
1141
  }
917
1142
  }
918
1143
 
919
- // 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
920
1162
  import fs2 from "fs";
921
- import os from "os";
922
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";
923
1238
  var CONFIG_DIR = ".lux";
924
1239
  var ENV_FILE = "env.txt";
925
1240
  function getEnvConfigPath() {
926
- return path5.join(os.homedir(), CONFIG_DIR, ENV_FILE);
1241
+ return path6.join(os.homedir(), CONFIG_DIR, ENV_FILE);
927
1242
  }
928
1243
  function getEnvConfig() {
929
1244
  let content;
930
1245
  try {
931
- content = fs2.readFileSync(getEnvConfigPath(), "utf-8");
1246
+ content = fs3.readFileSync(getEnvConfigPath(), "utf-8");
932
1247
  } catch {
933
1248
  return {};
934
1249
  }
@@ -951,7 +1266,7 @@ function setEnvConfig(data) {
951
1266
  }
952
1267
  function clearEnvConfig() {
953
1268
  try {
954
- fs2.unlinkSync(getEnvConfigPath());
1269
+ fs3.unlinkSync(getEnvConfigPath());
955
1270
  } catch {
956
1271
  }
957
1272
  }
@@ -973,27 +1288,6 @@ function registerShowCommand(program2) {
973
1288
  show.command("env").description("Display stored proxy environment variables").action(() => handleShowEnv());
974
1289
  }
975
1290
 
976
- // src/utils/execFileNoThrow.ts
977
- import { exec } from "child_process";
978
- import { promisify } from "util";
979
- var execAsync = promisify(exec);
980
- async function execFileNoThrow(command, args, options) {
981
- const cmdStr = args.length > 0 ? `${command} ${args.join(" ")}` : command;
982
- try {
983
- const { stdout, stderr } = await execAsync(cmdStr, {
984
- cwd: options?.cwd
985
- });
986
- return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
987
- } catch (err) {
988
- const error = err;
989
- return {
990
- stdout: (error.stdout ?? "").trim(),
991
- stderr: (error.stderr ?? "").trim(),
992
- exitCode: error.code === "ENOENT" ? null : 1
993
- };
994
- }
995
- }
996
-
997
1291
  // src/utils/version.ts
998
1292
  import { existsSync, readFileSync } from "fs";
999
1293
  import { dirname, join } from "path";
@@ -1191,6 +1485,138 @@ var webVueVscode = {
1191
1485
  ]
1192
1486
  };
1193
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
+
1194
1620
  // src/presets/vscode/electron.ts
1195
1621
  var electronVueVscode = {
1196
1622
  name: "electron-vue",
@@ -1682,6 +2108,7 @@ var goVscode = {
1682
2108
  // src/presets/vscode/index.ts
1683
2109
  var VSCODE_PRESETS = [
1684
2110
  webVueVscode,
2111
+ webReactVscode,
1685
2112
  electronVueVscode,
1686
2113
  uniappVscode,
1687
2114
  nodeVscode,
@@ -1754,20 +2181,45 @@ function generateVscodeSettings(preset, opts) {
1754
2181
  if (existingSettings) {
1755
2182
  const backupPath = `${settingsPath}.bak`;
1756
2183
  if (!fileExists(backupPath)) {
1757
- writeFile(backupPath, JSON.stringify(existingSettings, null, 2) + "\n");
1758
- 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;
1759
2201
  }
1760
- const merged = mergeVscodeSettings(presetSettings, existingSettings);
1761
- writeJson(settingsPath, merged);
1762
2202
  return "overwritten";
1763
2203
  }
1764
- 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
+ }
1765
2211
  return "created";
1766
2212
  }
1767
2213
  function generateVscodeExtensions(preset, opts) {
1768
2214
  if (opts.dryRun) return "created";
1769
2215
  const extensions = opts.noStylelint ? preset.extensions().filter((ext) => ext !== STYLELINT_EXTENSION) : preset.extensions();
1770
- 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
+ }
1771
2223
  return "created";
1772
2224
  }
1773
2225
  function generateAllVscode(preset, opts) {
@@ -1796,7 +2248,7 @@ function filterStylelintSettings(settings) {
1796
2248
  // src/commands/vscode.ts
1797
2249
  function registerVscodeCommand(program2) {
1798
2250
  const vscode = program2.command("vscode").description("Initialize VSCode config with preset");
1799
- 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(
1800
2252
  async (presetName, options) => {
1801
2253
  const preset = resolvePreset(VSCODE_PRESETS, presetName);
1802
2254
  if (!preset) return;
@@ -1805,7 +2257,8 @@ function registerVscodeCommand(program2) {
1805
2257
  cwd,
1806
2258
  force: options.force ?? false,
1807
2259
  dryRun: options.dryRun ?? false,
1808
- noStylelint: options.stylelint === false
2260
+ noStylelint: options.stylelint !== true,
2261
+ noEditorconfig: false
1809
2262
  };
1810
2263
  const result = generateAllVscode(preset, opts);
1811
2264
  const files = [...result.created, ...result.overwritten];
@@ -1906,6 +2359,7 @@ function registerVpnCommand(program2) {
1906
2359
  // src/index.ts
1907
2360
  program.name("lux").description("One-click project formatting & VSCode config CLI").version(getCurrentVersion());
1908
2361
  registerFmtCommand(program);
2362
+ registerInitCommand(program);
1909
2363
  registerVscodeCommand(program);
1910
2364
  registerVpnCommand(program);
1911
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.7",
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
  },