kythia-core 0.10.1-beta → 0.11.1-beta

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.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * 🔌 CLI Database & Migration Bootstrapper
3
+ *
4
+ * @file src/cli/utils/db.js
5
+ * @copyright © 2025 kenndeclouv
6
+ * @assistant chaa & graa
7
+ * @version 0.11.1-beta
8
+ *
9
+ * @description
10
+ * Initializes the database connection and migration engine (Umzug) specifically
11
+ * for CLI operations. It dynamically loads the project configuration and
12
+ * discovers migration files across all addons.
13
+ *
14
+ * ✨ Core Features:
15
+ * - Dynamic Config: Loads `kythia.config.js` from the user's project root.
16
+ * - Auto-Discovery: Scans `addons` directory for migration files.
17
+ * - Pretty Logging: Custom console output using `picocolors` for migration status.
18
+ * - Singleton-like: Exports shared instances of Sequelize and Umzug.
19
+ */
20
+
21
+ require('@dotenvx/dotenvx/config');
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+ const { Umzug } = require('umzug');
25
+ const createSequelizeInstance = require('../../database/KythiaSequelize');
26
+ const KythiaStorage = require('../../database/KythiaStorage');
27
+ const pc = require('picocolors');
28
+
29
+ const configPath = path.resolve(process.cwd(), 'kythia.config.js');
30
+
31
+ if (!fs.existsSync(configPath)) {
32
+ console.error('❌ kythia.config.js not found in root directory!');
33
+ process.exit(1);
34
+ }
35
+
36
+ const config = require(configPath);
37
+
38
+ const logger = {
39
+ info: () => {},
40
+ error: console.error,
41
+ debug: () => {},
42
+ };
43
+
44
+ const sequelize = createSequelizeInstance(config, logger);
45
+
46
+ function getMigrationFiles() {
47
+ const rootDir = process.cwd();
48
+ const addonsDir = path.join(rootDir, 'addons');
49
+
50
+ if (!fs.existsSync(addonsDir)) return [];
51
+
52
+ const migrationFiles = [];
53
+ const addonFolders = fs
54
+ .readdirSync(addonsDir)
55
+ .filter((f) => fs.statSync(path.join(addonsDir, f)).isDirectory());
56
+
57
+ for (const addon of addonFolders) {
58
+ const migrationDir = path.join(addonsDir, addon, 'database', 'migrations');
59
+ if (fs.existsSync(migrationDir)) {
60
+ const files = fs
61
+ .readdirSync(migrationDir)
62
+ .filter((f) => f.endsWith('.js'))
63
+ .map((f) => ({
64
+ name: f,
65
+ path: path.join(migrationDir, f),
66
+ }));
67
+ migrationFiles.push(...files);
68
+ }
69
+ }
70
+ return migrationFiles.sort((a, b) => a.name.localeCompare(b.name));
71
+ }
72
+
73
+ const storage = new KythiaStorage({ sequelize });
74
+
75
+ const umzugLogger = {
76
+ info: (event) => {
77
+ if (typeof event === 'object') {
78
+ if (event.event === 'migrated') {
79
+ console.log(
80
+ pc.green(`✅ Migrated: ${event.name} `) +
81
+ pc.gray(`(${event.durationSeconds}s)`),
82
+ );
83
+ } else if (event.event === 'reverting') {
84
+ console.log(pc.yellow(`↩️ Reverting: ${event.name}`));
85
+ } else if (event.event === 'reverted') {
86
+ console.log(
87
+ pc.red(`❌ Reverted: ${event.name} `) +
88
+ pc.gray(`(${event.durationSeconds}s)`),
89
+ );
90
+ }
91
+ } else {
92
+ console.log(pc.dim(event));
93
+ }
94
+ },
95
+ warn: (msg) => console.warn(pc.yellow(`⚠️ ${msg}`)),
96
+ error: (msg) => console.error(pc.red(`🔥 ${msg}`)),
97
+ };
98
+
99
+ const umzug = new Umzug({
100
+ migrations: getMigrationFiles().map((m) => ({
101
+ name: m.name,
102
+ path: m.path,
103
+ up: async ({ context }) => {
104
+ const migration = require(m.path);
105
+ return migration.up(context, require('sequelize').DataTypes);
106
+ },
107
+ down: async ({ context }) => {
108
+ const migration = require(m.path);
109
+ return migration.down(context, require('sequelize').DataTypes);
110
+ },
111
+ })),
112
+ context: sequelize.getQueryInterface(),
113
+ storage: storage,
114
+ logger: umzugLogger,
115
+ });
116
+
117
+ module.exports = { sequelize, umzug, storage };
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaMigrator.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Scans 'addons' folder for migration files and executes them using Umzug.
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaModel.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Caching layer for Sequelize Models, now sharding-aware. When config.db.redis.shard === true,
@@ -32,7 +32,7 @@ const REDIS_ERROR_TOLERANCE_INTERVAL_MS = 10 * 1000;
32
32
 
