@upgraide/ui-notes-cli 0.1.1 → 0.1.3

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,138 @@
1
+ import type { Config } from '../config.js';
2
+ import { apiFetch } from '../api.js';
3
+
4
+ interface BulkResolveOptions {
5
+ project?: string;
6
+ message?: string;
7
+ dryRun?: boolean;
8
+ force?: boolean;
9
+ status?: 'resolved' | 'wontfix';
10
+ }
11
+
12
+ export async function resolveByComponent(
13
+ config: Config,
14
+ componentName: string,
15
+ opts: BulkResolveOptions,
16
+ ): Promise<void> {
17
+ await bulkResolve(config, 'component', componentName, opts);
18
+ }
19
+
20
+ export async function resolveBySelector(
21
+ config: Config,
22
+ selector: string,
23
+ opts: BulkResolveOptions,
24
+ ): Promise<void> {
25
+ await bulkResolve(config, 'selector', selector, opts);
26
+ }
27
+
28
+ async function bulkResolve(
29
+ config: Config,
30
+ filterBy: 'component' | 'selector',
31
+ filterValue: string,
32
+ opts: BulkResolveOptions,
33
+ ): Promise<void> {
34
+ const project = opts.project || config.defaultProject;
35
+ if (!project) {
36
+ console.error('No project specified. Use --project or set defaultProject in config.');
37
+ process.exit(1);
38
+ }
39
+
40
+ const status = opts.status || 'resolved';
41
+ const resolution = opts.message || `Bulk ${status} (${filterBy}: ${filterValue})`;
42
+
43
+ if (opts.dryRun) {
44
+ // Fetch matching notes to show what would be resolved
45
+ const response = await apiFetch(config, `/notes?status=open`, { project }) as any;
46
+ const notes = response.data?.notes || [];
47
+
48
+ const matching = notes.filter((n: any) => {
49
+ if (filterBy === 'component') {
50
+ return n.component?.toLowerCase() === filterValue.toLowerCase();
51
+ }
52
+ if (filterBy === 'selector') {
53
+ return n.selector?.includes(filterValue);
54
+ }
55
+ return false;
56
+ });
57
+
58
+ if (matching.length === 0) {
59
+ console.log('No matching open notes found.');
60
+ return;
61
+ }
62
+
63
+ console.log(`[DRY RUN] Would resolve ${matching.length} notes:`);
64
+ for (const note of matching) {
65
+ console.log(` ${note.id} | ${note.type} | ${truncate(note.body, 60)}`);
66
+ }
67
+ return;
68
+ }
69
+
70
+ // If not forced, do a dry-run first to show count and confirm
71
+ if (!opts.force) {
72
+ const response = await apiFetch(config, `/notes?status=open`, { project }) as any;
73
+ const notes = response.data?.notes || [];
74
+
75
+ const matching = notes.filter((n: any) => {
76
+ if (filterBy === 'component') {
77
+ return n.component?.toLowerCase() === filterValue.toLowerCase();
78
+ }
79
+ if (filterBy === 'selector') {
80
+ return n.selector?.includes(filterValue);
81
+ }
82
+ return false;
83
+ });
84
+
85
+ if (matching.length === 0) {
86
+ console.log('No matching open notes found.');
87
+ return;
88
+ }
89
+
90
+ const label = filterBy === 'component' ? `Component=${filterValue}` : `Selector=${filterValue}`;
91
+ const answer = await confirm(`About to resolve ${matching.length} notes matching ${label}. Continue? y/n `);
92
+ if (!answer) {
93
+ console.log('Aborted.');
94
+ return;
95
+ }
96
+ }
97
+
98
+ // Call bulk endpoint
99
+ const response = await apiFetch(config, '/notes/bulk', {
100
+ method: 'PATCH',
101
+ body: {
102
+ filter: {
103
+ status: 'open',
104
+ ...(filterBy === 'component' ? { component: filterValue } : {}),
105
+ ...(filterBy === 'selector' ? { selector: filterValue } : {}),
106
+ },
107
+ update: {
108
+ status,
109
+ resolution,
110
+ },
111
+ },
112
+ project,
113
+ }) as any;
114
+
115
+ const count = response.data?.updated || 0;
116
+
117
+ if (count === 0) {
118
+ console.log('No matching open notes found.');
119
+ } else {
120
+ console.log(`Resolved ${count} notes.`);
121
+ }
122
+ }
123
+
124
+ async function confirm(question: string): Promise<boolean> {
125
+ const readline = await import('readline');
126
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
127
+
128
+ return new Promise((resolve) => {
129
+ rl.question(question, (answer) => {
130
+ rl.close();
131
+ resolve(answer.trim().toLowerCase() === 'y');
132
+ });
133
+ });
134
+ }
135
+
136
+ function truncate(s: string, max: number): string {
137
+ return s.length > max ? s.slice(0, max) + '...' : s;
138
+ }
@@ -0,0 +1,27 @@
1
+ import type { Config } from '../config.js';
2
+ import { apiFetch } from '../api.js';
3
+
4
+ export async function comment(
5
+ config: Config,
6
+ noteId: string,
7
+ body: string,
8
+ project?: string,
9
+ ): Promise<void> {
10
+ const data = await apiFetch(config, `/notes/${noteId}/comments`, {
11
+ method: 'POST',
12
+ body: { body },
13
+ project,
14
+ });
15
+ console.log(JSON.stringify(data, null, 2));
16
+ }
17
+
18
+ export async function listComments(
19
+ config: Config,
20
+ noteId: string,
21
+ project?: string,
22
+ ): Promise<void> {
23
+ const data = await apiFetch(config, `/notes/${noteId}/comments`, {
24
+ project,
25
+ });
26
+ console.log(JSON.stringify(data, null, 2));
27
+ }
@@ -0,0 +1,64 @@
1
+ import type { Config } from '../config.js';
2
+ import { apiFetch } from '../api.js';
3
+ import { formatNotesForAI } from '../formatters/ai-context.js';
4
+ import type { ResolvedNote } from '../resolvers/file-resolver.js';
5
+
6
+ interface ContextOptions {
7
+ project?: string;
8
+ type?: string;
9
+ status?: string;
10
+ component?: string;
11
+ format?: 'markdown' | 'xml' | 'json';
12
+ limit?: string;
13
+ resolveFiles?: boolean;
14
+ rootDir?: string;
15
+ }
16
+
17
+ export async function context(config: Config, opts: ContextOptions): Promise<void> {
18
+ const params = new URLSearchParams();
19
+ params.set('status', opts.status || 'open');
20
+ if (opts.type) params.set('type', opts.type);
21
+ if (opts.limit) params.set('limit', opts.limit);
22
+
23
+ const qs = params.toString();
24
+ const path = `/notes${qs ? `?${qs}` : ''}`;
25
+
26
+ const response = await apiFetch(config, path, { project: opts.project }) as any;
27
+ const notes = response.data?.notes || [];
28
+
29
+ // Optionally resolve notes to source files
30
+ let resolvedMap: Map<string, ResolvedNote> | undefined;
31
+ if (opts.resolveFiles) {
32
+ const { resolveNoteToFiles } = await import('../resolvers/file-resolver.js');
33
+ const resolveConfig = config.resolve || {};
34
+ const rootDir = opts.rootDir || resolveConfig.rootDir || process.cwd();
35
+ resolvedMap = new Map();
36
+
37
+ for (const note of notes) {
38
+ const resolved = await resolveNoteToFiles(
39
+ {
40
+ id: note.id,
41
+ component: note.component || null,
42
+ selector: note.selector || null,
43
+ tag: note.tag || null,
44
+ },
45
+ {
46
+ rootDir,
47
+ extensions: resolveConfig.extensions,
48
+ excludeDirs: resolveConfig.excludeDirs,
49
+ },
50
+ );
51
+ resolvedMap.set(note.id, resolved);
52
+ }
53
+ }
54
+
55
+ const output = formatNotesForAI(notes, {
56
+ format: opts.format || 'markdown',
57
+ componentFilter: opts.component,
58
+ projectName: opts.project || config.defaultProject || 'unknown',
59
+ resolvedNotes: resolvedMap,
60
+ });
61
+
62
+ // Write to stdout -- pure pipeable output, no JSON wrapper, no logging
63
+ process.stdout.write(output);
64
+ }
package/commands/pull.ts CHANGED
@@ -6,6 +6,8 @@ interface PullOptions {
6
6
  status?: string;
7
7
  type?: string;
8
8
  limit?: string;
9
+ resolveFiles?: boolean;
10
+ rootDir?: string;
9
11
  }
