@jkwd/inbase 0.1.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 +76 -0
- package/apps/explorer/index.html +12 -0
- package/apps/explorer/package.json +28 -0
- package/apps/explorer/scripts/js-source.mjs +188 -0
- package/apps/explorer/scripts/patch-lib.d.ts +115 -0
- package/apps/explorer/scripts/patch-lib.mjs +472 -0
- package/apps/explorer/scripts/scan-target.mjs +188 -0
- package/apps/explorer/scripts/session-store.d.ts +156 -0
- package/apps/explorer/scripts/session-store.mjs +809 -0
- package/apps/explorer/scripts/target-config.d.ts +8 -0
- package/apps/explorer/scripts/target-config.mjs +42 -0
- package/apps/explorer/src/App.tsx +941 -0
- package/apps/explorer/src/agentIntent.ts +182 -0
- package/apps/explorer/src/codebase.ts +15 -0
- package/apps/explorer/src/index.css +632 -0
- package/apps/explorer/src/layout.ts +508 -0
- package/apps/explorer/src/main.tsx +16 -0
- package/apps/explorer/src/scene/BlockPlacer.tsx +87 -0
- package/apps/explorer/src/scene/Bridge.tsx +290 -0
- package/apps/explorer/src/scene/FileBlock.tsx +256 -0
- package/apps/explorer/src/scene/FolderArea.tsx +96 -0
- package/apps/explorer/src/scene/IslandPlacer.tsx +40 -0
- package/apps/explorer/src/scene/MapSelectBorder.tsx +57 -0
- package/apps/explorer/src/scene/MapView.tsx +247 -0
- package/apps/explorer/src/scene/Player.tsx +245 -0
- package/apps/explorer/src/scene/RelationLines.tsx +223 -0
- package/apps/explorer/src/scene/SelectionController.tsx +89 -0
- package/apps/explorer/src/scene/UserContextTracker.tsx +152 -0
- package/apps/explorer/src/scene/World.tsx +323 -0
- package/apps/explorer/src/theme.ts +111 -0
- package/apps/explorer/src/types.ts +245 -0
- package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +38 -0
- package/apps/explorer/src/ui/HUD.tsx +1090 -0
- package/apps/explorer/src/ui/NameInput.tsx +45 -0
- package/apps/explorer/src/userContext.ts +73 -0
- package/apps/explorer/src/userCreated.ts +354 -0
- package/apps/explorer/src/vite-env.d.ts +1 -0
- package/apps/explorer/tsconfig.json +21 -0
- package/apps/explorer/vite.config.ts +295 -0
- package/bin/inbase.mjs +170 -0
- package/bin/project.mjs +94 -0
- package/bin/session.mjs +241 -0
- package/package.json +63 -0
- package/skill/inbase/SKILL.md +167 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
collectImportSpecifiers,
|
|
5
|
+
extractImportBindings,
|
|
6
|
+
extractJsSymbols,
|
|
7
|
+
resolveSpecifierAgainst,
|
|
8
|
+
} from './js-source.mjs'
|
|
9
|
+
import { targetPathPrefix } from './target-config.mjs'
|
|
10
|
+
|
|
11
|
+
export function toFileId(input) {
|
|
12
|
+
if (!input || input === '/dev/null' || input === 'dev/null') return null
|
|
13
|
+
let value = input.trim().replaceAll('\\', '/')
|
|
14
|
+
if (
|
|
15
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
16
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
17
|
+
) {
|
|
18
|
+
value = value.slice(1, -1)
|
|
19
|
+
}
|
|
20
|
+
if (value.startsWith('a/') || value.startsWith('b/')) value = value.slice(2)
|
|
21
|
+
if (targetPathPrefix) {
|
|
22
|
+
const index = value.indexOf(targetPathPrefix)
|
|
23
|
+
if (index >= 0) value = value.slice(index + targetPathPrefix.length)
|
|
24
|
+
}
|
|
25
|
+
if (value.startsWith('./')) value = value.slice(2)
|
|
26
|
+
return value || null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function splitLines(text) {
|
|
30
|
+
if (text === '') return []
|
|
31
|
+
const lines = text.split('\n')
|
|
32
|
+
if (text.endsWith('\n')) lines.pop()
|
|
33
|
+
return lines
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseHunks(section) {
|
|
37
|
+
const hunks = []
|
|
38
|
+
const lines = splitLines(section)
|
|
39
|
+
let current = null
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const header = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/)
|
|
43
|
+
if (header) {
|
|
44
|
+
if (current) hunks.push(current)
|
|
45
|
+
current = {
|
|
46
|
+
oldStart: Number(header[1]),
|
|
47
|
+
oldCount: Number(header[2] ?? '1'),
|
|
48
|
+
newStart: Number(header[3]),
|
|
49
|
+
newCount: Number(header[4] ?? '1'),
|
|
50
|
+
lines: [],
|
|
51
|
+
}
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
if (!current) continue
|
|
55
|
+
if (line === '\') continue
|
|
56
|
+
if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
|
|
57
|
+
current.lines.push(line)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (current) hunks.push(current)
|
|
61
|
+
return hunks
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fileKind(oldPath, newPath, section) {
|
|
65
|
+
if (!oldPath && newPath) return 'add'
|
|
66
|
+
if (oldPath && !newPath) return 'delete'
|
|
67
|
+
if (/^new file mode /m.test(section)) return 'add'
|
|
68
|
+
if (/^deleted file mode /m.test(section)) return 'delete'
|
|
69
|
+
return 'modify'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseUnifiedPatch(patch) {
|
|
73
|
+
const text = patch.replaceAll('\r\n', '\n')
|
|
74
|
+
const files = []
|
|
75
|
+
const sections = text.split(/^(?=diff --git |--- )/m).filter((part) => part.trim())
|
|
76
|
+
|
|
77
|
+
for (const section of sections) {
|
|
78
|
+
const oldMatch = section.match(/^---\s+(\S+)/m)
|
|
79
|
+
const newMatch = section.match(/^\+\+\+\s+(\S+)/m)
|
|
80
|
+
if (!oldMatch || !newMatch) continue
|
|
81
|
+
|
|
82
|
+
const oldPath = toFileId(oldMatch[1])
|
|
83
|
+
const newPath = toFileId(newMatch[1])
|
|
84
|
+
const id = newPath || oldPath
|
|
85
|
+
if (!id) continue
|
|
86
|
+
|
|
87
|
+
const hunks = parseHunks(section)
|
|
88
|
+
const kind = fileKind(oldPath, newPath, section)
|
|
89
|
+
let addedLines = 0
|
|
90
|
+
for (const hunk of hunks) {
|
|
91
|
+
for (const line of hunk.lines) {
|
|
92
|
+
if (line.startsWith('+')) addedLines += 1
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
files.push({ id, kind, addedLines, hunks })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
files: files.filter((file) => file.kind === 'modify').map((file) => file.id),
|
|
101
|
+
creates: files.filter((file) => file.kind === 'add').map((file) => file.id),
|
|
102
|
+
deletes: files.filter((file) => file.kind === 'delete').map((file) => file.id),
|
|
103
|
+
createLines: Object.fromEntries(
|
|
104
|
+
files
|
|
105
|
+
.filter((file) => file.kind === 'add')
|
|
106
|
+
.map((file) => [file.id, Math.max(1, file.addedLines)]),
|
|
107
|
+
),
|
|
108
|
+
entries: files,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function folderOfFile(fileId) {
|
|
113
|
+
return fileId.includes('/') ? fileId.split('/').slice(0, -1).join('/') : '.'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function folderParent(folderPath) {
|
|
117
|
+
if (!folderPath || folderPath === '.') return null
|
|
118
|
+
return folderPath.includes('/') ? folderPath.split('/').slice(0, -1).join('/') : '.'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function foldersFromFileIds(ids = []) {
|
|
122
|
+
const folders = new Set(['.'])
|
|
123
|
+
for (const id of ids) {
|
|
124
|
+
let current = folderOfFile(id)
|
|
125
|
+
while (current && current !== '.') {
|
|
126
|
+
folders.add(current)
|
|
127
|
+
current = folderParent(current) ?? '.'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return folders
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function collectCreateFolders(creates = [], existingFolders = []) {
|
|
134
|
+
const known = existingFolders instanceof Set ? existingFolders : new Set(existingFolders)
|
|
135
|
+
const created = new Set()
|
|
136
|
+
for (const id of creates) {
|
|
137
|
+
let current = folderOfFile(id)
|
|
138
|
+
while (current && current !== '.') {
|
|
139
|
+
if (!known.has(current)) created.add(current)
|
|
140
|
+
current = folderParent(current) ?? '.'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [...created].sort(
|
|
144
|
+
(left, right) =>
|
|
145
|
+
left.split('/').filter(Boolean).length - right.split('/').filter(Boolean).length ||
|
|
146
|
+
left.localeCompare(right),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function taggedSource(entry, tag) {
|
|
151
|
+
const chunks = []
|
|
152
|
+
for (const hunk of entry.hunks) {
|
|
153
|
+
for (const line of hunk.lines) {
|
|
154
|
+
if (line.startsWith(tag)) chunks.push(line.slice(1))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return chunks.join('\n')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function addedSource(entry) {
|
|
161
|
+
return taggedSource(entry, '+')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function removedSource(entry) {
|
|
165
|
+
return taggedSource(entry, '-')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractSymbols(source) {
|
|
169
|
+
// Patch previews only have function/variable buckets, so classes ride along
|
|
170
|
+
// as functions rather than disappearing from the HUD.
|
|
171
|
+
return extractJsSymbols(source).map((symbol) =>
|
|
172
|
+
symbol.kind === 'class' ? { ...symbol, kind: 'function' } : symbol,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function additionKey(file, name, extra = '') {
|
|
177
|
+
return extra ? `${file}:${name}:${extra}` : `${file}:${name}`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function emptyAdditions() {
|
|
181
|
+
return {
|
|
182
|
+
addedFunctions: [],
|
|
183
|
+
addedVariables: [],
|
|
184
|
+
addedImports: [],
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function applyEntriesToAdditions(current, entries) {
|
|
189
|
+
const functions = new Map(
|
|
190
|
+
current.addedFunctions.map((item) => [additionKey(item.file, item.name), item]),
|
|
191
|
+
)
|
|
192
|
+
const variables = new Map(
|
|
193
|
+
current.addedVariables.map((item) => [additionKey(item.file, item.name), item]),
|
|
194
|
+
)
|
|
195
|
+
const imports = new Map(
|
|
196
|
+
current.addedImports.map((item) => [additionKey(item.file, item.name, item.from), item]),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const dropFile = (fileId) => {
|
|
200
|
+
for (const key of [...functions.keys()]) {
|
|
201
|
+
if (functions.get(key).file === fileId) functions.delete(key)
|
|
202
|
+
}
|
|
203
|
+
for (const key of [...variables.keys()]) {
|
|
204
|
+
if (variables.get(key).file === fileId) variables.delete(key)
|
|
205
|
+
}
|
|
206
|
+
for (const key of [...imports.keys()]) {
|
|
207
|
+
if (imports.get(key).file === fileId) imports.delete(key)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry.kind === 'delete') {
|
|
213
|
+
dropFile(entry.id)
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const removedSymbols = extractSymbols(removedSource(entry))
|
|
218
|
+
const addedSymbols = extractSymbols(addedSource(entry))
|
|
219
|
+
const keptSymbolKeys = new Set(
|
|
220
|
+
addedSymbols.map((symbol) => `${symbol.kind}:${symbol.name}`),
|
|
221
|
+
)
|
|
222
|
+
const previousSymbolKeys = new Set(
|
|
223
|
+
removedSymbols.map((symbol) => `${symbol.kind}:${symbol.name}`),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
for (const symbol of removedSymbols) {
|
|
227
|
+
if (keptSymbolKeys.has(`${symbol.kind}:${symbol.name}`)) continue
|
|
228
|
+
if (symbol.kind === 'function') functions.delete(additionKey(entry.id, symbol.name))
|
|
229
|
+
else variables.delete(additionKey(entry.id, symbol.name))
|
|
230
|
+
}
|
|
231
|
+
for (const symbol of addedSymbols) {
|
|
232
|
+
if (previousSymbolKeys.has(`${symbol.kind}:${symbol.name}`)) continue
|
|
233
|
+
const item = { name: symbol.name, file: entry.id }
|
|
234
|
+
if (symbol.kind === 'function') functions.set(additionKey(entry.id, symbol.name), item)
|
|
235
|
+
else variables.set(additionKey(entry.id, symbol.name), item)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const removedBindings = extractImportBindings(removedSource(entry))
|
|
239
|
+
const addedBindings = extractImportBindings(addedSource(entry))
|
|
240
|
+
const keptImportKeys = new Set(
|
|
241
|
+
addedBindings.map((binding) => `${binding.name}\0${binding.from}`),
|
|
242
|
+
)
|
|
243
|
+
const previousImportKeys = new Set(
|
|
244
|
+
removedBindings.map((binding) => `${binding.name}\0${binding.from}`),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
for (const binding of removedBindings) {
|
|
248
|
+
if (keptImportKeys.has(`${binding.name}\0${binding.from}`)) continue
|
|
249
|
+
imports.delete(additionKey(entry.id, binding.name, binding.from))
|
|
250
|
+
}
|
|
251
|
+
for (const binding of addedBindings) {
|
|
252
|
+
if (previousImportKeys.has(`${binding.name}\0${binding.from}`)) continue
|
|
253
|
+
imports.set(additionKey(entry.id, binding.name, binding.from), {
|
|
254
|
+
name: binding.name,
|
|
255
|
+
from: binding.from,
|
|
256
|
+
file: entry.id,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
addedFunctions: [...functions.values()],
|
|
263
|
+
addedVariables: [...variables.values()],
|
|
264
|
+
addedImports: [...imports.values()],
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function extractPatchAdditions(entries) {
|
|
269
|
+
return applyEntriesToAdditions(emptyAdditions(), entries)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function accumulatePatchAdditions(patches = []) {
|
|
273
|
+
let additions = emptyAdditions()
|
|
274
|
+
for (const patch of patches) {
|
|
275
|
+
additions = applyEntriesToAdditions(additions, parseUnifiedPatch(patch).entries)
|
|
276
|
+
}
|
|
277
|
+
return additions
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function folderOfFileId(fileId) {
|
|
281
|
+
const index = fileId.lastIndexOf('/')
|
|
282
|
+
return index === -1 ? '.' : fileId.slice(0, index)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function posixJoin(fromDir, specifier) {
|
|
286
|
+
const raw = fromDir === '.' ? specifier : `${fromDir}/${specifier}`
|
|
287
|
+
const parts = []
|
|
288
|
+
for (const part of raw.split('/')) {
|
|
289
|
+
if (!part || part === '.') continue
|
|
290
|
+
if (part === '..') {
|
|
291
|
+
parts.pop()
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
parts.push(part)
|
|
295
|
+
}
|
|
296
|
+
return parts.join('/')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function extractPatchImports(entries, knownFileIds = []) {
|
|
300
|
+
const known = new Set(knownFileIds)
|
|
301
|
+
for (const entry of entries) known.add(entry.id)
|
|
302
|
+
const edges = []
|
|
303
|
+
const seen = new Set()
|
|
304
|
+
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
if (entry.kind === 'delete') continue
|
|
307
|
+
const fromDir = folderOfFileId(entry.id)
|
|
308
|
+
for (const specifier of collectImportSpecifiers(addedSource(entry))) {
|
|
309
|
+
if (!specifier.startsWith('.')) continue
|
|
310
|
+
const to = resolveSpecifierAgainst(posixJoin(fromDir, specifier), known)
|
|
311
|
+
if (!to || to === entry.id) continue
|
|
312
|
+
const key = `${entry.id}->${to}`
|
|
313
|
+
if (seen.has(key)) continue
|
|
314
|
+
seen.add(key)
|
|
315
|
+
edges.push({ from: entry.id, to })
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return edges
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function applyHunks(original, hunks) {
|
|
323
|
+
const lines = splitLines(original)
|
|
324
|
+
let delta = 0
|
|
325
|
+
|
|
326
|
+
for (const hunk of hunks) {
|
|
327
|
+
const start = hunk.oldStart === 0 ? 0 : hunk.oldStart - 1 + delta
|
|
328
|
+
const expected = []
|
|
329
|
+
const replacement = []
|
|
330
|
+
for (const line of hunk.lines) {
|
|
331
|
+
const tag = line[0]
|
|
332
|
+
const body = line.slice(1)
|
|
333
|
+
if (tag === ' ' || tag === '-') expected.push(body)
|
|
334
|
+
if (tag === ' ' || tag === '+') replacement.push(body)
|
|
335
|
+
}
|
|
336
|
+
const actual = lines.slice(start, start + expected.length)
|
|
337
|
+
if (actual.join('\n') !== expected.join('\n')) {
|
|
338
|
+
throw new Error(`Hunk does not apply at line ${hunk.oldStart}`)
|
|
339
|
+
}
|
|
340
|
+
lines.splice(start, expected.length, ...replacement)
|
|
341
|
+
delta += replacement.length - expected.length
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (lines.length === 0) return original.endsWith('\n') ? '\n' : ''
|
|
345
|
+
return `${lines.join('\n')}\n`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function applyUnifiedPatch(patch, targetRoot) {
|
|
349
|
+
const parsed = parseUnifiedPatch(patch)
|
|
350
|
+
if (parsed.entries.length === 0) {
|
|
351
|
+
throw new Error('Patch did not contain any file changes')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const file of parsed.entries) {
|
|
355
|
+
const absolute = path.join(targetRoot, file.id)
|
|
356
|
+
if (file.kind === 'delete') {
|
|
357
|
+
if (fs.existsSync(absolute)) fs.rmSync(absolute)
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const original =
|
|
362
|
+
file.kind === 'add' || !fs.existsSync(absolute)
|
|
363
|
+
? ''
|
|
364
|
+
: fs.readFileSync(absolute, 'utf8')
|
|
365
|
+
|
|
366
|
+
if (file.kind === 'modify' && original === '') {
|
|
367
|
+
throw new Error(`Cannot modify missing file ${file.id}`)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true })
|
|
371
|
+
fs.writeFileSync(absolute, applyHunks(original, file.hunks))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return parsed
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export const emptyIntent = {
|
|
378
|
+
updatedAt: null,
|
|
379
|
+
showMap: false,
|
|
380
|
+
status: 'idle',
|
|
381
|
+
feature: null,
|
|
382
|
+
steps: [],
|
|
383
|
+
step: null,
|
|
384
|
+
files: [],
|
|
385
|
+
creates: [],
|
|
386
|
+
deletes: [],
|
|
387
|
+
createFolders: [],
|
|
388
|
+
createLines: {},
|
|
389
|
+
imports: [],
|
|
390
|
+
addedFunctions: [],
|
|
391
|
+
addedVariables: [],
|
|
392
|
+
addedImports: [],
|
|
393
|
+
reason: null,
|
|
394
|
+
sessionId: null,
|
|
395
|
+
diffId: null,
|
|
396
|
+
parentDiffId: null,
|
|
397
|
+
chainIndex: null,
|
|
398
|
+
chain: [],
|
|
399
|
+
isActiveDiff: false,
|
|
400
|
+
preview: false,
|
|
401
|
+
phase: null,
|
|
402
|
+
working: false,
|
|
403
|
+
creationMode: false,
|
|
404
|
+
canEnterBlueprint: false,
|
|
405
|
+
blueprintSessionId: null,
|
|
406
|
+
userCreatedBlocks: [],
|
|
407
|
+
userCreatedIslands: [],
|
|
408
|
+
blueprintFunctions: [],
|
|
409
|
+
blueprintVariables: [],
|
|
410
|
+
blueprintImports: [],
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function isLastStep(intent) {
|
|
414
|
+
const steps = intent?.steps
|
|
415
|
+
if (!Array.isArray(steps) || steps.length === 0) return true
|
|
416
|
+
return typeof intent.step === 'number' && intent.step >= steps.length
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function overlayPatch(intent, patchText, knownFileIds = []) {
|
|
420
|
+
const base = { ...emptyIntent, ...intent }
|
|
421
|
+
const status =
|
|
422
|
+
base.status === 'approved' && isLastStep(base) ? 'finished' : base.status
|
|
423
|
+
const preview = status === 'pending' || status === 'extend'
|
|
424
|
+
|
|
425
|
+
if (!preview) {
|
|
426
|
+
return {
|
|
427
|
+
...base,
|
|
428
|
+
status,
|
|
429
|
+
files: [],
|
|
430
|
+
creates: [],
|
|
431
|
+
deletes: [],
|
|
432
|
+
createFolders: [],
|
|
433
|
+
createLines: {},
|
|
434
|
+
imports: [],
|
|
435
|
+
addedFunctions: [],
|
|
436
|
+
addedVariables: [],
|
|
437
|
+
addedImports: [],
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (!patchText?.trim()) {
|
|
442
|
+
return {
|
|
443
|
+
...base,
|
|
444
|
+
status,
|
|
445
|
+
files: base.files ?? [],
|
|
446
|
+
creates: base.creates ?? [],
|
|
447
|
+
deletes: base.deletes ?? [],
|
|
448
|
+
createFolders: base.createFolders ?? [],
|
|
449
|
+
createLines: base.createLines ?? {},
|
|
450
|
+
imports: base.imports ?? [],
|
|
451
|
+
addedFunctions: base.addedFunctions ?? [],
|
|
452
|
+
addedVariables: base.addedVariables ?? [],
|
|
453
|
+
addedImports: base.addedImports ?? [],
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const parsed = parseUnifiedPatch(patchText)
|
|
458
|
+
return {
|
|
459
|
+
...base,
|
|
460
|
+
status,
|
|
461
|
+
files: parsed.files,
|
|
462
|
+
creates: parsed.creates,
|
|
463
|
+
deletes: parsed.deletes,
|
|
464
|
+
createFolders: collectCreateFolders(
|
|
465
|
+
parsed.creates,
|
|
466
|
+
foldersFromFileIds(knownFileIds.filter((id) => !parsed.creates.includes(id))),
|
|
467
|
+
),
|
|
468
|
+
createLines: parsed.createLines,
|
|
469
|
+
imports: extractPatchImports(parsed.entries, knownFileIds),
|
|
470
|
+
...extractPatchAdditions(parsed.entries),
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import {
|
|
5
|
+
FILE_EXTENSIONS,
|
|
6
|
+
SOURCE_EXTENSIONS,
|
|
7
|
+
RESOLVE_EXTENSIONS,
|
|
8
|
+
collectImportSpecifiers,
|
|
9
|
+
extractJsSymbols,
|
|
10
|
+
} from './js-source.mjs'
|
|
11
|
+
import {
|
|
12
|
+
dataDir as defaultDataDir,
|
|
13
|
+
targetName as defaultTargetName,
|
|
14
|
+
targetRoot as defaultTargetRoot,
|
|
15
|
+
} from './target-config.mjs'
|
|
16
|
+
|
|
17
|
+
const defaultOutPath = path.join(defaultDataDir, 'codebase.json')
|
|
18
|
+
const IGNORE_DIRS = new Set([
|
|
19
|
+
'node_modules',
|
|
20
|
+
'dist',
|
|
21
|
+
'build',
|
|
22
|
+
'out',
|
|
23
|
+
'coverage',
|
|
24
|
+
'.git',
|
|
25
|
+
'.inbase',
|
|
26
|
+
])
|
|
27
|
+
const IGNORE_FILES = new Set(['package-lock.json'])
|
|
28
|
+
|
|
29
|
+
function toPosix(filePath) {
|
|
30
|
+
return filePath.split(path.sep).join('/')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function walk(dir, acc = []) {
|
|
34
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
35
|
+
if (entry.name.startsWith('.')) continue
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
if (IGNORE_DIRS.has(entry.name)) continue
|
|
38
|
+
walk(path.join(dir, entry.name), acc)
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (IGNORE_FILES.has(entry.name)) continue
|
|
42
|
+
if (!FILE_EXTENSIONS.has(path.extname(entry.name))) continue
|
|
43
|
+
acc.push(path.join(dir, entry.name))
|
|
44
|
+
}
|
|
45
|
+
return acc
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function languageOf(filePath) {
|
|
49
|
+
const ext = path.extname(filePath).slice(1).toLowerCase()
|
|
50
|
+
return ext || 'txt'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractSymbols(source, ext) {
|
|
54
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return []
|
|
55
|
+
return extractJsSymbols(source)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveExisting(candidate) {
|
|
59
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
60
|
+
return candidate
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
64
|
+
const withExt = candidate + ext
|
|
65
|
+
if (fs.existsSync(withExt) && fs.statSync(withExt).isFile()) {
|
|
66
|
+
return withExt
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const indexDir = candidate
|
|
71
|
+
if (fs.existsSync(indexDir) && fs.statSync(indexDir).isDirectory()) {
|
|
72
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
73
|
+
const indexFile = path.join(indexDir, `index${ext}`)
|
|
74
|
+
if (fs.existsSync(indexFile)) return indexFile
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveImport(fromFile, specifier, root) {
|
|
82
|
+
if (!specifier.startsWith('.')) return null
|
|
83
|
+
const fromDir = path.dirname(fromFile)
|
|
84
|
+
const candidate = path.resolve(fromDir, specifier)
|
|
85
|
+
const resolved = resolveExisting(candidate)
|
|
86
|
+
if (!resolved) return null
|
|
87
|
+
const relative = toPosix(path.relative(root, resolved))
|
|
88
|
+
if (relative.startsWith('..')) return null
|
|
89
|
+
return relative
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ensureFolder(folders, folderPath, rootName) {
|
|
93
|
+
if (folders.has(folderPath)) return
|
|
94
|
+
|
|
95
|
+
const parent =
|
|
96
|
+
folderPath === '.'
|
|
97
|
+
? null
|
|
98
|
+
: toPosix(path.posix.dirname(folderPath)) === '.'
|
|
99
|
+
? '.'
|
|
100
|
+
: toPosix(path.posix.dirname(folderPath))
|
|
101
|
+
|
|
102
|
+
folders.set(folderPath, {
|
|
103
|
+
path: folderPath,
|
|
104
|
+
name: folderPath === '.' ? rootName : path.posix.basename(folderPath),
|
|
105
|
+
parent,
|
|
106
|
+
files: [],
|
|
107
|
+
children: [],
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (parent) ensureFolder(folders, parent, rootName)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function scanTarget({
|
|
114
|
+
root = defaultTargetRoot,
|
|
115
|
+
name = path.basename(root),
|
|
116
|
+
dest = defaultOutPath,
|
|
117
|
+
} = {}) {
|
|
118
|
+
if (!fs.existsSync(root)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Target not found at ${root}. Set VISUAL_CODER_TARGET to the project you want to map.`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const absoluteFiles = walk(root)
|
|
125
|
+
const folders = new Map()
|
|
126
|
+
ensureFolder(folders, '.', name)
|
|
127
|
+
|
|
128
|
+
const files = absoluteFiles.map((absolutePath) => {
|
|
129
|
+
const relative = toPosix(path.relative(root, absolutePath))
|
|
130
|
+
const ext = path.extname(relative)
|
|
131
|
+
const source = fs.readFileSync(absolutePath, 'utf8')
|
|
132
|
+
const folder = relative.includes('/') ? relative.split('/').slice(0, -1).join('/') : '.'
|
|
133
|
+
|
|
134
|
+
ensureFolder(folders, folder, name)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
id: relative,
|
|
138
|
+
name: path.posix.basename(relative),
|
|
139
|
+
path: relative,
|
|
140
|
+
folder,
|
|
141
|
+
lines: source.split(/\r?\n/).length,
|
|
142
|
+
language: languageOf(relative),
|
|
143
|
+
symbols: extractSymbols(source, ext),
|
|
144
|
+
imports: collectImportSpecifiers(source)
|
|
145
|
+
.map((specifier) => resolveImport(absolutePath, specifier, root))
|
|
146
|
+
.filter(Boolean),
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const fileIds = new Set(files.map((file) => file.id))
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
file.imports = [...new Set(file.imports.filter((id) => fileIds.has(id)))]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
folders.get(file.folder).files.push(file.id)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const folder of folders.values()) {
|
|
160
|
+
if (!folder.parent) continue
|
|
161
|
+
const parent = folders.get(folder.parent)
|
|
162
|
+
if (!parent.children.includes(folder.path)) {
|
|
163
|
+
parent.children.push(folder.path)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const folder of folders.values()) {
|
|
168
|
+
folder.files.sort((a, b) => a.localeCompare(b))
|
|
169
|
+
folder.children.sort((a, b) => a.localeCompare(b))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const graph = {
|
|
173
|
+
root: '.',
|
|
174
|
+
targetName: name,
|
|
175
|
+
files,
|
|
176
|
+
folders: [...folders.values()],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
180
|
+
fs.writeFileSync(dest, `${JSON.stringify(graph, null, 2)}\n`)
|
|
181
|
+
console.log(`Scanned ${files.length} files -> ${path.relative(process.cwd(), dest)}`)
|
|
182
|
+
return graph
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : ''
|
|
186
|
+
if (invoked === fileURLToPath(import.meta.url)) {
|
|
187
|
+
scanTarget()
|
|
188
|
+
}
|