@gemini-designer/mcp-server 0.1.2 → 0.1.29
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/dist/components/catalog.d.ts.map +1 -1
- package/dist/components/catalog.js +10 -4
- package/dist/components/catalog.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -6
- package/dist/config/index.js.map +1 -1
- package/dist/context/builder.d.ts.map +1 -1
- package/dist/context/builder.js.map +1 -1
- package/dist/context/filter.d.ts.map +1 -1
- package/dist/context/filter.js +5 -1
- package/dist/context/filter.js.map +1 -1
- package/dist/context/grounding.d.ts.map +1 -1
- package/dist/context/grounding.js +7 -3
- package/dist/context/grounding.js.map +1 -1
- package/dist/context/guards.d.ts.map +1 -1
- package/dist/context/guards.js +53 -0
- package/dist/context/guards.js.map +1 -1
- package/dist/context/repo-hints.js.map +1 -1
- package/dist/context/styling-detector.d.ts +24 -0
- package/dist/context/styling-detector.d.ts.map +1 -0
- package/dist/context/styling-detector.js +337 -0
- package/dist/context/styling-detector.js.map +1 -0
- package/dist/design/principles.js.map +1 -1
- package/dist/generation/gemini-client.d.ts.map +1 -1
- package/dist/generation/gemini-client.js.map +1 -1
- package/dist/generation/litellm-client.d.ts.map +1 -1
- package/dist/generation/litellm-client.js +14 -7
- package/dist/generation/litellm-client.js.map +1 -1
- package/dist/generation/remote-client.d.ts +10 -5
- package/dist/generation/remote-client.d.ts.map +1 -1
- package/dist/generation/remote-client.js +13 -2
- package/dist/generation/remote-client.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output/file-writer.d.ts.map +1 -1
- package/dist/output/file-writer.js +4 -4
- package/dist/output/file-writer.js.map +1 -1
- package/dist/output/formatter.d.ts.map +1 -1
- package/dist/output/formatter.js +5 -2
- package/dist/output/formatter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/stack/detect.d.ts.map +1 -1
- package/dist/stack/detect.js +42 -9
- package/dist/stack/detect.js.map +1 -1
- package/dist/tokens/sync.d.ts.map +1 -1
- package/dist/tokens/sync.js +22 -5
- package/dist/tokens/sync.js.map +1 -1
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
- package/dist/tools/analyze-screenshot-ui.js +5 -5
- package/dist/tools/analyze-screenshot-ui.js.map +1 -1
- package/dist/tools/analyze-tokens.d.ts.map +1 -1
- package/dist/tools/analyze-tokens.js +3 -1
- package/dist/tools/analyze-tokens.js.map +1 -1
- package/dist/tools/catalog-components.d.ts.map +1 -1
- package/dist/tools/catalog-components.js +1 -4
- package/dist/tools/catalog-components.js.map +1 -1
- package/dist/tools/create-ui.d.ts +3 -0
- package/dist/tools/create-ui.d.ts.map +1 -1
- package/dist/tools/create-ui.js +203 -75
- package/dist/tools/create-ui.js.map +1 -1
- package/dist/tools/detect-ui-stack.js.map +1 -1
- package/dist/tools/generate-component-variants.d.ts.map +1 -1
- package/dist/tools/generate-component-variants.js +15 -4
- package/dist/tools/generate-component-variants.js.map +1 -1
- package/dist/tools/generate-vibes.d.ts.map +1 -1
- package/dist/tools/generate-vibes.js +7 -3
- package/dist/tools/generate-vibes.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/modify-ui.d.ts.map +1 -1
- package/dist/tools/modify-ui.js +7 -2
- package/dist/tools/modify-ui.js.map +1 -1
- package/dist/tools/scaffold-project.d.ts.map +1 -1
- package/dist/tools/scaffold-project.js +3 -1
- package/dist/tools/scaffold-project.js.map +1 -1
- package/dist/tools/snippet-ui.d.ts +3 -1
- package/dist/tools/snippet-ui.d.ts.map +1 -1
- package/dist/tools/snippet-ui.js +219 -88
- package/dist/tools/snippet-ui.js.map +1 -1
- package/dist/tools/sync-design-tokens.d.ts.map +1 -1
- package/dist/tools/sync-design-tokens.js +26 -11
- package/dist/tools/sync-design-tokens.js.map +1 -1
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +55 -55
- package/src/__tests__/builder.test.ts +19 -19
- package/src/__tests__/config.test.ts +63 -31
- package/src/__tests__/filter.test.ts +98 -92
- package/src/__tests__/remote-client.test.ts +179 -0
- package/src/components/catalog.ts +170 -166
- package/src/config/index.ts +185 -177
- package/src/context/builder.ts +157 -157
- package/src/context/filter.ts +110 -104
- package/src/context/grounding.ts +143 -129
- package/src/context/guards.ts +97 -38
- package/src/context/repo-hints.ts +24 -24
- package/src/context/styling-detector.ts +460 -0
- package/src/design/principles.ts +14 -14
- package/src/generation/gemini-client.ts +53 -56
- package/src/generation/litellm-client.ts +102 -86
- package/src/generation/remote-client.ts +100 -77
- package/src/index.ts +16 -16
- package/src/output/file-writer.ts +123 -123
- package/src/output/formatter.ts +139 -132
- package/src/server.ts +12 -11
- package/src/stack/detect.ts +226 -175
- package/src/tokens/sync.ts +189 -155
- package/src/tools/analyze-screenshot-ui.ts +89 -88
- package/src/tools/analyze-tokens.ts +80 -78
- package/src/tools/catalog-components.ts +68 -68
- package/src/tools/create-ui.ts +295 -142
- package/src/tools/detect-ui-stack.ts +36 -36
- package/src/tools/generate-component-variants.ts +155 -135
- package/src/tools/generate-vibes.ts +121 -117
- package/src/tools/index.ts +14 -14
- package/src/tools/modify-ui.ts +170 -165
- package/src/tools/scaffold-project.ts +68 -66
- package/src/tools/snippet-ui.ts +323 -172
- package/src/tools/sync-design-tokens.ts +217 -195
- package/src/utils/walk.ts +47 -45
- package/src/version.ts +6 -0
- package/tsconfig.json +23 -33
- package/vitest.config.ts +10 -10
- package/.prettierrc +0 -9
- package/eslint.config.js +0 -37
package/src/context/grounding.ts
CHANGED
|
@@ -16,134 +16,140 @@ import * as path from 'node:path';
|
|
|
16
16
|
import { Config } from '../config/index.js';
|
|
17
17
|
import { detectUiStack, type StackDetectionResult } from '../stack/detect.js';
|
|
18
18
|
import { walkFiles, toPosixPath } from '../utils/walk.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
buildComponentCatalog,
|
|
21
|
+
type CatalogResult,
|
|
22
|
+
type ComponentExport,
|
|
23
|
+
} from '../components/catalog.js';
|
|
20
24
|
|
|
21
25
|
type Cached = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
ts: number;
|
|
27
|
+
stack: StackDetectionResult;
|
|
28
|
+
catalog: CatalogResult;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
const CACHE_TTL_MS = 60_000;
|
|
28
32
|
const cache = new Map<string, Cached>();
|
|
29
33
|
|
|
30
34
|
function resolveRootForFile(absFile: string | null, config: Config): string {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const allowed = (config.allowedPaths || []).map((p) => path.resolve(p));
|
|
36
|
+
const fallback = allowed[0] || process.cwd();
|
|
37
|
+
if (!absFile) return fallback;
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
const abs = path.resolve(absFile);
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// pick the deepest allowed root that contains the file
|
|
42
|
+
const matches = allowed
|
|
43
|
+
.filter((root) => abs === root || abs.startsWith(root + path.sep))
|
|
44
|
+
.sort((a, b) => b.length - a.length);
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
return matches[0] || fallback;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
function summarizeStack(stack: StackDetectionResult): string {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
50
|
+
const summary = {
|
|
51
|
+
framework: stack.framework,
|
|
52
|
+
typescript: stack.language.typescript,
|
|
53
|
+
styling: stack.styling,
|
|
54
|
+
uiLibraries: stack.uiLibraries,
|
|
55
|
+
iconLibraries: stack.iconLibraries,
|
|
56
|
+
tooling: stack.tooling,
|
|
57
|
+
conventions: {
|
|
58
|
+
srcDir: stack.conventions.srcDir,
|
|
59
|
+
hasAppDir: stack.conventions.hasAppDir,
|
|
60
|
+
hasPagesDir: stack.conventions.hasPagesDir,
|
|
61
|
+
tsconfigPaths: stack.conventions.tsconfigPaths
|
|
62
|
+
? Object.keys(stack.conventions.tsconfigPaths)
|
|
63
|
+
: undefined,
|
|
64
|
+
},
|
|
65
|
+
files: {
|
|
66
|
+
tailwindConfig: stack.files.tailwindConfig,
|
|
67
|
+
componentsJson: stack.files.componentsJson,
|
|
68
|
+
storybookDir: stack.files.storybookDir,
|
|
69
|
+
},
|
|
70
|
+
warnings: stack.warnings,
|
|
71
|
+
};
|
|
72
|
+
return JSON.stringify(summary, null, 2);
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
function tokenizeInstruction(instruction?: string): Set<string> {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
const set = new Set<string>();
|
|
77
|
+
if (!instruction) return set;
|
|
78
|
+
const tokens = instruction.match(/[A-Za-z_][A-Za-z0-9_]*/g) || [];
|
|
79
|
+
for (const t of tokens) set.add(t.toLowerCase());
|
|
80
|
+
return set;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
function scoreComponent(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
c: ComponentExport,
|
|
85
|
+
focusDirRel: string | null,
|
|
86
|
+
instructionTokens: Set<string>
|
|
81
87
|
): number {
|
|
82
|
-
|
|
88
|
+
let score = 0;
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
// Mentioned explicitly in instruction
|
|
91
|
+
if (instructionTokens.has(c.name.toLowerCase())) score += 8;
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
// Same directory as focused file
|
|
94
|
+
if (focusDirRel && c.file.startsWith(focusDirRel + '/')) score += 6;
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
// Common reusable directories
|
|
97
|
+
if (
|
|
98
|
+
c.file.includes('/components/') ||
|
|
99
|
+
c.file.startsWith('components/') ||
|
|
100
|
+
c.file.includes('/ui/') ||
|
|
101
|
+
c.file.includes('/shared/')
|
|
102
|
+
) {
|
|
103
|
+
score += 3;
|
|
104
|
+
}
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
// Prefer TSX
|
|
107
|
+
if (c.file.endsWith('.tsx')) score += 1;
|
|
102
108
|
|
|
103
|
-
|
|
109
|
+
return score;
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
function formatCatalogSubset(catalog: CatalogResult, subset: ComponentExport[]): string {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
const lines: string[] = [];
|
|
114
|
+
const header = `scanned_files=${catalog.filesScanned}, total_exports=${catalog.components.length}`;
|
|
115
|
+
lines.push(header);
|
|
116
|
+
if (catalog.warnings.length) {
|
|
117
|
+
lines.push(`warnings: ${catalog.warnings.join(' | ')}`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('components:');
|
|
121
|
+
for (const c of subset) {
|
|
122
|
+
const extras: string[] = [];
|
|
123
|
+
if (c.exportType) extras.push(c.exportType);
|
|
124
|
+
if (c.propsType) extras.push(`props: ${c.propsType}`);
|
|
125
|
+
if (c.jsDoc) extras.push(`doc: ${c.jsDoc}`);
|
|
126
|
+
const extra = extras.length ? ` (${extras.join(', ')})` : '';
|
|
127
|
+
lines.push(`- ${c.name} — ${c.file}${extra}`);
|
|
128
|
+
}
|
|
129
|
+
return lines.join('\n');
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
async function getOrBuildRepoData(root: string): Promise<Cached> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const hit = cache.get(root);
|
|
135
|
+
if (hit && now - hit.ts < CACHE_TTL_MS) return hit;
|
|
136
|
+
|
|
137
|
+
const stack = detectUiStack(root);
|
|
138
|
+
const files = walkFiles(root, {
|
|
139
|
+
includeExtensions: ['.tsx', '.jsx'],
|
|
140
|
+
maxFiles: 5000,
|
|
141
|
+
});
|
|
142
|
+
const catalog = await buildComponentCatalog(root, files);
|
|
143
|
+
|
|
144
|
+
const fresh: Cached = { ts: now, stack, catalog };
|
|
145
|
+
cache.set(root, fresh);
|
|
146
|
+
return fresh;
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
export interface RepoGroundingOptions {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
focusFileAbs?: string;
|
|
151
|
+
instruction?: string;
|
|
152
|
+
maxComponents?: number;
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
/**
|
|
@@ -152,40 +158,48 @@ export interface RepoGroundingOptions {
|
|
|
152
158
|
* This is safe to include in prompts because it contains only local, deterministic
|
|
153
159
|
* project metadata (no secrets).
|
|
154
160
|
*/
|
|
155
|
-
export async function buildRepoGrounding(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
161
|
+
export async function buildRepoGrounding(
|
|
162
|
+
config: Config,
|
|
163
|
+
options: RepoGroundingOptions = {}
|
|
164
|
+
): Promise<string> {
|
|
165
|
+
const maxComponents = typeof options.maxComponents === 'number' ? options.maxComponents : 120;
|
|
166
|
+
|
|
167
|
+
const root = resolveRootForFile(
|
|
168
|
+
options.focusFileAbs ? path.resolve(options.focusFileAbs) : null,
|
|
169
|
+
config
|
|
170
|
+
);
|
|
171
|
+
const data = await getOrBuildRepoData(root);
|
|
172
|
+
|
|
173
|
+
const focusRel = options.focusFileAbs
|
|
174
|
+
? toPosixPath(path.relative(root, path.resolve(options.focusFileAbs)))
|
|
175
|
+
: null;
|
|
176
|
+
const focusDirRel = focusRel ? toPosixPath(path.posix.dirname(focusRel)) : null;
|
|
177
|
+
const instructionTokens = tokenizeInstruction(options.instruction);
|
|
178
|
+
|
|
179
|
+
// Score and select a small subset for reuse.
|
|
180
|
+
const scored = data.catalog.components
|
|
181
|
+
.map((c) => ({ c, s: scoreComponent(c, focusDirRel, instructionTokens) }))
|
|
182
|
+
.sort((a, b) => b.s - a.s);
|
|
183
|
+
|
|
184
|
+
const subset: ComponentExport[] = [];
|
|
185
|
+
const seen = new Set<string>();
|
|
186
|
+
for (const item of scored) {
|
|
187
|
+
if (subset.length >= maxComponents) break;
|
|
188
|
+
const key = `${item.c.name}|${item.c.file}|${item.c.exportType}`;
|
|
189
|
+
if (seen.has(key)) continue;
|
|
190
|
+
// Skip ultra-low relevance if we already have enough
|
|
191
|
+
if (subset.length > 30 && item.s <= 0) break;
|
|
192
|
+
subset.push(item.c);
|
|
193
|
+
seen.add(key);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
'AUTO PROJECT CONTEXT (deterministic):',
|
|
198
|
+
'',
|
|
199
|
+
'STACK (json):',
|
|
200
|
+
summarizeStack(data.stack),
|
|
201
|
+
'',
|
|
202
|
+
'COMPONENT CATALOG (subset for reuse):',
|
|
203
|
+
formatCatalogSubset(data.catalog, subset),
|
|
204
|
+
].join('\n');
|
|
191
205
|
}
|
package/src/context/guards.ts
CHANGED
|
@@ -14,11 +14,36 @@ import * as path from 'node:path';
|
|
|
14
14
|
import { Config } from '../config/index.js';
|
|
15
15
|
import { isPathAllowed, isSensitiveFile } from './filter.js';
|
|
16
16
|
|
|
17
|
+
function isRealPathAllowed(filePath: string, allowedPaths: string[]): boolean {
|
|
18
|
+
let absPath: string;
|
|
19
|
+
try {
|
|
20
|
+
absPath = fs.realpathSync(filePath);
|
|
21
|
+
} catch {
|
|
22
|
+
absPath = path.resolve(filePath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const allowed of allowedPaths) {
|
|
26
|
+
let absAllowed: string;
|
|
27
|
+
try {
|
|
28
|
+
absAllowed = fs.realpathSync(allowed);
|
|
29
|
+
} catch {
|
|
30
|
+
absAllowed = path.resolve(allowed);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rel = path.relative(absAllowed, absPath);
|
|
34
|
+
if (rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
/**
|
|
18
43
|
* Resolve a path to an absolute path (relative paths are resolved from process.cwd()).
|
|
19
44
|
*/
|
|
20
45
|
export function resolveToAbs(filePath: string): string {
|
|
21
|
-
|
|
46
|
+
return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
/**
|
|
@@ -26,26 +51,31 @@ export function resolveToAbs(filePath: string): string {
|
|
|
26
51
|
* Returns the resolved absolute path.
|
|
27
52
|
*/
|
|
28
53
|
export function assertReadablePath(filePath: string, config: Config): string {
|
|
29
|
-
|
|
54
|
+
const absPath = resolveToAbs(filePath);
|
|
30
55
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
if (!isPathAllowed(absPath, config.allowedPaths)) {
|
|
57
|
+
throw new Error(`Path is outside allowedPaths: ${filePath}`);
|
|
58
|
+
}
|
|
34
59
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
if (isSensitiveFile(absPath)) {
|
|
61
|
+
throw new Error(`Refusing to read sensitive file: ${filePath}`);
|
|
62
|
+
}
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
if (!fs.existsSync(absPath)) {
|
|
65
|
+
throw new Error(`File not found: ${filePath}`);
|
|
66
|
+
}
|
|
42
67
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
// Prevent symlink escapes (resolve real path and re-check allowedPaths).
|
|
69
|
+
if (!isRealPathAllowed(absPath, config.allowedPaths)) {
|
|
70
|
+
throw new Error(`Path resolves outside allowedPaths: ${filePath}`);
|
|
71
|
+
}
|
|
47
72
|
|
|
48
|
-
|
|
73
|
+
const stat = fs.statSync(absPath);
|
|
74
|
+
if (!stat.isFile()) {
|
|
75
|
+
throw new Error(`Not a file: ${filePath}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return absPath;
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
/**
|
|
@@ -53,17 +83,41 @@ export function assertReadablePath(filePath: string, config: Config): string {
|
|
|
53
83
|
* Returns the resolved absolute path.
|
|
54
84
|
*/
|
|
55
85
|
export function assertWritablePath(filePath: string, config: Config): string {
|
|
56
|
-
|
|
86
|
+
const absPath = resolveToAbs(filePath);
|
|
57
87
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
if (!isPathAllowed(absPath, config.allowedPaths)) {
|
|
89
|
+
throw new Error(`Path is outside allowedPaths: ${filePath}`);
|
|
90
|
+
}
|
|
61
91
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
if (isSensitiveFile(absPath)) {
|
|
93
|
+
throw new Error(`Refusing to write to sensitive path: ${filePath}`);
|
|
94
|
+
}
|
|
65
95
|
|
|
96
|
+
// If the target already exists, disallow writing through symlinks.
|
|
97
|
+
if (fs.existsSync(absPath)) {
|
|
98
|
+
const st = fs.lstatSync(absPath);
|
|
99
|
+
if (st.isSymbolicLink()) {
|
|
100
|
+
throw new Error(`Refusing to write through symlink: ${filePath}`);
|
|
101
|
+
}
|
|
102
|
+
if (!isRealPathAllowed(absPath, config.allowedPaths)) {
|
|
103
|
+
throw new Error(`Path resolves outside allowedPaths: ${filePath}`);
|
|
104
|
+
}
|
|
66
105
|
return absPath;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// If the file doesn't exist yet, validate the closest existing parent directory.
|
|
109
|
+
let parent = path.dirname(absPath);
|
|
110
|
+
while (parent !== path.dirname(parent) && !fs.existsSync(parent)) {
|
|
111
|
+
parent = path.dirname(parent);
|
|
112
|
+
}
|
|
113
|
+
if (!fs.existsSync(parent)) {
|
|
114
|
+
throw new Error(`Parent directory not found for path: ${filePath}`);
|
|
115
|
+
}
|
|
116
|
+
if (!isRealPathAllowed(parent, config.allowedPaths)) {
|
|
117
|
+
throw new Error(`Parent directory resolves outside allowedPaths: ${filePath}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return absPath;
|
|
67
121
|
}
|
|
68
122
|
|
|
69
123
|
/**
|
|
@@ -71,24 +125,29 @@ export function assertWritablePath(filePath: string, config: Config): string {
|
|
|
71
125
|
* Returns the resolved absolute path.
|
|
72
126
|
*/
|
|
73
127
|
export function assertReadableDir(dirPath: string, config: Config): string {
|
|
74
|
-
|
|
128
|
+
const absPath = resolveToAbs(dirPath);
|
|
75
129
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
if (!isPathAllowed(absPath, config.allowedPaths)) {
|
|
131
|
+
throw new Error(`Path is outside allowedPaths: ${dirPath}`);
|
|
132
|
+
}
|
|
79
133
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
134
|
+
if (isSensitiveFile(absPath)) {
|
|
135
|
+
throw new Error(`Refusing to read sensitive directory: ${dirPath}`);
|
|
136
|
+
}
|
|
83
137
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
138
|
+
if (!fs.existsSync(absPath)) {
|
|
139
|
+
throw new Error(`Directory not found: ${dirPath}`);
|
|
140
|
+
}
|
|
87
141
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
142
|
+
// Prevent symlink escapes.
|
|
143
|
+
if (!isRealPathAllowed(absPath, config.allowedPaths)) {
|
|
144
|
+
throw new Error(`Path resolves outside allowedPaths: ${dirPath}`);
|
|
145
|
+
}
|
|
92
146
|
|
|
93
|
-
|
|
147
|
+
const stat = fs.statSync(absPath);
|
|
148
|
+
if (!stat.isDirectory()) {
|
|
149
|
+
throw new Error(`Not a directory: ${dirPath}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return absPath;
|
|
94
153
|
}
|
|
@@ -13,31 +13,31 @@ import { Config } from '../config/index.js';
|
|
|
13
13
|
import { detectUiStack } from '../stack/detect.js';
|
|
14
14
|
|
|
15
15
|
export function buildRepoHints(config: Config, rootDir: string = process.cwd()): string {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
try {
|
|
17
|
+
const stack = detectUiStack(rootDir);
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
19
|
+
const hints = {
|
|
20
|
+
root: path.basename(stack.root),
|
|
21
|
+
framework: stack.framework,
|
|
22
|
+
typescript: stack.language.typescript,
|
|
23
|
+
styling: stack.styling,
|
|
24
|
+
uiLibraries: stack.uiLibraries,
|
|
25
|
+
iconLibraries: stack.iconLibraries,
|
|
26
|
+
tooling: stack.tooling,
|
|
27
|
+
conventions: {
|
|
28
|
+
srcDir: stack.conventions.srcDir,
|
|
29
|
+
hasAppDir: stack.conventions.hasAppDir,
|
|
30
|
+
hasPagesDir: stack.conventions.hasPagesDir,
|
|
31
|
+
tsconfigPaths: stack.conventions.tsconfigPaths,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return '';
|
|
35
|
+
return `REPO_HINTS (deterministic):\n${JSON.stringify(hints, null, 2)}`;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (config.debug) {
|
|
38
|
+
const msg = error instanceof Error ? error.message : 'unknown error';
|
|
39
|
+
console.error('[repo-hints] Failed to detect stack:', msg);
|
|
42
40
|
}
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
43
|
}
|