10
12
 
11
13
  export async function pull(config: Config, opts: PullOptions): Promise<void> {
@@ -17,6 +19,31 @@ export async function pull(config: Config, opts: PullOptions): Promise<void> {
17
19
  const qs = params.toString();
18
20
  const path = `/notes${qs ? `?${qs}` : ''}`;
19
21
 
20
- const data = await apiFetch(config, path, { project: opts.project });
22
+ const data = (await apiFetch(config, path, { project: opts.project })) as any;
23
+
24
+ if (opts.resolveFiles) {
25
+ const { resolveNoteToFiles } = await import('../resolvers/file-resolver.js');
26
+ const resolveConfig = config.resolve || {};
27
+ const rootDir = opts.rootDir || resolveConfig.rootDir || process.cwd();
28
+ const notes = data.data?.notes || data.notes || [];
29
+
30
+ for (const note of notes) {
31
+ const resolved = await resolveNoteToFiles(
32
+ {
33
+ id: note.id,
34
+ component: note.component || null,
35
+ selector: note.selector || null,
36
+ tag: note.tag || null,
37
+ },
38
+ {
39
+ rootDir,
40
+ extensions: resolveConfig.extensions,
41
+ excludeDirs: resolveConfig.excludeDirs,
42
+ },
43
+ );
44
+ note.resolved_files = resolved.files;
45
+ }
46
+ }
47
+
21
48
  console.log(JSON.stringify(data));
22
49
  }
@@ -0,0 +1,366 @@
1
+ import type { Config } from '../config.js';
2
+ import { apiFetch } from '../api.js';
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { createHash } from 'crypto';
6
+
7
+ interface VisualDiffOptions {
8
+ project?: string;
9
+ url?: string;
10
+ compare?: boolean;
11
+ width?: number;
12
+ fullPage?: boolean;
13
+ }
14
+
15
+ const SNAPSHOT_DIR = '.uinotes-snapshots';
16
+
17
+ function hashUrl(url: string): string {
18
+ return createHash('md5').update(url).digest('hex');
19
+ }
20
+
21
+ function ensureDir(dir: string): void {
22
+ if (!existsSync(dir)) {
23
+ mkdirSync(dir, { recursive: true });
24
+ }
25
+ }
26
+
27
+ async function loadPlaywright() {
28
+ try {
29
+ const pw = await import('playwright');
30
+ return pw;
31
+ } catch {
32
+ console.error('Playwright is not installed.');
33
+ console.error('Install it with: bun add playwright');
34
+ console.error('Then install the browser: bunx playwright install chromium');
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ export async function visualDiff(config: Config, opts: VisualDiffOptions): Promise<void> {
40
+ const project = opts.project || config.defaultProject;
41
+ if (!project) {
42
+ console.error('Error: --project is required (or set defaultProject in config)');
43
+ process.exit(1);
44
+ }
45
+
46
+ const width = opts.width || 1280;
47
+ const fullPage = opts.fullPage !== false;
48
+
49
+ // Step 1: Fetch open notes to collect URLs
50
+ console.log(`Fetching open notes for project "${project}"...`);
51
+ const data = (await apiFetch(config, '/notes?status=open&limit=500', { project })) as any;
52
+ const notes = data.data?.notes || [];
53
+
54
+ if (notes.length === 0) {
55
+ console.log('No open notes found. Nothing to capture.');
56
+ return;
57
+ }
58
+
59
+ // Step 2: Extract unique URLs
60
+ const urlSet = new Set<string>();
61
+ for (const note of notes) {
62
+ if (note.url) urlSet.add(note.url);
63
+ }
64
+
65
+ let urls = Array.from(urlSet);
66
+
67
+ // Filter to specific URL if provided
68
+ if (opts.url) {
69
+ urls = urls.filter((u) => u.includes(opts.url!));
70
+ if (urls.length === 0) {
71
+ console.log(`No notes found matching URL "${opts.url}".`);
72
+ return;
73
+ }
74
+ }
75
+
76
+ console.log(`Found ${urls.length} unique URL(s) across ${notes.length} open notes\n`);
77
+
78
+ if (opts.compare) {
79
+ await runComparison(config, project, notes, urls, width, fullPage);
80
+ } else {
81
+ await captureSnapshots(urls, width, fullPage);
82
+ }
83
+ }
84
+
85
+ async function captureSnapshots(urls: string[], width: number, fullPage: boolean): Promise<void> {
86
+ const pw = await loadPlaywright();
87
+
88
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
89
+ const snapshotDir = join(SNAPSHOT_DIR, timestamp);
90
+ ensureDir(snapshotDir);
91
+
92
+ let browser;
93
+ try {
94
+ browser = await pw.chromium.launch({ headless: true });
95
+ } catch {
96
+ console.error('Chromium browser not found.');
97
+ console.error('Run: bunx playwright install chromium');
98
+ process.exit(1);
99
+ }
100
+
101
+ try {
102
+ for (const url of urls) {
103
+ console.log(`Capturing: ${url}`);
104
+ try {
105
+ const context = await browser.newContext({ viewport: { width, height: 800 } });
106
+ const page = await context.newPage();
107
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
108
+ await page.waitForTimeout(1000);
109
+
110
+ const filename = `${hashUrl(url)}.png`;
111
+ const filepath = join(snapshotDir, filename);
112
+ await page.screenshot({ fullPage, path: filepath, type: 'png' });
113
+ await context.close();
114
+
115
+ console.log(` Saved: ${filepath}`);
116
+ } catch (err: any) {
117
+ console.error(` Failed: ${err.message}`);
118
+ }
119
+ }
120
+ } finally {
121
+ await browser.close();
122
+ }
123
+
124
+ // Save a manifest mapping URL hashes to URLs
125
+ const manifest: Record<string, string> = {};
126
+ for (const url of urls) {
127
+ manifest[hashUrl(url)] = url;
128
+ }
129
+ writeFileSync(join(snapshotDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
130
+
131
+ console.log(`\nSnapshots saved to ${snapshotDir}`);
132
+ console.log(`Run with --compare to diff against this snapshot set.`);
133
+ }
134
+
135
+ async function runComparison(
136
+ config: Config,
137
+ project: string,
138
+ notes: any[],
139
+ urls: string[],
140
+ width: number,
141
+ fullPage: boolean,
142
+ ): Promise<void> {
143
+ // Find the most recent previous snapshot set
144
+ if (!existsSync(SNAPSHOT_DIR)) {
145
+ console.error(`No previous snapshots found in ${SNAPSHOT_DIR}. Run without --compare first.`);
146
+ process.exit(1);
147
+ }
148
+
149
+ const snapshotDirs = readdirSync(SNAPSHOT_DIR)
150
+ .filter((d) => !d.startsWith('.'))
151
+ .sort()
152
+ .reverse();
153
+
154
+ if (snapshotDirs.length === 0) {
155
+ console.error(`No previous snapshots found in ${SNAPSHOT_DIR}. Run without --compare first.`);
156
+ process.exit(1);
157
+ }
158
+
159
+ const previousDir = join(SNAPSHOT_DIR, snapshotDirs[0]);
160
+ console.log(`Comparing against previous snapshot: ${snapshotDirs[0]}\n`);
161
+
162
+ // Load previous manifest
163
+ const manifestPath = join(previousDir, 'manifest.json');
164
+ if (!existsSync(manifestPath)) {
165
+ console.error(`No manifest.json found in ${previousDir}. Cannot compare.`);
166
+ process.exit(1);
167
+ }
168
+ const previousManifest: Record<string, string> = JSON.parse(readFileSync(manifestPath, 'utf-8'));
169
+
170
+ // Dynamically import pixelmatch and pngjs
171
+ let pixelmatch: any;
172
+ let PNG: any;
173
+ try {
174
+ pixelmatch = (await import('pixelmatch')).default;
175
+ PNG = (await import('pngjs')).PNG;
176
+ } catch {
177
+ console.error('pixelmatch or pngjs not installed.');
178
+ console.error('Install with: bun add pixelmatch pngjs');
179
+ process.exit(1);
180
+ }
181
+
182
+ // Capture current state
183
+ const pw = await loadPlaywright();
184
+ let browser;
185
+ try {
186
+ browser = await pw.chromium.launch({ headless: true });
187
+ } catch {
188
+ console.error('Chromium browser not found.');
189
+ console.error('Run: bunx playwright install chromium');
190
+ process.exit(1);
191
+ }
192
+
193
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
194
+ const currentDir = join(SNAPSHOT_DIR, timestamp);
195
+ const diffDir = join(SNAPSHOT_DIR, `${timestamp}-diffs`);
196
+ ensureDir(currentDir);
197
+ ensureDir(diffDir);
198
+
199
+ interface DiffResult {
200
+ url: string;
201
+ diffPercentage: number;
202
+ affectedNotes: number;
203
+ diffPath: string | null;
204
+ beforePath: string;
205
+ afterPath: string;
206
+ }
207
+
208
+ const results: DiffResult[] = [];
209
+
210
+ try {
211
+ for (const url of urls) {
212
+ const urlHash = hashUrl(url);
213
+ const previousScreenshot = join(previousDir, `${urlHash}.png`);
214
+
215
+ if (!existsSync(previousScreenshot)) {
216
+ console.log(`Skipping ${url} - no previous screenshot found`);
217
+ continue;
218
+ }
219
+
220
+ console.log(`Processing: ${url}`);
221
+
222
+ // Take current screenshot
223
+ let currentScreenshotPath: string;
224
+ try {
225
+ const context = await browser.newContext({ viewport: { width, height: 800 } });
226
+ const page = await context.newPage();
227
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
228
+ await page.waitForTimeout(1000);
229
+
230
+ currentScreenshotPath = join(currentDir, `${urlHash}.png`);
231
+ await page.screenshot({ fullPage, path: currentScreenshotPath, type: 'png' });
232
+ await context.close();
233
+ } catch (err: any) {
234
+ console.error(` Failed to capture: ${err.message}`);
235
+ continue;
236
+ }
237
+
238
+ // Run pixelmatch comparison
239
+ try {
240
+ const beforeBuf = readFileSync(previousScreenshot);
241
+ const afterBuf = readFileSync(currentScreenshotPath);
242
+
243
+ const beforePng = PNG.sync.read(beforeBuf);
244
+ const afterPng = PNG.sync.read(afterBuf);
245
+
246
+ // Ensure same dimensions - use the smaller of the two
247
+ const minWidth = Math.min(beforePng.width, afterPng.width);
248
+ const minHeight = Math.min(beforePng.height, afterPng.height);
249
+
250
+ // If dimensions differ, we need to crop to common area
251
+ let beforeData = beforePng.data;
252
+ let afterData = afterPng.data;
253
+
254
+ if (beforePng.width !== afterPng.width || beforePng.height !== afterPng.height) {
255
+ console.log(` Note: Image sizes differ (before: ${beforePng.width}x${beforePng.height}, after: ${afterPng.width}x${afterPng.height})`);
256
+ console.log(` Comparing common area: ${minWidth}x${minHeight}`);
257
+
258
+ // Crop both to the minimum dimensions
259
+ beforeData = cropImageData(beforePng.data, beforePng.width, minWidth, minHeight);
260
+ afterData = cropImageData(afterPng.data, afterPng.width, minWidth, minHeight);
261
+ }
262
+
263
+ const diffPng = new PNG({ width: minWidth, height: minHeight });
264
+
265
+ const numDiffPixels = pixelmatch(
266
+ beforeData,
267
+ afterData,
268
+ diffPng.data,
269
+ minWidth,
270
+ minHeight,
271
+ { threshold: 0.1 },
272
+ );
273
+
274
+ const totalPixels = minWidth * minHeight;
275
+ const diffPercentage = (numDiffPixels / totalPixels) * 100;
276
+
277
+ // Save diff image
278
+ const diffPath = join(diffDir, `${urlHash}-diff.png`);
279
+ writeFileSync(diffPath, PNG.sync.write(diffPng));
280
+
281
+ const affectedNotes = notes.filter((n: any) => n.url === url).length;
282
+
283
+ console.log(` Diff: ${diffPercentage.toFixed(2)}% (${numDiffPixels} pixels changed)`);
284
+ console.log(` Affected notes: ${affectedNotes}`);
285
+
286
+ results.push({
287
+ url,
288
+ diffPercentage,
289
+ affectedNotes,
290
+ diffPath,
291
+ beforePath: previousScreenshot,
292
+ afterPath: currentScreenshotPath,
293
+ });
294
+
295
+ // Upload diff records to API for each affected note
296
+ for (const note of notes.filter((n: any) => n.url === url)) {
297
+ try {
298
+ await apiFetch(config, `/notes/${note.id}/diffs`, {
299
+ method: 'POST',
300
+ project,
301
+ body: {
302
+ before_url: previousScreenshot,
303
+ after_url: currentScreenshotPath,
304
+ diff_url: diffPath,
305
+ diff_percentage: diffPercentage,
306
+ },
307
+ });
308
+ } catch (err: any) {
309
+ // Non-fatal: diff record upload failed
310
+ console.error(` Warning: Could not save diff record for note ${note.id}: ${err.message}`);
311
+ }
312
+ }
313
+ } catch (err: any) {
314
+ console.error(` Diff failed: ${err.message}`);
315
+ }
316
+ }
317
+ } finally {
318
+ await browser.close();
319
+ }
320
+
321
+ // Save manifest for current snapshot
322
+ const manifest: Record<string, string> = {};
323
+ for (const url of urls) {
324
+ manifest[hashUrl(url)] = url;
325
+ }
326
+ writeFileSync(join(currentDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
327
+
328
+ // Print summary
329
+ console.log('\n--- Visual Diff Summary ---');
330
+ console.log(`URLs compared: ${results.length}`);
331
+
332
+ const changed = results.filter((r) => r.diffPercentage > 0);
333
+ if (changed.length === 0) {
334
+ console.log('No visual changes detected.');
335
+ } else {
336
+ console.log(`\nVisual changes on ${changed.length} URL(s):\n`);
337
+ console.log(padRight('URL', 50) + padRight('Notes', 8) + padRight('Change %', 12) + 'Status');
338
+ console.log('-'.repeat(80));
339
+ for (const r of results) {
340
+ const status = r.diffPercentage === 0 ? 'No change' : r.diffPercentage < 1 ? 'Minor' : r.diffPercentage < 5 ? 'Moderate' : 'Significant';
341
+ const urlDisplay = r.url.length > 48 ? r.url.slice(0, 45) + '...' : r.url;
342
+ console.log(
343
+ padRight(urlDisplay, 50) +
344
+ padRight(String(r.affectedNotes), 8) +
345
+ padRight(r.diffPercentage.toFixed(2) + '%', 12) +
346
+ status,
347
+ );
348
+ }
349
+ }
350
+
351
+ console.log(`\nDiff images saved to: ${diffDir}`);
352
+ }
353
+
354
+ function cropImageData(data: Uint8Array, srcWidth: number, targetWidth: number, targetHeight: number): Uint8Array {
355
+ const cropped = new Uint8Array(targetWidth * targetHeight * 4);
356
+ for (let y = 0; y < targetHeight; y++) {
357
+ const srcOffset = y * srcWidth * 4;
358
+ const dstOffset = y * targetWidth * 4;
359
+ cropped.set(data.subarray(srcOffset, srcOffset + targetWidth * 4), dstOffset);
360
+ }
361
+ return cropped;
362
+ }
363
+
364
+ function padRight(str: string, len: number): string {
365
+ return str.length >= len ? str : str + ' '.repeat(len - str.length);
366
+ }
package/config.ts CHANGED
@@ -2,10 +2,17 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { homedir } from 'os';
4
4
 
5
+ export interface ResolveConfig {
6
+ rootDir?: string;
7
+ extensions?: string[];
8
+ excludeDirs?: string[];
9
+ }
10
+
5
11
  export interface Config {
6
12
  apiUrl: string;
7
13
  apiKey: string;
8
14
  defaultProject?: string;
15
+ resolve?: ResolveConfig;
9
16
  }
10
17
 
11
18
  const GLOBAL_PATH = join(homedir(), '.uinotes.json');