@agimon-ai/doompi-web 0.0.1-alpha.17 → 0.0.1-alpha.18
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.
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./bundledServer.cjs");let
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./webPluginGenerate.cjs"),n=require("./bundledServer.cjs");let r=require("node:path");r=e.__toESM(r,1);let i=require("node:module"),a=require("@agimon-ai/doompi/services"),o=require("node:child_process"),s=require("@agimon-ai/doompi/utils");const c=`DOOMPI_WEB_PACKAGE_ROOT`,l=[`dist`,`bin`,`cli.mjs`],u=[`sync`];function d(){return{ensureSynced:async()=>{},watch:()=>{},close:()=>{}}}function f(e){return process.env.DOOMPI_SYNC_COMMAND??n.repositoryDoomPiCli(e)??process.env.DOOMPI_AGENT_COMMAND??r.default.join(r.default.dirname((0,i.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/doompi/package.json`)),...l)}function p(e){let n=f(e);return new Promise(r=>{let i=(0,o.spawn)(process.execPath,[n,...u],{cwd:e,env:{...process.env,DOOMPI_ROOT:e,[c]:process.env[c]||t.webHostPackageRoot()},stdio:[`ignore`,`pipe`,`pipe`]}),a=[],s=e=>{a.push(e.toString()),a.length>20&&a.shift()};i.stdout?.on(`data`,s),i.stderr?.on(`data`,s),i.once(`error`,e=>r({ok:!1,detail:`it could not start: ${e.message}`})),i.once(`exit`,e=>{r(e===0?{ok:!0}:{ok:!1,detail:`exit ${String(e)}: ${a.join(``).trim().slice(-400)}`})})})}function m(e){let t=e.onNotice??(()=>{}),n=e.repoRoot;if(n===void 0)try{n=(e.findRoot??s.findRepositoryRoot)(e.cwd??process.cwd())}catch{return d()}let r=e.runSync??(e=>p(e)),i=process.env.DOOMPI_BOOTSTRAP_ENTRY,o=e.readDrift??((e,t)=>{let n=(0,a.readSyncDrift)({repoRoot:e,expectedBootstrapEntry:t});return n.fresh&&(0,a.readSyncRegistration)(e)?.webDirectory==null?{fresh:!1,reasons:[`cockpit-bundle-missing`]}:n}),c=e.intervalMs??2e3,l,u,f=!1,m=0,h,g=e=>(m+=1,e!==h&&(t(e),h=e),{ready:!1,rebuilt:!1,error:e}),_=async e=>{t(`syncing: ${(0,a.describeSyncDrift)({fresh:!1,reasons:e.reasons})}`);let s;try{s=await r(n)??{ok:!0}}catch(e){let t=e instanceof Error?e.message:String(e);return g(`sync failed (${t})`)}if(!s.ok)return g(`sync failed (${s.detail??`no detail`})`);let c=o(n,i);return c.fresh?(m=0,h=void 0,t(`sync complete`),{ready:!0,rebuilt:!0}):g(`sync completed but did not resolve drift (${(0,a.describeSyncDrift)({fresh:!1,reasons:c.reasons})})`)},v=async()=>{if(f)return{ready:!1,rebuilt:!1,error:`sync guard is closed`};if(l)return l;let e=o(n,i);return e.fresh?{ready:!0,rebuilt:!1}:(l=_(e).finally(()=>{l=void 0}),l)};return{async ensureSynced(){let e=await v();if(!e.ready)throw Error(`DoomPi cannot launch this session because ${e.error??`sync failed`}.`)},watch(e){if(u)return;let t=()=>{if(f)return;let e=Math.min(c*2**m,6e4);u=setTimeout(r,e),u.unref?.()},r=()=>{if(f||l){t();return}if(o(n,i).fresh){t();return}v().then(n=>{!f&&n.rebuilt&&e(),t()})};t()},close(){f=!0,u&&clearTimeout(u),u=void 0}}}exports.createSyncGuard=m;
|
|
2
2
|
//# sourceMappingURL=syncGuard.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncGuard.cjs","names":["repositoryDoomPiCli","path","createRequire","spawn","findRepositoryRoot","readSyncDrift","describeSyncDrift"],"sources":["../../../src/adapters/syncGuard.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { describeSyncDrift, readSyncDrift } from '@agimon-ai/doompi/services';\nimport { findRepositoryRoot } from '@agimon-ai/doompi/utils';\nimport { repositoryDoomPiCli } from './bundledServer.ts';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';\nconst SYNC_COMMAND_ENV = 'DOOMPI_SYNC_COMMAND';\nconst BOOTSTRAP_ENTRY_ENV = 'DOOMPI_BOOTSTRAP_ENTRY';\nconst HARNESS_ROOT_ENV = 'DOOMPI_ROOT';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\nconst SYNC_ARGS = ['sync'];\n/** How often the watcher re-reads the drift inputs. */\nconst WATCH_INTERVAL_MS = 2_000;\n/** Ceiling for the retry delay after a sync that keeps failing. */\nconst MAX_RETRY_INTERVAL_MS = 60_000;\n\n/** What one sync attempt did, so a failure is never reported as a rebuild. */\nexport interface SyncRunOutcome {\n ok: boolean;\n detail?: string;\n}\n\nexport interface SyncGuardOptions {\n /** Known repository root, or a launch directory to discover one from. */\n repoRoot?: string;\n cwd?: string;\n onNotice?: (message: string) => void;\n /** Test seam for running the sync itself; returning nothing reads as success. */\n runSync?: (repoRoot: string) => Promise<SyncRunOutcome | void>;\n /** Test seam for the per-repository drift read and launcher-owned Doom entry. */\n readDrift?: (repoRoot: string, expectedBootstrapEntry?: string) => { fresh: boolean; reasons: readonly string[] };\n /** Test seam over repository discovery. */\n findRoot?: (cwd: string) => string;\n intervalMs?: number;\n}\nexport interface SyncGuard {\n /**\n * Syncs the repository when anything a session reads has drifted.\n *\n * Serialized: several sessions starting at once wait on one sync rather\n * than racing to rebuild the same artifacts over each other.\n */\n ensureSynced(): Promise<void>;\n /** Re-syncs on drift and reports when artifacts changed under a running cockpit. */\n watch(onSynced: () => void): void;\n close(): void;\n}\n\nfunction inactiveSyncGuard(): SyncGuard {\n return {\n ensureSynced: async () => {},\n watch: () => {},\n close: () => {},\n };\n}\n\n/**\n * The DoomPi CLI this repository should sync with.\n *\n * A repository that pins its own DoomPi must be synced by that one: extensions\n * are version-coupled to the harness that resolves them, and the copy sitting\n * in the hub's dependency tree may not be the copy the repository runs.\n */\nfunction syncCliFor(repoRoot: string): string {\n return (\n process.env[SYNC_COMMAND_ENV] ??\n repositoryDoomPiCli(repoRoot) ??\n process.env[AGENT_COMMAND_ENV] ??\n path.join(path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)), ...CLI_SEGMENTS)\n );\n}\n\n/** Runs the launcher's own sync, in its own process so the hub keeps serving. */\nfunction spawnSync(repoRoot: string): Promise<SyncRunOutcome> {\n const cli = syncCliFor(repoRoot);\n return new Promise((resolve) => {\n const child = spawn(process.execPath, [cli, ...SYNC_ARGS], {\n cwd: repoRoot,\n env: { ...process.env, [HARNESS_ROOT_ENV]: repoRoot },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const tail: string[] = [];\n const keep = (chunk: Buffer): void => {\n tail.push(chunk.toString());\n if (tail.length > 20) tail.shift();\n };\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n // A failed sync is reported, not thrown: the session the caller wanted still\n // starts, and it starts against whatever the last good sync left. What it\n // must not do is claim the artifacts were rebuilt.\n child.once('error', (error) => resolve({ ok: false, detail: `it could not start: ${error.message}` }));\n child.once('exit', (code) => {\n if (code === 0) resolve({ ok: true });\n else resolve({ ok: false, detail: `exit ${String(code)}: ${tail.join('').trim().slice(-400)}` });\n });\n });\n}\n\n/**\n * Keeps the repository synced under a running cockpit.\n *\n * A session launched from the browser reads what sync produced: the resolved\n * composition, the plugin bundle, and the package API routes. Nothing forces\n * a person clicking \"new session\" to have run sync first, and a stale one\n * fails quietly rather than loudly, so the hub checks and syncs itself.\n */\nexport function createSyncGuard(options: SyncGuardOptions): SyncGuard {\n const notice = options.onNotice ?? ((): void => {});\n let repoRoot = options.repoRoot;\n if (repoRoot === undefined) {\n try {\n repoRoot = (options.findRoot ?? findRepositoryRoot)(options.cwd ?? process.cwd());\n } catch {\n return inactiveSyncGuard();\n }\n }\n const runSync = options.runSync ?? ((root: string) => spawnSync(root));\n const expectedBootstrapEntry = process.env[BOOTSTRAP_ENTRY_ENV];\n const readDrift =\n options.readDrift ??\n ((root: string, entry?: string) => readSyncDrift({ repoRoot: root, expectedBootstrapEntry: entry }));\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<{ ready: boolean; rebuilt: boolean; error?: string }> | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let closed = false;\n let consecutiveFailures = 0;\n let lastFailureNotice: string | undefined;\n\n /**\n * One sync attempt, reporting whether it actually rebuilt anything.\n *\n * A sync that exits non-zero published nothing, so saying \"complete\" and\n * telling every attached page to reload is two lies and a reload storm: the\n * repository is still drifted, so the next poll tries again immediately.\n */\n const failedResult = (failure: string): { ready: false; rebuilt: false; error: string } => {\n consecutiveFailures += 1;\n // The same failure every few seconds buries everything else in the log.\n if (failure !== lastFailureNotice) {\n notice(failure);\n lastFailureNotice = failure;\n }\n return { ready: false, rebuilt: false, error: failure };\n };\n\n const syncOnce = async (drift: {\n reasons: readonly string[];\n }): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n let outcome: SyncRunOutcome;\n try {\n outcome = (await runSync(repoRoot)) ?? { ok: true };\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n return failedResult(`sync failed (${detail})`);\n }\n if (!outcome.ok) return failedResult(`sync failed (${outcome.detail ?? 'no detail'})`);\n\n const remaining = readDrift(repoRoot, expectedBootstrapEntry);\n if (!remaining.fresh) {\n return failedResult(\n `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`,\n );\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return { ready: true, rebuilt: true };\n };\n\n const ensureSynced = async (): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n if (closed) return { ready: false, rebuilt: false, error: 'sync guard is closed' };\n // Joining the run already in flight is what makes concurrent launches\n // wait for one sync instead of starting several.\n if (inFlight) return inFlight;\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) return { ready: true, rebuilt: false };\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n const result = await ensureSynced();\n if (!result.ready) throw new Error(`DoomPi cannot launch this session because ${result.error ?? 'sync failed'}.`);\n },\n watch(onSynced) {\n if (timer) return;\n const schedule = (): void => {\n if (closed) return;\n // Backing off after a failure: a repository that cannot sync will not\n // start syncing because it was asked again two seconds later, and the\n // retries drown the reason in the log.\n const delay = Math.min(baseInterval * 2 ** consecutiveFailures, MAX_RETRY_INTERVAL_MS);\n timer = setTimeout(tick, delay);\n // The hub should not be held open by its own watcher.\n timer.unref?.();\n };\n const tick = (): void => {\n if (closed || inFlight) {\n schedule();\n return;\n }\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((result) => {\n if (!closed && result.rebuilt) onSynced();\n schedule();\n });\n };\n schedule();\n },\n close() {\n closed = true;\n if (timer) clearTimeout(timer);\n timer = undefined;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"syncGuard.cjs","names":["repositoryDoomPiCli","path","createRequire","spawn","webHostPackageRoot","findRepositoryRoot","readSyncDrift","readSyncRegistration","describeSyncDrift"],"sources":["../../../src/adapters/syncGuard.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { describeSyncDrift, readSyncDrift, readSyncRegistration } from '@agimon-ai/doompi/services';\nimport { findRepositoryRoot } from '@agimon-ai/doompi/utils';\nimport { repositoryDoomPiCli } from './bundledServer.ts';\nimport { webHostPackageRoot } from './webPluginGenerate.ts';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';\nconst SYNC_COMMAND_ENV = 'DOOMPI_SYNC_COMMAND';\nconst BOOTSTRAP_ENTRY_ENV = 'DOOMPI_BOOTSTRAP_ENTRY';\nconst HARNESS_ROOT_ENV = 'DOOMPI_ROOT';\nconst WEB_PACKAGE_ROOT_ENV = 'DOOMPI_WEB_PACKAGE_ROOT';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\nconst SYNC_ARGS = ['sync'];\n/** How often the watcher re-reads the drift inputs. */\nconst WATCH_INTERVAL_MS = 2_000;\n/** Ceiling for the retry delay after a sync that keeps failing. */\nconst MAX_RETRY_INTERVAL_MS = 60_000;\n\n/** What one sync attempt did, so a failure is never reported as a rebuild. */\nexport interface SyncRunOutcome {\n ok: boolean;\n detail?: string;\n}\n\nexport interface SyncGuardOptions {\n /** Known repository root, or a launch directory to discover one from. */\n repoRoot?: string;\n cwd?: string;\n onNotice?: (message: string) => void;\n /** Test seam for running the sync itself; returning nothing reads as success. */\n runSync?: (repoRoot: string) => Promise<SyncRunOutcome | void>;\n /** Test seam for the per-repository drift read and launcher-owned Doom entry. */\n readDrift?: (repoRoot: string, expectedBootstrapEntry?: string) => { fresh: boolean; reasons: readonly string[] };\n /** Test seam over repository discovery. */\n findRoot?: (cwd: string) => string;\n intervalMs?: number;\n}\nexport interface SyncGuard {\n /**\n * Syncs the repository when anything a session reads has drifted.\n *\n * Serialized: several sessions starting at once wait on one sync rather\n * than racing to rebuild the same artifacts over each other.\n */\n ensureSynced(): Promise<void>;\n /** Re-syncs on drift and reports when artifacts changed under a running cockpit. */\n watch(onSynced: () => void): void;\n close(): void;\n}\n\nfunction inactiveSyncGuard(): SyncGuard {\n return {\n ensureSynced: async () => {},\n watch: () => {},\n close: () => {},\n };\n}\n\n/**\n * The DoomPi CLI this repository should sync with.\n *\n * A repository that pins its own DoomPi must be synced by that one: extensions\n * are version-coupled to the harness that resolves them, and the copy sitting\n * in the hub's dependency tree may not be the copy the repository runs.\n */\nfunction syncCliFor(repoRoot: string): string {\n return (\n process.env[SYNC_COMMAND_ENV] ??\n repositoryDoomPiCli(repoRoot) ??\n process.env[AGENT_COMMAND_ENV] ??\n path.join(path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)), ...CLI_SEGMENTS)\n );\n}\n\n/** Runs the launcher's own sync, in its own process so the hub keeps serving. */\nfunction spawnSync(repoRoot: string): Promise<SyncRunOutcome> {\n const cli = syncCliFor(repoRoot);\n return new Promise((resolve) => {\n const child = spawn(process.execPath, [cli, ...SYNC_ARGS], {\n cwd: repoRoot,\n env: {\n ...process.env,\n [HARNESS_ROOT_ENV]: repoRoot,\n [WEB_PACKAGE_ROOT_ENV]: process.env[WEB_PACKAGE_ROOT_ENV] || webHostPackageRoot(),\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const tail: string[] = [];\n const keep = (chunk: Buffer): void => {\n tail.push(chunk.toString());\n if (tail.length > 20) tail.shift();\n };\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n // A failed sync is reported, not thrown: the session the caller wanted still\n // starts, and it starts against whatever the last good sync left. What it\n // must not do is claim the artifacts were rebuilt.\n child.once('error', (error) => resolve({ ok: false, detail: `it could not start: ${error.message}` }));\n child.once('exit', (code) => {\n if (code === 0) resolve({ ok: true });\n else resolve({ ok: false, detail: `exit ${String(code)}: ${tail.join('').trim().slice(-400)}` });\n });\n });\n}\n\n/**\n * Keeps the repository synced under a running cockpit.\n *\n * A session launched from the browser reads what sync produced: the resolved\n * composition, the plugin bundle, and the package API routes. Nothing forces\n * a person clicking \"new session\" to have run sync first, and a stale one\n * fails quietly rather than loudly, so the hub checks and syncs itself.\n */\nexport function createSyncGuard(options: SyncGuardOptions): SyncGuard {\n const notice = options.onNotice ?? ((): void => {});\n let repoRoot = options.repoRoot;\n if (repoRoot === undefined) {\n try {\n repoRoot = (options.findRoot ?? findRepositoryRoot)(options.cwd ?? process.cwd());\n } catch {\n return inactiveSyncGuard();\n }\n }\n const runSync = options.runSync ?? ((root: string) => spawnSync(root));\n const expectedBootstrapEntry = process.env[BOOTSTRAP_ENTRY_ENV];\n const readDrift =\n options.readDrift ??\n ((root: string, entry?: string) => {\n const drift = readSyncDrift({ repoRoot: root, expectedBootstrapEntry: entry });\n // A CLI-only sync is valid for the terminal, but cannot supply session UI.\n if (drift.fresh && readSyncRegistration(root)?.webDirectory == null) {\n return { fresh: false, reasons: ['cockpit-bundle-missing'] };\n }\n return drift;\n });\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<{ ready: boolean; rebuilt: boolean; error?: string }> | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let closed = false;\n let consecutiveFailures = 0;\n let lastFailureNotice: string | undefined;\n\n /**\n * One sync attempt, reporting whether it actually rebuilt anything.\n *\n * A sync that exits non-zero published nothing, so saying \"complete\" and\n * telling every attached page to reload is two lies and a reload storm: the\n * repository is still drifted, so the next poll tries again immediately.\n */\n const failedResult = (failure: string): { ready: false; rebuilt: false; error: string } => {\n consecutiveFailures += 1;\n // The same failure every few seconds buries everything else in the log.\n if (failure !== lastFailureNotice) {\n notice(failure);\n lastFailureNotice = failure;\n }\n return { ready: false, rebuilt: false, error: failure };\n };\n\n const syncOnce = async (drift: {\n reasons: readonly string[];\n }): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n let outcome: SyncRunOutcome;\n try {\n outcome = (await runSync(repoRoot)) ?? { ok: true };\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n return failedResult(`sync failed (${detail})`);\n }\n if (!outcome.ok) return failedResult(`sync failed (${outcome.detail ?? 'no detail'})`);\n\n const remaining = readDrift(repoRoot, expectedBootstrapEntry);\n if (!remaining.fresh) {\n return failedResult(\n `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`,\n );\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return { ready: true, rebuilt: true };\n };\n\n const ensureSynced = async (): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n if (closed) return { ready: false, rebuilt: false, error: 'sync guard is closed' };\n // Joining the run already in flight is what makes concurrent launches\n // wait for one sync instead of starting several.\n if (inFlight) return inFlight;\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) return { ready: true, rebuilt: false };\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n const result = await ensureSynced();\n if (!result.ready) throw new Error(`DoomPi cannot launch this session because ${result.error ?? 'sync failed'}.`);\n },\n watch(onSynced) {\n if (timer) return;\n const schedule = (): void => {\n if (closed) return;\n // Backing off after a failure: a repository that cannot sync will not\n // start syncing because it was asked again two seconds later, and the\n // retries drown the reason in the log.\n const delay = Math.min(baseInterval * 2 ** consecutiveFailures, MAX_RETRY_INTERVAL_MS);\n timer = setTimeout(tick, delay);\n // The hub should not be held open by its own watcher.\n timer.unref?.();\n };\n const tick = (): void => {\n if (closed || inFlight) {\n schedule();\n return;\n }\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((result) => {\n if (!closed && result.rebuilt) onSynced();\n schedule();\n });\n };\n schedule();\n },\n close() {\n closed = true;\n if (timer) clearTimeout(timer);\n timer = undefined;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{webHostPackageRoot as e}from"./webPluginGenerate.mjs";import{repositoryDoomPiCli as t}from"./bundledServer.mjs";import{createRequire as n}from"node:module";import r from"node:path";import{describeSyncDrift as i,readSyncDrift as a,readSyncRegistration as o}from"@agimon-ai/doompi/services";import{spawn as s}from"node:child_process";import{findRepositoryRoot as c}from"@agimon-ai/doompi/utils";const l=`DOOMPI_WEB_PACKAGE_ROOT`,u=[`dist`,`bin`,`cli.mjs`],d=[`sync`];function f(){return{ensureSynced:async()=>{},watch:()=>{},close:()=>{}}}function p(e){return process.env.DOOMPI_SYNC_COMMAND??t(e)??process.env.DOOMPI_AGENT_COMMAND??r.join(r.dirname(n(import.meta.url).resolve(`@agimon-ai/doompi/package.json`)),...u)}function m(t){let n=p(t);return new Promise(r=>{let i=s(process.execPath,[n,...d],{cwd:t,env:{...process.env,DOOMPI_ROOT:t,[l]:process.env[l]||e()},stdio:[`ignore`,`pipe`,`pipe`]}),a=[],o=e=>{a.push(e.toString()),a.length>20&&a.shift()};i.stdout?.on(`data`,o),i.stderr?.on(`data`,o),i.once(`error`,e=>r({ok:!1,detail:`it could not start: ${e.message}`})),i.once(`exit`,e=>{r(e===0?{ok:!0}:{ok:!1,detail:`exit ${String(e)}: ${a.join(``).trim().slice(-400)}`})})})}function h(e){let t=e.onNotice??(()=>{}),n=e.repoRoot;if(n===void 0)try{n=(e.findRoot??c)(e.cwd??process.cwd())}catch{return f()}let r=e.runSync??(e=>m(e)),s=process.env.DOOMPI_BOOTSTRAP_ENTRY,l=e.readDrift??((e,t)=>{let n=a({repoRoot:e,expectedBootstrapEntry:t});return n.fresh&&o(e)?.webDirectory==null?{fresh:!1,reasons:[`cockpit-bundle-missing`]}:n}),u=e.intervalMs??2e3,d,p,h=!1,g=0,_,v=e=>(g+=1,e!==_&&(t(e),_=e),{ready:!1,rebuilt:!1,error:e}),y=async e=>{t(`syncing: ${i({fresh:!1,reasons:e.reasons})}`);let a;try{a=await r(n)??{ok:!0}}catch(e){let t=e instanceof Error?e.message:String(e);return v(`sync failed (${t})`)}if(!a.ok)return v(`sync failed (${a.detail??`no detail`})`);let o=l(n,s);return o.fresh?(g=0,_=void 0,t(`sync complete`),{ready:!0,rebuilt:!0}):v(`sync completed but did not resolve drift (${i({fresh:!1,reasons:o.reasons})})`)},b=async()=>{if(h)return{ready:!1,rebuilt:!1,error:`sync guard is closed`};if(d)return d;let e=l(n,s);return e.fresh?{ready:!0,rebuilt:!1}:(d=y(e).finally(()=>{d=void 0}),d)};return{async ensureSynced(){let e=await b();if(!e.ready)throw Error(`DoomPi cannot launch this session because ${e.error??`sync failed`}.`)},watch(e){if(p)return;let t=()=>{if(h)return;let e=Math.min(u*2**g,6e4);p=setTimeout(r,e),p.unref?.()},r=()=>{if(h||d){t();return}if(l(n,s).fresh){t();return}b().then(n=>{!h&&n.rebuilt&&e(),t()})};t()},close(){h=!0,p&&clearTimeout(p),p=void 0}}}export{h as createSyncGuard};
|
|
2
2
|
//# sourceMappingURL=syncGuard.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncGuard.mjs","names":[],"sources":["../../../src/adapters/syncGuard.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { describeSyncDrift, readSyncDrift } from '@agimon-ai/doompi/services';\nimport { findRepositoryRoot } from '@agimon-ai/doompi/utils';\nimport { repositoryDoomPiCli } from './bundledServer.ts';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';\nconst SYNC_COMMAND_ENV = 'DOOMPI_SYNC_COMMAND';\nconst BOOTSTRAP_ENTRY_ENV = 'DOOMPI_BOOTSTRAP_ENTRY';\nconst HARNESS_ROOT_ENV = 'DOOMPI_ROOT';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\nconst SYNC_ARGS = ['sync'];\n/** How often the watcher re-reads the drift inputs. */\nconst WATCH_INTERVAL_MS = 2_000;\n/** Ceiling for the retry delay after a sync that keeps failing. */\nconst MAX_RETRY_INTERVAL_MS = 60_000;\n\n/** What one sync attempt did, so a failure is never reported as a rebuild. */\nexport interface SyncRunOutcome {\n ok: boolean;\n detail?: string;\n}\n\nexport interface SyncGuardOptions {\n /** Known repository root, or a launch directory to discover one from. */\n repoRoot?: string;\n cwd?: string;\n onNotice?: (message: string) => void;\n /** Test seam for running the sync itself; returning nothing reads as success. */\n runSync?: (repoRoot: string) => Promise<SyncRunOutcome | void>;\n /** Test seam for the per-repository drift read and launcher-owned Doom entry. */\n readDrift?: (repoRoot: string, expectedBootstrapEntry?: string) => { fresh: boolean; reasons: readonly string[] };\n /** Test seam over repository discovery. */\n findRoot?: (cwd: string) => string;\n intervalMs?: number;\n}\nexport interface SyncGuard {\n /**\n * Syncs the repository when anything a session reads has drifted.\n *\n * Serialized: several sessions starting at once wait on one sync rather\n * than racing to rebuild the same artifacts over each other.\n */\n ensureSynced(): Promise<void>;\n /** Re-syncs on drift and reports when artifacts changed under a running cockpit. */\n watch(onSynced: () => void): void;\n close(): void;\n}\n\nfunction inactiveSyncGuard(): SyncGuard {\n return {\n ensureSynced: async () => {},\n watch: () => {},\n close: () => {},\n };\n}\n\n/**\n * The DoomPi CLI this repository should sync with.\n *\n * A repository that pins its own DoomPi must be synced by that one: extensions\n * are version-coupled to the harness that resolves them, and the copy sitting\n * in the hub's dependency tree may not be the copy the repository runs.\n */\nfunction syncCliFor(repoRoot: string): string {\n return (\n process.env[SYNC_COMMAND_ENV] ??\n repositoryDoomPiCli(repoRoot) ??\n process.env[AGENT_COMMAND_ENV] ??\n path.join(path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)), ...CLI_SEGMENTS)\n );\n}\n\n/** Runs the launcher's own sync, in its own process so the hub keeps serving. */\nfunction spawnSync(repoRoot: string): Promise<SyncRunOutcome> {\n const cli = syncCliFor(repoRoot);\n return new Promise((resolve) => {\n const child = spawn(process.execPath, [cli, ...SYNC_ARGS], {\n cwd: repoRoot,\n env: { ...process.env, [HARNESS_ROOT_ENV]: repoRoot },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const tail: string[] = [];\n const keep = (chunk: Buffer): void => {\n tail.push(chunk.toString());\n if (tail.length > 20) tail.shift();\n };\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n // A failed sync is reported, not thrown: the session the caller wanted still\n // starts, and it starts against whatever the last good sync left. What it\n // must not do is claim the artifacts were rebuilt.\n child.once('error', (error) => resolve({ ok: false, detail: `it could not start: ${error.message}` }));\n child.once('exit', (code) => {\n if (code === 0) resolve({ ok: true });\n else resolve({ ok: false, detail: `exit ${String(code)}: ${tail.join('').trim().slice(-400)}` });\n });\n });\n}\n\n/**\n * Keeps the repository synced under a running cockpit.\n *\n * A session launched from the browser reads what sync produced: the resolved\n * composition, the plugin bundle, and the package API routes. Nothing forces\n * a person clicking \"new session\" to have run sync first, and a stale one\n * fails quietly rather than loudly, so the hub checks and syncs itself.\n */\nexport function createSyncGuard(options: SyncGuardOptions): SyncGuard {\n const notice = options.onNotice ?? ((): void => {});\n let repoRoot = options.repoRoot;\n if (repoRoot === undefined) {\n try {\n repoRoot = (options.findRoot ?? findRepositoryRoot)(options.cwd ?? process.cwd());\n } catch {\n return inactiveSyncGuard();\n }\n }\n const runSync = options.runSync ?? ((root: string) => spawnSync(root));\n const expectedBootstrapEntry = process.env[BOOTSTRAP_ENTRY_ENV];\n const readDrift =\n options.readDrift ??\n ((root: string, entry?: string) => readSyncDrift({ repoRoot: root, expectedBootstrapEntry: entry }));\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<{ ready: boolean; rebuilt: boolean; error?: string }> | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let closed = false;\n let consecutiveFailures = 0;\n let lastFailureNotice: string | undefined;\n\n /**\n * One sync attempt, reporting whether it actually rebuilt anything.\n *\n * A sync that exits non-zero published nothing, so saying \"complete\" and\n * telling every attached page to reload is two lies and a reload storm: the\n * repository is still drifted, so the next poll tries again immediately.\n */\n const failedResult = (failure: string): { ready: false; rebuilt: false; error: string } => {\n consecutiveFailures += 1;\n // The same failure every few seconds buries everything else in the log.\n if (failure !== lastFailureNotice) {\n notice(failure);\n lastFailureNotice = failure;\n }\n return { ready: false, rebuilt: false, error: failure };\n };\n\n const syncOnce = async (drift: {\n reasons: readonly string[];\n }): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n let outcome: SyncRunOutcome;\n try {\n outcome = (await runSync(repoRoot)) ?? { ok: true };\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n return failedResult(`sync failed (${detail})`);\n }\n if (!outcome.ok) return failedResult(`sync failed (${outcome.detail ?? 'no detail'})`);\n\n const remaining = readDrift(repoRoot, expectedBootstrapEntry);\n if (!remaining.fresh) {\n return failedResult(\n `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`,\n );\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return { ready: true, rebuilt: true };\n };\n\n const ensureSynced = async (): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n if (closed) return { ready: false, rebuilt: false, error: 'sync guard is closed' };\n // Joining the run already in flight is what makes concurrent launches\n // wait for one sync instead of starting several.\n if (inFlight) return inFlight;\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) return { ready: true, rebuilt: false };\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n const result = await ensureSynced();\n if (!result.ready) throw new Error(`DoomPi cannot launch this session because ${result.error ?? 'sync failed'}.`);\n },\n watch(onSynced) {\n if (timer) return;\n const schedule = (): void => {\n if (closed) return;\n // Backing off after a failure: a repository that cannot sync will not\n // start syncing because it was asked again two seconds later, and the\n // retries drown the reason in the log.\n const delay = Math.min(baseInterval * 2 ** consecutiveFailures, MAX_RETRY_INTERVAL_MS);\n timer = setTimeout(tick, delay);\n // The hub should not be held open by its own watcher.\n timer.unref?.();\n };\n const tick = (): void => {\n if (closed || inFlight) {\n schedule();\n return;\n }\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((result) => {\n if (!closed && result.rebuilt) onSynced();\n schedule();\n });\n };\n schedule();\n },\n close() {\n closed = true;\n if (timer) clearTimeout(timer);\n timer = undefined;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"syncGuard.mjs","names":[],"sources":["../../../src/adapters/syncGuard.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { describeSyncDrift, readSyncDrift, readSyncRegistration } from '@agimon-ai/doompi/services';\nimport { findRepositoryRoot } from '@agimon-ai/doompi/utils';\nimport { repositoryDoomPiCli } from './bundledServer.ts';\nimport { webHostPackageRoot } from './webPluginGenerate.ts';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';\nconst SYNC_COMMAND_ENV = 'DOOMPI_SYNC_COMMAND';\nconst BOOTSTRAP_ENTRY_ENV = 'DOOMPI_BOOTSTRAP_ENTRY';\nconst HARNESS_ROOT_ENV = 'DOOMPI_ROOT';\nconst WEB_PACKAGE_ROOT_ENV = 'DOOMPI_WEB_PACKAGE_ROOT';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\nconst SYNC_ARGS = ['sync'];\n/** How often the watcher re-reads the drift inputs. */\nconst WATCH_INTERVAL_MS = 2_000;\n/** Ceiling for the retry delay after a sync that keeps failing. */\nconst MAX_RETRY_INTERVAL_MS = 60_000;\n\n/** What one sync attempt did, so a failure is never reported as a rebuild. */\nexport interface SyncRunOutcome {\n ok: boolean;\n detail?: string;\n}\n\nexport interface SyncGuardOptions {\n /** Known repository root, or a launch directory to discover one from. */\n repoRoot?: string;\n cwd?: string;\n onNotice?: (message: string) => void;\n /** Test seam for running the sync itself; returning nothing reads as success. */\n runSync?: (repoRoot: string) => Promise<SyncRunOutcome | void>;\n /** Test seam for the per-repository drift read and launcher-owned Doom entry. */\n readDrift?: (repoRoot: string, expectedBootstrapEntry?: string) => { fresh: boolean; reasons: readonly string[] };\n /** Test seam over repository discovery. */\n findRoot?: (cwd: string) => string;\n intervalMs?: number;\n}\nexport interface SyncGuard {\n /**\n * Syncs the repository when anything a session reads has drifted.\n *\n * Serialized: several sessions starting at once wait on one sync rather\n * than racing to rebuild the same artifacts over each other.\n */\n ensureSynced(): Promise<void>;\n /** Re-syncs on drift and reports when artifacts changed under a running cockpit. */\n watch(onSynced: () => void): void;\n close(): void;\n}\n\nfunction inactiveSyncGuard(): SyncGuard {\n return {\n ensureSynced: async () => {},\n watch: () => {},\n close: () => {},\n };\n}\n\n/**\n * The DoomPi CLI this repository should sync with.\n *\n * A repository that pins its own DoomPi must be synced by that one: extensions\n * are version-coupled to the harness that resolves them, and the copy sitting\n * in the hub's dependency tree may not be the copy the repository runs.\n */\nfunction syncCliFor(repoRoot: string): string {\n return (\n process.env[SYNC_COMMAND_ENV] ??\n repositoryDoomPiCli(repoRoot) ??\n process.env[AGENT_COMMAND_ENV] ??\n path.join(path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)), ...CLI_SEGMENTS)\n );\n}\n\n/** Runs the launcher's own sync, in its own process so the hub keeps serving. */\nfunction spawnSync(repoRoot: string): Promise<SyncRunOutcome> {\n const cli = syncCliFor(repoRoot);\n return new Promise((resolve) => {\n const child = spawn(process.execPath, [cli, ...SYNC_ARGS], {\n cwd: repoRoot,\n env: {\n ...process.env,\n [HARNESS_ROOT_ENV]: repoRoot,\n [WEB_PACKAGE_ROOT_ENV]: process.env[WEB_PACKAGE_ROOT_ENV] || webHostPackageRoot(),\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const tail: string[] = [];\n const keep = (chunk: Buffer): void => {\n tail.push(chunk.toString());\n if (tail.length > 20) tail.shift();\n };\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n // A failed sync is reported, not thrown: the session the caller wanted still\n // starts, and it starts against whatever the last good sync left. What it\n // must not do is claim the artifacts were rebuilt.\n child.once('error', (error) => resolve({ ok: false, detail: `it could not start: ${error.message}` }));\n child.once('exit', (code) => {\n if (code === 0) resolve({ ok: true });\n else resolve({ ok: false, detail: `exit ${String(code)}: ${tail.join('').trim().slice(-400)}` });\n });\n });\n}\n\n/**\n * Keeps the repository synced under a running cockpit.\n *\n * A session launched from the browser reads what sync produced: the resolved\n * composition, the plugin bundle, and the package API routes. Nothing forces\n * a person clicking \"new session\" to have run sync first, and a stale one\n * fails quietly rather than loudly, so the hub checks and syncs itself.\n */\nexport function createSyncGuard(options: SyncGuardOptions): SyncGuard {\n const notice = options.onNotice ?? ((): void => {});\n let repoRoot = options.repoRoot;\n if (repoRoot === undefined) {\n try {\n repoRoot = (options.findRoot ?? findRepositoryRoot)(options.cwd ?? process.cwd());\n } catch {\n return inactiveSyncGuard();\n }\n }\n const runSync = options.runSync ?? ((root: string) => spawnSync(root));\n const expectedBootstrapEntry = process.env[BOOTSTRAP_ENTRY_ENV];\n const readDrift =\n options.readDrift ??\n ((root: string, entry?: string) => {\n const drift = readSyncDrift({ repoRoot: root, expectedBootstrapEntry: entry });\n // A CLI-only sync is valid for the terminal, but cannot supply session UI.\n if (drift.fresh && readSyncRegistration(root)?.webDirectory == null) {\n return { fresh: false, reasons: ['cockpit-bundle-missing'] };\n }\n return drift;\n });\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<{ ready: boolean; rebuilt: boolean; error?: string }> | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let closed = false;\n let consecutiveFailures = 0;\n let lastFailureNotice: string | undefined;\n\n /**\n * One sync attempt, reporting whether it actually rebuilt anything.\n *\n * A sync that exits non-zero published nothing, so saying \"complete\" and\n * telling every attached page to reload is two lies and a reload storm: the\n * repository is still drifted, so the next poll tries again immediately.\n */\n const failedResult = (failure: string): { ready: false; rebuilt: false; error: string } => {\n consecutiveFailures += 1;\n // The same failure every few seconds buries everything else in the log.\n if (failure !== lastFailureNotice) {\n notice(failure);\n lastFailureNotice = failure;\n }\n return { ready: false, rebuilt: false, error: failure };\n };\n\n const syncOnce = async (drift: {\n reasons: readonly string[];\n }): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n let outcome: SyncRunOutcome;\n try {\n outcome = (await runSync(repoRoot)) ?? { ok: true };\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n return failedResult(`sync failed (${detail})`);\n }\n if (!outcome.ok) return failedResult(`sync failed (${outcome.detail ?? 'no detail'})`);\n\n const remaining = readDrift(repoRoot, expectedBootstrapEntry);\n if (!remaining.fresh) {\n return failedResult(\n `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`,\n );\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return { ready: true, rebuilt: true };\n };\n\n const ensureSynced = async (): Promise<{ ready: boolean; rebuilt: boolean; error?: string }> => {\n if (closed) return { ready: false, rebuilt: false, error: 'sync guard is closed' };\n // Joining the run already in flight is what makes concurrent launches\n // wait for one sync instead of starting several.\n if (inFlight) return inFlight;\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) return { ready: true, rebuilt: false };\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n const result = await ensureSynced();\n if (!result.ready) throw new Error(`DoomPi cannot launch this session because ${result.error ?? 'sync failed'}.`);\n },\n watch(onSynced) {\n if (timer) return;\n const schedule = (): void => {\n if (closed) return;\n // Backing off after a failure: a repository that cannot sync will not\n // start syncing because it was asked again two seconds later, and the\n // retries drown the reason in the log.\n const delay = Math.min(baseInterval * 2 ** consecutiveFailures, MAX_RETRY_INTERVAL_MS);\n timer = setTimeout(tick, delay);\n // The hub should not be held open by its own watcher.\n timer.unref?.();\n };\n const tick = (): void => {\n if (closed || inFlight) {\n schedule();\n return;\n }\n const drift = readDrift(repoRoot, expectedBootstrapEntry);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((result) => {\n if (!closed && result.rebuilt) onSynced();\n schedule();\n });\n };\n schedule();\n },\n close() {\n closed = true;\n if (timer) clearTimeout(timer);\n timer = undefined;\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-web",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.18",
|
|
4
4
|
"description": "Web cockpit for DoomPi: serves a browser session console and bridges it to an authenticated doompi-server socket",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cockpit",
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"tailwindcss": "4.3.3",
|
|
80
80
|
"vite": "8.2.2",
|
|
81
81
|
"web-push": "3.6.7",
|
|
82
|
-
"@agimon-ai/doompi": "0.0.1-alpha.
|
|
83
|
-
"@agimon-ai/doompi-config": "0.0.1-alpha.55",
|
|
82
|
+
"@agimon-ai/doompi": "0.0.1-alpha.57",
|
|
84
83
|
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.55",
|
|
85
|
-
"@agimon-ai/doompi-
|
|
84
|
+
"@agimon-ai/doompi-config": "0.0.1-alpha.55",
|
|
85
|
+
"@agimon-ai/doompi-server": "0.0.1-alpha.20",
|
|
86
86
|
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.55",
|
|
87
87
|
"@agimon-ai/doompi-web-components": "0.0.1-alpha.16",
|
|
88
88
|
"@agimon-ai/doompi-web-contracts": "0.0.1-alpha.19",
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { describeSyncDrift, readSyncDrift } from '@agimon-ai/doompi/services';
|
|
4
|
+
import { describeSyncDrift, readSyncDrift, readSyncRegistration } from '@agimon-ai/doompi/services';
|
|
5
5
|
import { findRepositoryRoot } from '@agimon-ai/doompi/utils';
|
|
6
6
|
import { repositoryDoomPiCli } from './bundledServer.ts';
|
|
7
|
+
import { webHostPackageRoot } from './webPluginGenerate.ts';
|
|
7
8
|
|
|
8
9
|
const DOOMPI_PACKAGE = '@agimon-ai/doompi';
|
|
9
10
|
const AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';
|
|
10
11
|
const SYNC_COMMAND_ENV = 'DOOMPI_SYNC_COMMAND';
|
|
11
12
|
const BOOTSTRAP_ENTRY_ENV = 'DOOMPI_BOOTSTRAP_ENTRY';
|
|
12
13
|
const HARNESS_ROOT_ENV = 'DOOMPI_ROOT';
|
|
14
|
+
const WEB_PACKAGE_ROOT_ENV = 'DOOMPI_WEB_PACKAGE_ROOT';
|
|
13
15
|
const CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];
|
|
14
16
|
const SYNC_ARGS = ['sync'];
|
|
15
17
|
/** How often the watcher re-reads the drift inputs. */
|
|
@@ -79,7 +81,11 @@ function spawnSync(repoRoot: string): Promise<SyncRunOutcome> {
|
|
|
79
81
|
return new Promise((resolve) => {
|
|
80
82
|
const child = spawn(process.execPath, [cli, ...SYNC_ARGS], {
|
|
81
83
|
cwd: repoRoot,
|
|
82
|
-
env: {
|
|
84
|
+
env: {
|
|
85
|
+
...process.env,
|
|
86
|
+
[HARNESS_ROOT_ENV]: repoRoot,
|
|
87
|
+
[WEB_PACKAGE_ROOT_ENV]: process.env[WEB_PACKAGE_ROOT_ENV] || webHostPackageRoot(),
|
|
88
|
+
},
|
|
83
89
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
90
|
});
|
|
85
91
|
const tail: string[] = [];
|
|
@@ -122,7 +128,14 @@ export function createSyncGuard(options: SyncGuardOptions): SyncGuard {
|
|
|
122
128
|
const expectedBootstrapEntry = process.env[BOOTSTRAP_ENTRY_ENV];
|
|
123
129
|
const readDrift =
|
|
124
130
|
options.readDrift ??
|
|
125
|
-
((root: string, entry?: string) =>
|
|
131
|
+
((root: string, entry?: string) => {
|
|
132
|
+
const drift = readSyncDrift({ repoRoot: root, expectedBootstrapEntry: entry });
|
|
133
|
+
// A CLI-only sync is valid for the terminal, but cannot supply session UI.
|
|
134
|
+
if (drift.fresh && readSyncRegistration(root)?.webDirectory == null) {
|
|
135
|
+
return { fresh: false, reasons: ['cockpit-bundle-missing'] };
|
|
136
|
+
}
|
|
137
|
+
return drift;
|
|
138
|
+
});
|
|
126
139
|
const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;
|
|
127
140
|
let inFlight: Promise<{ ready: boolean; rebuilt: boolean; error?: string }> | undefined;
|
|
128
141
|
let timer: ReturnType<typeof setTimeout> | undefined;
|