@luxkit/cli 1.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +583 -131
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -111,6 +111,7 @@ lux fmt <preset> [options]
111
111
  --dry-run Preview without writing files
112
112
  --stylelint Include Stylelint config generation (opt-in)
113
113
  --editorconfig Include EditorConfig config generation (opt-in)
114
+ --reset Reset local preset and re-create from built-in defaults
114
115
  ```
115
116
 
116
117
  <br />
@@ -124,6 +125,12 @@ lux fmt web-vue
124
125
  Parse CLI args ──► Resolve preset (fuzzy match on typo)
125
126
 
126
127
 
128
+ Local preset exists in ~/.lux/preset/?
129
+
130
+ ├── Yes ──► Copy files from local preset (editable, survives updates)
131
+ └── No ──► Generate from built-in ──► Save to ~/.lux/preset/ for reuse
132
+
133
+
127
134
  For each config file:
128
135
 
129
136
  ├── File not found? ──► Create
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 = {
@@ -979,6 +980,347 @@ async function installDevDeps(packages, cwd, pm) {
979
980
  }
980
981
  }
981
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
+
982
1324
  // src/commands/fmt.ts
983
1325
  function filterStylelintScripts(scripts) {
984
1326
  const filtered = {};
@@ -998,58 +1340,30 @@ function isNotEditorconfigDep(dep) {
998
1340
  }
999
1341
  function registerFmtCommand(program2) {
1000
1342
  const fmt = program2.command("fmt").description("Initialize formatting config with preset");
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(
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(
1002
1344
  async (presetName, options) => {
1003
1345
  const preset = resolvePreset(FMT_PRESETS, presetName);
1004
1346
  if (!preset) return;
1005
1347
  const cwd = process.cwd();
1006
- const pm = fileExists(path4.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
1007
- const opts = {
1008
- cwd,
1009
- force: options.force ?? false,
1010
- dryRun: options.dryRun ?? false,
1011
- noStylelint: options.stylelint !== true,
1012
- noEditorconfig: options.editorconfig !== true,
1013
- lockfile: pm ? getLockfileName(pm) : void 0
1014
- };
1015
- const result = generateAllFmt(preset, opts);
1016
- const allFiles = [...result.created, ...result.overwritten];
1017
- if (allFiles.length === 0 && result.skipped.length === 0) {
1018
- logger.warn("No files to generate for this preset");
1019
- return;
1020
- }
1021
- logGenerationResult(result, opts.dryRun);
1022
- if (!pm) {
1023
- warnMissingPackageJson(preset, options.install !== false);
1024
- return;
1025
- }
1026
- const scripts = opts.noStylelint && preset.scripts ? filterStylelintScripts(preset.scripts) : preset.scripts;
1027
- if (scripts) {
1028
- await injectScripts(scripts, opts, pm);
1029
- }
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;
1033
- if (options.install === false) {
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");
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;
1039
1357
  }
1040
- return;
1041
1358
  }
1042
- if (opts.dryRun) {
1043
- logger.log(`[dry-run] Would install: ${finalDeps.join(", ")}`);
1044
- return;
1359
+ if (options.reset) {
1360
+ resetLocalPreset("fmt", presetName);
1045
1361
  }
1046
- try {
1047
- logger.log(`Installing dependencies with ${pm}...`);
1048
- await installDevDeps(finalDeps, cwd, pm);
1049
- logger.success("Dependencies installed successfully");
1050
- } catch (error) {
1051
- const message = error instanceof Error ? error.message : String(error);
1052
- logger.warn(`Dependency installation failed: ${message}. 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);
1053
1367
  }
1054
1368
  }
1055
1369
  );
@@ -1059,6 +1373,139 @@ function registerFmtCommand(program2) {
1059
1373
  }
1060
1374
  });
