@aperdomoll90/ledger-ai 1.1.3 → 1.3.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
@@ -41,7 +41,7 @@ ledger init # connect to existing Supabase, pull persona, set up platform
41
41
 
42
42
  **For you:** Define who you are, how you want AI to behave, your coding conventions, communication style. Do it once. Every AI agent on every device follows the same rules.
43
43
 
44
- **For your agents:** Semantic search over your knowledge base. Notes are embedded with OpenAI and stored in Postgres with pgvector. Agents find relevant context by meaning, not keywords.
44
+ **For your agents:** A full RAG (Retrieval-Augmented Generation) system. Notes are embedded with OpenAI and stored in Postgres with pgvector. Agents find relevant context by semantic meaning, not keywords. Retrieved context is injected into agent prompts automatically via MCP.
45
45
 
46
46
  **For your workflow:** Automatic sync at session start, conflict detection, session-end checks. Hooks enforce rules (block credential file access, auto-ingest notes to Ledger).
47
47
 
@@ -153,10 +153,16 @@ npm run release # version bump → build → test → publish to npm
153
153
 
154
154
  ## Roadmap
155
155
 
156
- - Multi-provider embeddings (Supabase built-in, OpenAI, others)
156
+ ### RAG Enhancements
157
+ - Hybrid search (vector + BM25 keyword via PostgreSQL full-text search)
158
+ - Re-ranking — score retrieved chunks before feeding to LLM
159
+ - Chunking strategies for large documents (auto-split on ingest)
160
+ - Multi-format ingest (PDF, Excel, images, audio)
161
+ - Multi-provider embeddings (Supabase built-in, OpenAI, Ollama, Cohere)
162
+
163
+ ### Platform
157
164
  - Note versioning / history
158
165
  - Soft delete (trash)
159
- - Multi-format ingest (PDF, Excel, images, audio)
160
166
  - Web dashboard
161
167
  - Skills system (context-aware convention enforcement)
162
168
  - VS Code extension
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ const require = createRequire(import.meta.url);
6
6
  const { version } = require('../package.json');
7
7
  import { pull } from './commands/pull.js';
8
8
  import { push } from './commands/push.js';
9
- import { check } from './commands/check.js';
9
+ import { check, checkChunks } from './commands/check.js';
10
10
  import { sync } from './commands/sync.js';
11
11
  import { show } from './commands/show.js';
12
12
  import { exportNote } from './commands/export.js';
@@ -17,13 +17,14 @@ import { setupClaudeCode, setupOpenclaw, setupChatgpt } from './commands/setup.j
17
17
  import { backup, enableBackupCron, disableBackupCron } from './commands/backup.js';
18
18
  import { restore } from './commands/restore.js';
19
19
  import { onboard } from './commands/onboard.js';
20
- import { configGet, configSet, configList } from './commands/config.js';
20
+ import { configGet, configSet, configUnset, configList } from './commands/config.js';
21
21
  import { migrate } from './commands/migrate.js';
22
22
  import { add } from './commands/add.js';
23
23
  import { update } from './commands/update.js';
24
24
  import { deleteNote } from './commands/delete.js';
25
25
  import { list } from './commands/list.js';
26
26
  import { tag } from './commands/tag.js';
27
+ import { lint } from './commands/lint.js';
27
28
  process.on('unhandledRejection', (err) => {
28
29
  console.error(err instanceof Error ? err.message : String(err));
29
30
  process.exit(1);
@@ -51,10 +52,16 @@ program
51
52
  });
52
53
  program
53
54
  .command('check')
54
- .description('Compare local files vs Ledger, report sync status (alias for sync --dry-run)')
55
- .action(async () => {
55
+ .description('Compare local files vs Ledger, report sync status')
56
+ .option('--chunks', 'Check chunk group integrity')
57
+ .action(async (opts) => {
56
58
  const config = loadConfig();
57
- await check(config);
59
+ if (opts.chunks) {
60
+ await checkChunks(config);
61
+ }
62
+ else {
63
+ await check(config);
64
+ }
58
65
  });
59
66
  program
60
67
  .command('sync')
@@ -133,7 +140,25 @@ configCmd
133
140
  .command('set <key> <value>')
134
141
  .description('Set a config value')
135
142
  .action(async (key, value) => {
136
- await configSet(key, value);
143
+ if (key.startsWith('types.')) {
144
+ const config = loadConfig();
145
+ await configSet(key, value, { supabase: config.supabase, openai: config.openai });
146
+ }
147
+ else {
148
+ await configSet(key, value);
149
+ }
150
+ });
151
+ configCmd
152
+ .command('unset <key>')
153
+ .description('Remove a config override (types.* keys only)')
154
+ .action(async (key) => {
155
+ if (key.startsWith('types.')) {
156
+ const config = loadConfig();
157
+ await configUnset(key, { supabase: config.supabase, openai: config.openai });
158
+ }
159
+ else {
160
+ await configUnset(key);
161
+ }
137
162
  });
138
163
  configCmd
139
164
  .command('list')
@@ -254,4 +279,12 @@ program
254
279
  scope: options.scope,
255
280
  });