33
33
  function safeStringify(obj, logger) {
34
34
  try {
35
- return JSON.stringify(obj, (value) =>
35
+ return JSON.stringify(obj, (_key, value) =>
36
36
  typeof value === 'bigint' ? value.toString() : value,
37
37
  );
38
38
  } catch (err) {
@@ -659,10 +659,6 @@ class KythiaModel extends Model {
659
659
  * @param {string|Object} queryIdentifier - A unique string or a Sequelize query object.
660
660
  * @returns {string} The final cache key, prefixed with the model's name (e.g., "User:{\"id\":1}").
661
661
  */
662
- /**
663
- * 🔑 Generates a consistent, model-specific cache key from a query identifier.
664
- * FIXED: Handle BigInt in json-stable-stringify
665
- */
666
662
  static getCacheKey(queryIdentifier) {
667
663
  let dataToHash = queryIdentifier;
668
664
 
@@ -676,7 +672,7 @@ class KythiaModel extends Model {
676
672
  }
677
673
 
678
674
  const opts = {
679
- replacer: (value) =>
675
+ replacer: (_key, value) =>
680
676
  typeof value === 'bigint' ? value.toString() : value,
681
677
  };
682
678
 
@@ -712,6 +708,38 @@ class KythiaModel extends Model {
712
708
  return normalized;
713
709
  }
714
710
 
711
+ /**
712
+ * 🧠 Generate tags based on PK and static cacheKeys definition.
713
+ * Used for smart invalidation (e.g. invalidate all items belonging to a userId).
714
+ */
715
+ static _generateSmartTags(instance) {
716
+ if (!instance) return [`${this.name}`];
717
+
718
+ const tags = [`${this.name}`];
719
+
720
+ const pk = this.primaryKeyAttribute;
721
+ if (instance[pk]) {
722
+ tags.push(`${this.name}:${pk}:${instance[pk]}`);
723
+ }
724
+
725
+ const smartKeys = this.cacheKeys || this.CACHE_KEYS || [];
726
+
727
+ if (Array.isArray(smartKeys)) {
728
+ for (const keyGroup of smartKeys) {
729
+ const keys = Array.isArray(keyGroup) ? keyGroup : [keyGroup];
730
+ const hasAllValues = keys.every(
731
+ (k) => instance[k] !== undefined && instance[k] !== null,
732
+ );
733
+
734
+ if (hasAllValues) {
735
+ const tagParts = keys.map((k) => `${k}:${instance[k]}`).join(':');
736
+ tags.push(`${this.name}:${tagParts}`);
737
+ }
738
+ }
739
+ }
740
+ return tags;
741
+ }
742
+
715
743
  /**
716
744
  * 📥 [HYBRID/SHARD ROUTER] Sets a value in the currently active cache engine.
717
745
  * In shard mode, if Redis down, nothing is cached.
@@ -774,6 +802,7 @@ class KythiaModel extends Model {
774
802
  static async _redisSetCacheEntry(cacheKey, data, ttl, tags = []) {
775
803
  try {
776
804
  let plainData = data;
805
+
777
806
  if (data && typeof data.toJSON === 'function') {
778
807
  plainData = data.toJSON();
779
808
  } else if (Array.isArray(data)) {
@@ -789,6 +818,7 @@ class KythiaModel extends Model {
789
818
 
790
819
  const multi = this.redis.multi();
791
820
  multi.set(cacheKey, valueToStore, 'PX', ttl);
821
+
792
822
  for (const tag of tags) {
793
823
  multi.sadd(tag, cacheKey);
794
824
  }
@@ -799,42 +829,43 @@ class KythiaModel extends Model {
799
829
  }
800
830
  }
801
831
 
802
- /**
803
- * 🔴 (Private) Retrieves and deserializes an entry specifically from Redis.
804
- */
805
832
  static async _redisGetCachedEntry(cacheKey, includeOptions) {
806
833
  try {
807
834
  const result = await this.redis.get(cacheKey);
835
+
808
836
  if (result === null || result === undefined)
809
837
  return { hit: false, data: undefined };
810
838
 
811
839
  this.cacheStats.redisHits++;
812
- if (result === NEGATIVE_CACHE_PLACEHOLDER)
840
+ if (result === NEGATIVE_CACHE_PLACEHOLDER) {
813
841
  return { hit: true, data: null };
842
+ }
814
843
 
815
844
  const parsedData = safeParse(result, this.logger);
816
845
 
817
- if (typeof parsedData !== 'object' || parsedData === null) {
818
- return { hit: true, data: parsedData };
846
+ if (parsedData === null) {
847
+ return { hit: false, data: undefined };
819
848
  }
820
849
 
821
850
  const includeAsArray = includeOptions
822
851
  ? Array.isArray(includeOptions)
823
852
  ? includeOptions
824
853
  : [includeOptions]
825
- : null;
854
+ : undefined;
826
855
 
827
- if (Array.isArray(parsedData)) {
828
- const instances = this.bulkBuild(parsedData, {
856
+ const buildInstance = (data) => {
857
+ const instance = this.build(data, {
829
858
  isNewRecord: false,
830
859
  include: includeAsArray,
831
860
  });
861
+ return instance;
862
+ };
863
+
864
+ if (Array.isArray(parsedData)) {
865
+ const instances = parsedData.map((d) => buildInstance(d));
832
866
  return { hit: true, data: instances };
833
867
  } else {
834
- const instance = this.build(parsedData, {
835
- isNewRecord: false,
836
- include: includeAsArray,
837
- });
868
+ const instance = buildInstance(parsedData);
838
869
  return { hit: true, data: instance };
839
870
  }
840
871
  } catch (err) {
@@ -1056,14 +1087,6 @@ class KythiaModel extends Model {
1056
1087
  static async getCache(keys, options = {}) {
1057
1088
  const { noCache, customCacheKey, ttl, ...explicitQueryOptions } = options;
1058
1089
 
1059
- if (noCache) {
1060
- const queryToRun = {
1061
- ...this._normalizeFindOptions(keys),
1062
- ...explicitQueryOptions,
1063
- };
1064
- return this.findOne(queryToRun);
1065
- }
1066
-
1067
1090
  if (Array.isArray(keys)) {
1068
1091
  const pk = this.primaryKeyAttribute;
1069
1092
  return this.findAll({ where: { [pk]: keys.map((m) => m[pk]) } });
@@ -1075,11 +1098,15 @@ class KythiaModel extends Model {
1075
1098
  ...normalizedKeys,
1076
1099
  ...explicitQueryOptions,
1077
1100
  where: {
1078
- ...normalizedKeys.where,
1101
+ ...(normalizedKeys.where || {}),
1079
1102
  ...(explicitQueryOptions.where || {}),
1080
1103
  },
1081
1104
  };
1082
1105
 
1106
+ if (noCache) {
1107
+ return this.findOne(finalQuery);
1108
+ }
1109
+
1083
1110
  if (!finalQuery.where || Object.keys(finalQuery.where).length === 0) {
1084
1111
  return null;
1085
1112
  }
@@ -1095,7 +1122,7 @@ class KythiaModel extends Model {
1095
1122
  return this.pendingQueries.get(cacheKey);
1096
1123
 
1097
1124
  const queryPromise = this.findOne(finalQuery)
1098
- .then((record) => {
1125
+ .then(async (record) => {
1099
1126
  if (this.isRedisConnected || !this.isShardMode) {
1100
1127
  const tags = [`${this.name}`];
1101
1128
  if (record)
@@ -1105,7 +1132,7 @@ class KythiaModel extends Model {
1105
1132
  }`,
1106
1133
  );
1107
1134
 
1108
- this.setCacheEntry(cacheKey, record, ttl, tags);
1135
+ await this.setCacheEntry(cacheKey, record, ttl, tags);
1109
1136
  }
1110
1137
  return record;
1111
1138
  })
@@ -1143,12 +1170,12 @@ class KythiaModel extends Model {
1143
1170
  return this.pendingQueries.get(cacheKey);
1144
1171
 
1145
1172
  const queryPromise = this.findAll(normalizedOptions)
1146
- .then((records) => {
1173
+ .then(async (records) => {
1147
1174
  if (this.isRedisConnected || !this.isShardMode) {
1148
1175
  const tags = [`${this.name}`];
1149
1176
  if (Array.isArray(cacheTags)) tags.push(...cacheTags);
1150
1177
 
1151
- this.setCacheEntry(cacheKey, records, ttl, tags);
1178
+ await this.setCacheEntry(cacheKey, records, ttl, tags);
1152
1179
  }
1153
1180
  return records;
1154
1181
  })
@@ -1341,29 +1368,40 @@ class KythiaModel extends Model {
1341
1368
  }
1342
1369
 
1343
1370
  /**
1344
- * 📦 An instance method that saves the current model instance to the database and then
1345
- * intelligently updates its corresponding entry in the active cache.
1371
+ * 📦 FIXED: Save data to DB, then INVALIDATE the cache tags.
1372
+ * Don't try to setCache here, because .save() result doesn't have associations/includes.
1373
+ * Let the next getCache() fetch the full fresh data tree.
1346
1374
  */
1347
1375
  async saveAndUpdateCache() {
1348
1376
  const savedInstance = await this.save();
1349
1377
  const pk = this.constructor.primaryKeyAttribute;
1350
1378
  const pkValue = this[pk];
1379
+
1351
1380
  if (
1352
1381
  pkValue &&
1353
1382
  (this.constructor.isRedisConnected || !this.constructor.isShardMode)
1354
1383
  ) {
1355
- const cacheKey = this.constructor.getCacheKey({ [pk]: pkValue });
1384
+ const cacheKey = this.constructor.getCacheKey({
1385
+ where: { [pk]: pkValue },
1386
+ });
1387
+
1356
1388
  const tags = [
1357
1389
  `${this.constructor.name}`,
1358
1390
  `${this.constructor.name}:${pk}:${pkValue}`,
1359
1391
  ];
1392
+
1360
1393
  await this.constructor.setCacheEntry(
1361
1394
  cacheKey,
1362
1395
  savedInstance,
1363
1396
  undefined,
1364
1397
  tags,
1365
1398
  );
1399
+
1400
+ this.constructor.logger.info(
1401
+ `🔄 [CACHE] Updated cache for ${this.constructor.name}:${pk}:${pkValue}`,
1402
+ );
1366
1403
  }
1404
+
1367
1405
  return savedInstance;
1368
1406
  }
1369
1407
 
@@ -1414,16 +1452,11 @@ class KythiaModel extends Model {
1414
1452
  return;
1415
1453
  }
1416
1454
 
1417
- /**
1418
- * Logika setelah data disimpan (Create atau Update)
1419
- */
1420
1455
  const afterSaveLogic = async (instance) => {
1421
1456
  const modelClass = instance.constructor;
1422
1457
 
1423
1458
  if (modelClass.isRedisConnected) {
1424
- const tagsToInvalidate = [`${modelClass.name}`];
1425
- const pk = modelClass.primaryKeyAttribute;
1426
- tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
1459
+ const tagsToInvalidate = modelClass._generateSmartTags(instance);
1427
1460
 
1428
1461
  if (Array.isArray(modelClass.customInvalidationTags)) {
1429
1462
  tagsToInvalidate.push(...modelClass.customInvalidationTags);
@@ -1434,16 +1467,11 @@ class KythiaModel extends Model {
1434
1467
  }
1435
1468
  };
1436
1469
 
1437
- /**
1438
- * Logika setelah data dihapus
1439
- */
1440
1470
  const afterDestroyLogic = async (instance) => {
1441
1471
  const modelClass = instance.constructor;
1442
1472
 
1443
1473
  if (modelClass.isRedisConnected) {
1444
- const tagsToInvalidate = [`${modelClass.name}`];
1445
- const pk = modelClass.primaryKeyAttribute;
1446
- tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
1474
+ const tagsToInvalidate = modelClass._generateSmartTags(instance);
1447
1475
 
1448
1476
  if (Array.isArray(modelClass.customInvalidationTags)) {
1449
1477
  tagsToInvalidate.push(...modelClass.customInvalidationTags);
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaSequelize.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Main Sequelize connection factory for the application.
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaStorage.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Custom storage adapter for Umzug that mimics Laravel's migration table structure.
@@ -4,7 +4,7 @@
4
4
  * @file src/loaders/ModelLoader.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Scans the `addons` directory for KythiaModel definitions, requires them,
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/AddonManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all addon loading, command registration, and component management.
@@ -466,6 +466,7 @@ class AddonManager {
466
466
  async _loadTopLevelCommandGroup(
467
467
  commandsPath,
468
468
  addon,
469
+ addonPermissionDefaults,
469
470
  commandNamesSet,
470
471
  commandDataForDeployment,
471
472
  ) {
@@ -476,6 +477,14 @@ class AddonManager {
476
477
  commandDef = this._instantiateBaseCommand(commandDef);
477
478
  }
478
479
 
480
+ const category = addon.name;
481
+ const categoryDefaults = addonPermissionDefaults[category] || {};
482
+
483
+ commandDef = {
484
+ ...categoryDefaults,
485
+ ...commandDef,
486
+ };
487
+
479
488
  const mainBuilder = this._createBuilderFromData(
480
489
  commandDef.data,
481
490
  SlashCommandBuilder,
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/EventManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord event listeners except InteractionCreate.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/InteractionManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord interaction events including slash commands, buttons, modals,
@@ -56,6 +56,10 @@ class InteractionManager {
56
56
  this.KythiaVoter = this.models.KythiaVoter;
57
57
  this.isTeam = this.helpers.discord.isTeam;
58
58
  this.isOwner = this.helpers.discord.isOwner;
59
+
60
+ if (!this.client.restartNoticeCooldowns) {
61
+ this.client.restartNoticeCooldowns = new Collection();
62
+ }
59
63
  }
60
64
 
61
65
  /**
@@ -305,6 +309,8 @@ class InteractionManager {
305
309
  } else {
306
310
  await command.execute(interaction);
307
311
  }
312
+
313
+ await this._checkRestartSchedule(interaction);
308
314
  } else {
309
315
  this.logger.error(
310
316
  "Command doesn't have a valid 'execute' function:",
@@ -637,6 +643,9 @@ class InteractionManager {
637
643
  if (this.container && !this.container.logger) {
638
644
  this.container.logger = this.logger;
639
645
  }
646
+
647
+ await this._checkRestartSchedule(interaction);
648
+
640
649
  await command.execute(interaction, this.container);
641
650
  }
642
651
 
@@ -706,6 +715,51 @@ class InteractionManager {
706
715
  }
707
716
  }
708
717
 
718
+ /**
719
+ * Check for scheduled restart and notify user
720
+ * @private
721
+ */
722
+ async _checkRestartSchedule(interaction) {
723
+ const restartTs = this.client.kythiaRestartTimestamp;
724
+
725
+ if (!restartTs || interaction.commandName === 'restart') return;
726
+
727
+ const userId = interaction.user.id;
728
+ const cooldowns = this.client.restartNoticeCooldowns;
729
+ const now = Date.now();
730
+ const cooldownTime = 5 * 60 * 1000;
731
+
732
+ if (cooldowns.has(userId)) {
733
+ const lastNotified = cooldowns.get(userId);
734
+ if (now - lastNotified < cooldownTime) {
735
+ return;
736
+ }
737
+ }
738
+
739
+ const timeLeft = restartTs - now;
740
+
741
+ if (timeLeft > 0) {
742
+ try {
743
+ const timeString = `<t:${Math.floor(restartTs / 1000)}:R>`;
744
+ // TODO: Add translation
745
+ const msg = `## ⚠️ System Notice\nKythia is scheduled to restart **${timeString}**.`;
746
+
747
+ if (interaction.replied || interaction.deferred) {
748
+ await interaction.followUp({
749
+ content: msg,
750
+ flags: MessageFlags.Ephemeral,
751
+ });
752
+
753
+ cooldowns.set(userId, now);
754
+
755
+ setTimeout(() => cooldowns.delete(userId), cooldownTime);
756
+ }
757
+ } catch (err) {
758
+ this.logger.error(err);
759
+ }
760
+ }
761
+ }
762
+
709
763
  /**
710
764
  * Handle interaction errors
711
765
  * @private
@@ -763,7 +817,7 @@ class InteractionManager {
763
817
  .setLabel(
764
818
  await this.t(interaction, 'common.error.button.contact.owner'),
765
819
  )
766
- .setURL(`discord://-/users/${ownerFirstId}`),
820
+ .setURL(`https://discord.com/users/${ownerFirstId}`),
767
821
  ),
768
822
  ),
769
823
  ];
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/ShutdownManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.10.0-beta
7
+ * @version 0.11.1-beta
8
8
  *
9
9
  * @description
10
10
  * Handles graceful shutdown procedures including interval tracking,
package/.husky/pre-commit DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
- npx lint-staged
package/biome.json DELETED
@@ -1,40 +0,0 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "files": {
9
- "ignoreUnknown": false
10
- },
11
- "formatter": {
12
- "enabled": true,
13
- "indentStyle": "tab"
14
- },
15
- "linter": {
16
- "enabled": true,
17
- "rules": {
18
- "recommended": true,
19
- "correctness": {
20
- "noUnusedVariables": "error"
21
- },
22
- "complexity": {
23
- "noThisInStatic": "off"
24
- }
25
- }
26
- },
27
- "javascript": {
28
- "formatter": {
29
- "quoteStyle": "single"
30
- }
31
- },
32
- "assist": {
33
- "enabled": true,
34
- "actions": {
35
- "source": {
36
- "organizeImports": "off"
37
- }
38
- }
39
- }
40
- }