1061
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
+ }
1062
1509
  function logGenerationResult(result, dryRun) {
1063
1510
  const files = [...result.created, ...result.overwritten];
1064
1511
  if (dryRun) {
@@ -1086,6 +1533,23 @@ function logGenerationResult(result, dryRun) {
1086
1533
  );
1087
1534
  }
1088
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`
1540
+ );
1541
+ }
1542
+ if (result.overwritten.length > 0) {
1543
+ logger.log(
1544
+ `Overwritten ${summarizeFiles(result.overwritten)} config ${result.overwritten.length} file${result.overwritten.length > 1 ? "s" : ""} from local preset`
1545
+ );
1546
+ }
1547
+ if (result.skipped.length > 0) {
1548
+ logger.log(
1549
+ `Skipped ${result.skipped.length} file${result.skipped.length > 1 ? "s" : ""} (already exists)`
1550
+ );
1551
+ }
1552
+ }
1089
1553
  function warnMissingPackageJson(preset, installEnabled) {
1090
1554
  const tasks = [];
1091
1555
  if (preset.scripts) tasks.push("script injection");
@@ -1106,7 +1570,7 @@ function summarizeFiles(filenames) {
1106
1570
  return [...categories].join(", ");
1107
1571
  }
1108
1572
  async function injectScripts(scripts, opts, pm) {
1109
- const pkgPath = path4.join(opts.cwd, "package.json");
1573
+ const pkgPath = path5.join(opts.cwd, "package.json");
1110
1574
  const pkg = readJson(pkgPath);
1111
1575
  if (!pkg) {
1112
1576
  logger.warn("package.json not found, skipping script injection");
@@ -1159,18 +1623,18 @@ var INIT_TOOLS = [
1159
1623
  ];
1160
1624
 
1161
1625
  // src/generators/init.ts
1162
- import fs2 from "fs";
1163
- import path5 from "path";
1626
+ import fs4 from "fs";
1627
+ import path6 from "path";
1164
1628
  function resolveSkillsDir() {
1165
- const entryDir = path5.dirname(process.argv[1] ?? "");
1166
- return path5.resolve(entryDir, "skills");
1629
+ const entryDir = path6.dirname(process.argv[1] ?? "");
1630
+ return path6.resolve(entryDir, "skills");
1167
1631
  }
1168
1632
  function listFilesRecursive(dir, base) {
1169
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
1633
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
1170
1634
  const files = [];
1171
1635
  for (const entry of entries) {
1172
1636
  const childBase = `${base}/${entry.name}`;
1173
- const fullPath = path5.join(dir, entry.name);
1637
+ const fullPath = path6.join(dir, entry.name);
1174
1638
  if (entry.isDirectory()) {
1175
1639
  files.push(...listFilesRecursive(fullPath, childBase));
1176
1640
  } else {
@@ -1181,20 +1645,20 @@ function listFilesRecursive(dir, base) {
1181
1645
  }
1182
1646
  function generateInitSkills(targetBaseDir, cwd) {
1183
1647
  const skillsDir = resolveSkillsDir();
1184
- if (!fs2.existsSync(skillsDir)) {
1648
+ if (!fs4.existsSync(skillsDir)) {
1185
1649
  logger.error(`Bundled skills directory not found: ${skillsDir}`);
1186
1650
  logger.error('Please run "lux build" or reinstall lux.');
1187
1651
  return { copiedFiles: [], targetDir: targetBaseDir };
1188
1652
  }
1189
- const targetPath = path5.resolve(cwd, targetBaseDir);
1653
+ const targetPath = path6.resolve(cwd, targetBaseDir);
1190
1654
  try {
1191
- fs2.cpSync(skillsDir, targetPath, { recursive: true, force: true });
1655
+ fs4.cpSync(skillsDir, targetPath, { recursive: true, force: true });
1192
1656
  } catch (error) {
1193
1657
  const message = error instanceof Error ? error.message : String(error);
1194
1658
  logger.error(`Failed to copy skills to ${targetPath}: ${message}`);
1195
1659
  return { copiedFiles: [], targetDir: targetBaseDir };
1196
1660
  }
1197
- const copiedFiles = fs2.existsSync(targetPath) ? listFilesRecursive(targetPath, targetBaseDir) : [];
1661
+ const copiedFiles = fs4.existsSync(targetPath) ? listFilesRecursive(targetPath, targetBaseDir) : [];
1198
1662
  return { copiedFiles, targetDir: targetBaseDir };
1199
1663
  }
1200
1664
 
@@ -1232,18 +1696,18 @@ function registerInitCommand(program2) {
1232
1696
  }
1233
1697
 
1234
1698
  // src/utils/config.ts
1235
- import fs3 from "fs";
1236
- import os from "os";
1237
- import path6 from "path";
1699
+ import fs5 from "fs";
1700
+ import os2 from "os";
1701
+ import path7 from "path";
1238
1702
  var CONFIG_DIR = ".lux";
1239
1703
  var ENV_FILE = "env.txt";
1240
1704
  function getEnvConfigPath() {
1241
- return path6.join(os.homedir(), CONFIG_DIR, ENV_FILE);
1705
+ return path7.join(os2.homedir(), CONFIG_DIR, ENV_FILE);
1242
1706
  }
1243
1707
  function getEnvConfig() {
1244
1708
  let content;
1245
1709
  try {
1246
- content = fs3.readFileSync(getEnvConfigPath(), "utf-8");
1710
+ content = fs5.readFileSync(getEnvConfigPath(), "utf-8");
1247
1711
  } catch {
1248
1712
  return {};
1249
1713
  }
@@ -1266,7 +1730,7 @@ function setEnvConfig(data) {
1266
1730
  }
1267
1731
  function clearEnvConfig() {
1268
1732
  try {
1269
- fs3.unlinkSync(getEnvConfigPath());
1733
+ fs5.unlinkSync(getEnvConfigPath());
1270
1734
  } catch {
1271
1735
  }
1272
1736
  }
@@ -2116,59 +2580,14 @@ var VSCODE_PRESETS = [
2116
2580
  goVscode
2117
2581
  ];
2118
2582
 
2119
- // src/core/merge-settings.ts
2120
- var USER_PRIORITY_KEYS = /* @__PURE__ */ new Set([
2121
- // Cursor/animation
2122
- "editor.cursorBlinking",
2123
- "editor.cursorSmoothCaretAnimation",
2124
- "editor.renderWhitespace",
2125
- "editor.guides.indentation",
2126
- "editor.largeFileOptimizations",
2127
- // Theme/appearance
2128
- "workbench.iconTheme",
2129
- "workbench.colorTheme",
2130
- // Suggestions
2131
- "editor.inlineSuggest.enabled",
2132
- "editor.suggestSelection",
2133
- "editor.acceptSuggestionOnEnter",
2134
- "editor.bracketPairColorization.enabled",
2135
- "editor.autoClosingBrackets",
2136
- "editor.autoClosingOvertype"
2137
- ]);
2138
- function mergeVscodeSettings(preset, existing) {
2139
- const result = { ...existing };
2140
- for (const [key, presetVal] of Object.entries(preset)) {
2141
- const existingVal = existing[key];
2142
- if (existingVal === void 0) {
2143
- result[key] = presetVal;
2144
- continue;
2145
- }
2146
- if (USER_PRIORITY_KEYS.has(key)) {
2147
- continue;
2148
- }
2149
- if (isPlainObject(presetVal) && isPlainObject(existingVal)) {
2150
- result[key] = mergeVscodeSettings(
2151
- presetVal,
2152
- existingVal
2153
- );
2154
- continue;
2155
- }
2156
- result[key] = presetVal;
2157
- }
2158
- return result;
2159
- }
2160
- function isPlainObject(val) {
2161
- return typeof val === "object" && val !== null && !Array.isArray(val);
2162
- }
2163
-
2164
2583
  // src/generators/vscode.ts
2165
- var STYLELINT_SETTINGS_PREFIXES = [
2584
+ var STYLELINT_SETTINGS_PREFIXES2 = [
2166
2585
  "stylelint.",
2167
2586
  "css.validate",
2168
2587
  "less.validate",
2169
2588
  "scss.validate"
2170
2589
  ];
2171
- var STYLELINT_EXTENSION = "stylelint.vscode-stylelint";
2590
+ var STYLELINT_EXTENSION2 = "stylelint.vscode-stylelint";
2172
2591
  function generateVscodeSettings(preset, opts) {
2173
2592
  const settingsPath = `${opts.cwd}/.vscode/settings.json`;
2174
2593
  if (opts.dryRun) {
@@ -2176,7 +2595,7 @@ function generateVscodeSettings(preset, opts) {
2176
2595
  return existingSettings2 ? "overwritten" : "created";
2177
2596
  }
2178
2597
  const rawSettings = preset.settings();
2179
- const presetSettings = opts.noStylelint ? filterStylelintSettings(rawSettings) : rawSettings;
2598
+ const presetSettings = opts.noStylelint ? filterStylelintSettings2(rawSettings) : rawSettings;
2180
2599
  const existingSettings = readJson(settingsPath);
2181
2600
  if (existingSettings) {
2182
2601
  const backupPath = `${settingsPath}.bak`;
@@ -2212,7 +2631,7 @@ function generateVscodeSettings(preset, opts) {
2212
2631
  }
2213
2632
  function generateVscodeExtensions(preset, opts) {
2214
2633
  if (opts.dryRun) return "created";
2215
- const extensions = opts.noStylelint ? preset.extensions().filter((ext) => ext !== STYLELINT_EXTENSION) : preset.extensions();
2634
+ const extensions = opts.noStylelint ? preset.extensions().filter((ext) => ext !== STYLELINT_EXTENSION2) : preset.extensions();
2216
2635
  try {
2217
2636
  writeJson(`${opts.cwd}/.vscode/extensions.json`, { recommendations: extensions });
2218
2637
  } catch (error) {
@@ -2231,10 +2650,10 @@ function generateAllVscode(preset, opts) {
2231
2650
  if (extAction === "created") result.created.push(".vscode/extensions.json");
2232
2651
  return result;
2233
2652
  }
2234
- function filterStylelintSettings(settings) {
2653
+ function filterStylelintSettings2(settings) {
2235
2654
  const filtered = Object.fromEntries(
2236
2655
  Object.entries(settings).filter(
2237
- ([key]) => !STYLELINT_SETTINGS_PREFIXES.some((prefix) => key.startsWith(prefix))
2656
+ ([key]) => !STYLELINT_SETTINGS_PREFIXES2.some((prefix) => key.startsWith(prefix))
2238
2657
  )
2239
2658
  );
2240
2659
  if (typeof filtered["editor.codeActionsOnSave"] === "object" && filtered["editor.codeActionsOnSave"] !== null) {
@@ -2248,29 +2667,20 @@ function filterStylelintSettings(settings) {
2248
2667
  // src/commands/vscode.ts
2249
2668
  function registerVscodeCommand(program2) {
2250
2669
  const vscode = program2.command("vscode").description("Initialize VSCode config with preset");
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(
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(
2252
2671
  async (presetName, options) => {
2253
2672
  const preset = resolvePreset(VSCODE_PRESETS, presetName);
2254
2673
  if (!preset) return;
2255
2674
  const cwd = process.cwd();
2256
- const opts = {
2257
- cwd,
2258
- force: options.force ?? false,
2259
- dryRun: options.dryRun ?? false,
2260
- noStylelint: options.stylelint !== true,
2261
- noEditorconfig: false
2262
- };
2263
- const result = generateAllVscode(preset, opts);
2264
- const files = [...result.created, ...result.overwritten];
2265
- if (files.length === 0) {
2266
- logger.warn("No files generated");
2267
- return;
2675
+ if (options.reset) {
2676
+ resetLocalPreset("vscode", presetName);
2268
2677
  }
2269
- if (opts.dryRun) {
2270
- logger.log(`[dry-run] Would create ${files.join(", ")}`);
2271
- return;
2678
+ const useLocal = localPresetExists("vscode", presetName);
2679
+ if (useLocal) {
2680
+ executeVscodeLocalPath(cwd, presetName, options);
2681
+ } else {
2682
+ executeVscodeBuiltinPath(cwd, presetName, preset, options);
2272
2683
  }
2273
- logger.log(`Created ${files.join(", ")}`);
2274
2684
  }
2275
2685
  );
2276
2686
  vscode.command("list").description("List available vscode presets").action(() => {
@@ -2279,6 +2689,48 @@ function registerVscodeCommand(program2) {
2279
2689
  }
2280
2690
  });
2281
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
+ }
2282
2734
 
2283
2735
  // src/commands/vpn.ts
2284
2736
  import { spawnSync } from "child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxkit/cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "One-click project formatting & VSCode config CLI",
5
5
  "type": "module",
6
6
  "bin": {