@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,295 @@
|
|
|
1
|
+
import { defineConfig, type Plugin } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
8
|
+
import { emptyIntent } from './scripts/patch-lib.mjs'
|
|
9
|
+
import { dataDir, targetRoot } from './scripts/target-config.mjs'
|
|
10
|
+
import {
|
|
11
|
+
answerBlueprint,
|
|
12
|
+
continueDiff,
|
|
13
|
+
invokeStep,
|
|
14
|
+
readActiveSession,
|
|
15
|
+
requestReplan,
|
|
16
|
+
sendBlueprint,
|
|
17
|
+
sessionIntent,
|
|
18
|
+
stopSession,
|
|
19
|
+
updateBlueprint,
|
|
20
|
+
} from './scripts/session-store.mjs'
|
|
21
|
+
|
|
22
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const userContextFile = path.join(dataDir, 'user-context.json')
|
|
24
|
+
const codebaseFile = path.join(dataDir, 'codebase.json')
|
|
25
|
+
const scanScript = path.resolve(here, 'scripts/scan-target.mjs')
|
|
26
|
+
|
|
27
|
+
// The rescan runs with cwd set to the explorer, so hand it the already-resolved
|
|
28
|
+
// root and data dir instead of letting relative env values resolve differently.
|
|
29
|
+
const scanEnv = {
|
|
30
|
+
...process.env,
|
|
31
|
+
VISUAL_CODER_TARGET: targetRoot,
|
|
32
|
+
INBASE_DATA_DIR: dataDir,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readBody(req: IncomingMessage) {
|
|
36
|
+
return new Promise<string>((resolve, reject) => {
|
|
37
|
+
const chunks: Buffer[] = []
|
|
38
|
+
req.on('data', (chunk) => {
|
|
39
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
40
|
+
})
|
|
41
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
42
|
+
req.on('error', reject)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function knownFileIds() {
|
|
47
|
+
try {
|
|
48
|
+
const graph = JSON.parse(fs.readFileSync(codebaseFile, 'utf8')) as {
|
|
49
|
+
files?: Array<{ id?: string }>
|
|
50
|
+
}
|
|
51
|
+
return Array.isArray(graph.files)
|
|
52
|
+
? graph.files.map((file) => file.id).filter((id): id is string => Boolean(id))
|
|
53
|
+
: []
|
|
54
|
+
} catch {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sendJson(res: ServerResponse, status: number, body: unknown) {
|
|
60
|
+
res.statusCode = status
|
|
61
|
+
res.setHeader('Content-Type', 'application/json')
|
|
62
|
+
res.end(JSON.stringify(body))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function jsonFilePlugin(): Plugin {
|
|
66
|
+
return {
|
|
67
|
+
name: 'visual-coder-json-files',
|
|
68
|
+
configureServer(server) {
|
|
69
|
+
server.middlewares.use('/api/user-context', (req, res, next) => {
|
|
70
|
+
if (req.method === 'GET') {
|
|
71
|
+
sendJson(res, 200, readUserContext())
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (req.method === 'POST') {
|
|
75
|
+
void writeUserContext(req, res)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
next()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
server.middlewares.use('/api/codebase', (req, res, next) => {
|
|
82
|
+
if (req.method === 'GET') {
|
|
83
|
+
sendJson(res, 200, readCodebase())
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
next()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
server.middlewares.use('/api/agent-intent', (req, res, next) => {
|
|
90
|
+
if (req.method === 'GET') {
|
|
91
|
+
const url = new URL(req.url ?? '/', 'http://visual-coder.local')
|
|
92
|
+
const sessionId = url.searchParams.get('sessionId') ?? readActiveSession(dataDir)
|
|
93
|
+
const diffId = url.searchParams.get('diffId') ?? undefined
|
|
94
|
+
const intent = sessionId
|
|
95
|
+
? sessionIntent(dataDir, sessionId, knownFileIds(), diffId)
|
|
96
|
+
: null
|
|
97
|
+
sendJson(res, 200, intent ?? { ...emptyIntent })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (req.method === 'POST') {
|
|
102
|
+
void decideIntent(req, res)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
next()
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
113
|
+
try {
|
|
114
|
+
const body = JSON.parse(await readBody(req)) as {
|
|
115
|
+
action?: string
|
|
116
|
+
sessionId?: string
|
|
117
|
+
diffId?: string
|
|
118
|
+
instruction?: string
|
|
119
|
+
step?: number
|
|
120
|
+
userCreatedBlocks?: unknown[]
|
|
121
|
+
userCreatedIslands?: unknown[]
|
|
122
|
+
addedFunctions?: unknown[]
|
|
123
|
+
addedVariables?: unknown[]
|
|
124
|
+
addedImports?: unknown[]
|
|
125
|
+
}
|
|
126
|
+
const action = body.action
|
|
127
|
+
if (
|
|
128
|
+
action !== 'invoke' &&
|
|
129
|
+
action !== 'continue' &&
|
|
130
|
+
action !== 'instruct' &&
|
|
131
|
+
action !== 'stop' &&
|
|
132
|
+
action !== 'blueprint_yes' &&
|
|
133
|
+
action !== 'blueprint_no' &&
|
|
134
|
+
action !== 'blueprint_send' &&
|
|
135
|
+
action !== 'blueprint_update'
|
|
136
|
+
) {
|
|
137
|
+
sendJson(res, 400, { error: 'invalid workflow action' })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (!body.sessionId) {
|
|
141
|
+
sendJson(res, 400, { error: 'sessionId is required' })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
body.instruction !== undefined &&
|
|
146
|
+
(typeof body.instruction !== 'string' || body.instruction.length > 4000)
|
|
147
|
+
) {
|
|
148
|
+
sendJson(res, 400, { error: 'instruction must be a string up to 4000 characters' })
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action === 'invoke') {
|
|
153
|
+
if (!Number.isInteger(body.step)) {
|
|
154
|
+
sendJson(res, 400, { error: 'step is required for invoke' })
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
invokeStep(dataDir, body.sessionId, body.step as number, targetRoot)
|
|
158
|
+
const scan = spawnSync(process.execPath, [scanScript], {
|
|
159
|
+
cwd: here,
|
|
160
|
+
encoding: 'utf8',
|
|
161
|
+
env: scanEnv,
|
|
162
|
+
})
|
|
163
|
+
if (scan.status !== 0) {
|
|
164
|
+
console.error(scan.stderr || scan.stdout || 'scan failed after invoking step')
|
|
165
|
+
}
|
|
166
|
+
} else if (action === 'continue') {
|
|
167
|
+
if (!body.diffId) {
|
|
168
|
+
sendJson(res, 400, { error: 'diffId is required for continue' })
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
continueDiff(dataDir, targetRoot, body.sessionId, body.diffId)
|
|
172
|
+
const scan = spawnSync(process.execPath, [scanScript], {
|
|
173
|
+
cwd: here,
|
|
174
|
+
encoding: 'utf8',
|
|
175
|
+
env: scanEnv,
|
|
176
|
+
})
|
|
177
|
+
if (scan.status !== 0) {
|
|
178
|
+
console.error(scan.stderr || scan.stdout || 'scan failed after applying patch')
|
|
179
|
+
}
|
|
180
|
+
} else if (action === 'instruct') {
|
|
181
|
+
if (!body.diffId) {
|
|
182
|
+
sendJson(res, 400, { error: 'diffId is required for instruct' })
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
requestReplan(
|
|
186
|
+
dataDir,
|
|
187
|
+
body.sessionId,
|
|
188
|
+
body.diffId,
|
|
189
|
+
body.instruction ?? '',
|
|
190
|
+
)
|
|
191
|
+
} else if (action === 'blueprint_yes') {
|
|
192
|
+
answerBlueprint(dataDir, body.sessionId, true)
|
|
193
|
+
} else if (action === 'blueprint_no') {
|
|
194
|
+
answerBlueprint(dataDir, body.sessionId, false)
|
|
195
|
+
} else if (action === 'blueprint_update') {
|
|
196
|
+
updateBlueprint(dataDir, body.sessionId, {
|
|
197
|
+
userCreatedBlocks: body.userCreatedBlocks,
|
|
198
|
+
userCreatedIslands: body.userCreatedIslands,
|
|
199
|
+
addedFunctions: body.addedFunctions,
|
|
200
|
+
addedVariables: body.addedVariables,
|
|
201
|
+
addedImports: body.addedImports,
|
|
202
|
+
})
|
|
203
|
+
} else if (action === 'blueprint_send') {
|
|
204
|
+
sendBlueprint(dataDir, body.sessionId, {
|
|
205
|
+
userCreatedBlocks: body.userCreatedBlocks,
|
|
206
|
+
userCreatedIslands: body.userCreatedIslands,
|
|
207
|
+
addedFunctions: body.addedFunctions,
|
|
208
|
+
addedVariables: body.addedVariables,
|
|
209
|
+
addedImports: body.addedImports,
|
|
210
|
+
})
|
|
211
|
+
} else {
|
|
212
|
+
stopSession(dataDir, body.sessionId, body.diffId)
|
|
213
|
+
}
|
|
214
|
+
const next = sessionIntent(dataDir, body.sessionId, knownFileIds())
|
|
215
|
+
sendJson(res, 200, next ?? { ...emptyIntent })
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const message = error instanceof Error ? error.message : 'invalid request'
|
|
218
|
+
sendJson(res, 400, { error: message })
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readCodebase() {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(fs.readFileSync(codebaseFile, 'utf8')) as {
|
|
225
|
+
root?: string
|
|
226
|
+
targetName?: string
|
|
227
|
+
files?: unknown[]
|
|
228
|
+
folders?: unknown[]
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
return {
|
|
232
|
+
root: '.',
|
|
233
|
+
targetName: path.basename(targetRoot),
|
|
234
|
+
files: [],
|
|
235
|
+
folders: [
|
|
236
|
+
{
|
|
237
|
+
path: '.',
|
|
238
|
+
name: path.basename(targetRoot),
|
|
239
|
+
parent: null,
|
|
240
|
+
files: [],
|
|
241
|
+
children: [],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readUserContext() {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(fs.readFileSync(userContextFile, 'utf8')) as Record<
|
|
251
|
+
string,
|
|
252
|
+
unknown
|
|
253
|
+
>
|
|
254
|
+
return {
|
|
255
|
+
...parsed,
|
|
256
|
+
followLook: Boolean(parsed.followLook),
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
return { followLook: false }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
|
|
264
|
+
try {
|
|
265
|
+
const incoming = JSON.parse(await readBody(req)) as Record<string, unknown>
|
|
266
|
+
const existing = readUserContext()
|
|
267
|
+
const next = {
|
|
268
|
+
...existing,
|
|
269
|
+
...incoming,
|
|
270
|
+
followLook:
|
|
271
|
+
typeof incoming.followLook === 'boolean'
|
|
272
|
+
? incoming.followLook
|
|
273
|
+
: Boolean(existing.followLook),
|
|
274
|
+
}
|
|
275
|
+
delete next.userCreatedBlocks
|
|
276
|
+
delete next.userCreatedIslands
|
|
277
|
+
fs.mkdirSync(path.dirname(userContextFile), { recursive: true })
|
|
278
|
+
fs.writeFileSync(userContextFile, `${JSON.stringify(next, null, 2)}\n`)
|
|
279
|
+
res.statusCode = 204
|
|
280
|
+
res.end()
|
|
281
|
+
} catch {
|
|
282
|
+
res.statusCode = 400
|
|
283
|
+
res.end('invalid json')
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default defineConfig({
|
|
288
|
+
plugins: [react(), jsonFilePlugin()],
|
|
289
|
+
server: {
|
|
290
|
+
port: 5173,
|
|
291
|
+
fs: {
|
|
292
|
+
allow: [here, dataDir],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
})
|
package/bin/inbase.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
import {
|
|
7
|
+
applyHostEnv,
|
|
8
|
+
copyDir,
|
|
9
|
+
ensureDataDir,
|
|
10
|
+
ensureGitignoreEntry,
|
|
11
|
+
explorerRoot,
|
|
12
|
+
skillTemplateDir,
|
|
13
|
+
takeFlagValue,
|
|
14
|
+
} from './project.mjs'
|
|
15
|
+
import {
|
|
16
|
+
proposePatch,
|
|
17
|
+
reportPlan,
|
|
18
|
+
startSession,
|
|
19
|
+
waitForApproval,
|
|
20
|
+
waitForBlueprint,
|
|
21
|
+
} from './session.mjs'
|
|
22
|
+
|
|
23
|
+
const HELP = `inbase — a first-person 3D map of a codebase
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
inbase init Install the Cursor skill in this repo
|
|
27
|
+
inbase run Scan this repo and start the local map
|
|
28
|
+
inbase help Show this help
|
|
29
|
+
|
|
30
|
+
Agent commands (used by the Cursor skill):
|
|
31
|
+
inbase start-session --session <id> [--feature "name"]
|
|
32
|
+
inbase wait-for-blueprint --session <id>
|
|
33
|
+
inbase report-plan --session <id> --feature "name" --steps "one" [--steps "two"]
|
|
34
|
+
inbase wait-for-approval --session <id>
|
|
35
|
+
inbase propose-patch --session <id> <file.patch|->
|
|
36
|
+
inbase propose-patch --session <id> --clear
|
|
37
|
+
|
|
38
|
+
Options for run:
|
|
39
|
+
--target <dir> Project to map (default: current directory)
|
|
40
|
+
--port <number> Dev server port (default: 5173)
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(HELP.trim())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function initProject(projectRoot = process.cwd()) {
|
|
48
|
+
if (!fs.existsSync(skillTemplateDir)) {
|
|
49
|
+
throw new Error(`Inbase skill template missing at ${skillTemplateDir}`)
|
|
50
|
+
}
|
|
51
|
+
const skillDir = path.join(projectRoot, '.cursor/skills/inbase')
|
|
52
|
+
copyDir(skillTemplateDir, skillDir)
|
|
53
|
+
const { dataDir } = applyHostEnv({
|
|
54
|
+
cwd: projectRoot,
|
|
55
|
+
target: projectRoot,
|
|
56
|
+
dataDir: path.join(projectRoot, '.inbase'),
|
|
57
|
+
})
|
|
58
|
+
ensureDataDir(dataDir)
|
|
59
|
+
const gitignoreAdded = ensureGitignoreEntry(projectRoot)
|
|
60
|
+
return { skillDir, dataDir, gitignoreAdded }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function explorerHref(relative) {
|
|
64
|
+
return pathToFileURL(path.join(explorerRoot, relative)).href
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function runServer(args) {
|
|
68
|
+
const target = takeFlagValue(args, '--target')
|
|
69
|
+
const portValue = takeFlagValue(args, '--port')
|
|
70
|
+
const port = portValue ? Number(portValue) : 5173
|
|
71
|
+
if (portValue && !Number.isInteger(port)) {
|
|
72
|
+
console.error('inbase run --port must be an integer')
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { targetRoot, dataDir } = applyHostEnv({ target })
|
|
77
|
+
if (!fs.existsSync(targetRoot)) {
|
|
78
|
+
console.error(`Target not found at ${targetRoot}`)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
ensureDataDir(dataDir)
|
|
82
|
+
|
|
83
|
+
const { scanTarget } = await import(explorerHref('scripts/scan-target.mjs'))
|
|
84
|
+
const { targetName } = await import(explorerHref('scripts/target-config.mjs'))
|
|
85
|
+
scanTarget({
|
|
86
|
+
root: targetRoot,
|
|
87
|
+
name: targetName,
|
|
88
|
+
dest: path.join(dataDir, 'codebase.json'),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const { createServer } = await import('vite')
|
|
92
|
+
const server = await createServer({
|
|
93
|
+
configFile: path.join(explorerRoot, 'vite.config.ts'),
|
|
94
|
+
root: explorerRoot,
|
|
95
|
+
server: {
|
|
96
|
+
port,
|
|
97
|
+
host: '127.0.0.1',
|
|
98
|
+
fs: {
|
|
99
|
+
allow: [explorerRoot, targetRoot, dataDir],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
await server.listen()
|
|
104
|
+
const local = server.resolvedUrls?.local?.[0] ?? `http://localhost:${port}/`
|
|
105
|
+
console.log(`Inbase is mapping ${targetRoot}`)
|
|
106
|
+
console.log(`Open ${local}`)
|
|
107
|
+
console.log('Leave this running. In Cursor, the inbase skill talks to this server.')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
111
|
+
const [command, ...args] = argv
|
|
112
|
+
if (
|
|
113
|
+
!command ||
|
|
114
|
+
command === 'help' ||
|
|
115
|
+
command === '-h' ||
|
|
116
|
+
command === '--help'
|
|
117
|
+
) {
|
|
118
|
+
printHelp()
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (command === 'init') {
|
|
123
|
+
const result = initProject()
|
|
124
|
+
console.log(`Installed Cursor skill at ${result.skillDir}`)
|
|
125
|
+
if (result.gitignoreAdded) console.log('Added .inbase/ to .gitignore')
|
|
126
|
+
console.log('Next: run `inbase run`, then ask Cursor to change source files.')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (command === 'run') {
|
|
131
|
+
await runServer(args)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
applyHostEnv()
|
|
136
|
+
ensureDataDir(process.env.INBASE_DATA_DIR)
|
|
137
|
+
|
|
138
|
+
if (command === 'start-session') {
|
|
139
|
+
await startSession(args)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (command === 'wait-for-blueprint') {
|
|
143
|
+
await waitForBlueprint(args)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (command === 'report-plan') {
|
|
147
|
+
await reportPlan(args)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (command === 'wait-for-approval') {
|
|
151
|
+
await waitForApproval(args)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (command === 'propose-patch') {
|
|
155
|
+
await proposePatch(args)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.error(`Unknown command: ${command}\n`)
|
|
160
|
+
printHelp()
|
|
161
|
+
process.exitCode = 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : ''
|
|
165
|
+
if (invoked === fileURLToPath(import.meta.url)) {
|
|
166
|
+
main().catch((error) => {
|
|
167
|
+
console.error(error instanceof Error ? error.message : error)
|
|
168
|
+
process.exit(1)
|
|
169
|
+
})
|
|
170
|
+
}
|
package/bin/project.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
export const packageRoot = path.resolve(here, '..')
|
|
7
|
+
export const explorerRoot = path.join(packageRoot, 'apps/explorer')
|
|
8
|
+
export const skillTemplateDir = path.join(packageRoot, 'skill/inbase')
|
|
9
|
+
|
|
10
|
+
export function resolveOptionalPath(value, fallback) {
|
|
11
|
+
const raw = value?.trim()
|
|
12
|
+
if (!raw) return fallback
|
|
13
|
+
return path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(process.cwd(), raw)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function applyHostEnv({
|
|
17
|
+
cwd = process.cwd(),
|
|
18
|
+
target = process.env.VISUAL_CODER_TARGET,
|
|
19
|
+
dataDir = process.env.INBASE_DATA_DIR,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const targetRoot = resolveOptionalPath(target, cwd)
|
|
22
|
+
const resolvedDataDir = resolveOptionalPath(
|
|
23
|
+
dataDir,
|
|
24
|
+
path.join(targetRoot, '.inbase'),
|
|
25
|
+
)
|
|
26
|
+
process.env.VISUAL_CODER_TARGET = targetRoot
|
|
27
|
+
process.env.INBASE_DATA_DIR = resolvedDataDir
|
|
28
|
+
return { cwd, targetRoot, dataDir: resolvedDataDir }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ensureDataDir(dataDir) {
|
|
32
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
33
|
+
const userContextFile = path.join(dataDir, 'user-context.json')
|
|
34
|
+
if (!fs.existsSync(userContextFile)) {
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
userContextFile,
|
|
37
|
+
`${JSON.stringify({ followLook: false }, null, 2)}\n`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return dataDir
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function takeFlagValue(args, flag) {
|
|
44
|
+
const index = args.indexOf(flag)
|
|
45
|
+
return index >= 0 ? args[index + 1] : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function takeFlagValues(args, flag) {
|
|
49
|
+
const values = []
|
|
50
|
+
const rest = []
|
|
51
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
52
|
+
if (args[index] === flag && args[index + 1]) {
|
|
53
|
+
values.push(args[index + 1])
|
|
54
|
+
index += 1
|
|
55
|
+
} else {
|
|
56
|
+
rest.push(args[index])
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { values, rest }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function withoutFlag(args, flag) {
|
|
63
|
+
return args.filter((arg) => arg !== flag)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function copyDir(from, to) {
|
|
67
|
+
fs.mkdirSync(to, { recursive: true })
|
|
68
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
69
|
+
const source = path.join(from, entry.name)
|
|
70
|
+
const dest = path.join(to, entry.name)
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
copyDir(source, dest)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
fs.copyFileSync(source, dest)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function ensureGitignoreEntry(projectRoot, entry = '.inbase/') {
|
|
80
|
+
const gitignore = path.join(projectRoot, '.gitignore')
|
|
81
|
+
const line = entry.endsWith('\n') ? entry : `${entry}\n`
|
|
82
|
+
if (!fs.existsSync(gitignore)) {
|
|
83
|
+
fs.writeFileSync(gitignore, line)
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
const current = fs.readFileSync(gitignore, 'utf8')
|
|
87
|
+
const hasEntry = current
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.some((row) => row.trim() === entry || row.trim() === entry.replace(/\/$/, ''))
|
|
90
|
+
if (hasEntry) return false
|
|
91
|
+
const prefix = current.endsWith('\n') || current === '' ? '' : '\n'
|
|
92
|
+
fs.appendFileSync(gitignore, `${prefix}${line}`)
|
|
93
|
+
return true
|
|
94
|
+
}
|