Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions apps/Next_test_app/.holo-js/framework/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { existsSync, readFileSync, readlinkSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execFileSync, spawn } from 'node:child_process'

const mode = process.argv[2]
const manifestPath = fileURLToPath(new URL('./project.json', import.meta.url))
const projectRoot = resolve(dirname(manifestPath), '../..')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const framework = String(manifest.framework ?? '')
const commandName = "next"
const commandArgs = mode === 'dev'
? ['dev']
: mode === 'build'
? framework === 'sveltekit' ? ['build', '--logLevel', 'error'] : ['build']
: undefined

if (!commandArgs) {
console.error(`[holo] Unknown framework runner mode: ${String(mode)}`)
process.exit(1)
}

const binaryPath = resolve(
projectRoot,
'node_modules',
'.bin',
process.platform === 'win32' ? `${commandName}.cmd` : commandName,
)

const suppressedOutput = framework === 'sveltekit'
? new Set([
'"try_get_request_store" is imported from external module "@sveltejs/kit/internal/server" but never used in ".svelte-kit/adapter-node/index.js".',
])
: new Set()

function pipeOutput(stream, target, onLine) {
if (!stream) {
return
}

let buffered = ''
stream.on('data', (chunk) => {
buffered += chunk.toString()
const lines = buffered.split(/\r?\n/)
buffered = lines.pop() ?? ''
for (const line of lines) {
onLine?.(line)
if (!suppressedOutput.has(line)) {
target.write(`${line}\n`)
}
}
})

stream.on('end', () => {
if (buffered.length > 0) {
onLine?.(buffered)
}
if (buffered.length > 0 && !suppressedOutput.has(buffered)) {
target.write(buffered)
}
})
}

function extractNextConflictInfo(lines) {
if (framework !== 'next' || mode !== 'dev') {
return undefined
}

if (!lines.some(line => line.includes('Another next dev server is already running.'))) {
return undefined
}

let pid
let dir

for (const line of lines) {
const match = line.match(/^- PID:\s+(\d+)\s*$/)
if (match) {
pid = Number.parseInt(match[1], 10)
continue
}

const dirMatch = line.match(/^- Dir:\s+(.+?)\s*$/)
if (dirMatch) {
dir = dirMatch[1]
}
}

return typeof pid === 'number' ? { pid, dir } : undefined
}

async function waitForProcessExit(pid, timeoutMs = 5000) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
try {
process.kill(pid, 0)
} catch (error) {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {
return true
}
throw error
}

await new Promise(resolve => setTimeout(resolve, 100))
}

return false
}

function inspectProcess(pid) {
try {
if (process.platform === 'linux' && existsSync(`/proc/${pid}`)) {
return {
cwd: readlinkSync(`/proc/${pid}/cwd`),
args: readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\u0000', ' ').trim(),
}
}
} catch {}

try {
return {
args: execFileSync('ps', ['-p', String(pid), '-o', 'args='], {
encoding: 'utf8',
}).trim(),
}
} catch {
return undefined
}
}

function isOwnedNextDevServer(pid, reportedDir) {
const expectedDir = typeof reportedDir === 'string' ? resolve(reportedDir) : undefined
if (expectedDir && expectedDir !== projectRoot) {
return false
}

const details = inspectProcess(pid)
if (!details) {
return expectedDir === projectRoot
}

const argsMatch = details.args.includes('next') && details.args.includes('dev')
const cwdMatches = typeof details.cwd === 'string' && resolve(details.cwd) === projectRoot
const argsReferenceProject = details.args.includes(projectRoot)

return argsMatch && (cwdMatches || argsReferenceProject || expectedDir === projectRoot)
}

async function stopStaleNextDevServer(pid, reportedDir) {
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) {
return false
}

if (!isOwnedNextDevServer(pid, reportedDir)) {
return false
}

try {
process.kill(pid, 'SIGTERM')
} catch (error) {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {
return true
}
return false
}

return waitForProcessExit(pid)
Comment on lines +149 to +167
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t auto-terminate a live Next dev server during conflict recovery

At Line 159, this kills the reported PID whenever ownership checks pass. That can terminate a legitimately running dev server (same project, different terminal), not just a stale lock owner.

