Makes a new git worktree of a Laravel app ready to use with no hands: clones vendor, node_modules and built assets with copy-on-write, links it with Valet or…

Make a new git worktree of a Laravel app ready to use with no hands, and tidy it away when you are done.
worktree-ready is a Claude Code mod. When Claude enters a worktree of a Laravel app, it brings the worktree up to the same state as your main checkout, links it as its own .test site over HTTPS, and shows you the URL. When the worktree is exited, it unlinks the site and removes the folder if the work is finished.
cp -c, a copy-on-write clone, so the worktree must be on the same volume as the main checkout (worktrees under .claude/worktrees/ always are).composer, and your JS package manager, on the PATH.The mod only acts in a linked worktree that has an artisan file. Each step is skipped when it has nothing to do.
vendor, node_modules and public/build from the main checkout with copy-on-write. This takes seconds and shares disk blocks until a file changes. public/hot and bootstrap/cache are never cloned..env from the main checkout, if the worktree has none. The worktree shares main's database. A sqlite database is shared by pointing DB_DATABASE at main's file.composer install runs when composer.lock differs from main's, and the JS install runs when its lockfile differs. The package manager is picked from the lockfile (bun.lock, pnpm-lock.yaml, yarn.lock or package-lock.json).build script runs when the worktree's resources/, package.json or Vite, Tailwind, PostCSS or TypeScript config differ from main's working tree, or when main has no public/build.<project>-<worktree> (for example acme-blue-heron.test), secured, and checked against links. The word worktree is never part of the name. A name that already points at a different folder is left alone and reported. APP_URL in the worktree's .env is set to the new URL, and VITE_DEV_SERVER_KEY and VITE_DEV_SERVER_CERT to the site's certificate, so the Vite dev server runs on https at that host. Without them, laravel-vite-plugin looks for a certificate named after the folder, finds none, and serves plain http assets that an https page blocks.Claude does not get control back until this finishes, as with a SessionStart hook, so its first command in the worktree already has what it needs. Progress shows under the prompt.
A worktree that a session starts in (claude --worktree) is set up the same way.
Whether the exit keeps or removes the worktree, the site is unsecured and unlinked, and the mod checks that it is gone from links.
If the exit kept the worktree, the mod then removes the folder and its branch when there is nothing to lose: no uncommitted changes, and the branch has commits of its own that are merged into the default branch. A worktree you entered and left without committing is kept. Anything else is kept, and you are told why once. The same check runs for leftovers at the start of the next session, along with the links of worktrees whose folder no longer exists. Only worktrees under the project's .claude/worktrees/ that the mod set up are ever removed.
| Option | Default | What it does |
|---|---|---|
| Site tool | auto | valet, herd or auto. Auto uses whichever is installed and does nothing when both are, with a toast asking you to choose. |
links output is assumed to match Valet's table. It has not been tried.The pure helpers (link names, .env edits, the links table, lockfile and asset checks) are in hooks/lib.ts, and the enter and exit flows in hooks/register.ts. Both are covered by tests that fake the file system, git, composer, the package manager and Valet:
claude plugin validate plugins/worktree-ready
claude plugin test plugins/worktree-ready
MIT
hooks/register.ts 475 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { ASSET_INPUTS, CLONES, JS_LOCK_FILES, TOOLS, basename, certBase, chooseTool, jsPlan, linkName, parseLinks, parseWorktree, setEnv, shareSqlite, underWorktrees } from './lib'
4import type { Link, Tool, WorktreeInfo, WorktreeRecord } from './lib'
5
6const PLUGIN = 'worktree-ready'
7const PREFIX = 'wt:'
8const LONG_MS = 600_000
9const SITE_MS = 120_000
10const STALE_LOCK_MS = 1_800_000
11const NOT_LINKED = { url: null, linked: false }
12
13let configured = 'auto'
14
15type Outcome = { exitCode: number; stdout: string; stderr: string }
16
17async function run($: EngineInterface, argv: string[], cwd?: string, timeoutMs?: number): Promise<Outcome> {
18 try {
19 return await $.process.run(argv, { ...(cwd ? { cwd } : {}), ...(timeoutMs ? { timeoutMs } : {}) })
20 } catch (error) {
21 return { exitCode: 127, stdout: '', stderr: String(error) }
22 }
23}
24
25const passed = (outcome: Outcome): boolean => outcome.exitCode === 0
26
27const deniedOrFailed = (result: unknown): boolean => {
28 const shape = result as { deny?: unknown; isError?: unknown } | undefined
29
30 return shape?.deny !== undefined || shape?.isError === true
31}
32
33const outcomeOf = (result: unknown): { worktreePath?: string; action?: string } | undefined => {
34 const inner = (result as { result?: unknown } | undefined)?.result
35
36 return typeof inner === 'object' && inner !== null ? (inner as { worktreePath?: string; action?: string }) : undefined
37}
38
39function must(outcome: Outcome, what: string) {
40 if (!passed(outcome)) {
41 throw new Error(`${what}: ${outcome.stderr.trim().slice(0, 160) || `exit ${outcome.exitCode}`}`)
42 }
43}
44
45const say = ($: EngineInterface, text?: string) => $.ui.status(text)
46
47const toast = ($: EngineInterface, text: string, timeoutMs = 6000) => $.ui.toast(text, { timeoutMs })
48
49const has = ($: EngineInterface, root: string, rel: string) => $.fs.exists(`${root}/${rel}`)
50
51const recordKey = (path: string) => `${PREFIX}${path}`
52
53async function readRecord($: EngineInterface, path: string): Promise<WorktreeRecord | null> {
54 return ((await $.store.get(recordKey(path))) as WorktreeRecord | undefined) ?? null
55}
56
57async function saveRecord($: EngineInterface, record: WorktreeRecord) {
58 await $.store.set(recordKey(record.path), record)
59}
60
61async function worktreeAt($: EngineInterface, dir: string): Promise<WorktreeInfo | null> {
62 const rev = await run($, ['git', 'rev-parse', '--path-format=absolute', '--show-toplevel', '--git-dir', '--git-common-dir'], dir)
63
64 return passed(rev) ? parseWorktree(rev.stdout) : null
65}
66
67async function findTool($: EngineInterface, notify: boolean): Promise<Tool | null> {
68 const installed: Tool[] = []
69
70 for (const tool of TOOLS) {
71 if (passed(await run($, ['sh', '-c', `command -v ${tool}`]))) {
72 installed.push(tool)
73 }
74 }
75
76 const { tool, reason } = chooseTool(configured, installed)
77
78 if (notify && reason === 'both') {
79 toast($, 'Valet and Herd are both installed: set the "Site tool" option to choose one', 10_000)
80 }
81
82 if (notify && reason === 'missing') {
83 toast($, `${configured} is not installed, so the site is not linked`, 10_000)
84 }
85
86 return tool
87}
88
89async function listLinks($: EngineInterface, tool: Tool): Promise<Link[] | null> {
90 const listed = await run($, [tool, 'links'])
91
92 return passed(listed) ? parseLinks(listed.stdout) : null
93}
94
95async function copyEnv($: EngineInterface, { main, top }: WorktreeInfo) {
96 if ((await has($, top, '.env')) || !(await has($, main, '.env'))) {
97 return
98 }
99
100 await $.fs.write(`${top}/.env`, String(await $.fs.read(`${main}/.env`)))
101}
102
103async function editEnv($: EngineInterface, top: string, change: (text: string) => string) {
104 if (!(await has($, top, '.env'))) {
105 return
106 }
107
108 const text = String(await $.fs.read(`${top}/.env`))
109 const next = change(text)
110
111 if (next !== text) {
112 await $.fs.write(`${top}/.env`, next)
113 }
114}
115
116async function shareDatabase($: EngineInterface, { main, top }: WorktreeInfo) {
117 const database = `${main}/database/database.sqlite`
118
119 if (await $.fs.exists(database)) {
120 await editEnv($, top, text => shareSqlite(text, database))
121 }
122}
123
124const setAppUrl = ($: EngineInterface, top: string, url: string) => editEnv($, top, text => setEnv(text, 'APP_URL', url))
125
126async function useSiteCert($: EngineInterface, top: string, tool: Tool, url: string) {
127 const home = (await run($, ['printenv', 'HOME'])).stdout.trim()
128 const base = certBase(tool, home, new URL(url).hostname)
129
130 if (!home || !(await $.fs.exists(`${base}.key`)) || !(await $.fs.exists(`${base}.crt`))) {
131 return
132 }
133
134 await editEnv($, top, text => setEnv(setEnv(text, 'VITE_DEV_SERVER_KEY', `${base}.key`), 'VITE_DEV_SERVER_CERT', `${base}.crt`))
135}
136
137async function lockDiffers($: EngineInterface, { main, top }: WorktreeInfo, file: string): Promise<boolean> {
138 return !passed(await run($, ['cmp', '-s', `${main}/${file}`, `${top}/${file}`]))
139}
140
141async function hasBuildScript($: EngineInterface, top: string): Promise<boolean> {
142 if (!(await has($, top, 'package.json'))) {
143 return false
144 }
145
146 try {
147 const scripts = (JSON.parse(String(await $.fs.read(`${top}/package.json`))) as { scripts?: Record<string, unknown> }).scripts
148
149 return typeof scripts?.build === 'string'
150 } catch {
151 return false
152 }
153}
154
155async function needsBuild($: EngineInterface, { main, top }: WorktreeInfo): Promise<boolean> {
156 if (!(await has($, top, 'public/build'))) {
157 return true
158 }
159
160 const head = await run($, ['git', 'rev-parse', 'HEAD'], top)
161
162 if (!passed(head)) {
163 return true
164 }
165
166 // Main's working tree is what its public/build was built from, so that is what the worktree's commit is compared with.
167 return !passed(await run($, ['git', '-C', main, 'diff', '--quiet', head.stdout.trim(), '--', ...ASSET_INPUTS]))
168}
169
170async function link($: EngineInterface, tool: Tool, name: string, top: string): Promise<{ url: string | null; linked: boolean }> {
171 const clash = (await listLinks($, tool))?.find(site => site.name === name)
172
173 if (clash && clash.path !== top) {
174 toast($, `${name} already points at ${clash.path}, so it was not linked`, 10_000)
175
176 return NOT_LINKED
177 }
178
179 if (!clash && !passed(await run($, [tool, 'link', name], top, SITE_MS))) {
180 toast($, `${tool} link ${name} failed`, 10_000)
181
182 return NOT_LINKED
183 }
184
185 await run($, [tool, 'secure', name], top, SITE_MS)
186
187 const site = (await listLinks($, tool))?.find(entry => entry.name === name && entry.path === top)
188
189 if (!site?.secure) {
190 toast($, `${name} is linked but not secure`, 10_000)
191 }
192
193 return { url: site?.url ?? null, linked: true }
194}
195
196async function setUp($: EngineInterface, info: WorktreeInfo): Promise<string | null> {
197 const { top, main } = info
198 const earlier = await readRecord($, top)
199
200 say($, 'cloning dependencies')
201
202 for (const dir of CLONES) {
203 if ((await has($, main, dir)) && !(await has($, top, dir))) {
204 await run($, ['cp', '-c', '-R', `${main}/${dir}`, `${top}/${dir}`], undefined, LONG_MS)
205 }
206 }
207
208 await copyEnv($, info)
209 await shareDatabase($, info)
210
211 if ((await has($, top, 'composer.lock')) && ((await lockDiffers($, info, 'composer.lock')) || !(await has($, top, 'vendor')))) {
212 say($, 'composer install')
213 must(await run($, ['composer', 'install', '--no-interaction'], top, LONG_MS), 'composer install failed')
214 }
215
216 if ((await has($, main, 'public/storage')) && !(await has($, top, 'public/storage'))) {
217 await run($, ['php', 'artisan', 'storage:link'], top)
218 }
219
220 const locks: string[] = []
221
222 for (const file of JS_LOCK_FILES) {
223 if (await has($, top, file)) {
224 locks.push(file)
225 }
226 }
227
228 const plan = jsPlan(locks, await hasBuildScript($, top))
229
230 if (plan.install && plan.lock && ((await lockDiffers($, info, plan.lock)) || !(await has($, top, 'node_modules')))) {
231 say($, `${plan.install[0]} install`)
232 must(await run($, plan.install, top, LONG_MS), `${plan.install[0]} install failed`)
233 }
234
235 if (plan.build && (await needsBuild($, info))) {
236 say($, 'building assets')
237 must(await run($, plan.build, top, LONG_MS), 'asset build failed')
238 }
239
240 const project = basename(main)
241 const name = linkName(project, basename(top), top)
242 const tool = await findTool($, true)
243 const site = tool ? await link($, tool, name, top) : NOT_LINKED
244
245 if (site.url) {
246 await setAppUrl($, top, site.url)
247 }
248
249 if (site.url && tool) {
250 await useSiteCert($, top, tool, site.url)
251 }
252
253 const branch = (await run($, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], top)).stdout.trim()
254 const head = (await run($, ['git', 'rev-parse', 'HEAD'], top)).stdout.trim()
255
256 await saveRecord($, { path: top, main, project, branch, base: earlier?.base ?? head, tool, link: site.linked ? name : null, state: 'active', notified: false })
257 toast($, site.url ? `Worktree ready at ${site.url}` : 'Worktree ready', 15_000)
258
259 if (site.url) {
260 $.ui.log(`${basename(top)} is served at ${site.url}`)
261 }
262
263 return site.url
264}
265
266async function claimLock($: EngineInterface, lock: string): Promise<boolean> {
267 if (passed(await run($, ['mkdir', lock]))) {
268 return true
269 }
270
271 const made = Number((await run($, ['stat', '-f', '%m', lock])).stdout.trim())
272
273 if (!(made > 0) || (await $.clock.now()) - made * 1000 < STALE_LOCK_MS) {
274 return false
275 }
276
277 await run($, ['rmdir', lock])
278
279 return passed(await run($, ['mkdir', lock]))
280}
281
282async function provision($: EngineInterface, dir: string): Promise<string | null> {
283 const info = await worktreeAt($, dir)
284
285 if (!info || !(await has($, info.top, 'artisan'))) {
286 return null
287 }
288
289 const lock = `${info.gitDir}/${PLUGIN}.lock`
290
291 if (!(await claimLock($, lock))) {
292 toast($, `another setup is running at ${lock}`, 10_000)
293
294 return null
295 }
296
297 try {
298 return await setUp($, info)
299 } catch (error) {
300 toast($, String(error instanceof Error ? error.message : error), 12_000)
301
302 return null
303 } finally {
304 say($)
305 await run($, ['rmdir', lock])
306 }
307}
308
309async function dropSite($: EngineInterface, tool: Tool, name: string) {
310 await run($, [tool, 'unsecure', name], undefined, SITE_MS)
311 await run($, [tool, 'unlink', name], undefined, SITE_MS)
312}
313
314async function unlink($: EngineInterface, record: WorktreeRecord): Promise<WorktreeRecord> {
315 if (!record.link || !record.tool) {
316 return record
317 }
318
319 await dropSite($, record.tool, record.link)
320
321 if ((await listLinks($, record.tool))?.some(site => site.name === record.link)) {
322 toast($, `${record.link} is still linked after unlink`, 10_000)
323
324 return record
325 }
326
327 return { ...record, link: null }
328}
329
330async function isMerged($: EngineInterface, record: WorktreeRecord): Promise<boolean> {
331 if (!record.branch || record.branch === 'HEAD') {
332 return false
333 }
334
335 const head = await run($, ['git', 'rev-parse', 'HEAD'], record.path)
336
337 if (!passed(head) || (record.base !== undefined && head.stdout.trim() === record.base)) {
338 return false
339 }
340
341 const origin = await run($, ['git', '-C', record.main, 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'])
342
343 for (const ref of [...(passed(origin) ? [origin.stdout.trim()] : []), 'main', 'master']) {
344 if (passed(await run($, ['git', '-C', record.main, 'merge-base', '--is-ancestor', record.branch, ref]))) {
345 return true
346 }
347 }
348
349 return false
350}
351
352async function tryRemove($: EngineInterface, record: WorktreeRecord) {
353 const forget = () => $.store.delete(recordKey(record.path))
354
355 if (!underWorktrees(record.main, record.path)) {
356 await forget()
357
358 return
359 }
360
361 if (!(await $.fs.exists(record.path))) {
362 await forget()
363
364 return
365 }
366
367 const status = await run($, ['git', 'status', '--porcelain'], record.path)
368 const dirty = !passed(status) || status.stdout.trim() !== ''
369
370 if (dirty || !(await isMerged($, record))) {
371 if (!record.notified) {
372 toast($, `${basename(record.path)} was kept: ${dirty ? 'it has uncommitted changes' : 'its work is not merged into the default branch'}`, 10_000)
373 await saveRecord($, { ...record, notified: true })
374 }
375
376 return
377 }
378
379 if (!passed(await run($, ['git', '-C', record.main, 'worktree', 'remove', record.path]))) {
380 toast($, `${basename(record.path)} could not be removed`, 10_000)
381
382 return
383 }
384
385 const branchDropped = passed(await run($, ['git', '-C', record.main, 'branch', '-d', record.branch]))
386
387 await forget()
388 toast($, branchDropped ? `Removed the finished worktree ${basename(record.path)}` : `Removed ${basename(record.path)}, but its branch ${record.branch} was kept`, 10_000)
389}
390
391async function retire($: EngineInterface, top: string, removedByTool: boolean) {
392 const record = await readRecord($, top)
393
394 if (!record) {
395 return
396 }
397
398 // Unlinking goes by site name, so it works after the tool has deleted the directory.
399 const unlinked = await unlink($, record)
400
401 if (removedByTool) {
402 await $.store.delete(recordKey(top))
403
404 return
405 }
406
407 const exited: WorktreeRecord = { ...unlinked, state: 'exited' }
408
409 await saveRecord($, exited)
410 await tryRemove($, exited)
411}
412
413async function sweep($: EngineInterface) {
414 const main = (await $.session.repo().catch(() => null))?.root
415 const here = await $.session.cwd()
416
417 if (!main) {
418 return
419 }
420
421 for (const key of (await $.store.keys()).filter(key => key.startsWith(PREFIX))) {
422 const record = (await $.store.get(key)) as WorktreeRecord | undefined
423
424 if (!record || record.main !== main || record.path === here) {
425 continue
426 }
427
428 if (record.state === 'exited' || !(await $.fs.exists(record.path))) {
429 await tryRemove($, await unlink($, record))
430 }
431 }
432}
433
434export const register: Register = (on, options) => {
435 configured = String(options.tool ?? 'auto')
436
437 on('session.start', async ($, e, next) => {
438 await provision($, await $.session.cwd()).catch(() => undefined)
439
440 $.clock.after(0, () => {
441 void sweep($).catch(() => undefined)
442 })
443
444 return next(e)
445 })
446
447 on('tool.call', { tool: 'EnterWorktree' }, async ($, e, next) => {
448 const result = await next(e)
449 const entered = outcomeOf(result)?.worktreePath
450
451 if (!entered || deniedOrFailed(result)) {
452 return result
453 }
454
455 const url = await provision($, entered).catch(() => null)
456
457 if (!url) {
458 return result
459 }
460
461 return { ...result, context: [...((result as { context?: readonly string[] }).context ?? []), `This worktree is served at ${url}. Tell the user this URL.`] }
462 })
463
464 on('tool.call', { tool: 'ExitWorktree' }, async ($, e, next) => {
465 const result = await next(e)
466 const left = outcomeOf(result)
467
468 if (left?.worktreePath) {
469 await retire($, left.worktreePath, (left.action ?? (e as { action?: string }).action) === 'remove').catch(error => toast($, String(error), 10_000))
470 }
471
472 return result
473 })
474}
475hooks/lib.ts 135 lines1export type Tool = 'valet' | 'herd'
2
3export type Link = { name: string; secure: boolean; url: string; path: string }
4
5export type WorktreeInfo = { top: string; gitDir: string; main: string }
6
7export type WorktreeRecord = {
8 path: string
9 main: string
10 project: string
11 branch: string
12 base?: string
13 tool: Tool | null
14 link: string | null
15 state: 'active' | 'exited'
16 notified: boolean
17}
18
19export type JsPlan = { lock: string | null; install: string[] | null; build: string[] | null }
20
21export const TOOLS: Tool[] = ['valet', 'herd']
22
23// Never add public/hot or bootstrap/cache: they carry main's dev-server URL and its cached config.
24export const CLONES = ['vendor', 'node_modules', 'public/build']
25
26const ANSI = /\u001b\[[0-9;]*m/g
27
28const JS_LOCKS: [string, string[], string[]][] = [
29 ['bun.lock', ['bun', 'install', '--frozen-lockfile'], ['bun', 'run', 'build']],
30 ['bun.lockb', ['bun', 'install', '--frozen-lockfile'], ['bun', 'run', 'build']],
31 ['pnpm-lock.yaml', ['pnpm', 'install', '--frozen-lockfile'], ['pnpm', 'run', 'build']],
32 ['yarn.lock', ['yarn', 'install', '--frozen-lockfile'], ['yarn', 'run', 'build']],
33 ['package-lock.json', ['npm', 'ci'], ['npm', 'run', 'build']],
34]
35
36export const JS_LOCK_FILES = JS_LOCKS.map(([lock]) => lock)
37
38export const ASSET_INPUTS = ['resources', 'package.json', ...JS_LOCK_FILES, 'vite.config.*', 'tailwind.config.*', 'postcss.config.*', 'tsconfig.json']
39
40const slug = (text: string): string =>
41 text
42 .toLowerCase()
43 .replace(/[^a-z0-9]+/g, '-')
44 .replace(/^-+|-+$/g, '')
45
46const parts = (text: string): string[] => slug(text).replaceAll('worktree', '').split('-').filter(Boolean)
47
48const hash = (text: string): string => {
49 let value = 5381
50
51 for (const char of text) {
52 value = ((value * 33) ^ char.charCodeAt(0)) >>> 0
53 }
54
55 return value.toString(36).slice(0, 6)
56}
57
58export const linkName = (project: string, worktree: string, path: string): string => {
59 const own = parts(worktree)
60 const name = [...parts(project), ...(own.length > 0 ? own : [hash(path)])].join('-')
61
62 return name.slice(0, 60).replace(/-+$/, '')
63}
64
65export const parseLinks = (stdout: string): Link[] =>
66 stdout
67 .replace(ANSI, '')
68 .split('\n')
69 .filter(line => line.trim().startsWith('|'))
70 .map(line => line.split('|').slice(1, -1).map(cell => cell.trim()))
71 .filter(cells => cells.length >= 4 && cells[0] !== 'Site')
72 .map(([name = '', ssl = '', url = '', path = '']) => ({ name, secure: ssl !== '', url, path }))
73
74export const parseWorktree = (revParse: string): WorktreeInfo | null => {
75 const [top, gitDir, commonDir] = revParse.split('\n').map(line => line.trim())
76
77 if (!top || !gitDir || !commonDir || gitDir === commonDir) {
78 return null
79 }
80
81 return { top, gitDir, main: commonDir.replace(/\/\.git\/?$/, '') }
82}
83
84export const basename = (path: string): string => path.split('/').filter(Boolean).pop() ?? path
85
86export const underWorktrees = (main: string, path: string): boolean => path.startsWith(`${main}/.claude/worktrees/`)
87
88export const chooseTool = (
89 configured: string,
90 installed: Tool[],
91): { tool: Tool | null; reason: 'none' | 'both' | 'missing' | null } => {
92 const wanted = configured.trim().toLowerCase()
93
94 if (wanted === 'valet' || wanted === 'herd') {
95 return installed.includes(wanted) ? { tool: wanted, reason: null } : { tool: null, reason: 'missing' }
96 }
97
98 if (installed.length === 1) {
99 return { tool: installed[0] ?? null, reason: null }
100 }
101
102 return { tool: null, reason: installed.length === 0 ? 'none' : 'both' }
103}
104
105export const jsPlan = (files: string[], hasBuild: boolean): JsPlan => {
106 const found = JS_LOCKS.find(([lock]) => files.includes(lock))
107
108 if (!found) {
109 return { lock: null, install: null, build: null }
110 }
111
112 return { lock: found[0], install: found[1], build: hasBuild ? found[2] : null }
113}
114
115export const certBase = (tool: Tool, home: string, host: string): string =>
116 tool === 'herd' ? `${home}/Library/Application Support/Herd/config/valet/Certificates/${host}` : `${home}/.config/valet/Certificates/${host}`
117
118export const setEnv = (text: string, key: string, value: string): string => {
119 const line = `${key}=${/\s/.test(value) ? `"${value}"` : value}`
120 const pattern = new RegExp(`^${key}=.*$`, 'm')
121
122 if (pattern.test(text)) {
123 return text.replace(pattern, () => line)
124 }
125
126 return `${text}${text === '' || text.endsWith('\n') ? '' : '\n'}${line}\n`
127}
128
129export const shareSqlite = (text: string, mainDatabase: string): string => {
130 const sqlite = /^DB_CONNECTION=["']?sqlite/m.test(text)
131 const absolute = /^DB_DATABASE=["']?\//m.test(text)
132
133 return sqlite && !absolute ? setEnv(text, 'DB_DATABASE', mainDatabase) : text
134}
135