@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,809 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
accumulatePatchAdditions,
|
|
6
|
+
applyUnifiedPatch,
|
|
7
|
+
collectCreateFolders,
|
|
8
|
+
extractPatchImports,
|
|
9
|
+
foldersFromFileIds,
|
|
10
|
+
parseUnifiedPatch,
|
|
11
|
+
} from './patch-lib.mjs'
|
|
12
|
+
|
|
13
|
+
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
14
|
+
|
|
15
|
+
export function assertSessionId(value) {
|
|
16
|
+
if (typeof value !== 'string' || !SESSION_ID.test(value) || value === '.' || value === '..') {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'sessionId must be 1-128 letters, numbers, dots, underscores, or hyphens',
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson(file, fallback) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
27
|
+
} catch {
|
|
28
|
+
return fallback
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function atomicWrite(file, contents) {
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
34
|
+
const temporary = `${file}.${process.pid}.tmp`
|
|
35
|
+
fs.writeFileSync(temporary, contents)
|
|
36
|
+
fs.renameSync(temporary, file)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sessionPaths(dataDir, sessionId) {
|
|
40
|
+
const safeId = assertSessionId(sessionId)
|
|
41
|
+
const root = path.join(dataDir, 'diff-sessions', safeId)
|
|
42
|
+
return {
|
|
43
|
+
root,
|
|
44
|
+
diffs: path.join(root, 'diffs'),
|
|
45
|
+
manifest: path.join(root, 'manifest.json'),
|
|
46
|
+
blueprint: path.join(root, 'blueprint.json'),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readActiveSession(dataDir) {
|
|
51
|
+
const value = readJson(path.join(dataDir, 'active-session.json'), null)
|
|
52
|
+
return value?.sessionId ? assertSessionId(value.sessionId) : null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeActiveSession(dataDir, sessionId) {
|
|
56
|
+
atomicWrite(
|
|
57
|
+
path.join(dataDir, 'active-session.json'),
|
|
58
|
+
`${JSON.stringify({ sessionId: sessionId ? assertSessionId(sessionId) : null }, null, 2)}\n`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function readBlueprintSession(dataDir) {
|
|
63
|
+
const value = readJson(path.join(dataDir, 'blueprint-session.json'), null)
|
|
64
|
+
return value?.sessionId ? assertSessionId(value.sessionId) : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function writeBlueprintSession(dataDir, sessionId) {
|
|
68
|
+
atomicWrite(
|
|
69
|
+
path.join(dataDir, 'blueprint-session.json'),
|
|
70
|
+
`${JSON.stringify({ sessionId: sessionId ? assertSessionId(sessionId) : null }, null, 2)}\n`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertBlueprintSessionAvailable(dataDir, sessionId) {
|
|
75
|
+
const safeId = assertSessionId(sessionId)
|
|
76
|
+
const locked = readBlueprintSession(dataDir)
|
|
77
|
+
if (locked && locked !== safeId) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Blueprint edit mode is active in session ${locked}. Finish or stop it before starting another.`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
return safeId
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function claimBlueprintSession(dataDir, sessionId) {
|
|
86
|
+
const safeId = assertBlueprintSessionAvailable(dataDir, sessionId)
|
|
87
|
+
writeBlueprintSession(dataDir, safeId)
|
|
88
|
+
return safeId
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function releaseBlueprintSession(dataDir, sessionId) {
|
|
92
|
+
const safeId = assertSessionId(sessionId)
|
|
93
|
+
if (readBlueprintSession(dataDir) === safeId) writeBlueprintSession(dataDir, null)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function focusSession(dataDir, sessionId) {
|
|
97
|
+
const safeId = assertSessionId(sessionId)
|
|
98
|
+
const locked = readBlueprintSession(dataDir)
|
|
99
|
+
if (!locked || locked === safeId) writeActiveSession(dataDir, safeId)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function readManifest(dataDir, sessionId) {
|
|
103
|
+
const { manifest } = sessionPaths(dataDir, sessionId)
|
|
104
|
+
const value = readJson(manifest, null)
|
|
105
|
+
if (!value || value.sessionId !== sessionId || !Array.isArray(value.diffs)) return null
|
|
106
|
+
if (!value.phase) {
|
|
107
|
+
const active = value.diffs.at(-1)
|
|
108
|
+
value.phase =
|
|
109
|
+
value.status === 'finished'
|
|
110
|
+
? 'finished'
|
|
111
|
+
: value.status === 'rejected'
|
|
112
|
+
? 'stopped'
|
|
113
|
+
: active?.status === 'pending'
|
|
114
|
+
? 'review'
|
|
115
|
+
: active?.status === 'extend'
|
|
116
|
+
? 'replanning'
|
|
117
|
+
: 'plan_ready'
|
|
118
|
+
value.currentStep =
|
|
119
|
+
value.currentStep ??
|
|
120
|
+
(active?.status === 'applied' ? active.step + 1 : active?.step ?? 1)
|
|
121
|
+
value.pendingInstruction ??= null
|
|
122
|
+
value.workStartedAt ??= null
|
|
123
|
+
}
|
|
124
|
+
return value
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function writeManifest(dataDir, manifest) {
|
|
128
|
+
const paths = sessionPaths(dataDir, manifest.sessionId)
|
|
129
|
+
manifest.updatedAt = new Date().toISOString()
|
|
130
|
+
atomicWrite(paths.manifest, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function readDiff(dataDir, sessionId, entry) {
|
|
134
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
135
|
+
const absolute = path.resolve(paths.root, entry.file)
|
|
136
|
+
if (!absolute.startsWith(`${paths.root}${path.sep}`)) {
|
|
137
|
+
throw new Error(`Invalid diff path for ${entry.id}`)
|
|
138
|
+
}
|
|
139
|
+
return fs.readFileSync(absolute, 'utf8')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function entryIndex(manifest, diffId) {
|
|
143
|
+
const index = manifest.diffs.findIndex((entry) => entry.id === diffId)
|
|
144
|
+
if (index < 0) throw new Error(`Unknown diff ${diffId}`)
|
|
145
|
+
return index
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function chainThrough(manifest, diffId = manifest.activeDiffId) {
|
|
149
|
+
if (!diffId) return []
|
|
150
|
+
return manifest.diffs.slice(0, entryIndex(manifest, diffId) + 1)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unresolvedEntries(manifest, diffId = manifest.activeDiffId) {
|
|
154
|
+
const chain = chainThrough(manifest, diffId)
|
|
155
|
+
let lastApplied = -1
|
|
156
|
+
chain.forEach((entry, index) => {
|
|
157
|
+
if (entry.status === 'applied') lastApplied = index
|
|
158
|
+
})
|
|
159
|
+
return chain.slice(lastApplied + 1).filter((entry) => entry.status !== 'rejected')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function previewPatchChain(patches, knownFileIds = []) {
|
|
163
|
+
const state = new Map()
|
|
164
|
+
const lineCounts = new Map()
|
|
165
|
+
const imports = new Map()
|
|
166
|
+
|
|
167
|
+
for (const patch of patches) {
|
|
168
|
+
const parsed = parseUnifiedPatch(patch)
|
|
169
|
+
for (const edge of extractPatchImports(parsed.entries, [
|
|
170
|
+
...knownFileIds,
|
|
171
|
+
...state.keys(),
|
|
172
|
+
])) {
|
|
173
|
+
imports.set(`${edge.from}->${edge.to}`, edge)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const entry of parsed.entries) {
|
|
177
|
+
const previous = state.get(entry.id)
|
|
178
|
+
if (entry.kind === 'add') {
|
|
179
|
+
state.set(entry.id, 'add')
|
|
180
|
+
lineCounts.set(entry.id, Math.max(1, entry.addedLines))
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
if (entry.kind === 'delete') {
|
|
184
|
+
if (previous === 'add') {
|
|
185
|
+
state.delete(entry.id)
|
|
186
|
+
lineCounts.delete(entry.id)
|
|
187
|
+
} else {
|
|
188
|
+
state.set(entry.id, 'delete')
|
|
189
|
+
}
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
state.set(entry.id, previous === 'add' ? 'add' : 'modify')
|
|
194
|
+
if (previous === 'add') {
|
|
195
|
+
const delta = entry.hunks.reduce(
|
|
196
|
+
(total, hunk) => total + hunk.newCount - hunk.oldCount,
|
|
197
|
+
0,
|
|
198
|
+
)
|
|
199
|
+
lineCounts.set(entry.id, Math.max(1, (lineCounts.get(entry.id) ?? 1) + delta))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const deleted = new Set(
|
|
205
|
+
[...state.entries()].filter(([, kind]) => kind === 'delete').map(([id]) => id),
|
|
206
|
+
)
|
|
207
|
+
const creates = [...state.entries()]
|
|
208
|
+
.filter(([, kind]) => kind === 'add')
|
|
209
|
+
.map(([id]) => id)
|
|
210
|
+
return {
|
|
211
|
+
files: [...state.entries()]
|
|
212
|
+
.filter(([, kind]) => kind === 'modify')
|
|
213
|
+
.map(([id]) => id),
|
|
214
|
+
creates,
|
|
215
|
+
deletes: [...deleted],
|
|
216
|
+
createLines: Object.fromEntries(lineCounts),
|
|
217
|
+
createFolders: collectCreateFolders(
|
|
218
|
+
creates,
|
|
219
|
+
foldersFromFileIds(knownFileIds.filter((id) => !creates.includes(id))),
|
|
220
|
+
),
|
|
221
|
+
imports: [...imports.values()].filter(
|
|
222
|
+
(edge) => !deleted.has(edge.from) && !deleted.has(edge.to),
|
|
223
|
+
),
|
|
224
|
+
...accumulatePatchAdditions(patches),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function sessionIntent(dataDir, sessionId, knownFileIds = [], selectedDiffId) {
|
|
229
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
230
|
+
if (!manifest) return null
|
|
231
|
+
const selectedId = selectedDiffId || manifest.activeDiffId
|
|
232
|
+
const selectedIndex = selectedId ? entryIndex(manifest, selectedId) : null
|
|
233
|
+
const selected =
|
|
234
|
+
selectedIndex === null ? null : manifest.diffs[selectedIndex]
|
|
235
|
+
const patches =
|
|
236
|
+
selectedIndex === null
|
|
237
|
+
? []
|
|
238
|
+
: manifest.diffs
|
|
239
|
+
.slice(0, selectedIndex + 1)
|
|
240
|
+
.filter((entry) => entry.status !== 'rejected')
|
|
241
|
+
.map((entry) => readDiff(dataDir, sessionId, entry))
|
|
242
|
+
const preview = previewPatchChain(patches, knownFileIds)
|
|
243
|
+
const activeView = !selectedDiffId || selectedId === manifest.activeDiffId
|
|
244
|
+
const phaseStatus = {
|
|
245
|
+
blueprint_ask: 'blueprint_ask',
|
|
246
|
+
blueprint: 'blueprint',
|
|
247
|
+
preparing: 'preparing',
|
|
248
|
+
plan_ready: 'planned',
|
|
249
|
+
working: 'working',
|
|
250
|
+
review: 'pending',
|
|
251
|
+
replanning: 'replanning',
|
|
252
|
+
finished: 'finished',
|
|
253
|
+
stopped: 'rejected',
|
|
254
|
+
}[manifest.phase]
|
|
255
|
+
const historicalStatus =
|
|
256
|
+
selected?.status === 'applied'
|
|
257
|
+
? 'approved'
|
|
258
|
+
: selected?.status ?? phaseStatus ?? 'idle'
|
|
259
|
+
const currentPlanStep = manifest.steps.find(
|
|
260
|
+
(step) => step.index === manifest.currentStep,
|
|
261
|
+
)
|
|
262
|
+
const previewVisible = patches.length > 0
|
|
263
|
+
const blueprint = readBlueprint(dataDir, sessionId)
|
|
264
|
+
const blueprintSessionId = readBlueprintSession(dataDir)
|
|
265
|
+
const ownsBlueprintLock = blueprintSessionId === sessionId
|
|
266
|
+
const canEnterBlueprint =
|
|
267
|
+
manifest.phase === 'blueprint_ask' &&
|
|
268
|
+
(!blueprintSessionId || ownsBlueprintLock)
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
updatedAt: manifest.updatedAt,
|
|
272
|
+
showMap: previewVisible,
|
|
273
|
+
status: activeView ? phaseStatus ?? historicalStatus : historicalStatus,
|
|
274
|
+
phase: manifest.phase,
|
|
275
|
+
feature: manifest.feature,
|
|
276
|
+
steps: manifest.steps,
|
|
277
|
+
step: activeView ? manifest.currentStep : selected?.step ?? manifest.currentStep,
|
|
278
|
+
reason: activeView ? currentPlanStep?.title ?? null : selected?.title ?? null,
|
|
279
|
+
sessionId,
|
|
280
|
+
diffId: selected?.id ?? null,
|
|
281
|
+
parentDiffId: selected?.parentId ?? null,
|
|
282
|
+
chainIndex: selectedIndex,
|
|
283
|
+
chain: manifest.diffs.map((entry, index) => ({
|
|
284
|
+
id: entry.id,
|
|
285
|
+
index,
|
|
286
|
+
step: entry.step,
|
|
287
|
+
title: entry.title,
|
|
288
|
+
status: entry.status,
|
|
289
|
+
})),
|
|
290
|
+
isActiveDiff: Boolean(selected && selected.id === manifest.activeDiffId),
|
|
291
|
+
preview: previewVisible,
|
|
292
|
+
working:
|
|
293
|
+
manifest.phase === 'preparing' ||
|
|
294
|
+
manifest.phase === 'working' ||
|
|
295
|
+
manifest.phase === 'replanning',
|
|
296
|
+
creationMode: manifest.phase === 'blueprint' && ownsBlueprintLock,
|
|
297
|
+
canEnterBlueprint,
|
|
298
|
+
blueprintSessionId,
|
|
299
|
+
userCreatedBlocks: blueprint.userCreatedBlocks,
|
|
300
|
+
userCreatedIslands: blueprint.userCreatedIslands,
|
|
301
|
+
...preview,
|
|
302
|
+
blueprintFunctions: blueprint.addedFunctions,
|
|
303
|
+
blueprintVariables: blueprint.addedVariables,
|
|
304
|
+
blueprintImports: blueprint.addedImports,
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function withVirtualTarget(targetRoot, patches, action) {
|
|
309
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'visual-coder-chain-'))
|
|
310
|
+
try {
|
|
311
|
+
fs.cpSync(targetRoot, temporary, { recursive: true })
|
|
312
|
+
for (const patchText of patches) applyUnifiedPatch(patchText, temporary)
|
|
313
|
+
return action(temporary)
|
|
314
|
+
} finally {
|
|
315
|
+
fs.rmSync(temporary, { recursive: true, force: true })
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function validateContinuation(dataDir, manifest, targetRoot, patchText) {
|
|
320
|
+
const prior = unresolvedEntries(manifest).map((entry) =>
|
|
321
|
+
readDiff(dataDir, manifest.sessionId, entry),
|
|
322
|
+
)
|
|
323
|
+
withVirtualTarget(targetRoot, [...prior, patchText], () => undefined)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function planSteps(titles, startAt = 1) {
|
|
327
|
+
if (!Array.isArray(titles) || titles.length === 0) {
|
|
328
|
+
throw new Error('A plan needs at least one step')
|
|
329
|
+
}
|
|
330
|
+
return titles.map((title, offset) => {
|
|
331
|
+
const trimmed = typeof title === 'string' ? title.trim() : ''
|
|
332
|
+
if (!trimmed) throw new Error('Plan step titles cannot be empty')
|
|
333
|
+
return { index: startAt + offset, title: trimmed }
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function featureName(value) {
|
|
338
|
+
const trimmed = typeof value === 'string' ? value.trim() : ''
|
|
339
|
+
return trimmed
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function startSession(dataDir, input) {
|
|
343
|
+
const sessionId = assertSessionId(input.sessionId)
|
|
344
|
+
const existing = readManifest(dataDir, sessionId)
|
|
345
|
+
if (existing) {
|
|
346
|
+
focusSession(dataDir, sessionId)
|
|
347
|
+
return existing
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const now = new Date().toISOString()
|
|
351
|
+
const manifest = {
|
|
352
|
+
version: 2,
|
|
353
|
+
sessionId,
|
|
354
|
+
feature: featureName(input.feature),
|
|
355
|
+
steps: [],
|
|
356
|
+
status: 'active',
|
|
357
|
+
phase: 'blueprint_ask',
|
|
358
|
+
currentStep: 1,
|
|
359
|
+
activeDiffId: null,
|
|
360
|
+
pendingInstruction: null,
|
|
361
|
+
workStartedAt: null,
|
|
362
|
+
createdAt: now,
|
|
363
|
+
updatedAt: now,
|
|
364
|
+
diffs: [],
|
|
365
|
+
}
|
|
366
|
+
writeManifest(dataDir, manifest)
|
|
367
|
+
writeBlueprint(dataDir, sessionId, emptyBlueprint())
|
|
368
|
+
focusSession(dataDir, sessionId)
|
|
369
|
+
return manifest
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
373
|
+
const manifest = readManifest(dataDir, assertSessionId(sessionId))
|
|
374
|
+
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
375
|
+
if (manifest.phase !== 'blueprint_ask') {
|
|
376
|
+
throw new Error(`Session ${sessionId} is not asking for a blueprint`)
|
|
377
|
+
}
|
|
378
|
+
if (enabled) claimBlueprintSession(dataDir, sessionId)
|
|
379
|
+
const blueprint = {
|
|
380
|
+
...emptyBlueprint(),
|
|
381
|
+
enabled: Boolean(enabled),
|
|
382
|
+
sent: !enabled,
|
|
383
|
+
}
|
|
384
|
+
writeBlueprint(dataDir, sessionId, blueprint)
|
|
385
|
+
manifest.phase = enabled ? 'blueprint' : 'preparing'
|
|
386
|
+
if (!enabled) manifest.workStartedAt = new Date().toISOString()
|
|
387
|
+
writeManifest(dataDir, manifest)
|
|
388
|
+
return manifest
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
392
|
+
const safeId = assertSessionId(sessionId)
|
|
393
|
+
const manifest = readManifest(dataDir, safeId)
|
|
394
|
+
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
395
|
+
if (manifest.phase !== 'blueprint') {
|
|
396
|
+
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
397
|
+
}
|
|
398
|
+
assertBlueprintSessionAvailable(dataDir, safeId)
|
|
399
|
+
const current = readBlueprint(dataDir, safeId)
|
|
400
|
+
writeBlueprint(dataDir, safeId, {
|
|
401
|
+
...current,
|
|
402
|
+
enabled: true,
|
|
403
|
+
sent: false,
|
|
404
|
+
userCreatedBlocks: input.userCreatedBlocks ?? current.userCreatedBlocks,
|
|
405
|
+
userCreatedIslands: input.userCreatedIslands ?? current.userCreatedIslands,
|
|
406
|
+
addedFunctions: input.addedFunctions ?? current.addedFunctions,
|
|
407
|
+
addedVariables: input.addedVariables ?? current.addedVariables,
|
|
408
|
+
addedImports: input.addedImports ?? current.addedImports,
|
|
409
|
+
})
|
|
410
|
+
return readBlueprint(dataDir, safeId)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
414
|
+
const safeId = assertSessionId(sessionId)
|
|
415
|
+
const manifest = readManifest(dataDir, safeId)
|
|
416
|
+
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
417
|
+
if (manifest.phase !== 'blueprint') {
|
|
418
|
+
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
419
|
+
}
|
|
420
|
+
assertBlueprintSessionAvailable(dataDir, safeId)
|
|
421
|
+
const current = readBlueprint(dataDir, safeId)
|
|
422
|
+
writeBlueprint(dataDir, safeId, {
|
|
423
|
+
enabled: true,
|
|
424
|
+
sent: true,
|
|
425
|
+
userCreatedBlocks: input.userCreatedBlocks ?? current.userCreatedBlocks,
|
|
426
|
+
userCreatedIslands: input.userCreatedIslands ?? current.userCreatedIslands,
|
|
427
|
+
addedFunctions: input.addedFunctions ?? current.addedFunctions,
|
|
428
|
+
addedVariables: input.addedVariables ?? current.addedVariables,
|
|
429
|
+
addedImports: input.addedImports ?? current.addedImports,
|
|
430
|
+
})
|
|
431
|
+
manifest.phase = 'preparing'
|
|
432
|
+
manifest.workStartedAt = new Date().toISOString()
|
|
433
|
+
writeManifest(dataDir, manifest)
|
|
434
|
+
releaseBlueprintSession(dataDir, safeId)
|
|
435
|
+
return manifest
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function reportPlan(dataDir, input) {
|
|
439
|
+
const sessionId = assertSessionId(input.sessionId)
|
|
440
|
+
const existing = readManifest(dataDir, sessionId)
|
|
441
|
+
const now = new Date().toISOString()
|
|
442
|
+
|
|
443
|
+
if (existing?.phase === 'blueprint_ask' || existing?.phase === 'blueprint') {
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Session ${sessionId} is waiting for the user to finish the blueprint handshake`,
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!existing || existing.phase === 'preparing') {
|
|
450
|
+
const manifest = existing ?? {
|
|
451
|
+
version: 2,
|
|
452
|
+
sessionId,
|
|
453
|
+
feature: input.feature,
|
|
454
|
+
steps: [],
|
|
455
|
+
status: 'active',
|
|
456
|
+
phase: 'preparing',
|
|
457
|
+
currentStep: 1,
|
|
458
|
+
activeDiffId: null,
|
|
459
|
+
pendingInstruction: null,
|
|
460
|
+
workStartedAt: null,
|
|
461
|
+
createdAt: now,
|
|
462
|
+
updatedAt: now,
|
|
463
|
+
diffs: [],
|
|
464
|
+
}
|
|
465
|
+
manifest.feature = input.feature
|
|
466
|
+
manifest.steps = planSteps(input.stepTitles)
|
|
467
|
+
manifest.status = 'active'
|
|
468
|
+
manifest.phase = 'plan_ready'
|
|
469
|
+
manifest.pendingInstruction = null
|
|
470
|
+
manifest.workStartedAt = null
|
|
471
|
+
writeManifest(dataDir, manifest)
|
|
472
|
+
focusSession(dataDir, sessionId)
|
|
473
|
+
return manifest
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (existing.phase !== 'replanning') {
|
|
477
|
+
throw new Error(`Session ${sessionId} is not waiting for a revised plan`)
|
|
478
|
+
}
|
|
479
|
+
const startAt = existing.currentStep
|
|
480
|
+
existing.steps = [
|
|
481
|
+
...existing.steps.filter((step) => step.index < startAt),
|
|
482
|
+
...planSteps(input.stepTitles, startAt),
|
|
483
|
+
]
|
|
484
|
+
existing.phase = 'plan_ready'
|
|
485
|
+
existing.status = 'active'
|
|
486
|
+
existing.pendingInstruction = null
|
|
487
|
+
existing.workStartedAt = null
|
|
488
|
+
writeManifest(dataDir, existing)
|
|
489
|
+
focusSession(dataDir, sessionId)
|
|
490
|
+
return existing
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
494
|
+
const manifest = readManifest(dataDir, assertSessionId(sessionId))
|
|
495
|
+
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
496
|
+
|
|
497
|
+
if (manifest.phase === 'review') {
|
|
498
|
+
if (!targetRoot) throw new Error('A target root is required to apply the current step')
|
|
499
|
+
const active = pendingActive(manifest, manifest.activeDiffId)
|
|
500
|
+
const last = active.step >= manifest.steps.length
|
|
501
|
+
const expected = last ? active.step : active.step + 1
|
|
502
|
+
if (step !== expected) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
last
|
|
505
|
+
? `Run step ${active.step} to finish`
|
|
506
|
+
: `Run step ${expected} to continue`,
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
applyUnresolved(dataDir, targetRoot, manifest, active.id)
|
|
510
|
+
if (last) {
|
|
511
|
+
manifest.phase = 'finished'
|
|
512
|
+
manifest.status = 'finished'
|
|
513
|
+
writeManifest(dataDir, manifest)
|
|
514
|
+
finalizeFinishedSession(dataDir, sessionId)
|
|
515
|
+
return manifest
|
|
516
|
+
}
|
|
517
|
+
manifest.currentStep = expected
|
|
518
|
+
manifest.phase = 'plan_ready'
|
|
519
|
+
manifest.workStartedAt = null
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (manifest.phase !== 'plan_ready') {
|
|
523
|
+
throw new Error(`Session ${sessionId} is not ready to invoke a step`)
|
|
524
|
+
}
|
|
525
|
+
if (step !== manifest.currentStep || !manifest.steps.some((item) => item.index === step)) {
|
|
526
|
+
throw new Error(`Step ${step} is not the current plan step`)
|
|
527
|
+
}
|
|
528
|
+
manifest.phase = 'working'
|
|
529
|
+
manifest.workStartedAt = new Date().toISOString()
|
|
530
|
+
writeManifest(dataDir, manifest)
|
|
531
|
+
return manifest
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function appendDiff(dataDir, targetRoot, input) {
|
|
535
|
+
const sessionId = assertSessionId(input.sessionId)
|
|
536
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
537
|
+
if (!manifest) throw new Error(`Report a plan for session ${sessionId} first`)
|
|
538
|
+
if (manifest.phase !== 'working') {
|
|
539
|
+
throw new Error(`Step ${manifest.currentStep} has not been invoked`)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const now = new Date().toISOString()
|
|
543
|
+
const parent = manifest.diffs.at(-1) ?? null
|
|
544
|
+
const step = manifest.currentStep
|
|
545
|
+
const title = manifest.steps.find((item) => item.index === step)?.title
|
|
546
|
+
if (!title) throw new Error(`Plan step ${step} does not exist`)
|
|
547
|
+
if (parent && parent.status !== 'extend' && parent.status !== 'applied') {
|
|
548
|
+
throw new Error(`Diff ${parent.id} must be continued or replanned first`)
|
|
549
|
+
}
|
|
550
|
+
if (parent?.status === 'extend' && step !== parent.step) {
|
|
551
|
+
throw new Error(`A revised diff must continue step ${parent.step}`)
|
|
552
|
+
}
|
|
553
|
+
if (parent?.status === 'applied' && step !== parent.step + 1) {
|
|
554
|
+
throw new Error(`The next diff must implement step ${parent.step + 1}`)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
validateContinuation(dataDir, manifest, targetRoot, input.patchText)
|
|
558
|
+
if (parent?.status === 'extend') parent.status = 'extended'
|
|
559
|
+
|
|
560
|
+
const id = String(manifest.diffs.length + 1).padStart(4, '0')
|
|
561
|
+
const file = `diffs/${id}.patch`
|
|
562
|
+
const entry = {
|
|
563
|
+
id,
|
|
564
|
+
file,
|
|
565
|
+
parentId: parent?.id ?? null,
|
|
566
|
+
step,
|
|
567
|
+
title,
|
|
568
|
+
status: 'pending',
|
|
569
|
+
instruction: null,
|
|
570
|
+
createdAt: now,
|
|
571
|
+
decidedAt: null,
|
|
572
|
+
}
|
|
573
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
574
|
+
fs.mkdirSync(paths.diffs, { recursive: true })
|
|
575
|
+
atomicWrite(
|
|
576
|
+
path.join(paths.root, file),
|
|
577
|
+
input.patchText.endsWith('\n') ? input.patchText : `${input.patchText}\n`,
|
|
578
|
+
)
|
|
579
|
+
manifest.activeDiffId = id
|
|
580
|
+
manifest.phase = 'review'
|
|
581
|
+
manifest.workStartedAt = null
|
|
582
|
+
manifest.diffs.push(entry)
|
|
583
|
+
writeManifest(dataDir, manifest)
|
|
584
|
+
focusSession(dataDir, sessionId)
|
|
585
|
+
return { manifest, entry }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function pendingActive(manifest, diffId) {
|
|
589
|
+
if (manifest.activeDiffId !== diffId) throw new Error('Stale diff decision')
|
|
590
|
+
const active = manifest.diffs.at(-1)
|
|
591
|
+
if (
|
|
592
|
+
manifest.phase !== 'review' ||
|
|
593
|
+
!active ||
|
|
594
|
+
active.id !== diffId ||
|
|
595
|
+
active.status !== 'pending'
|
|
596
|
+
) {
|
|
597
|
+
throw new Error(`Diff ${diffId} is not ready for review`)
|
|
598
|
+
}
|
|
599
|
+
return active
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function applyUnresolved(dataDir, targetRoot, manifest, diffId) {
|
|
603
|
+
const unresolved = unresolvedEntries(manifest, diffId)
|
|
604
|
+
const patches = unresolved.map((entry) =>
|
|
605
|
+
readDiff(dataDir, manifest.sessionId, entry),
|
|
606
|
+
)
|
|
607
|
+
const touched = new Set()
|
|
608
|
+
for (const patch of patches) {
|
|
609
|
+
for (const entry of parseUnifiedPatch(patch).entries) touched.add(entry.id)
|
|
610
|
+
}
|
|
611
|
+
withVirtualTarget(targetRoot, patches, (virtualRoot) => {
|
|
612
|
+
for (const id of touched) {
|
|
613
|
+
const source = path.join(virtualRoot, id)
|
|
614
|
+
const destination = path.join(targetRoot, id)
|
|
615
|
+
if (!fs.existsSync(source)) {
|
|
616
|
+
fs.rmSync(destination, { recursive: true, force: true })
|
|
617
|
+
continue
|
|
618
|
+
}
|
|
619
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
620
|
+
fs.copyFileSync(source, destination)
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
for (const entry of unresolved) {
|
|
624
|
+
entry.status = 'applied'
|
|
625
|
+
entry.decidedAt = new Date().toISOString()
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
630
|
+
const manifest = readManifest(dataDir, assertSessionId(sessionId))
|
|
631
|
+
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
632
|
+
const active = pendingActive(manifest, diffId)
|
|
633
|
+
applyUnresolved(dataDir, targetRoot, manifest, diffId)
|
|
634
|
+
|
|
635
|
+
if (active.step >= manifest.steps.length) {
|
|
636
|
+
manifest.phase = 'finished'
|
|
637
|
+
manifest.status = 'finished'
|
|
638
|
+
writeManifest(dataDir, manifest)
|
|
639
|
+
finalizeFinishedSession(dataDir, sessionId)
|
|
640
|
+
return manifest
|
|
641
|
+
} else {
|
|
642
|
+
manifest.currentStep = active.step + 1
|
|
643
|
+
manifest.phase = 'plan_ready'
|
|
644
|
+
manifest.workStartedAt = null
|
|
645
|
+
}
|
|
646
|
+
writeManifest(dataDir, manifest)
|
|
647
|
+
return manifest
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
651
|
+
const manifest = readManifest(dataDir, assertSessionId(sessionId))
|
|
652
|
+
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
653
|
+
const active = pendingActive(manifest, diffId)
|
|
654
|
+
const guidance = typeof instruction === 'string' ? instruction.trim() : ''
|
|
655
|
+
if (!guidance) throw new Error('An alternative instruction is required')
|
|
656
|
+
active.status = 'extend'
|
|
657
|
+
active.instruction = guidance
|
|
658
|
+
active.decidedAt = new Date().toISOString()
|
|
659
|
+
manifest.phase = 'replanning'
|
|
660
|
+
manifest.pendingInstruction = guidance
|
|
661
|
+
manifest.workStartedAt = new Date().toISOString()
|
|
662
|
+
writeManifest(dataDir, manifest)
|
|
663
|
+
return manifest
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function stopSession(dataDir, sessionId, _diffId) {
|
|
667
|
+
finalizeFinishedSession(dataDir, sessionId)
|
|
668
|
+
return null
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function decideDiff(
|
|
672
|
+
dataDir,
|
|
673
|
+
targetRoot,
|
|
674
|
+
sessionId,
|
|
675
|
+
diffId,
|
|
676
|
+
decision,
|
|
677
|
+
instruction = '',
|
|
678
|
+
) {
|
|
679
|
+
if (decision === 'approved') {
|
|
680
|
+
return continueDiff(dataDir, targetRoot, sessionId, diffId)
|
|
681
|
+
}
|
|
682
|
+
if (decision === 'extend') {
|
|
683
|
+
return requestReplan(dataDir, sessionId, diffId, instruction)
|
|
684
|
+
}
|
|
685
|
+
return stopSession(dataDir, sessionId, diffId)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function closeSession(dataDir, sessionId) {
|
|
689
|
+
releaseBlueprintSession(dataDir, sessionId)
|
|
690
|
+
const active = readActiveSession(dataDir)
|
|
691
|
+
if (active === assertSessionId(sessionId)) writeActiveSession(dataDir, null)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function finalizeFinishedSession(dataDir, sessionId) {
|
|
695
|
+
const safeId = assertSessionId(sessionId)
|
|
696
|
+
const paths = sessionPaths(dataDir, safeId)
|
|
697
|
+
releaseBlueprintSession(dataDir, safeId)
|
|
698
|
+
if (fs.existsSync(paths.root)) {
|
|
699
|
+
fs.rmSync(paths.root, { recursive: true, force: true })
|
|
700
|
+
}
|
|
701
|
+
const active = readActiveSession(dataDir)
|
|
702
|
+
if (active === safeId) writeActiveSession(dataDir, null)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function emptyBlueprint() {
|
|
706
|
+
return {
|
|
707
|
+
enabled: false,
|
|
708
|
+
sent: false,
|
|
709
|
+
userCreatedBlocks: [],
|
|
710
|
+
userCreatedIslands: [],
|
|
711
|
+
addedFunctions: [],
|
|
712
|
+
addedVariables: [],
|
|
713
|
+
addedImports: [],
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function namedBlueprintBlocks(value) {
|
|
718
|
+
if (!Array.isArray(value)) return []
|
|
719
|
+
return value.filter((item) => {
|
|
720
|
+
if (!item || typeof item !== 'object') return false
|
|
721
|
+
return (
|
|
722
|
+
typeof item.id === 'string' &&
|
|
723
|
+
typeof item.name === 'string' &&
|
|
724
|
+
item.name.trim() !== '' &&
|
|
725
|
+
typeof item.path === 'string' &&
|
|
726
|
+
typeof item.folder === 'string' &&
|
|
727
|
+
typeof item.x === 'number' &&
|
|
728
|
+
typeof item.z === 'number' &&
|
|
729
|
+
!item.naming
|
|
730
|
+
)
|
|
731
|
+
})
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function namedBlueprintIslands(value) {
|
|
735
|
+
if (!Array.isArray(value)) return []
|
|
736
|
+
return value.filter((item) => {
|
|
737
|
+
if (!item || typeof item !== 'object') return false
|
|
738
|
+
return (
|
|
739
|
+
typeof item.id === 'string' &&
|
|
740
|
+
typeof item.name === 'string' &&
|
|
741
|
+
item.name.trim() !== '' &&
|
|
742
|
+
typeof item.path === 'string' &&
|
|
743
|
+
typeof item.parent === 'string' &&
|
|
744
|
+
!item.naming
|
|
745
|
+
)
|
|
746
|
+
})
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function namedBlueprintSymbols(value) {
|
|
750
|
+
if (!Array.isArray(value)) return []
|
|
751
|
+
return value.filter((item) => {
|
|
752
|
+
if (!item || typeof item !== 'object') return false
|
|
753
|
+
return (
|
|
754
|
+
typeof item.name === 'string' &&
|
|
755
|
+
item.name.trim() !== '' &&
|
|
756
|
+
typeof item.file === 'string' &&
|
|
757
|
+
item.file.trim() !== ''
|
|
758
|
+
)
|
|
759
|
+
})
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function namedBlueprintImportAdditions(value) {
|
|
763
|
+
if (!Array.isArray(value)) return []
|
|
764
|
+
return value.filter((item) => {
|
|
765
|
+
if (!item || typeof item !== 'object') return false
|
|
766
|
+
return (
|
|
767
|
+
typeof item.name === 'string' &&
|
|
768
|
+
item.name.trim() !== '' &&
|
|
769
|
+
typeof item.from === 'string' &&
|
|
770
|
+
item.from.trim() !== '' &&
|
|
771
|
+
typeof item.file === 'string' &&
|
|
772
|
+
item.file.trim() !== ''
|
|
773
|
+
)
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export function readBlueprint(dataDir, sessionId) {
|
|
778
|
+
const { blueprint } = sessionPaths(dataDir, sessionId)
|
|
779
|
+
const value = readJson(blueprint, emptyBlueprint())
|
|
780
|
+
return {
|
|
781
|
+
enabled: Boolean(value?.enabled),
|
|
782
|
+
sent: Boolean(value?.sent),
|
|
783
|
+
userCreatedBlocks: namedBlueprintBlocks(value?.userCreatedBlocks),
|
|
784
|
+
userCreatedIslands: namedBlueprintIslands(value?.userCreatedIslands),
|
|
785
|
+
addedFunctions: namedBlueprintSymbols(value?.addedFunctions),
|
|
786
|
+
addedVariables: namedBlueprintSymbols(value?.addedVariables),
|
|
787
|
+
addedImports: namedBlueprintImportAdditions(value?.addedImports),
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function writeBlueprint(dataDir, sessionId, blueprint) {
|
|
792
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
793
|
+
atomicWrite(
|
|
794
|
+
paths.blueprint,
|
|
795
|
+
`${JSON.stringify(
|
|
796
|
+
{
|
|
797
|
+
enabled: Boolean(blueprint.enabled),
|
|
798
|
+
sent: Boolean(blueprint.sent),
|
|
799
|
+
userCreatedBlocks: namedBlueprintBlocks(blueprint.userCreatedBlocks),
|
|
800
|
+
userCreatedIslands: namedBlueprintIslands(blueprint.userCreatedIslands),
|
|
801
|
+
addedFunctions: namedBlueprintSymbols(blueprint.addedFunctions),
|
|
802
|
+
addedVariables: namedBlueprintSymbols(blueprint.addedVariables),
|
|
803
|
+
addedImports: namedBlueprintImportAdditions(blueprint.addedImports),
|
|
804
|
+
},
|
|
805
|
+
null,
|
|
806
|
+
2,
|
|
807
|
+
)}\n`,
|
|
808
|
+
)
|
|
809
|
+
}
|