Suggested safer recovery behavior
 async function stopStaleNextDevServer(pid, reportedDir) {
   if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) {
     return false
   }

   if (!isOwnedNextDevServer(pid, reportedDir)) {
     return false
   }

   try {
-    process.kill(pid, 'SIGTERM')
+    // Treat as stale only when the reported PID no longer exists.
+    process.kill(pid, 0)
   } catch (error) {
     if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {
       return true
     }
     return false
   }

-  return waitForProcessExit(pid)
+  // Live process found: avoid killing an active dev server automatically.
+  return false
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/Next_test_app/.holo-js/framework/run.mjs` around lines 149 - 167, The
current stopStaleNextDevServer function unconditionally sends SIGTERM when
isOwnedNextDevServer returns true, which can kill a legitimately running dev
server; change the logic so it does not auto-terminate live servers: update
stopStaleNextDevServer to accept an optional force flag (or check lock age) and
only call process.kill(pid, 'SIGTERM') when force===true or when the
lock/ownership metadata indicates the owner is stale (e.g., lock mtime older
than a configurable threshold); otherwise return false and let the caller
present a prompt or take explicit action. Reference: stopStaleNextDevServer and
isOwnedNextDevServer — add the force parameter and/or stale-age check and gate
the process.kill call behind that condition.

}

if (!existsSync(binaryPath)) {
console.error(`[holo] Missing framework binary "${commandName}" for "${framework}". Run your package manager install first.`)
process.exit(1)
}

let child = null
let forwardedSignal = null

function detachSignalForwarders() {
process.removeListener('SIGINT', onSigint)
process.removeListener('SIGTERM', onSigterm)
}

function forwardSignal(signal) {
if (forwardedSignal || !child || child.exitCode !== null) {
return
}

forwardedSignal = signal
child.kill(signal)
}

function onSigint() {
detachSignalForwarders()
forwardSignal('SIGINT')
}

function onSigterm() {
detachSignalForwarders()
forwardSignal('SIGTERM')
}

process.on('SIGINT', onSigint)
process.on('SIGTERM', onSigterm)

async function run() {
let restartedAfterConflict = false
const maxStderrLines = 200

while (true) {
const stderrLines = []
child = spawn(binaryPath, commandArgs, {
cwd: projectRoot,
env: process.env,
stdio: ['inherit', 'pipe', 'pipe'],
})
forwardedSignal = null

pipeOutput(child.stdout, process.stdout)
pipeOutput(child.stderr, process.stderr, line => {
if (stderrLines.length >= maxStderrLines) {
stderrLines.shift()
}
stderrLines.push(line)
})

const result = await new Promise((resolve, reject) => {
child.on('error', reject)
child.on('close', (code, signal) => resolve({ code, signal }))
})

if (result.code === 0) {
process.exit(0)
}

const conflictInfo = extractNextConflictInfo(stderrLines)
if (!restartedAfterConflict && conflictInfo) {
const stopped = await stopStaleNextDevServer(conflictInfo.pid, conflictInfo.dir)
if (stopped) {
restartedAfterConflict = true
console.error(`[holo] Stopped stale Next dev server ${conflictInfo.pid}. Restarting dev server.`)
continue
}
}

if (result.signal) {
detachSignalForwarders()
process.kill(process.pid, result.signal)
} else {
process.exit(result.code ?? 1)
}
}
}

run().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
})
7 changes: 7 additions & 0 deletions apps/blog-next/server/models/Category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('categories', {
fillable: ['name', 'slug', 'description'],
})
20 changes: 20 additions & 0 deletions apps/blog-next/server/models/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import '../db/schema.generated'

import { belongsTo, belongsToMany, defineModel } from '@holo-js/db'

import Category from './Category'
import Tag from './Tag'

const relations = {
category: belongsTo(() => Category, { foreignKey: 'category_id' }),
tags: belongsToMany(() => Tag, {
pivotTable: 'post_tags',
foreignPivotKey: 'post_id',
relatedPivotKey: 'tag_id',
}),
}

export default defineModel('posts', {
fillable: ['title', 'slug', 'excerpt', 'body', 'status', 'published_at', 'user_id', 'category_id'],
relations,
})
7 changes: 7 additions & 0 deletions apps/blog-next/server/models/Tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('tags', {
fillable: ['name', 'slug'],
})
8 changes: 8 additions & 0 deletions apps/blog-next/server/models/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('users', {
fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],
hidden: ['password'],
})
7 changes: 7 additions & 0 deletions apps/blog-nuxt/server/models/Category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('categories', {
fillable: ['name', 'slug', 'description'],
})
20 changes: 20 additions & 0 deletions apps/blog-nuxt/server/models/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import '../db/schema.generated'

import { belongsTo, belongsToMany, defineModel } from '@holo-js/db'

import Category from './Category'
import Tag from './Tag'

const relations = {
category: belongsTo(() => Category, { foreignKey: 'category_id' }),
tags: belongsToMany(() => Tag, {
pivotTable: 'post_tags',
foreignPivotKey: 'post_id',
relatedPivotKey: 'tag_id',
}),
}

export default defineModel('posts', {
fillable: ['title', 'slug', 'excerpt', 'body', 'status', 'published_at', 'user_id', 'category_id'],
relations,
})
7 changes: 7 additions & 0 deletions apps/blog-nuxt/server/models/Tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('tags', {
fillable: ['name', 'slug'],
})
8 changes: 8 additions & 0 deletions apps/blog-nuxt/server/models/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('users', {
fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not keep email_verified_at mass-assignable.

Line 6 permits direct write access to a trust-sensitive field (email_verified_at), which weakens verification guarantees in typical auth flows.

Suggested fix
 export default defineModel('users', {
-  fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],
+  fillable: ['name', 'email', 'password', 'avatar'],
   hidden: ['password'],
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],
export default defineModel('users', {
fillable: ['name', 'email', 'password', 'avatar'],
hidden: ['password'],
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blog-nuxt/server/models/User.ts` at line 6, The User model's fillable
array currently includes the trust-sensitive field email_verified_at; remove
'email_verified_at' from the fillable array in the User model (the fillable
property in models/User.ts or the User class) so it cannot be mass-assigned, and
instead ensure verification logic updates this field explicitly (e.g., via a
dedicated method like verifyEmail or within the auth controller) so only trusted
code paths can set email_verified_at.

