Skip to content

Commit ebb7e2e

Browse files
feat(desktop): add input context and inline comments
1 parent abf7836 commit ebb7e2e

22 files changed

Lines changed: 1398 additions & 106 deletions
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import type { Dirent } from 'node:fs';
2+
import { readFile, readdir } from 'node:fs/promises';
3+
import { basename, extname, join, relative } from 'node:path';
4+
import type { StoredDesignSystem } from '@open-codesign/shared';
5+
6+
const IGNORED_DIRS = new Set([
7+
'.git',
8+
'.idea',
9+
'.next',
10+
'.turbo',
11+
'.vscode',
12+
'build',
13+
'coverage',
14+
'dist',
15+
'node_modules',
16+
'out',
17+
]);
18+
19+
const CANDIDATE_EXTS = new Set([
20+
'.css',
21+
'.scss',
22+
'.sass',
23+
'.less',
24+
'.js',
25+
'.jsx',
26+
'.ts',
27+
'.tsx',
28+
'.json',
29+
'.md',
30+
]);
31+
32+
const PRIORITY_PATTERNS = [
33+
/tailwind\.config/i,
34+
/tokens?/i,
35+
/theme/i,
36+
/brand/i,
37+
/palette/i,
38+
/typography/i,
39+
/global\.(css|scss|sass|less)$/i,
40+
/variables?\.(css|scss|sass|less)$/i,
41+
];
42+
43+
const MAX_FILES = 160;
44+
const MAX_SELECTED_FILES = 12;
45+
const MAX_FILE_CHARS = 32_000;
46+
47+
interface CandidateFile {
48+
fullPath: string;
49+
relativePath: string;
50+
score: number;
51+
}
52+
53+
function pushUnique(target: string[], value: string, max: number): void {
54+
if (!value || target.includes(value) || target.length >= max) return;
55+
target.push(value);
56+
}
57+
58+
function cleanValue(value: string): string {
59+
return value
60+
.replace(/\s+/g, ' ')
61+
.replace(/["'`,]/g, '')
62+
.trim();
63+
}
64+
65+
function scoreCandidate(relativePath: string): number {
66+
const fileName = basename(relativePath);
67+
let score = 1;
68+
for (const pattern of PRIORITY_PATTERNS) {
69+
if (pattern.test(relativePath) || pattern.test(fileName)) score += 20;
70+
}
71+
if (/\.(css|scss|sass|less)$/i.test(fileName)) score += 8;
72+
if (/tailwind/i.test(relativePath)) score += 8;
73+
if (/src\//i.test(relativePath)) score += 4;
74+
return score;
75+
}
76+
77+
async function collectCandidateFiles(
78+
rootPath: string,
79+
dirPath: string,
80+
files: CandidateFile[],
81+
): Promise<void> {
82+
if (files.length >= MAX_FILES) return;
83+
84+
let entries: Dirent<string>[];
85+
try {
86+
entries = await readdir(dirPath, { withFileTypes: true, encoding: 'utf8' });
87+
} catch {
88+
return;
89+
}
90+
91+
for (const entry of entries) {
92+
if (files.length >= MAX_FILES) return;
93+
const fullPath = join(dirPath, entry.name);
94+
if (entry.isDirectory()) {
95+
if (!IGNORED_DIRS.has(entry.name)) {
96+
await collectCandidateFiles(rootPath, fullPath, files);
97+
}
98+
continue;
99+
}
100+
if (!entry.isFile()) continue;
101+
const extension = extname(entry.name).toLowerCase();
102+
if (!CANDIDATE_EXTS.has(extension) && !/tailwind\.config/i.test(entry.name)) continue;
103+
const relativePath = relative(rootPath, fullPath).replace(/\\/g, '/');
104+
files.push({ fullPath, relativePath, score: scoreCandidate(relativePath) });
105+
}
106+
}
107+
108+
function collectCssVarValues(
109+
raw: string,
110+
colors: string[],
111+
spacing: string[],
112+
radius: string[],
113+
shadows: string[],
114+
): void {
115+
for (const match of raw.matchAll(/--([a-z0-9-]+)\s*:\s*([^;}{\n]+)/gi)) {
116+
const name = match[1]?.toLowerCase() ?? '';
117+
const value = cleanValue(match[2] ?? '');
118+
if (!value) continue;
119+
if (/color|accent|surface|text|brand|primary|secondary/.test(name))
120+
pushUnique(colors, value, 24);
121+
if (/space|spacing|gap|padding|margin/.test(name)) pushUnique(spacing, value, 16);
122+
if (/radius|rounded/.test(name)) pushUnique(radius, value, 16);
123+
if (/shadow/.test(name)) pushUnique(shadows, value, 16);
124+
}
125+
}
126+
127+
function collectLooseValues(
128+
raw: string,
129+
colors: string[],
130+
fonts: string[],
131+
spacing: string[],
132+
radius: string[],
133+
shadows: string[],
134+
): void {
135+
for (const match of raw.matchAll(/#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)/gi)) {
136+
pushUnique(colors, cleanValue(match[0] ?? ''), 24);
137+
}
138+
139+
for (const match of raw.matchAll(/(?:font-family|fontFamily)[^:=]*[:=]\s*([^\n;]+)/gi)) {
140+
const value = cleanValue(match[1] ?? '');
141+
for (const part of value.split(',')) pushUnique(fonts, cleanValue(part), 16);
142+
}
143+
144+
for (const match of raw.matchAll(
145+
/(?:spacing|space|gap|padding|margin)[^:=\n]*[:=]\s*([^\n,;]+)/gi,
146+
)) {
147+
pushUnique(spacing, cleanValue(match[1] ?? ''), 16);
148+
}
149+
150+
for (const match of raw.matchAll(/(?:radius|rounded|borderRadius)[^:=\n]*[:=]\s*([^\n,;]+)/gi)) {
151+
pushUnique(radius, cleanValue(match[1] ?? ''), 16);
152+
}
153+
154+
for (const match of raw.matchAll(/(?:shadow|boxShadow)[^:=\n]*[:=]\s*([^\n,;]+)/gi)) {
155+
pushUnique(shadows, cleanValue(match[1] ?? ''), 16);
156+
}
157+
}
158+
159+
function buildSummary(snapshot: Omit<StoredDesignSystem, 'summary' | 'extractedAt'>): string {
160+
const parts = [
161+
`Scanned ${snapshot.sourceFiles.length} likely design-system files under ${snapshot.rootPath}.`,
162+
];
163+
if (snapshot.colors.length > 0)
164+
parts.push(`Color language: ${snapshot.colors.slice(0, 5).join(', ')}.`);
165+
if (snapshot.fonts.length > 0)
166+
parts.push(`Typography: ${snapshot.fonts.slice(0, 4).join(', ')}.`);
167+
if (snapshot.spacing.length > 0)
168+
parts.push(`Spacing cues: ${snapshot.spacing.slice(0, 4).join(', ')}.`);
169+
if (snapshot.radius.length > 0)
170+
parts.push(`Corner radius cues: ${snapshot.radius.slice(0, 4).join(', ')}.`);
171+
if (snapshot.shadows.length > 0)
172+
parts.push(`Shadow cues: ${snapshot.shadows.slice(0, 4).join(', ')}.`);
173+
if (parts.length === 1) {
174+
parts.push(
175+
'No strong structured tokens were extracted, so lean on the referenced styling files and keep the output conservative and cohesive.',
176+
);
177+
}
178+
return parts.join(' ');
179+
}
180+
181+
export async function scanDesignSystem(rootPath: string): Promise<StoredDesignSystem> {
182+
const candidates: CandidateFile[] = [];
183+
await collectCandidateFiles(rootPath, rootPath, candidates);
184+
185+
const selected = candidates
186+
.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath))
187+
.slice(0, MAX_SELECTED_FILES);
188+
189+
const colors: string[] = [];
190+
const fonts: string[] = [];
191+
const spacing: string[] = [];
192+
const radius: string[] = [];
193+
const shadows: string[] = [];
194+
195+
for (const file of selected) {
196+
let raw = '';
197+
try {
198+
raw = await readFile(file.fullPath, 'utf8');
199+
} catch {
200+
continue;
201+
}
202+
const snippet = raw.slice(0, MAX_FILE_CHARS);
203+
collectCssVarValues(snippet, colors, spacing, radius, shadows);
204+
collectLooseValues(snippet, colors, fonts, spacing, radius, shadows);
205+
}
206+
207+
const baseSnapshot = {
208+
rootPath,
209+
sourceFiles: selected.map((file) => file.relativePath),
210+
colors,
211+
fonts,
212+
spacing,
213+
radius,
214+
shadows,
215+
};
216+
217+
return {
218+
...baseSnapshot,
219+
summary: buildSummary(baseSnapshot),
220+
extractedAt: new Date().toISOString(),
221+
};
222+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createRequire } from 'node:module';
2+
3+
const require = createRequire(import.meta.url);
4+
5+
export const electron = require('electron') as typeof import('electron');
6+
7+
export const { app, BrowserWindow, dialog, ipcMain, safeStorage, shell } = electron;

‎apps/desktop/src/main/exporter-ipc.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ExporterFormat, exportArtifact } from '@open-codesign/exporters';
22
import { CodesignError } from '@open-codesign/shared';
3-
import { type BrowserWindow, dialog, ipcMain } from 'electron';
3+
import type { BrowserWindow } from 'electron';
4+
import { dialog, ipcMain } from './electron-runtime';
45

56
const FORMAT_FILTERS: Record<ExporterFormat, Electron.FileFilter[]> = {
67
html: [{ name: 'HTML', extensions: ['html'] }],

0 commit comments

Comments
 (0)