256
281
  });
282
+ program
283
+ .command('lint')
284
+ .description('Add lint configs to the current project based on detected stack')
285
+ .option('--personal', 'include personal conventions (BEM, SCSS patterns)')
286
+ .option('--diff', 'compare local configs against Ledger versions')
287
+ .action(async (options) => {
288
+ await lint({ personal: options.personal ?? false, diff: options.diff ?? false });
289
+ });
257
290
  program.parse();
@@ -1,5 +1,5 @@
1
1
  import { loadConfigFile } from '../lib/config.js';
2
- import { opAddNote, NOTE_TYPES, NOTE_STATUSES } from '../lib/notes.js';
2
+ import { opAddNote, getRegisteredTypes, isRegisteredType, registerType, validateTypeName, inferDelivery, NOTE_STATUSES } from '../lib/notes.js';
3
3
  import { ask, confirm, choose } from '../lib/prompt.js';
4
4
  export async function add(config, content, options) {
5
5
  const configFile = loadConfigFile();
@@ -18,12 +18,37 @@ export async function add(config, content, options) {
18
18
  if (interactive && !options.force) {
19
19
  // Type
20
20
  if (!type) {
21
+ const registeredTypes = getRegisteredTypes();
21
22
  const typeChoice = await choose('What type of note is this?', [
22
- ...NOTE_TYPES,
23
+ ...registeredTypes,
23
24
  'skip — use default (general)',
24
25
  ]);
25
26
  type = typeChoice.startsWith('skip') ? 'general' : typeChoice;
26
27
  }
28
+ // Handle unknown type from --type flag
29
+ if (type && !isRegisteredType(type)) {
30
+ console.error(`\nType "${type}" is not registered.`);
31
+ const action = await choose('What would you like to do?', [
32
+ 'register — register it now (pick a delivery tier)',
33
+ 'existing — use an existing type instead',
34
+ 'proceed — save anyway (defaults to "knowledge" delivery)',
35
+ ]);
36
+ if (action.startsWith('register')) {
37
+ const nameError = validateTypeName(type);
38
+ if (nameError) {
39
+ console.error(nameError);
40
+ process.exit(1);
41
+ }
42
+ const deliveryChoice = await choose('Delivery tier?', ['persona', 'project', 'knowledge', 'protected']);
43
+ registerType(type, deliveryChoice);
44
+ console.error(`Registered type "${type}" with delivery "${deliveryChoice}".`);
45
+ }
46
+ else if (action.startsWith('existing')) {
47
+ const registeredTypes = getRegisteredTypes();
48
+ type = await choose('Choose a type:', registeredTypes);
49
+ }
50
+ // 'proceed' — use the type as-is, will default to knowledge delivery
51
+ }
27
52
  // Description
28
53
  if (!metadata.description) {
29
54
  const desc = await ask('One-line description (what is this note for?): ');
@@ -43,8 +68,7 @@ export async function add(config, content, options) {
43
68
  metadata.project = proj;
44
69
  }
45
70
  // Status (only for project-scoped types)
46
- const projectTypes = ['architecture-decision', 'project-status', 'event', 'error'];
47
- if (projectTypes.includes(type) && !metadata.status) {
71
+ if (inferDelivery(type) === 'project' && !metadata.status) {
48
72
  const statusChoice = await choose('What stage is this?', [
49
73
  ...NOTE_STATUSES,
50
74
  'skip — no status',
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, readdirSync, existsSync } from 'fs';
2
2
  import { resolve } from 'path';
3
- import { fetchNoteHashes } from '../lib/notes.js';
3
+ import { fetchNoteHashes, checkChunkIntegrity } from '../lib/notes.js';
4
4
  import { contentHash } from '../lib/hash.js';
5
5
  export async function check(config) {
6
6
  const result = {
@@ -77,3 +77,15 @@ export async function check(config) {
77
77
  }
78
78
  return result;
79
79
  }
80
+ export async function checkChunks(config) {
81
+ console.error('Checking chunk integrity...');
82
+ const result = await checkChunkIntegrity(config.supabase);
83
+ if (result.incompleteGroups.length === 0) {
84
+ console.log('All chunk groups are complete.');
85
+ return;
86
+ }
87
+ console.error(`Found ${result.incompleteGroups.length} incomplete chunk group(s):`);
88
+ for (const group of result.incompleteGroups) {
89
+ console.error(` group ${group.groupId}: expected ${group.expected} chunks, found ${group.found}`);
90
+ }
91
+ }
@@ -1,22 +1,8 @@
1
- import { readFileSync, writeFileSync, existsSync } from 'fs';
1
+ import { getLedgerDir, saveConfigFile, loadConfigFile } from '../lib/config.js';
2
+ import { BUILTIN_TYPES, getTypeRegistry, opUpdateMetadata, validateTypeName } from '../lib/notes.js';
3
+ import { choose, confirm } from '../lib/prompt.js';
2
4
  import { resolve } from 'path';
3
- import { getLedgerDir } from '../lib/config.js';
4
- import { confirm } from '../lib/prompt.js';
5
5
  const CONFIG_PATH = resolve(getLedgerDir(), 'config.json');
6
- function loadFullConfig() {
7
- if (existsSync(CONFIG_PATH)) {
8
- try {
9
- return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
10
- }
11
- catch {
12
- return {};
13
- }
14
- }
15
- return {};
16
- }
17
- function saveConfig(config) {
18
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
19
- }
20
6
  const SECURITY_WARNINGS = {
21
7
  envBlocking: 'This disables .env file protection.\nYour API keys and credentials will be readable by the AI agent.',
22
8
  mcpJsonBlocking: 'This allows direct editing of mcp.json.\nMCP servers should be registered via `claude mcp add`, not by editing config files.',
@@ -35,7 +21,7 @@ const DEVICE_DESCRIPTIONS = {
35
21
  'device.alias': 'Name for this device',
36
22
  };
37
23
  export async function configGet(key) {
38
- const config = loadFullConfig();
24
+ const config = loadConfigFile();
39
25
  if (key === 'all') {
40
26
  console.log(JSON.stringify(config, null, 2));
41
27
  return;
@@ -76,13 +62,42 @@ export async function configGet(key) {
76
62
  console.log(`${key}: ${defaults[key]} (default)`);
77
63
  return;
78
64
  }
79
- const allKeys = [...Object.keys(DESCRIPTIONS), ...Object.keys(NAMING_DESCRIPTIONS), ...Object.keys(DEVICE_DESCRIPTIONS), 'memoryDir', 'claudeMdPath', 'all'];
65
+ // Handle type registry
66
+ if (key === 'types') {
67
+ const registry = getTypeRegistry();
68
+ const userTypes = config.types ?? {};
69
+ console.error('Type registry:');
70
+ for (const [typeName, delivery] of Object.entries(registry)) {
71
+ const isBuiltin = typeName in BUILTIN_TYPES;
72
+ const isOverridden = isBuiltin && typeName in userTypes;
73
+ const isCustom = !isBuiltin;
74
+ let annotation = isCustom ? '(custom)' : '(built-in)';
75
+ if (isOverridden) {
76
+ annotation = `(built-in, overridden — default: ${BUILTIN_TYPES[typeName]})`;
77
+ }
78
+ console.error(` ${typeName}: ${delivery} ${annotation}`);
79
+ }
80
+ return;
81
+ }
82
+ if (key.startsWith('types.')) {
83
+ const typeName = key.slice(6);
84
+ const registry = getTypeRegistry();
85
+ if (typeName in registry) {
86
+ const isBuiltin = typeName in BUILTIN_TYPES;
87
+ console.log(`${typeName}: ${registry[typeName]} (${isBuiltin ? 'built-in' : 'custom'})`);
88
+ }
89
+ else {
90
+ console.log(`${typeName}: not registered`);
91
+ }
92
+ return;
93
+ }
94
+ const allKeys = [...Object.keys(DESCRIPTIONS), ...Object.keys(NAMING_DESCRIPTIONS), ...Object.keys(DEVICE_DESCRIPTIONS), 'memoryDir', 'claudeMdPath', 'types', 'all'];
80
95
  console.error(`Unknown config key: ${key}`);
81
96
  console.error(`Available: ${allKeys.join(', ')}`);
82
97
  process.exit(1);
83
98
  }
84
- export async function configSet(key, value) {
85
- const config = loadFullConfig();
99
+ export async function configSet(key, value, clients) {
100
+ const config = loadConfigFile();
86
101
  // Handle hook settings
87
102
  const hookKeys = ['envBlocking', 'mcpJsonBlocking', 'writeInterception', 'sessionEndCheck'];
88
103
  if (hookKeys.includes(key)) {
@@ -104,7 +119,7 @@ export async function configSet(key, value) {
104
119
  if (!config.hooks)
105
120
  config.hooks = {};
106
121
  config.hooks[key] = boolValue;
107
- saveConfig(config);
122
+ saveConfigFile(config);
108
123
  const state = boolValue ? 'enabled' : 'disabled';
109
124
  console.error(`${key}: ${state}`);
110
125
  console.error('Run `ledger setup claude-code` to apply hook changes.');
@@ -118,31 +133,132 @@ export async function configSet(key, value) {
118
133
  config.naming = {};
119
134
  const field = key.split('.')[1];
120
135
  config.naming[field] = boolValue;
121
- saveConfig(config);
136
+ saveConfigFile(config);
122
137
  console.error(`${key}: ${boolValue ? 'enabled' : 'disabled'}`);
123
138
  return;
124
139
  }
125
140
  // Handle device alias
126
141
  if (key === 'device.alias') {
127
142
  config.device = { alias: value };
128
- saveConfig(config);
143
+ saveConfigFile(config);
129
144
  console.error(`device.alias: ${value}`);
130
145
  return;
131
146
  }
132
147
  // Handle path settings
133
148
  if (key === 'memoryDir' || key === 'claudeMdPath') {
134
149
  config[key] = value;
135
- saveConfig(config);
150
+ saveConfigFile(config);
136
151
  console.error(`${key}: ${value}`);
137
152
  return;
138
153
  }
139
- const allKeys = [...hookKeys, ...namingKeys, 'device.alias', 'memoryDir', 'claudeMdPath'];
154
+ // Handle type registry
155
+ if (key.startsWith('types.')) {
156
+ const typeName = key.slice(6);
157
+ const delivery = value;
158
+ if (!['persona', 'project', 'knowledge', 'protected'].includes(delivery)) {
159
+ console.error(`Invalid delivery tier: "${value}". Must be: persona, project, knowledge, or protected.`);
160
+ process.exit(1);
161
+ }
162
+ const nameError = validateTypeName(typeName);
163
+ if (nameError) {
164
+ console.error(nameError);
165
+ process.exit(1);
166
+ }
167
+ const oldDelivery = config.types?.[typeName] ?? BUILTIN_TYPES[typeName];
168
+ if (!config.types)
169
+ config.types = {};
170
+ config.types[typeName] = delivery;
171
+ saveConfigFile(config);
172
+ const isBuiltin = typeName in BUILTIN_TYPES;
173
+ const action = isBuiltin ? 'overridden' : 'registered';
174
+ console.error(`types.${typeName}: ${delivery} (${action})`);
175
+ // Delivery change propagation — only if we have DB access and delivery actually changed
176
+ if (clients && oldDelivery && oldDelivery !== delivery) {
177
+ const { data: notes } = await clients.supabase
178
+ .from('notes')
179
+ .select('id, metadata')
180
+ .eq('metadata->>type', typeName);
181
+ const affected = (notes ?? []).filter((n) => n.metadata.delivery !== delivery);
182
+ if (affected.length > 0) {
183
+ console.error(`\n${affected.length} note(s) currently have a different delivery:`);
184
+ for (const note of affected) {
185
+ const meta = note.metadata;
186
+ const uKey = meta.upsert_key || `id-${note.id}`;
187
+ console.error(` [${note.id}] ${uKey} — delivery: ${meta.delivery}`);
188
+ }
189
+ const action = await choose('\nUpdate delivery on these notes?', [
190
+ 'all — update all notes',
191
+ 'select — choose individually',
192
+ 'none — only affect new notes',
193
+ ]);
194
+ if (action.startsWith('all')) {
195
+ for (const note of affected) {
196
+ await opUpdateMetadata(clients, note.id, { delivery });
197
+ }
198
+ console.error(`Updated delivery to "${delivery}" on ${affected.length} note(s).`);
199
+ }
200
+ else if (action.startsWith('select')) {
201
+ let updated = 0;
202
+ for (const note of affected) {
203
+ const meta = note.metadata;
204
+ const uKey = meta.upsert_key || `id-${note.id}`;
205
+ const yes = await confirm(` Update [${note.id}] ${uKey}?`);
206
+ if (yes) {
207
+ await opUpdateMetadata(clients, note.id, { delivery });
208
+ updated++;
209
+ }
210
+ }
211
+ console.error(`Updated delivery on ${updated} note(s).`);
212
+ }
213
+ }
214
+ }
215
+ return;
216
+ }
217
+ const allKeys = [...hookKeys, ...namingKeys, 'device.alias', 'memoryDir', 'claudeMdPath', 'types.*'];
140
218
  console.error(`Unknown config key: ${key}`);
141
219
  console.error(`Available: ${allKeys.join(', ')}`);
142
220
  process.exit(1);
143
221
  }
222
+ export async function configUnset(key, clients) {
223
+ if (!key.startsWith('types.')) {
224
+ console.error(`Unset is only supported for types.* keys. Got: ${key}`);
225
+ process.exit(1);
226
+ }
227
+ const typeName = key.slice(6);
228
+ const config = loadConfigFile();
229
+ const userTypes = config.types ?? {};
230
+ if (!(typeName in userTypes)) {
231
+ console.error(`No user override for "${typeName}".`);
232
+ return;
233
+ }
234
+ const isBuiltin = typeName in BUILTIN_TYPES;
235
+ if (!isBuiltin && clients) {
236
+ const { data: notes } = await clients.supabase
237
+ .from('notes')
238
+ .select('id')
239
+ .eq('metadata->>type', typeName);
240
+ if (notes && notes.length > 0) {
241
+ console.error(`\n${notes.length} note(s) use type "${typeName}". They will become unregistered (delivery defaults to "knowledge").`);
242
+ const proceed = await confirm('Proceed?');
243
+ if (!proceed) {
244
+ console.error('Cancelled.');
245
+ return;
246
+ }
247
+ }
248
+ }
249
+ delete config.types[typeName];
250
+ if (config.types && Object.keys(config.types).length === 0)
251
+ delete config.types;
252
+ saveConfigFile(config);
253
+ if (isBuiltin) {
254
+ console.error(`Reverted "${typeName}" to built-in default: ${BUILTIN_TYPES[typeName]}`);
255
+ }
256
+ else {
257
+ console.error(`Removed custom type "${typeName}".`);
258
+ }
259
+ }
144
260
  export async function configList() {
145
- const config = loadFullConfig();
261
+ const config = loadConfigFile();
146
262
  const hooks = config.hooks || {};
147
263
  console.error('Hook settings:');
148
264
  for (const [key, desc] of Object.entries(DESCRIPTIONS)) {
@@ -163,5 +279,20 @@ export async function configList() {
163
279
  console.error('\nPaths:');
164
280
  console.error(` memoryDir: ${config.memoryDir || '(default)'}`);
165
281
  console.error(` claudeMdPath: ${config.claudeMdPath || '(default)'}`);
282
+ const registry = getTypeRegistry();
283
+ const userTypes = config.types ?? {};
284
+ const customTypes = Object.keys(userTypes).filter(t => !(t in BUILTIN_TYPES));
285
+ const overrides = Object.keys(userTypes).filter(t => t in BUILTIN_TYPES);
286
+ console.error('\nType registry:');
287
+ if (customTypes.length > 0) {
288
+ console.error(` Custom types: ${customTypes.map(t => `${t} (${userTypes[t]})`).join(', ')}`);
289
+ }
290
+ if (overrides.length > 0) {
291
+ console.error(` Overrides: ${overrides.map(t => `${t}: ${userTypes[t]} (default: ${BUILTIN_TYPES[t]})`).join(', ')}`);
292
+ }
293
+ if (customTypes.length === 0 && overrides.length === 0) {
294
+ console.error(' No custom types or overrides. Using built-in defaults.');
295
+ }
296
+ console.error(` Built-in types: ${Object.keys(BUILTIN_TYPES).length}`);
166
297
  console.error('\nConfig file: ' + CONFIG_PATH);
167
298
  }
@@ -108,7 +108,7 @@ async function ingestFile(config, filePath, existingNotes) {
108
108
  const defaultDelivery = inferDelivery(noteType);
109
109
  const deliveryChoice = await choose(`Delivery tier (default: ${defaultDelivery}):`, [
110
110
  `${defaultDelivery} (default)`,
111
- ...['persona', 'project', 'knowledge'].filter(d => d !== defaultDelivery),
111
+ ...['persona', 'project', 'knowledge', 'protected'].filter(d => d !== defaultDelivery),
112
112
  ]);
113
113
  const delivery = deliveryChoice.replace(' (default)', '');
114
114
  const { openai } = config;
@@ -0,0 +1,179 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { createInterface } from 'readline';
5
+ import { detectStack, getConfigsForStack, ESLINT_TS, ESLINT_TS_REACT, STYLELINT_UNIVERSAL, } from '../lib/lint-configs.js';
6
+ // --- Helpers ---
7
+ async function confirm(question) {
8
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
9
+ return new Promise((resolve) => {
10
+ rl.question(`${question} [y/N] `, (answer) => {
11
+ rl.close();
12
+ resolve(answer.trim() === 'y' || answer.trim() === 'yes');
13
+ });
14
+ });
15
+ }
16
+ // --- writeConfig ---
17
+ export async function writeConfig(projectDir, config) {
18
+ const filePath = resolve(projectDir, config.filename);
19
+ if (existsSync(filePath)) {
20
+ const existing = readFileSync(filePath, 'utf-8');
21
+ if (existing === config.content) {
22
+ console.error(` ${config.filename} — already up to date`);
23
+ return;
24
+ }
25
+ const overwrite = await confirm(` ${config.filename} already exists. Overwrite?`);
26
+ if (!overwrite) {
27
+ console.error(` ${config.filename} — skipped`);
28
+ return;
29
+ }
30
+ }
31
+ writeFileSync(filePath, config.content, 'utf-8');
32
+ console.error(` ${config.filename} — written`);
33
+ }
34
+ function getUniversalConfigs(hasReact) {
35
+ return {
36
+ eslint: { filename: 'eslint.config.js', content: hasReact ? ESLINT_TS_REACT : ESLINT_TS },
37
+ stylelint: { filename: '.stylelintrc.json', content: STYLELINT_UNIVERSAL },
38
+ };
39
+ }
40
+ export async function handleDiff(projectDir, configs, universalConfigs) {
41
+ const toCheck = [];
42
+ if (configs.eslint) {
43
+ toCheck.push({ filename: configs.eslint.filename, template: configs.eslint.content });
44
+ // Also check universal if writing personal
45
+ if (configs.eslint.filename !== universalConfigs.eslint.filename) {
46
+ toCheck.push({ filename: universalConfigs.eslint.filename, template: universalConfigs.eslint.content });
47
+ }
48
+ }
49
+ if (configs.stylelint) {
50
+ toCheck.push({ filename: configs.stylelint.filename, template: configs.stylelint.content });
51
+ if (configs.stylelint.filename !== universalConfigs.stylelint.filename) {
52
+ toCheck.push({ filename: universalConfigs.stylelint.filename, template: universalConfigs.stylelint.content });
53
+ }
54
+ }
55
+ if (toCheck.length === 0) {
56
+ console.error('No lint configs applicable for this project stack.');
57
+ return;
58
+ }
59
+ let anyDiff = false;
60
+ for (const { filename, template } of toCheck) {
61
+ const filePath = resolve(projectDir, filename);
62
+ if (!existsSync(filePath)) {
63
+ console.error(` ${filename} — not found (run without --diff to create)`);
64
+ anyDiff = true;
65
+ continue;
66
+ }
67
+ const existing = readFileSync(filePath, 'utf-8');
68
+ if (existing === template) {
69
+ console.error(` ${filename} — up to date`);
70
+ }
71
+ else {
72
+ console.error(` ${filename} — differs from Ledger template`);
73
+ anyDiff = true;
74
+ const update = await confirm(` Update ${filename} to Ledger version?`);
75
+ if (update) {
76
+ writeFileSync(filePath, template, 'utf-8');
77
+ console.error(` ${filename} — updated`);
78
+ }
79
+ }
80
+ }
81
+ if (!anyDiff) {
82
+ console.error('\nAll configs are up to date.');
83
+ }
84
+ }
85
+ // --- checkDependencies ---
86
+ const ESLINT_TS_DEPS = ['typescript-eslint', 'eslint-plugin-import'];
87
+ const ESLINT_REACT_DEPS = ['eslint-plugin-react', 'eslint-plugin-jsx-a11y'];
88
+ const STYLELINT_DEPS = ['stylelint', 'stylelint-config-standard-scss'];
89
+ export async function checkDependencies(projectDir, stack, personal) {
90
+ const packageJsonPath = resolve(projectDir, 'package.json');
91
+ if (!existsSync(packageJsonPath))
92
+ return;
93
+ let pkg;
94
+ try {
95
+ pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
96
+ }
97
+ catch {
98
+ return;
99
+ }
100
+ const installed = new Set([
101
+ ...Object.keys(pkg.dependencies ?? {}),
102
+ ...Object.keys(pkg.devDependencies ?? {}),
103
+ ]);
104
+ const needed = [];
105
+ const needsEslint = stack.hasTypeScript || stack.hasReact;
106
+ const needsStylelint = stack.hasScss;
107
+ if (needsEslint) {
108
+ for (const dep of ESLINT_TS_DEPS) {
109
+ if (!installed.has(dep))
110
+ needed.push(dep);
111
+ }
112
+ if (stack.hasReact) {
113
+ for (const dep of ESLINT_REACT_DEPS) {
114
+ if (!installed.has(dep))
115
+ needed.push(dep);
116
+ }
117
+ }
118
+ }
119
+ if (needsStylelint) {
120
+ for (const dep of STYLELINT_DEPS) {
121
+ if (!installed.has(dep))
122
+ needed.push(dep);
123
+ }
124
+ }
125
+ if (personal) {
126
+ // no extra deps for personal configs
127
+ }
128
+ if (needed.length === 0)
129
+ return;
130
+ console.error('\nMissing devDependencies:');
131
+ for (const dep of needed) {
132
+ console.error(` - ${dep}`);
133
+ }
134
+ const install = await confirm('\nInstall missing dependencies now?');
135
+ if (install) {
136
+ execSync(`npm install --save-dev ${needed.join(' ')}`, { stdio: 'inherit', cwd: projectDir });
137
+ }
138
+ }
139
+ // --- Main lint command ---
140
+ export async function lint(options) {
141
+ const projectDir = process.cwd();
142
+ console.error('Detecting project stack...');
143
+ const stack = detectStack(projectDir);
144
+ console.error(` package.json: ${stack.hasPackageJson}`);
145
+ console.error(` TypeScript: ${stack.hasTypeScript}`);
146
+ console.error(` React: ${stack.hasReact}`);
147
+ console.error(` SCSS: ${stack.hasScss}`);
148
+ console.error('');
149
+ const configs = getConfigsForStack(stack, options.personal);
150
+ const universalConfigs = getUniversalConfigs(stack.hasReact);
151
+ if (!configs.eslint && !configs.stylelint) {
152
+ console.error('No lint configs applicable for this stack (needs TypeScript, React, or SCSS).');
153
+ return;
154
+ }
155
+ if (options.diff) {
156
+ console.error('Comparing local configs against Ledger templates...\n');
157
+ await handleDiff(projectDir, configs, universalConfigs);
158
+ return;
159
+ }
160
+ console.error('Writing lint configs...\n');
161
+ // When personal, also write the universal configs that personal ones extend
162
+ if (options.personal) {
163
+ if (configs.eslint) {
164
+ await writeConfig(projectDir, universalConfigs.eslint);
165
+ }
166
+ if (configs.stylelint) {
167
+ await writeConfig(projectDir, universalConfigs.stylelint);
168
+ }
169
+ }
170
+ if (configs.eslint) {
171
+ await writeConfig(projectDir, configs.eslint);
172
+ }
173
+ if (configs.stylelint) {
174
+ await writeConfig(projectDir, configs.stylelint);
175
+ }
176
+ console.error('');
177
+ await checkDependencies(projectDir, stack, options.personal);
178
+ console.error('\nDone.');
179
+ }