hidden: ['password'],
})
7 changes: 7 additions & 0 deletions apps/blog-sveltekit/server/models/Category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('categories', {
fillable: ['name', 'slug', 'description'],
})
20 changes: 20 additions & 0 deletions apps/blog-sveltekit/server/models/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import '../db/schema.generated'

import { belongsTo, belongsToMany, defineModel } from '@holo-js/db'

import Category from './Category'
import Tag from './Tag'

const relations = {
category: belongsTo(() => Category, { foreignKey: 'category_id' }),
tags: belongsToMany(() => Tag, {
pivotTable: 'post_tags',
foreignPivotKey: 'post_id',
relatedPivotKey: 'tag_id',
}),
}

export default defineModel('posts', {
fillable: ['title', 'slug', 'excerpt', 'body', 'status', 'published_at', 'user_id', 'category_id'],
relations,
})
7 changes: 7 additions & 0 deletions apps/blog-sveltekit/server/models/Tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('tags', {
fillable: ['name', 'slug'],
})
8 changes: 8 additions & 0 deletions apps/blog-sveltekit/server/models/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '../db/schema.generated'

import { defineModel } from '@holo-js/db'

export default defineModel('users', {
fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid mass-assigning email_verified_at in the base user model.

Line 6 allows callers to set verification state through normal create/update payloads, which can bypass email verification flows.

Suggested fix
 export default defineModel('users', {
-  fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],
+  fillable: ['name', 'email', 'password', 'avatar'],
   hidden: ['password'],
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fillable: ['name', 'email', 'password', 'avatar', 'email_verified_at'],
export default defineModel('users', {
fillable: ['name', 'email', 'password', 'avatar'],
hidden: ['password'],
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blog-sveltekit/server/models/User.ts` at line 6, The fillable array in
User.ts currently includes 'email_verified_at', allowing callers to mass-assign
verification state; remove 'email_verified_at' from the fillable list and ensure
verification is set only via a dedicated method (e.g., User.verifyEmail() or a
controller/service that sets email_verified_at) or by marking it as
guarded/hidden so create/update payloads cannot set it directly; update any
tests or factories that relied on mass-assignment to use the dedicated setter
instead.

hidden: ['password'],
})
Loading