@jkwd/inbase 0.1.10 → 0.1.12
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/LICENSE +21 -0
- package/README.md +22 -9
- package/apps/explorer/package.json +1 -0
- package/apps/explorer/scripts/branch-changes.d.ts +24 -0
- package/apps/explorer/scripts/branch-changes.mjs +237 -0
- package/apps/explorer/scripts/js-source.mjs +12 -15
- package/apps/explorer/scripts/patch-lib.d.ts +5 -0
- package/apps/explorer/scripts/patch-lib.mjs +5 -0
- package/apps/explorer/scripts/scan-target.mjs +46 -13
- package/apps/explorer/scripts/session-store.d.ts +58 -1
- package/apps/explorer/scripts/session-store.mjs +276 -52
- package/apps/explorer/scripts/tree-diff.mjs +121 -0
- package/apps/explorer/src/App.tsx +170 -32
- package/apps/explorer/src/agentIntent.ts +63 -0
- package/apps/explorer/src/branchChanges.ts +54 -0
- package/apps/explorer/src/index.css +153 -0
- package/apps/explorer/src/scene/FileBlock.tsx +55 -5
- package/apps/explorer/src/scene/MapView.tsx +32 -1
- package/apps/explorer/src/scene/World.tsx +6 -1
- package/apps/explorer/src/types.ts +44 -0
- package/apps/explorer/src/ui/HUD.tsx +583 -96
- package/apps/explorer/src/ui/MapContextMenu.tsx +89 -0
- package/apps/explorer/src/userContext.ts +11 -0
- package/apps/explorer/src/userCreated.ts +50 -0
- package/apps/explorer/vite.config.ts +61 -7
- package/bin/inbase.mjs +23 -4
- package/bin/project.mjs +62 -4
- package/bin/session.mjs +231 -125
- package/package.json +20 -2
- package/skill/commands/inbase.md +17 -0
- package/skill/commands/skipinbase.md +9 -0
- package/skill/inbase/SKILL.md +143 -112
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
|
|
4
|
+
export type MapContextMenuState = {
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
folder: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type MapContextMenuProps = {
|
|
11
|
+
menu: MapContextMenuState | null
|
|
12
|
+
onAddFile: (folder: string) => void
|
|
13
|
+
onAddFolder: (folder: string) => void
|
|
14
|
+
onClose: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function MapContextMenu({
|
|
18
|
+
menu,
|
|
19
|
+
onAddFile,
|
|
20
|
+
onAddFolder,
|
|
21
|
+
onClose,
|
|
22
|
+
}: MapContextMenuProps) {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!menu) return
|
|
25
|
+
const onKey = (event: KeyboardEvent) => {
|
|
26
|
+
if (event.code !== 'Escape') return
|
|
27
|
+
event.preventDefault()
|
|
28
|
+
onClose()
|
|
29
|
+
}
|
|
30
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
31
|
+
const target = event.target
|
|
32
|
+
if (target instanceof Element && target.closest('.map-context-menu')) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
onClose()
|
|
36
|
+
}
|
|
37
|
+
window.addEventListener('keydown', onKey)
|
|
38
|
+
window.addEventListener('pointerdown', onPointerDown, true)
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener('keydown', onKey)
|
|
41
|
+
window.removeEventListener('pointerdown', onPointerDown, true)
|
|
42
|
+
}
|
|
43
|
+
}, [menu, onClose])
|
|
44
|
+
|
|
45
|
+
if (!menu) return null
|
|
46
|
+
|
|
47
|
+
const pad = 8
|
|
48
|
+
const width = 176
|
|
49
|
+
const height = 84
|
|
50
|
+
const left = Math.min(
|
|
51
|
+
Math.max(pad, menu.x),
|
|
52
|
+
window.innerWidth - width - pad,
|
|
53
|
+
)
|
|
54
|
+
const top = Math.min(
|
|
55
|
+
Math.max(pad, menu.y),
|
|
56
|
+
window.innerHeight - height - pad,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return createPortal(
|
|
60
|
+
<div
|
|
61
|
+
className="map-context-menu"
|
|
62
|
+
role="menu"
|
|
63
|
+
style={{ left, top }}
|
|
64
|
+
onContextMenu={(event) => event.preventDefault()}
|
|
65
|
+
>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
role="menuitem"
|
|
69
|
+
onClick={() => {
|
|
70
|
+
onAddFile(menu.folder)
|
|
71
|
+
onClose()
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
Add file
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
role="menuitem"
|
|
79
|
+
onClick={() => {
|
|
80
|
+
onAddFolder(menu.folder)
|
|
81
|
+
onClose()
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
Add folder
|
|
85
|
+
</button>
|
|
86
|
+
</div>,
|
|
87
|
+
document.body,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -38,6 +38,16 @@ export function persistFollowLook(followLook: boolean) {
|
|
|
38
38
|
})
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function persistShowBranchChanges(showBranchChanges: boolean) {
|
|
42
|
+
fetch('/api/user-context', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: `${JSON.stringify({ showBranchChanges })}\n`,
|
|
46
|
+
}).catch(() => {
|
|
47
|
+
lastWritten = ''
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
export function persistUserContext(context: UserContext) {
|
|
42
52
|
pending = context
|
|
43
53
|
if (timer !== null) return
|
|
@@ -51,6 +61,7 @@ function flushUserContext() {
|
|
|
51
61
|
if (!context) return
|
|
52
62
|
const {
|
|
53
63
|
followLook: _followLook,
|
|
64
|
+
showBranchChanges: _showBranchChanges,
|
|
54
65
|
userCreatedBlocks: _userCreatedBlocks,
|
|
55
66
|
userCreatedIslands: _userCreatedIslands,
|
|
56
67
|
...gaze
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
FileNode,
|
|
6
6
|
PatchImportAddition,
|
|
7
7
|
PatchSymbolAddition,
|
|
8
|
+
PlacedFolder,
|
|
8
9
|
UserCreatedBlock,
|
|
9
10
|
UserCreatedIsland,
|
|
10
11
|
WorldLayout,
|
|
@@ -193,6 +194,54 @@ export function withUserCreatedGraph(
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
function placedFolderParent(
|
|
198
|
+
folder: PlacedFolder,
|
|
199
|
+
islands: UserCreatedIsland[],
|
|
200
|
+
) {
|
|
201
|
+
const created = islands.find((item) => islandKey(item) === folder.path)
|
|
202
|
+
if (created) return created.parent
|
|
203
|
+
return folderParent(folder.path)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function expandParentsToChildren(
|
|
207
|
+
folders: Record<string, PlacedFolder>,
|
|
208
|
+
islands: UserCreatedIsland[],
|
|
209
|
+
) {
|
|
210
|
+
const parentPaths = new Set<string>()
|
|
211
|
+
for (const island of islands) {
|
|
212
|
+
let current: string | null = island.parent
|
|
213
|
+
while (current) {
|
|
214
|
+
parentPaths.add(current)
|
|
215
|
+
current = folderParent(current)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const ordered = [...parentPaths].sort(
|
|
219
|
+
(left, right) =>
|
|
220
|
+
right.split('/').filter(Boolean).length -
|
|
221
|
+
left.split('/').filter(Boolean).length,
|
|
222
|
+
)
|
|
223
|
+
for (const parentPath of ordered) {
|
|
224
|
+
const parent = folders[parentPath]
|
|
225
|
+
if (!parent) continue
|
|
226
|
+
const children = Object.values(folders).filter(
|
|
227
|
+
(folder) =>
|
|
228
|
+
folder.path !== parentPath &&
|
|
229
|
+
placedFolderParent(folder, islands) === parentPath,
|
|
230
|
+
)
|
|
231
|
+
if (children.length === 0) continue
|
|
232
|
+
let extent = parent.width / 2
|
|
233
|
+
for (const child of children) {
|
|
234
|
+
extent = Math.max(
|
|
235
|
+
extent,
|
|
236
|
+
Math.abs(child.x - child.width / 2 - parent.x),
|
|
237
|
+
Math.abs(child.x + child.width / 2 - parent.x),
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
const width = extent * 2
|
|
241
|
+
if (width > parent.width) folders[parentPath] = { ...parent, width }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
196
245
|
function overlayIslands(
|
|
197
246
|
layout: WorldLayout,
|
|
198
247
|
islands: UserCreatedIsland[],
|
|
@@ -245,6 +294,7 @@ function overlayIslands(
|
|
|
245
294
|
})
|
|
246
295
|
}
|
|
247
296
|
|
|
297
|
+
expandParentsToChildren(folders, islands)
|
|
248
298
|
return { ...layout, folders, bridges }
|
|
249
299
|
}
|
|
250
300
|
|
|
@@ -6,11 +6,13 @@ import path from 'node:path'
|
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
8
8
|
import { emptyIntent } from './scripts/patch-lib.mjs'
|
|
9
|
+
import { readBranchChanges } from './scripts/branch-changes.mjs'
|
|
10
|
+
import { writeRunningInstance } from '../../bin/project.mjs'
|
|
9
11
|
import { dataDir, targetRoot } from './scripts/target-config.mjs'
|
|
10
12
|
import { editorFileUri, openInEditor } from './scripts/open-editor.mjs'
|
|
11
13
|
import {
|
|
12
14
|
answerBlueprint,
|
|
13
|
-
|
|
15
|
+
clearDiffSessions,
|
|
14
16
|
continueDiff,
|
|
15
17
|
inspectTargetFile,
|
|
16
18
|
invokeStep,
|
|
@@ -19,7 +21,9 @@ import {
|
|
|
19
21
|
requestReplan,
|
|
20
22
|
sendBlueprint,
|
|
21
23
|
sessionIntent,
|
|
24
|
+
setInitialInstruction,
|
|
22
25
|
setStepByStep,
|
|
26
|
+
setupSession,
|
|
23
27
|
focusSession,
|
|
24
28
|
stopSession,
|
|
25
29
|
updateBlueprint,
|
|
@@ -85,8 +89,14 @@ function jsonFilePlugin(): Plugin {
|
|
|
85
89
|
return {
|
|
86
90
|
name: 'visual-coder-json-files',
|
|
87
91
|
configureServer(server) {
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
writeRunningInstance({
|
|
93
|
+
dataDir,
|
|
94
|
+
targetRoot,
|
|
95
|
+
port: server.config.server.port ?? 5173,
|
|
96
|
+
})
|
|
97
|
+
// Always boot with no LLM session. Leftover diffs and pointers are not restored.
|
|
98
|
+
clearDiffSessions(dataDir, targetRoot)
|
|
99
|
+
rescanTarget('after discarding leftover LLM sessions')
|
|
90
100
|
server.middlewares.use('/api/user-context', (req, res, next) => {
|
|
91
101
|
if (req.method === 'GET') {
|
|
92
102
|
sendJson(res, 200, readUserContext())
|
|
@@ -145,6 +155,14 @@ function jsonFilePlugin(): Plugin {
|
|
|
145
155
|
next()
|
|
146
156
|
})
|
|
147
157
|
|
|
158
|
+
server.middlewares.use('/api/branch-changes', (req, res, next) => {
|
|
159
|
+
if (req.method === 'GET') {
|
|
160
|
+
sendJson(res, 200, readBranchChanges(targetRoot, knownFileIds()))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
next()
|
|
164
|
+
})
|
|
165
|
+
|
|
148
166
|
server.middlewares.use('/api/inspect-file', (req, res, next) => {
|
|
149
167
|
if (req.method === 'POST') {
|
|
150
168
|
void inspectFile(req, res)
|
|
@@ -191,6 +209,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
191
209
|
sessionId?: string
|
|
192
210
|
diffId?: string
|
|
193
211
|
instruction?: string
|
|
212
|
+
name?: string
|
|
194
213
|
step?: number
|
|
195
214
|
stepByStep?: boolean
|
|
196
215
|
userCreatedBlocks?: unknown[]
|
|
@@ -210,12 +229,14 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
210
229
|
action !== 'blueprint_send' &&
|
|
211
230
|
action !== 'blueprint_update' &&
|
|
212
231
|
action !== 'focus' &&
|
|
213
|
-
action !== 'set_step_by_step'
|
|
232
|
+
action !== 'set_step_by_step' &&
|
|
233
|
+
action !== 'set_initial_instruction' &&
|
|
234
|
+
action !== 'setup_session'
|
|
214
235
|
) {
|
|
215
236
|
sendJson(res, 400, { error: 'invalid workflow action' })
|
|
216
237
|
return
|
|
217
238
|
}
|
|
218
|
-
if (!body.sessionId) {
|
|
239
|
+
if (action !== 'setup_session' && !body.sessionId) {
|
|
219
240
|
sendJson(res, 400, { error: 'sessionId is required' })
|
|
220
241
|
return
|
|
221
242
|
}
|
|
@@ -227,13 +248,20 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
227
248
|
return
|
|
228
249
|
}
|
|
229
250
|
|
|
251
|
+
if (
|
|
252
|
+
body.name !== undefined &&
|
|
253
|
+
(typeof body.name !== 'string' || body.name.length > 200)
|
|
254
|
+
) {
|
|
255
|
+
sendJson(res, 400, { error: 'name must be a string up to 200 characters' })
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
230
259
|
if (action === 'invoke') {
|
|
231
260
|
if (!Number.isInteger(body.step)) {
|
|
232
261
|
sendJson(res, 400, { error: 'step is required for invoke' })
|
|
233
262
|
return
|
|
234
263
|
}
|
|
235
264
|
invokeStep(dataDir, body.sessionId, body.step as number, targetRoot)
|
|
236
|
-
rescanTarget('after invoking step')
|
|
237
265
|
} else if (action === 'continue') {
|
|
238
266
|
if (!body.diffId) {
|
|
239
267
|
sendJson(res, 400, { error: 'diffId is required for continue' })
|
|
@@ -274,6 +302,16 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
274
302
|
addedVariables: body.addedVariables,
|
|
275
303
|
addedImports: body.addedImports,
|
|
276
304
|
})
|
|
305
|
+
} else if (action === 'setup_session') {
|
|
306
|
+
const manifest = setupSession(dataDir, {
|
|
307
|
+
sessionId: body.sessionId,
|
|
308
|
+
name: body.name,
|
|
309
|
+
})
|
|
310
|
+
const next = sessionIntent(dataDir, manifest.sessionId, knownFileIds())
|
|
311
|
+
sendJson(res, 200, next ?? { ...emptyIntent })
|
|
312
|
+
return
|
|
313
|
+
} else if (action === 'set_initial_instruction') {
|
|
314
|
+
setInitialInstruction(dataDir, body.sessionId, body.instruction ?? '')
|
|
277
315
|
} else if (action === 'focus') {
|
|
278
316
|
focusSession(dataDir, body.sessionId)
|
|
279
317
|
} else if (action === 'set_step_by_step') {
|
|
@@ -330,9 +368,10 @@ function readUserContext() {
|
|
|
330
368
|
return {
|
|
331
369
|
...parsed,
|
|
332
370
|
followLook: Boolean(parsed.followLook),
|
|
371
|
+
showBranchChanges: Boolean(parsed.showBranchChanges),
|
|
333
372
|
}
|
|
334
373
|
} catch {
|
|
335
|
-
return { followLook: false }
|
|
374
|
+
return { followLook: false, showBranchChanges: false }
|
|
336
375
|
}
|
|
337
376
|
}
|
|
338
377
|
|
|
@@ -347,6 +386,10 @@ async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
|
|
|
347
386
|
typeof incoming.followLook === 'boolean'
|
|
348
387
|
? incoming.followLook
|
|
349
388
|
: Boolean(existing.followLook),
|
|
389
|
+
showBranchChanges:
|
|
390
|
+
typeof incoming.showBranchChanges === 'boolean'
|
|
391
|
+
? incoming.showBranchChanges
|
|
392
|
+
: Boolean(existing.showBranchChanges),
|
|
350
393
|
}
|
|
351
394
|
delete next.userCreatedBlocks
|
|
352
395
|
delete next.userCreatedIslands
|
|
@@ -360,10 +403,21 @@ async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
|
|
|
360
403
|
}
|
|
361
404
|
}
|
|
362
405
|
|
|
406
|
+
function isDataDirPath(filePath: string) {
|
|
407
|
+
const file = path.resolve(filePath)
|
|
408
|
+
const root = path.resolve(dataDir)
|
|
409
|
+
return file === root || file.startsWith(root + path.sep)
|
|
410
|
+
}
|
|
411
|
+
|
|
363
412
|
export default defineConfig({
|
|
364
413
|
plugins: [react(), jsonFilePlugin()],
|
|
365
414
|
server: {
|
|
366
415
|
port: 5173,
|
|
416
|
+
watch: {
|
|
417
|
+
// Session snapshots copy target source into the data dir. If Vite
|
|
418
|
+
// watches those writes, Create proposal full-reloads the visualizer.
|
|
419
|
+
ignored: ['**/src/data/**', isDataDirPath],
|
|
420
|
+
},
|
|
367
421
|
fs: {
|
|
368
422
|
allow: [here, dataDir],
|
|
369
423
|
},
|
package/bin/inbase.mjs
CHANGED
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
ensureGitignoreEntry,
|
|
11
11
|
explorerRoot,
|
|
12
12
|
skillTemplateDir,
|
|
13
|
+
commandTemplateDir,
|
|
13
14
|
takeFlagValue,
|
|
14
15
|
} from './project.mjs'
|
|
15
16
|
import {
|
|
16
17
|
proposePatch,
|
|
17
18
|
reportPlan,
|
|
18
19
|
startSession,
|
|
20
|
+
attachSession,
|
|
19
21
|
waitForApproval,
|
|
20
22
|
waitForBlueprint,
|
|
21
23
|
} from './session.mjs'
|
|
@@ -28,11 +30,12 @@ Usage:
|
|
|
28
30
|
inbase help Show this help
|
|
29
31
|
|
|
30
32
|
Agent commands (used by the Cursor skill):
|
|
31
|
-
inbase start-session --session <id>
|
|
33
|
+
inbase start-session --session <id> --name "short name"
|
|
34
|
+
inbase attach [--session <id>]
|
|
32
35
|
inbase wait-for-blueprint --session <id>
|
|
33
36
|
inbase report-plan --session <id> --feature "name" --steps "one" [--steps "two"]
|
|
34
37
|
inbase wait-for-approval --session <id>
|
|
35
|
-
inbase propose-patch --session <id>
|
|
38
|
+
inbase propose-patch --session <id> [file.patch|-]
|
|
36
39
|
inbase propose-patch --session <id> --clear
|
|
37
40
|
|
|
38
41
|
Options for run:
|
|
@@ -50,6 +53,10 @@ export function initProject(projectRoot = process.cwd()) {
|
|
|
50
53
|
}
|
|
51
54
|
const skillDir = path.join(projectRoot, '.cursor/skills/inbase')
|
|
52
55
|
copyDir(skillTemplateDir, skillDir)
|
|
56
|
+
const commandDir = path.join(projectRoot, '.cursor/commands')
|
|
57
|
+
if (fs.existsSync(commandTemplateDir)) {
|
|
58
|
+
copyDir(commandTemplateDir, commandDir)
|
|
59
|
+
}
|
|
53
60
|
const { dataDir } = applyHostEnv({
|
|
54
61
|
cwd: projectRoot,
|
|
55
62
|
target: projectRoot,
|
|
@@ -57,7 +64,7 @@ export function initProject(projectRoot = process.cwd()) {
|
|
|
57
64
|
})
|
|
58
65
|
ensureDataDir(dataDir)
|
|
59
66
|
const gitignoreAdded = ensureGitignoreEntry(projectRoot)
|
|
60
|
-
return { skillDir, dataDir, gitignoreAdded }
|
|
67
|
+
return { skillDir, commandDir, dataDir, gitignoreAdded }
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
function explorerHref(relative) {
|
|
@@ -122,6 +129,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
122
129
|
if (command === 'init') {
|
|
123
130
|
const result = initProject()
|
|
124
131
|
console.log(`Installed Cursor skill at ${result.skillDir}`)
|
|
132
|
+
if (fs.existsSync(path.join(result.commandDir, 'inbase.md'))) {
|
|
133
|
+
console.log(`Installed /inbase command at ${result.commandDir}`)
|
|
134
|
+
}
|
|
125
135
|
if (result.gitignoreAdded) console.log('Added .inbase/ to .gitignore')
|
|
126
136
|
console.log('Next: run `inbase run`, then ask Cursor to change source files.')
|
|
127
137
|
return
|
|
@@ -132,13 +142,22 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
132
142
|
return
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
applyHostEnv()
|
|
145
|
+
const host = applyHostEnv()
|
|
136
146
|
ensureDataDir(process.env.INBASE_DATA_DIR)
|
|
147
|
+
if (host.instance) {
|
|
148
|
+
console.log(
|
|
149
|
+
`INBASE_ATTACHED Using the running visualizer (${host.instance.dataDir}). Run wait-for-blueprint to read the optional blueprint; it does not wait.`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
137
152
|
|
|
138
153
|
if (command === 'start-session') {
|
|
139
154
|
await startSession(args)
|
|
140
155
|
return
|
|
141
156
|
}
|
|
157
|
+
if (command === 'attach') {
|
|
158
|
+
await attachSession(args)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
142
161
|
if (command === 'wait-for-blueprint') {
|
|
143
162
|
await waitForBlueprint(args)
|
|
144
163
|
return
|
package/bin/project.mjs
CHANGED
|
@@ -6,6 +6,7 @@ const here = path.dirname(fileURLToPath(import.meta.url))
|
|
|
6
6
|
export const packageRoot = path.resolve(here, '..')
|
|
7
7
|
export const explorerRoot = path.join(packageRoot, 'apps/explorer')
|
|
8
8
|
export const skillTemplateDir = path.join(packageRoot, 'skill/inbase')
|
|
9
|
+
export const commandTemplateDir = path.join(packageRoot, 'skill/commands')
|
|
9
10
|
|
|
10
11
|
export function resolveOptionalPath(value, fallback) {
|
|
11
12
|
const raw = value?.trim()
|
|
@@ -13,19 +14,76 @@ export function resolveOptionalPath(value, fallback) {
|
|
|
13
14
|
return path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(process.cwd(), raw)
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
export const INSTANCE_FILE = 'instance.json'
|
|
18
|
+
|
|
19
|
+
function isPidAlive(pid) {
|
|
20
|
+
if (!Number.isInteger(pid)) return true
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0)
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function instanceFile(dataDir) {
|
|
30
|
+
return path.join(dataDir, INSTANCE_FILE)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function writeRunningInstance({ dataDir, targetRoot, port = null }) {
|
|
34
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
35
|
+
const instance = {
|
|
36
|
+
dataDir: path.resolve(dataDir),
|
|
37
|
+
targetRoot: path.resolve(targetRoot),
|
|
38
|
+
port: port ?? null,
|
|
39
|
+
pid: process.pid,
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(instanceFile(dataDir), `${JSON.stringify(instance, null, 2)}\n`)
|
|
43
|
+
return instance
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readInstanceFile(file) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
49
|
+
if (!parsed?.dataDir || !parsed?.targetRoot) return null
|
|
50
|
+
if (!isPidAlive(parsed.pid)) return null
|
|
51
|
+
return parsed
|
|
52
|
+
} catch {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readRunningInstance(cwd = process.cwd()) {
|
|
58
|
+
const files = [
|
|
59
|
+
path.join(cwd, '.inbase', INSTANCE_FILE),
|
|
60
|
+
path.join(explorerRoot, 'src/data', INSTANCE_FILE),
|
|
61
|
+
]
|
|
62
|
+
const seen = new Set()
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const resolved = path.resolve(file)
|
|
65
|
+
if (seen.has(resolved) || !fs.existsSync(resolved)) continue
|
|
66
|
+
seen.add(resolved)
|
|
67
|
+
const instance = readInstanceFile(resolved)
|
|
68
|
+
if (instance) return instance
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
16
73
|
export function applyHostEnv({
|
|
17
74
|
cwd = process.cwd(),
|
|
18
75
|
target = process.env.VISUAL_CODER_TARGET,
|
|
19
76
|
dataDir = process.env.INBASE_DATA_DIR,
|
|
20
77
|
} = {}) {
|
|
21
|
-
const
|
|
78
|
+
const running = !target && !dataDir ? readRunningInstance(cwd) : null
|
|
79
|
+
const targetRoot = resolveOptionalPath(target, running?.targetRoot ?? cwd)
|
|
22
80
|
const resolvedDataDir = resolveOptionalPath(
|
|
23
81
|
dataDir,
|
|
24
|
-
path.join(targetRoot, '.inbase'),
|
|
82
|
+
running?.dataDir ?? path.join(targetRoot, '.inbase'),
|
|
25
83
|
)
|
|
26
84
|
process.env.VISUAL_CODER_TARGET = targetRoot
|
|
27
85
|
process.env.INBASE_DATA_DIR = resolvedDataDir
|
|
28
|
-
return { cwd, targetRoot, dataDir: resolvedDataDir }
|
|
86
|
+
return { cwd, targetRoot, dataDir: resolvedDataDir, instance: running }
|
|
29
87
|
}
|
|
30
88
|
|
|
31
89
|
export function ensureDataDir(dataDir) {
|
|
@@ -34,7 +92,7 @@ export function ensureDataDir(dataDir) {
|
|
|
34
92
|
if (!fs.existsSync(userContextFile)) {
|
|
35
93
|
fs.writeFileSync(
|
|
36
94
|
userContextFile,
|
|
37
|
-
`${JSON.stringify({ followLook: false }, null, 2)}\n`,
|
|
95
|
+
`${JSON.stringify({ followLook: false, showBranchChanges: false }, null, 2)}\n`,
|
|
38
96
|
)
|
|
39
97
|
}
|
|
40
98
|
return dataDir
|