@agimon-ai/doompi-web 0.0.1-alpha.11 → 0.0.1-alpha.13

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 n=require("node:path");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi/services"),i=require("node:child_process"),a=require("node:module"),o=require("@agimon-ai/doompi/utils");const s=[`dist`,`bin`,`cli.mjs`],c=[`sync`];function l(){return{ensureSynced:async()=>{},watch:()=>{},close:()=>{}}}function u(e){return t.repositoryDoomPiCli(e)??process.env.DOOMPI_AGENT_COMMAND??n.default.join(n.default.dirname((0,a.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/doompi/package.json`)),...s)}function d(e){let t=u(e);return new Promise(n=>{let r=(0,i.spawn)(process.execPath,[t,...c],{cwd:e,stdio:[`ignore`,`pipe`,`pipe`]}),a=[],o=e=>{a.push(e.toString()),a.length>20&&a.shift()};r.stdout?.on(`data`,o),r.stderr?.on(`data`,o),r.once(`error`,e=>n({ok:!1,detail:`it could not start: ${e.message}`})),r.once(`exit`,e=>{n(e===0?{ok:!0}:{ok:!1,detail:`exit ${String(e)}: ${a.join(``).trim().slice(-400)}`})})})}function f(e){let t=e.onNotice??(()=>{}),n=e.repoRoot;if(n===void 0)try{n=(e.findRoot??o.findRepositoryRoot)(e.cwd??process.cwd())}catch{return l()}let i=e.runSync??(e=>d(e)),a=e.readDrift??(e=>(0,r.readSyncDrift)({repoRoot:e})),s=e.intervalMs??2e3,c,u,f=!1,p=0,m,h=async e=>{t(`syncing: ${(0,r.describeSyncDrift)({fresh:!1,reasons:e.reasons})}`);let o=await i(n)??{ok:!0};if(!o.ok){p+=1;let e=`sync failed (${o.detail??`no detail`})`;return e!==m&&(t(e),m=e),!1}let s=a(n);if(!s.fresh){p+=1;let e=`sync completed but did not resolve drift (${(0,r.describeSyncDrift)({fresh:!1,reasons:s.reasons})})`;return e!==m&&(t(e),m=e),!1}return p=0,m=void 0,t(`sync complete`),!0},g=async()=>{if(f)return!1;if(c)return c;let e=a(n);return!e.fresh&&(c=h(e).finally(()=>{c=void 0}),c)};return{async ensureSynced(){await g()},watch(e){if(u)return;let t=()=>{if(f)return;let e=Math.min(s*2**p,6e4);u=setTimeout(r,e),u.unref?.()},r=()=>{if(f||c){t();return}if(a(n).fresh){t();return}g().then(n=>{!f&&n&&e(),t()})};t()},close(){f=!0,u&&clearTimeout(u),u=void 0}}}exports.createSyncGuard=f;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./bundledServer.cjs");let n=require("node:path");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi/services"),i=require("node:child_process"),a=require("node:module"),o=require("@agimon-ai/doompi/utils");const s=[`dist`,`bin`,`cli.mjs`],c=[`sync`];function l(){return{ensureSynced:async()=>{},watch:()=>{},close:()=>{}}}function u(e){return process.env.DOOMPI_SYNC_COMMAND??t.repositoryDoomPiCli(e)??process.env.DOOMPI_AGENT_COMMAND??n.default.join(n.default.dirname((0,a.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/doompi/package.json`)),...s)}function d(e){let t=u(e);return new Promise(n=>{let r=(0,i.spawn)(process.execPath,[t,...c],{cwd:e,stdio:[`ignore`,`pipe`,`pipe`]}),a=[],o=e=>{a.push(e.toString()),a.length>20&&a.shift()};r.stdout?.on(`data`,o),r.stderr?.on(`data`,o),r.once(`error`,e=>n({ok:!1,detail:`it could not start: ${e.message}`})),r.once(`exit`,e=>{n(e===0?{ok:!0}:{ok:!1,detail:`exit ${String(e)}: ${a.join(``).trim().slice(-400)}`})})})}function f(e){let t=e.onNotice??(()=>{}),n=e.repoRoot;if(n===void 0)try{n=(e.findRoot??o.findRepositoryRoot)(e.cwd??process.cwd())}catch{return l()}let i=e.runSync??(e=>d(e)),a=e.readDrift??(e=>(0,r.readSyncDrift)({repoRoot:e})),s=e.intervalMs??2e3,c,u,f=!1,p=0,m,h=async e=>{t(`syncing: ${(0,r.describeSyncDrift)({fresh:!1,reasons:e.reasons})}`);let o=await i(n)??{ok:!0};if(!o.ok){p+=1;let e=`sync failed (${o.detail??`no detail`})`;return e!==m&&(t(e),m=e),!1}let s=a(n);if(!s.fresh){p+=1;let e=`sync completed but did not resolve drift (${(0,r.describeSyncDrift)({fresh:!1,reasons:s.reasons})})`;return e!==m&&(t(e),m=e),!1}return p=0,m=void 0,t(`sync complete`),!0},g=async()=>{if(f)return!1;if(c)return c;let e=a(n);return!e.fresh&&(c=h(e).finally(()=>{c=void 0}),c)};return{async ensureSynced(){await g()},watch(e){if(u)return;let t=()=>{if(f)return;let e=Math.min(s*2**p,6e4);u=setTimeout(r,e),u.unref?.()},r=()=>{if(f||c){t();return}if(a(n).fresh){t();return}g().then(n=>{!f&&n&&e(),t()})};t()},close(){f=!0,u&&clearTimeout(u),u=void 0}}}exports.createSyncGuard=f;
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 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 drift read. */\n readDrift?: (repoRoot: 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 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], { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });\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 readDrift = options.readDrift ?? ((root: string) => readSyncDrift({ repoRoot: root }));\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<boolean> | 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 syncOnce = async (drift: { reasons: readonly string[] }): Promise<boolean> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n const outcome = (await runSync(repoRoot)) ?? { ok: true };\n if (!outcome.ok) {\n consecutiveFailures += 1;\n const message = `sync failed (${outcome.detail ?? 'no detail'})`;\n // The same failure every few seconds buries everything else in the log.\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n const remaining = readDrift(repoRoot);\n if (!remaining.fresh) {\n consecutiveFailures += 1;\n const message = `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`;\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return true;\n };\n\n const ensureSynced = async (): Promise<boolean> => {\n if (closed) return false;\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);\n if (drift.fresh) return false;\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n await ensureSynced();\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);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((rebuilt) => {\n if (!closed && 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","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 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 drift read. */\n readDrift?: (repoRoot: 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], { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });\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 readDrift = options.readDrift ?? ((root: string) => readSyncDrift({ repoRoot: root }));\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<boolean> | 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 syncOnce = async (drift: { reasons: readonly string[] }): Promise<boolean> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n const outcome = (await runSync(repoRoot)) ?? { ok: true };\n if (!outcome.ok) {\n consecutiveFailures += 1;\n const message = `sync failed (${outcome.detail ?? 'no detail'})`;\n // The same failure every few seconds buries everything else in the log.\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n const remaining = readDrift(repoRoot);\n if (!remaining.fresh) {\n consecutiveFailures += 1;\n const message = `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`;\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return true;\n };\n\n const ensureSynced = async (): Promise<boolean> => {\n if (closed) return false;\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);\n if (drift.fresh) return false;\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n await ensureSynced();\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);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((rebuilt) => {\n if (!closed && 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":"+QAOA,MAGM,EAAe,CAAC,OAAQ,MAAO,SAAS,EACxC,EAAY,CAAC,MAAM,EAsCzB,SAAS,GAA+B,CACtC,MAAO,CACL,aAAc,SAAY,CAAC,EAC3B,UAAa,CAAC,EACd,UAAa,CAAC,CAChB,CACF,CASA,SAAS,EAAW,EAA0B,CAC5C,OACE,QAAQ,IAAI,qBACZA,EAAAA,oBAAoB,CAAQ,GAC5B,QAAQ,IAAI,sBACZC,EAAAA,QAAK,KAAKA,EAAAA,QAAK,SAAA,EAAQC,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,CAAC,CAAC,QAAQ,gCAAgC,CAAC,EAAG,GAAG,CAAY,CAErH,CAGA,SAAS,EAAU,EAA2C,CAC5D,IAAM,EAAM,EAAW,CAAQ,EAC/B,OAAO,IAAI,QAAS,GAAY,CAC9B,IAAM,GAAA,EAAQC,EAAAA,MAAAA,CAAM,QAAQ,SAAU,CAAC,EAAK,GAAG,CAAS,EAAG,CAAE,IAAK,EAAU,MAAO,CAAC,SAAU,OAAQ,MAAM,CAAE,CAAC,EACzG,EAAiB,CAAC,EAClB,EAAQ,GAAwB,CACpC,EAAK,KAAK,EAAM,SAAS,CAAC,EACtB,EAAK,OAAS,IAAI,EAAK,MAAM,CACnC,EACA,EAAM,QAAQ,GAAG,OAAQ,CAAI,EAC7B,EAAM,QAAQ,GAAG,OAAQ,CAAI,EAI7B,EAAM,KAAK,QAAU,GAAU,EAAQ,CAAE,GAAI,GAAO,OAAQ,uBAAuB,EAAM,SAAU,CAAC,CAAC,EACrG,EAAM,KAAK,OAAS,GAAS,CACX,EAAZ,IAAS,EAAW,CAAE,GAAI,EAAK,EACtB,CAAE,GAAI,GAAO,OAAQ,QAAQ,OAAO,CAAI,EAAE,IAAI,EAAK,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,GAAI,CAAC,CACjG,CAAC,CACH,CAAC,CACH,CAUA,SAAgB,EAAgB,EAAsC,CACpE,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC7C,EAAW,EAAQ,SACvB,GAAI,IAAa,IAAA,GACf,GAAI,CACF,GAAY,EAAQ,UAAYC,EAAAA,mBAAAA,CAAoB,EAAQ,KAAO,QAAQ,IAAI,CAAC,CAClF,MAAQ,CACN,OAAO,EAAkB,CAC3B,CAEF,IAAM,EAAU,EAAQ,UAAa,GAAiB,EAAU,CAAI,GAC9D,EAAY,EAAQ,YAAe,IAAA,EAAiBC,EAAAA,cAAAA,CAAc,CAAE,SAAU,CAAK,CAAC,GACpF,EAAe,EAAQ,YAAc,IACvC,EACA,EACA,EAAS,GACT,EAAsB,EACtB,EASE,EAAW,KAAO,IAA4D,CAClF,EAAO,aAAA,EAAYC,EAAAA,kBAAAA,CAAkB,CAAE,MAAO,GAAO,QAAS,EAAM,OAAiB,CAAC,GAAG,EACzF,IAAM,EAAW,MAAM,EAAQ,CAAQ,GAAM,CAAE,GAAI,EAAK,EACxD,GAAI,CAAC,EAAQ,GAAI,CACf,GAAuB,EACvB,IAAM,EAAU,gBAAgB,EAAQ,QAAU,YAAY,GAM9D,OAJI,IAAY,IACd,EAAO,CAAO,EACd,EAAoB,GAEf,EACT,CACA,IAAM,EAAY,EAAU,CAAQ,EACpC,GAAI,CAAC,EAAU,MAAO,CACpB,GAAuB,EACvB,IAAM,EAAU,8CAAA,EAA6CA,EAAAA,kBAAAA,CAAkB,CAAE,MAAO,GAAO,QAAS,EAAU,OAAiB,CAAC,EAAE,GAKtI,OAJI,IAAY,IACd,EAAO,CAAO,EACd,EAAoB,GAEf,EACT,CAIA,MAHA,GAAsB,EACtB,EAAoB,IAAA,GACpB,EAAO,eAAe,EACf,EACT,EAEM,EAAe,SAA8B,CACjD,GAAI,EAAQ,MAAO,GAGnB,GAAI,EAAU,OAAO,EACrB,IAAM,EAAQ,EAAU,CAAQ,EAKhC,MAJA,CAAI,EAAM,QACV,EAAW,EAAS,CAAK,CAAC,CAAC,YAAc,CACvC,EAAW,IAAA,EACb,CAAC,EACM,EACT,EAEA,MAAO,CACL,MAAM,cAAe,CACnB,MAAM,EAAa,CACrB,EACA,MAAM,EAAU,CACd,GAAI,EAAO,OACX,IAAM,MAAuB,CAC3B,GAAI,EAAQ,OAIZ,IAAM,EAAQ,KAAK,IAAI,EAAe,GAAK,EAAqB,GAAqB,EACrF,EAAQ,WAAW,EAAM,CAAK,EAE9B,EAAM,QAAQ,CAChB,EACM,MAAmB,CACvB,GAAI,GAAU,EAAU,CACtB,EAAS,EACT,MACF,CAEA,GADc,EAAU,CAChB,CAAC,CAAC,MAAO,CACf,EAAS,EACT,MACF,CACA,EAAkB,CAAC,CAAC,KAAM,GAAY,CAChC,CAAC,GAAU,GAAS,EAAS,EACjC,EAAS,CACX,CAAC,CACH,EACA,EAAS,CACX,EACA,OAAQ,CACN,EAAS,GACL,GAAO,aAAa,CAAK,EAC7B,EAAQ,IAAA,EACV,CACF,CACF"}
@@ -1,2 +1,2 @@
1
- import{repositoryDoomPiCli as e}from"./bundledServer.mjs";import{createRequire as t}from"node:module";import n from"node:path";import{describeSyncDrift as r,readSyncDrift as i}from"@agimon-ai/doompi/services";import{spawn as a}from"node:child_process";import{findRepositoryRoot as o}from"@agimon-ai/doompi/utils";const s=[`dist`,`bin`,`cli.mjs`],c=[`sync`];function l(){return{ensureSynced:async()=>{},watch:()=>{},close:()=>{}}}function u(r){return e(r)??process.env.DOOMPI_AGENT_COMMAND??n.join(n.dirname(t(import.meta.url).resolve(`@agimon-ai/doompi/package.json`)),...s)}function d(e){let t=u(e);return new Promise(n=>{let r=a(process.execPath,[t,...c],{cwd:e,stdio:[`ignore`,`pipe`,`pipe`]}),i=[],o=e=>{i.push(e.toString()),i.length>20&&i.shift()};r.stdout?.on(`data`,o),r.stderr?.on(`data`,o),r.once(`error`,e=>n({ok:!1,detail:`it could not start: ${e.message}`})),r.once(`exit`,e=>{n(e===0?{ok:!0}:{ok:!1,detail:`exit ${String(e)}: ${i.join(``).trim().slice(-400)}`})})})}function f(e){let t=e.onNotice??(()=>{}),n=e.repoRoot;if(n===void 0)try{n=(e.findRoot??o)(e.cwd??process.cwd())}catch{return l()}let a=e.runSync??(e=>d(e)),s=e.readDrift??(e=>i({repoRoot:e})),c=e.intervalMs??2e3,u,f,p=!1,m=0,h,g=async e=>{t(`syncing: ${r({fresh:!1,reasons:e.reasons})}`);let i=await a(n)??{ok:!0};if(!i.ok){m+=1;let e=`sync failed (${i.detail??`no detail`})`;return e!==h&&(t(e),h=e),!1}let o=s(n);if(!o.fresh){m+=1;let e=`sync completed but did not resolve drift (${r({fresh:!1,reasons:o.reasons})})`;return e!==h&&(t(e),h=e),!1}return m=0,h=void 0,t(`sync complete`),!0},_=async()=>{if(p)return!1;if(u)return u;let e=s(n);return!e.fresh&&(u=g(e).finally(()=>{u=void 0}),u)};return{async ensureSynced(){await _()},watch(e){if(f)return;let t=()=>{if(p)return;let e=Math.min(c*2**m,6e4);f=setTimeout(r,e),f.unref?.()},r=()=>{if(p||u){t();return}if(s(n).fresh){t();return}_().then(n=>{!p&&n&&e(),t()})};t()},close(){p=!0,f&&clearTimeout(f),f=void 0}}}export{f as createSyncGuard};
1
+ import{repositoryDoomPiCli as e}from"./bundledServer.mjs";import{createRequire as t}from"node:module";import n from"node:path";import{describeSyncDrift as r,readSyncDrift as i}from"@agimon-ai/doompi/services";import{spawn as a}from"node:child_process";import{findRepositoryRoot as o}from"@agimon-ai/doompi/utils";const s=[`dist`,`bin`,`cli.mjs`],c=[`sync`];function l(){return{ensureSynced:async()=>{},watch:()=>{},close:()=>{}}}function u(r){return process.env.DOOMPI_SYNC_COMMAND??e(r)??process.env.DOOMPI_AGENT_COMMAND??n.join(n.dirname(t(import.meta.url).resolve(`@agimon-ai/doompi/package.json`)),...s)}function d(e){let t=u(e);return new Promise(n=>{let r=a(process.execPath,[t,...c],{cwd:e,stdio:[`ignore`,`pipe`,`pipe`]}),i=[],o=e=>{i.push(e.toString()),i.length>20&&i.shift()};r.stdout?.on(`data`,o),r.stderr?.on(`data`,o),r.once(`error`,e=>n({ok:!1,detail:`it could not start: ${e.message}`})),r.once(`exit`,e=>{n(e===0?{ok:!0}:{ok:!1,detail:`exit ${String(e)}: ${i.join(``).trim().slice(-400)}`})})})}function f(e){let t=e.onNotice??(()=>{}),n=e.repoRoot;if(n===void 0)try{n=(e.findRoot??o)(e.cwd??process.cwd())}catch{return l()}let a=e.runSync??(e=>d(e)),s=e.readDrift??(e=>i({repoRoot:e})),c=e.intervalMs??2e3,u,f,p=!1,m=0,h,g=async e=>{t(`syncing: ${r({fresh:!1,reasons:e.reasons})}`);let i=await a(n)??{ok:!0};if(!i.ok){m+=1;let e=`sync failed (${i.detail??`no detail`})`;return e!==h&&(t(e),h=e),!1}let o=s(n);if(!o.fresh){m+=1;let e=`sync completed but did not resolve drift (${r({fresh:!1,reasons:o.reasons})})`;return e!==h&&(t(e),h=e),!1}return m=0,h=void 0,t(`sync complete`),!0},_=async()=>{if(p)return!1;if(u)return u;let e=s(n);return!e.fresh&&(u=g(e).finally(()=>{u=void 0}),u)};return{async ensureSynced(){await _()},watch(e){if(f)return;let t=()=>{if(p)return;let e=Math.min(c*2**m,6e4);f=setTimeout(r,e),f.unref?.()},r=()=>{if(p||u){t();return}if(s(n).fresh){t();return}_().then(n=>{!p&&n&&e(),t()})};t()},close(){p=!0,f&&clearTimeout(f),f=void 0}}}export{f 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 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 drift read. */\n readDrift?: (repoRoot: 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 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], { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });\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 readDrift = options.readDrift ?? ((root: string) => readSyncDrift({ repoRoot: root }));\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<boolean> | 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 syncOnce = async (drift: { reasons: readonly string[] }): Promise<boolean> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n const outcome = (await runSync(repoRoot)) ?? { ok: true };\n if (!outcome.ok) {\n consecutiveFailures += 1;\n const message = `sync failed (${outcome.detail ?? 'no detail'})`;\n // The same failure every few seconds buries everything else in the log.\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n const remaining = readDrift(repoRoot);\n if (!remaining.fresh) {\n consecutiveFailures += 1;\n const message = `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`;\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return true;\n };\n\n const ensureSynced = async (): Promise<boolean> => {\n if (closed) return false;\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);\n if (drift.fresh) return false;\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n await ensureSynced();\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);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((rebuilt) => {\n if (!closed && 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 } 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 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 drift read. */\n readDrift?: (repoRoot: 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], { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });\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 readDrift = options.readDrift ?? ((root: string) => readSyncDrift({ repoRoot: root }));\n const baseInterval = options.intervalMs ?? WATCH_INTERVAL_MS;\n let inFlight: Promise<boolean> | 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 syncOnce = async (drift: { reasons: readonly string[] }): Promise<boolean> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n const outcome = (await runSync(repoRoot)) ?? { ok: true };\n if (!outcome.ok) {\n consecutiveFailures += 1;\n const message = `sync failed (${outcome.detail ?? 'no detail'})`;\n // The same failure every few seconds buries everything else in the log.\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n const remaining = readDrift(repoRoot);\n if (!remaining.fresh) {\n consecutiveFailures += 1;\n const message = `sync completed but did not resolve drift (${describeSyncDrift({ fresh: false, reasons: remaining.reasons as never })})`;\n if (message !== lastFailureNotice) {\n notice(message);\n lastFailureNotice = message;\n }\n return false;\n }\n consecutiveFailures = 0;\n lastFailureNotice = undefined;\n notice('sync complete');\n return true;\n };\n\n const ensureSynced = async (): Promise<boolean> => {\n if (closed) return false;\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);\n if (drift.fresh) return false;\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n async ensureSynced() {\n await ensureSynced();\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);\n if (drift.fresh) {\n schedule();\n return;\n }\n void ensureSynced().then((rebuilt) => {\n if (!closed && 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.11",
3
+ "version": "0.0.1-alpha.13",
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",
@@ -76,14 +76,14 @@
76
76
  "tailwindcss": "4.3.3",
77
77
  "vite": "8.2.2",
78
78
  "web-push": "3.6.7",
79
- "@agimon-ai/doompi-config": "0.0.1-alpha.49",
80
- "@agimon-ai/doompi": "0.0.1-alpha.50",
81
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.49",
82
- "@agimon-ai/doompi-server": "0.0.1-alpha.13",
83
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.49",
84
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.10",
85
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.13",
86
- "@agimon-ai/doompi-web-security": "0.0.1-alpha.11"
79
+ "@agimon-ai/doompi": "0.0.1-alpha.52",
80
+ "@agimon-ai/doompi-config": "0.0.1-alpha.51",
81
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.51",
82
+ "@agimon-ai/doompi-server": "0.0.1-alpha.15",
83
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.51",
84
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.12",
85
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.15",
86
+ "@agimon-ai/doompi-web-security": "0.0.1-alpha.13"
87
87
  },
88
88
  "devDependencies": {
89
89
  "@playwright/test": "1.62.1",
@@ -97,7 +97,7 @@
97
97
  "typescript": "7.0.2",
98
98
  "vitest": "4.1.11",
99
99
  "ws": "8.21.3",
100
- "@agimon-ai/vibe-lint-plugin-doom-web": "0.0.1-alpha.46"
100
+ "@agimon-ai/vibe-lint-plugin-doom-web": "0.0.1-alpha.48"
101
101
  },
102
102
  "engines": {
103
103
  "node": ">=22.19.0"
@@ -7,6 +7,7 @@ import { repositoryDoomPiCli } from './bundledServer.ts';
7
7
 
8
8
  const DOOMPI_PACKAGE = '@agimon-ai/doompi';
9
9
  const AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';
10
+ const SYNC_COMMAND_ENV = 'DOOMPI_SYNC_COMMAND';
10
11
  const CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];
11
12
  const SYNC_ARGS = ['sync'];
12
13
  /** How often the watcher re-reads the drift inputs. */
@@ -63,6 +64,7 @@ function inactiveSyncGuard(): SyncGuard {
63
64
  */
64
65
  function syncCliFor(repoRoot: string): string {
65
66
  return (
67
+ process.env[SYNC_COMMAND_ENV] ??
66
68
  repositoryDoomPiCli(repoRoot) ??
67
69
  process.env[AGENT_COMMAND_ENV] ??
68
70
  path.join(path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)), ...CLI_SEGMENTS)