11import { Hono , type Context } from 'hono'
2+ import { bodyLimit } from 'hono/body-limit'
23import { z } from 'zod'
34import { execSync , spawnSync } from 'child_process'
45import { randomUUID } from 'crypto'
@@ -39,7 +40,8 @@ import { encryptSecret } from '../utils/crypto'
3940import { compareVersions , isValidVersion } from '../utils/version-utils'
4041import { getImportedSessionDirectories , getOpenCodeImportStatus , OpenCodeImportProtectionError , syncOpenCodeImport } from '../services/opencode-import'
4142import { relinkReposFromSessionDirectories } from '../services/repo'
42- import { ENV } from '@opencode-manager/shared/config/env'
43+ import { ENV , FILE_LIMITS } from '@opencode-manager/shared/config/env'
44+ import { MAX_OPENCODE_CONFIG_DIRECTORY_FILES , OPENCODE_CONFIG_UPLOAD_ERRORS } from '@opencode-manager/shared/utils'
4345import {
4446 listManagedSkills ,
4547 getSkill ,
@@ -50,14 +52,15 @@ import {
5052 installSkillFromUploadedFiles ,
5153} from '../services/skills'
5254import { replaceOpenCodeConfigDirectory } from '../services/opencode-config-directory'
55+ import { ensureDefaultAgentsMdExists } from '../services/agents-md'
5356import {
5457 installOpenCodeDirectoryFiles ,
5558 listOpenCodeDirectoryFiles ,
5659 getOpenCodeDirectoryFile ,
5760 updateOpenCodeDirectoryFile ,
5861 deleteOpenCodeDirectoryFile ,
5962} from '../services/opencode-directory-files'
60- import { parseUploadManifest , readUploadedManifestFiles , UploadValidationError } from './upload-utils'
63+ import { parseUploadManifest , parseUploadPreamble , readUploadedManifestFiles , resolveUploadedManifestFiles , UploadValidationError , UPLOAD_PATH_ERROR_STATUS } from './upload-utils'
6164import { getRepoById } from '../db/queries'
6265import { githubFetch } from '../utils/github'
6366
@@ -147,25 +150,16 @@ async function reloadAfterSkillInstall(
147150
148151const OPENCODE_DIRECTORY_UPLOAD_ERROR_STATUS : ReadonlyArray < readonly [ string , 400 ] > = [
149152 [ 'No markdown' , 400 ] ,
150- [ 'Path must be relative' , 400 ] ,
151- [ 'Path must not contain' , 400 ] ,
152153 [ 'Path must reference' , 400 ] ,
153- [ 'escapes' , 400 ] ,
154- [ 'Missing upload file' , 400 ] ,
155- [ 'not a valid file' , 400 ] ,
154+ ...UPLOAD_PATH_ERROR_STATUS ,
156155]
157156
158157const OPENCODE_CONFIG_DIRECTORY_REPLACE_ERROR_STATUS : ReadonlyArray < readonly [ string , 400 | 413 ] > = [
159158 [ 'No files were provided' , 400 ] ,
160159 [ 'must contain opencode.json' , 400 ] ,
161160 [ 'too many files' , 400 ] ,
162- [ 'contains too many files' , 400 ] ,
163161 [ 'exceed maximum upload size' , 413 ] ,
164- [ 'Path must be relative' , 400 ] ,
165- [ 'Path must not contain' , 400 ] ,
166- [ 'escapes' , 400 ] ,
167- [ 'Missing upload file' , 400 ] ,
168- [ 'not a valid file' , 400 ] ,
162+ ...UPLOAD_PATH_ERROR_STATUS ,
169163]
170164
171165function matchErrorStatus < T extends number > (
@@ -204,14 +198,10 @@ const SKILL_INSTALL_ERROR_STATUS: ReadonlyArray<readonly [string, 400 | 404 | 40
204198 [ 'Invalid skill name' , 400 ] ,
205199 [ 'Only one skill' , 400 ] ,
206200 [ 'Skill source must contain' , 400 ] ,
207- [ 'Path must be relative' , 400 ] ,
208- [ 'Path must not contain' , 400 ] ,
209- [ 'escapes' , 400 ] ,
210201 [ 'no downloadable files' , 400 ] ,
211202 [ 'repoId is required' , 400 ] ,
212- [ 'Missing upload file' , 400 ] ,
213203 [ 'Invalid repoId' , 400 ] ,
214- [ 'not a valid file' , 400 ] ,
204+ ... UPLOAD_PATH_ERROR_STATUS ,
215205]
216206
217207function didConfigFieldChange (
@@ -1280,15 +1270,9 @@ export function createSettingsRoutes(db: Database, gitAuthService: GitAuthServic
12801270
12811271 app . post ( '/opencode-directory-files/install' , async ( c ) => {
12821272 try {
1283- const contentType = c . req . header ( 'content-type' ) || ''
1284- if ( ! contentType . includes ( 'multipart/form-data' ) ) {
1285- return c . json ( { error : 'Unsupported content type. Use multipart/form-data' } , 400 )
1286- }
1287-
1288- const formData = await c . req . parseBody ( { all : true } )
1273+ const { formData, manifest } = await parseUploadPreamble ( c )
12891274 const kind = z . enum ( [ 'agents' , 'commands' ] ) . parse ( formData [ 'kind' ] )
12901275
1291- const manifest = parseUploadManifest ( formData [ 'fileManifest' ] )
12921276 const markdownManifest = getMarkdownUploadManifest ( manifest )
12931277 if ( markdownManifest . length === 0 ) {
12941278 return c . json ( { error : `No markdown ${ kind } files found` } , 400 )
@@ -1322,40 +1306,50 @@ export function createSettingsRoutes(db: Database, gitAuthService: GitAuthServic
13221306 }
13231307 } )
13241308
1309+ app . use ( '/opencode-config-directory/replace' , bodyLimit ( {
1310+ maxSize : FILE_LIMITS . MAX_UPLOAD_SIZE_BYTES ,
1311+ onError : ( c ) => c . json ( { error : OPENCODE_CONFIG_UPLOAD_ERRORS . EXCEEDS_MAX_UPLOAD_SIZE } , 413 ) ,
1312+ } ) )
1313+
13251314 app . post ( '/opencode-config-directory/replace' , async ( c ) => {
13261315 try {
1327- const contentType = c . req . header ( 'content-type' ) || ''
1328- if ( ! contentType . includes ( 'multipart/form-data' ) ) {
1329- return c . json ( { error : 'Unsupported content type. Use multipart/form-data' } , 400 )
1330- }
1331-
1332- const formData = await c . req . parseBody ( { all : true } )
13331316 const userId = c . req . query ( 'userId' ) || 'default'
13341317
1335- let manifest : ReturnType < typeof parseUploadManifest >
1318+ let preamble : Awaited < ReturnType < typeof parseUploadPreamble > >
13361319 try {
1337- manifest = parseUploadManifest ( formData [ 'fileManifest' ] )
1320+ preamble = await parseUploadPreamble ( c )
13381321 } catch ( error ) {
13391322 if ( error instanceof z . ZodError ) {
13401323 return c . json ( { error : 'Invalid upload manifest' , details : error . issues } , 400 )
13411324 }
13421325 throw error
13431326 }
1327+ const { formData, manifest } = preamble
1328+
13441329 if ( manifest . length === 0 ) {
13451330 return c . json ( { error : 'fileManifest must contain at least one entry' } , 400 )
13461331 }
1332+ if ( manifest . length > MAX_OPENCODE_CONFIG_DIRECTORY_FILES ) {
1333+ return c . json ( { error : OPENCODE_CONFIG_UPLOAD_ERRORS . TOO_MANY_FILES } , 400 )
1334+ }
13471335
1348- const files = await readUploadedManifestFiles ( formData , manifest )
1336+ const files = resolveUploadedManifestFiles ( formData , manifest )
13491337
13501338 settingsService . saveLastKnownGoodConfig ( userId )
13511339
13521340 const result = await replaceOpenCodeConfigDirectory ( db , files , userId )
13531341
1342+ try {
1343+ await ensureDefaultAgentsMdExists ( )
1344+ } catch ( error ) {
1345+ logger . warn ( 'Failed to ensure AGENTS.md after OpenCode config directory replace' , error )
1346+ }
1347+
13541348 opencodeServerManager . markRestartPending ( )
13551349 opencodeServerManager . clearStartupError ( )
13561350 await restartOpenCodeSafe ( openCodeSupervisor , 'OpenCode config directory replace' )
13571351
1358- return c . json ( { ... result , restartRequired : true } )
1352+ return c . json ( result )
13591353 } catch ( error ) {
13601354 logger . error ( 'Failed to replace OpenCode config directory:' , error )
13611355
@@ -1457,14 +1451,12 @@ export function createSettingsRoutes(db: Database, gitAuthService: GitAuthServic
14571451 }
14581452
14591453 if ( contentType . includes ( 'multipart/form-data' ) ) {
1460- const formData = await c . req . parseBody ( { all : true } )
1454+ const { formData, manifest } = await parseUploadPreamble ( c )
14611455
14621456 const scope = formData [ 'scope' ]
14631457 const repoIdValue = formData [ 'repoId' ]
14641458 const overwriteValue = formData [ 'overwrite' ]
14651459
1466- const manifest = parseUploadManifest ( formData [ 'fileManifest' ] )
1467-
14681460 const repoId = parseOptionalRepoId ( repoIdValue as string | undefined )
14691461 const overwrite = parseBooleanFormValue ( overwriteValue )
14701462
0 commit comments