diff --git a/.gitignore b/.gitignore index 5e0266c..aff9a41 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ target/ .env .env.* *.curio +!/source/packages/**/*.curio .DS_Store /curios/*/backend/ diff --git a/docs/development.md b/docs/development.md index 54b0cc7..733533c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -38,7 +38,7 @@ pnpm run serve 完整步骤、覆盖矩阵与当前限制见 [测试指南](testing.md)。 -插件各自通过 `curio.json.version` 管理版本;公开服务契约版本与底座兼容范围分别维护。历史发布索引只追加版本、禁止覆盖同版本产物,详见 [版本与发布](versioning.md)。`pnpm run release:prepare --base-url ` 可准备保留历史的发布材料,`pnpm run test:release` 验证这些约束。 +插件各自通过 `curio.json.version` 管理版本;公开服务契约版本与底座兼容范围分别维护。历史发布索引只追加版本、禁止覆盖同版本产物,详见 [版本与发布](versioning.md)。`pnpm run release:prepare` 将安装包与索引准备到 `source/`,提交并推送后即可通过 Git 仓库源安装。`pnpm run test:release` 验证版本约束。 ## 目录 @@ -54,7 +54,7 @@ pnpm run serve 1. 各插件独立声明版本、平台、依赖与权限。 2. 构建并执行 trove curio check / pack。macOS 原生后端使用 Mac Runner 构建。 -3. 先上传不可变的版本包,再更新索引中的下载 URL、大小和 SHA-256。 -4. 最后原子发布 index.json。私有 Git 仓库不等于私有下载源;下载地址应符合分发对象的访问权限。 +3. 执行 `pnpm run release:prepare`,检查 `source/index.json` 的新增版本与 `source/packages/` 中的不可变版本包。 +4. 将索引和安装包在同一提交中推送,再使用 Git 仓库地址完成远程安装与运行验证。私有仓库沿用本机 Git 认证。 官网:https://trove.render.ink diff --git a/docs/testing.md b/docs/testing.md index 1a7409b..347657f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -99,17 +99,18 @@ pnpm run verify ## 远程软件源验收 -发布完成后,必须使用底座实际访问的索引 URL 单独验收: +发布完成后,必须使用底座实际访问的 Git 仓库单独验收: ```sh TROVE_APP_DIR=/absolute/path/to/app pnpm run verify \ - --remote-source https://downloads.example/source/index.json \ + --remote-source git@git.render.ink:trove/curios.git \ + --source-kind git \ --source-id trove-official ``` -此模式不读取本地 `.curio`:在新的临时目录中通过真实 PackageManager 从远程刷新索引、下载并安装其中的每个版本,检查 UI 入口,启动安装后的原生后端,再执行 consumer → provider 调用。结果写入 `dist/remote-verification.json`;失败时非零退出,不能视为安装通过。 +此模式不读取工作区的 `.curio`:在新的临时目录中通过真实 PackageManager 获取远程仓库、读取索引、安装该提交中的每个版本,检查 UI 入口,启动安装后的原生后端,再执行 consumer → provider 调用。结果写入 `dist/remote-verification.json`;失败时非零退出,不能视为安装通过。独立 HTTP 源可使用 `--source-kind http` 并传入索引 URL。 -当前运行探针适用于生产源中的首批验收插件;后续新增产品插件时,应为它们补充对应的运行验收。故障源包含故意无法启动的后端,不能以全部启动成功作为标准。示例域名须替换为真实分发地址;直接读取私有 Gitea 仓库需要登录,推送源码本身不代表已发布软件源。下载可见性和历史发布管理见 [版本与发布](versioning.md)。 +当前运行探针适用于生产源中的首批验收插件;后续新增产品插件时,应为它们补充对应的运行验收。故障源包含故意无法启动的后端,不能以全部启动成功作为标准。仓库结构、认证和历史发布管理见 [版本与发布](versioning.md)。 ## 当前能力边界 diff --git a/docs/versioning.md b/docs/versioning.md index 9d470f0..241a288 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -32,12 +32,11 @@ packages/dev.trove.qa.self/1.0.0/x86_64-apple-darwin.curio 先修改需要发布的 Manifest,执行 `pnpm run pack` 和验证。`dist/index.json` 是可反复生成的本地开发索引;不能把它当作历史发布记录。 ```sh -# 必须是确定的、可由底座访问的 HTTPS 分发目录;示例域名须替换。 -pnpm run release:prepare --base-url https://downloads.example/source/ +pnpm run release:prepare pnpm run test:release ``` -命令会将本次打包的常规插件合并到生产源 `source/index.json`(源 ID 为 `trove-official`),并在 `dist/release/` 准备同目录结构的上传材料。故障注入插件和无效包不会进入生产源。未确定分发地址时不预先填入不可访问的下载地址。 +命令会将本次打包的常规插件合并到生产源 `source/index.json`(源 ID 为 `trove-official`),并把不可变安装包复制到 `source/packages/`。索引中的包地址相对于 `source/`,索引与包在同一 Git 提交中发布。`dist/release/` 保留本次发布材料副本。故障注入插件和无效包不会进入生产源。 校验规则: @@ -48,21 +47,27 @@ pnpm run test:release - 重复准备相同材料不会改变首次发布日期。 - 先完整校验本次所有条目,再复制产物和更新本地索引。 -`--catalog <目录>` 可在隔离目录试运行。正式发布应将 `source/` 的历史索引提交回仓库。该脚本只准备本地材料,不表示上传或远程安装已经成功。 +`--catalog <目录>` 可在隔离目录试运行。正式发布应将 `source/` 的历史索引与安装包提交回仓库。该脚本只准备本地材料,不表示推送或远程安装已经成功。 + +需要独立 HTTP 分发时,可显式传入 `--base-url ` 生成绝对下载地址,并使用 `dist/release/` 上传;已有版本的 URL 仍不能覆盖。 ## 发布顺序 -1. 提交并推送源代码,记录 Git commit;需要时为各插件打独立 tag。 -2. 构建和验证,准备发布索引;审阅本次新增的版本与产物。 -3. 上传新安装包,拒绝覆盖已存在且内容不同的文件。 -4. 从底座实际使用的网络路径下载每个新增包,核对 SHA-256 和大小。 -5. 最后原子替换远程索引,提交并推送仓库中的发布索引。 -6. 从该远程源刷新、安装并运行验证;不能用本地文件安装冒充远程源验收。 +1. 更新需要发布的插件版本,构建并完成验证。 +2. 执行 `pnpm run release:prepare`;审阅本次新增的版本、架构、大小和 SHA-256。 +3. 将源码、`source/index.json` 和 `source/packages/` 在同一提交中推送;需要时为各插件打独立 tag。 +4. 从远程 Git 仓库刷新、安装并运行验证;不能用本地文件安装冒充远程源验收。 发布工作串行进行;两种架构先各自构建,再将产物合并至同一版本。若更新 `shared/ui/` 或 `shared/backend/`,所有受影响且需要重新发布的插件都要增加插件版本,未受影响的插件不必修改。 -## 私有源码与下载认证 +## 添加仓库源 -当前 `https://git.render.ink/trove/curios` 是私有源码仓库,匿名访问原始 JSON 会跳转到登录页。推送源代码本身不能使它成为可安装的软件源。 +在 Trove「软件源 → 添加软件源」中填写: -Trove 当前 HTTP 软件源没有仓库登录凭据配置。生产分发可使用免登录的安装包/索引下载入口,或在底座支持私有软件源认证后使用受保护的入口;不要把访问令牌写进 Manifest、源码索引或下载 URL。源码仓库可保持私有,安装包的可见性单独决定。 +- 显示名称:Trove 官方软件源 +- 唯一 ID:`trove-official` +- 源类型:Git 仓库 +- 仓库地址:`git@git.render.ink:trove/curios.git` +- 优先级:`0` + +也支持 HTTPS 克隆地址 `https://git.render.ink/trove/curios.git`。私有仓库沿用本机 Git 的 SSH 密钥或 HTTPS credential helper;不要把访问令牌写进 Manifest、索引或 URL。添加后刷新,底座自动读取默认分支的 `source/index.json`,并固定该提交读取仓库内的安装包。用户不需要填写原始 JSON 地址或配置额外的静态下载站点。 diff --git a/source/index.json b/source/index.json index 8ec7e26..0d911f2 100644 --- a/source/index.json +++ b/source/index.json @@ -2,6 +2,198 @@ "schemaVersion": 1, "id": "trove-official", "name": "Trove 官方软件源", - "generatedAt": "2026-09-08T07:48:26.789Z", - "curios": [] + "generatedAt": "2026-09-09T12:17:24.404Z", + "curios": [ + { + "id": "dev.trove.qa.host", + "name": "01 · Host 能力检查", + "description": "UI、SDK 桥接、服务发现、存储、文件和桌面交互。", + "versions": [ + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "any", + "url": "packages/dev.trove.qa.host/1.0.0/any.curio", + "sha256": "15a644d1231b237092b3c4ed19ccece12e92e8f78aac681af2279150da65bad1", + "size": 20082 + } + ] + } + ] + }, + { + "id": "dev.trove.qa.isolation", + "name": "02 · 数据隔离对照", + "description": "与 Host 检查使用相同存储键,验证插件间独立命名空间。", + "versions": [ + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "any", + "url": "packages/dev.trove.qa.isolation/1.0.0/any.curio", + "sha256": "8bfc084b75e91874b6c6c488c8703b2ccd5529a92f9f2cb56ab57f69c491e687", + "size": 20106 + } + ] + } + ] + }, + { + "id": "dev.trove.qa.self", + "name": "03 · 原生后端检查", + "description": "UI → @self → 原生 Rust 后端 → Host;并发、取消、事件。", + "versions": [ + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "aarch64-apple-darwin", + "url": "packages/dev.trove.qa.self/1.0.0/aarch64-apple-darwin.curio", + "sha256": "328d39a3ce95d33eae8b1575c307a9aec8d3c5319e89c250b04c1389ecea5b5e", + "size": 346745 + } + ] + } + ] + }, + { + "id": "dev.trove.qa.provider", + "name": "04 · 公共服务提供方", + "description": "无 UI,按需启动;20 秒空闲后退出。", + "versions": [ + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "aarch64-apple-darwin", + "url": "packages/dev.trove.qa.provider/1.0.0/aarch64-apple-darwin.curio", + "sha256": "726f0f47bf50294f9be067437995dbbb59b3b52017b95bff665273165ff2a46d", + "size": 327830 + } + ] + } + ] + }, + { + "id": "dev.trove.qa.consumer", + "name": "05 · 跨插件调用", + "description": "依赖公共服务;验证发现、契约、事件与后端回调。", + "versions": [ + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "any", + "url": "packages/dev.trove.qa.consumer/1.0.0/any.curio", + "sha256": "503112dd4fc316e370c6f95fdca717f02f23ac68a02e751780a43a721dfb2bd3", + "size": 20079 + } + ] + } + ] + }, + { + "id": "dev.trove.qa.optional", + "name": "06 · 可选依赖缺失", + "description": "缺失可选服务时 UI 仍可工作,调用返回 service.not_found。", + "versions": [ + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "any", + "url": "packages/dev.trove.qa.optional/1.0.0/any.curio", + "sha256": "ce01d0508b7b1610e835f87095b5f1e45ad1652c924ecb58d113efa4a3c47cc7", + "size": 20101 + } + ] + } + ] + }, + { + "id": "dev.trove.qa.autostart", + "name": "07 · 随底座启动", + "description": "无 UI 的 app-start 后端;重启底座后检查状态和 PID。", + "versions": [ + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "aarch64-apple-darwin", + "url": "packages/dev.trove.qa.autostart/1.0.0/aarch64-apple-darwin.curio", + "sha256": "38605052d1c7fade9301ea4972b2668a94ab5e563058a1de63c8937b4717d5b7", + "size": 327826 + } + ] + } + ] + }, + { + "id": "dev.trove.qa.upgrade", + "name": "08 · 升级与数据保留", + "description": "1.1.0 更新;显示版本并读取基线标记。", + "versions": [ + { + "version": "1.1.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "any", + "url": "packages/dev.trove.qa.upgrade/1.1.0/any.curio", + "sha256": "500493b4156c3f143c8bc685bad495a41e320f9d7b8346a5ea939141d037a0f2", + "size": 20033 + } + ] + }, + { + "version": "1.0.0", + "engine": { + "trove": ">=0.1.0 <1.0.0" + }, + "releasedAt": "2026-09-09T12:17:24.404Z", + "artifacts": [ + { + "target": "any", + "url": "packages/dev.trove.qa.upgrade/1.0.0/any.curio", + "sha256": "834a1159d18b1d44b5aeb38cf9b60f5a1868df06cdc370d4d08026170e1bb19f", + "size": 20030 + } + ] + } + ] + } + ] } diff --git a/source/packages/dev.trove.qa.autostart/1.0.0/aarch64-apple-darwin.curio b/source/packages/dev.trove.qa.autostart/1.0.0/aarch64-apple-darwin.curio new file mode 100644 index 0000000..88d2653 Binary files /dev/null and b/source/packages/dev.trove.qa.autostart/1.0.0/aarch64-apple-darwin.curio differ diff --git a/source/packages/dev.trove.qa.consumer/1.0.0/any.curio b/source/packages/dev.trove.qa.consumer/1.0.0/any.curio new file mode 100644 index 0000000..d72e2d4 Binary files /dev/null and b/source/packages/dev.trove.qa.consumer/1.0.0/any.curio differ diff --git a/source/packages/dev.trove.qa.host/1.0.0/any.curio b/source/packages/dev.trove.qa.host/1.0.0/any.curio new file mode 100644 index 0000000..5f5414f Binary files /dev/null and b/source/packages/dev.trove.qa.host/1.0.0/any.curio differ diff --git a/source/packages/dev.trove.qa.isolation/1.0.0/any.curio b/source/packages/dev.trove.qa.isolation/1.0.0/any.curio new file mode 100644 index 0000000..e431203 Binary files /dev/null and b/source/packages/dev.trove.qa.isolation/1.0.0/any.curio differ diff --git a/source/packages/dev.trove.qa.optional/1.0.0/any.curio b/source/packages/dev.trove.qa.optional/1.0.0/any.curio new file mode 100644 index 0000000..1065008 Binary files /dev/null and b/source/packages/dev.trove.qa.optional/1.0.0/any.curio differ diff --git a/source/packages/dev.trove.qa.provider/1.0.0/aarch64-apple-darwin.curio b/source/packages/dev.trove.qa.provider/1.0.0/aarch64-apple-darwin.curio new file mode 100644 index 0000000..e49794e Binary files /dev/null and b/source/packages/dev.trove.qa.provider/1.0.0/aarch64-apple-darwin.curio differ diff --git a/source/packages/dev.trove.qa.self/1.0.0/aarch64-apple-darwin.curio b/source/packages/dev.trove.qa.self/1.0.0/aarch64-apple-darwin.curio new file mode 100644 index 0000000..1106f9c Binary files /dev/null and b/source/packages/dev.trove.qa.self/1.0.0/aarch64-apple-darwin.curio differ diff --git a/source/packages/dev.trove.qa.upgrade/1.0.0/any.curio b/source/packages/dev.trove.qa.upgrade/1.0.0/any.curio new file mode 100644 index 0000000..52ce761 Binary files /dev/null and b/source/packages/dev.trove.qa.upgrade/1.0.0/any.curio differ diff --git a/source/packages/dev.trove.qa.upgrade/1.1.0/any.curio b/source/packages/dev.trove.qa.upgrade/1.1.0/any.curio new file mode 100644 index 0000000..51218b1 Binary files /dev/null and b/source/packages/dev.trove.qa.upgrade/1.1.0/any.curio differ diff --git a/tools/host-check.rs b/tools/host-check.rs index c5f3d76..25b9dc6 100644 --- a/tools/host-check.rs +++ b/tools/host-check.rs @@ -377,6 +377,7 @@ async fn main() -> Result<()> { return remote_source_check( args.get(2).context("remote source URL required")?, args.get(3).context("remote source ID required")?, + args.get(4).map(String::as_str).unwrap_or("http"), ) .await; } @@ -477,7 +478,12 @@ async fn main() -> Result<()> { Ok(()) } -async fn remote_source_check(url: &str, source_id: &str) -> Result<()> { +async fn remote_source_check(url: &str, source_id: &str, kind: &str) -> Result<()> { + let kind = match kind { + "git" => SourceKind::Git, + "http" => SourceKind::Http, + _ => anyhow::bail!("Remote source kind must be git or http"), + }; let sandbox = tempfile::Builder::new() .prefix("trove-remote-qa-") .tempdir_in("/tmp")?; @@ -497,7 +503,7 @@ async fn remote_source_check(url: &str, source_id: &str) -> Result<()> { packages.add_source(SourceConfig { id: source_id.into(), name: source_id.into(), - kind: SourceKind::Http, + kind, index: url.into(), enabled: true, priority: 0, @@ -599,8 +605,8 @@ async fn remote_source_check(url: &str, source_id: &str) -> Result<()> { sandbox.close()?; let failed = rows.iter().filter(|row| row["status"] == "fail").count(); emit(json!({ - "schemaVersion":1,"sourceUrl":url,"sourceId":source_id,"target":target(), - "scope":"Remote HTTP index and packages via real PackageManager; installed native backends via real UDS. Desktop UI rendering is not included.", + "schemaVersion":1,"sourceUrl":url,"sourceId":source_id,"sourceKind":kind,"target":target(), + "scope":"Remote source and packages via real PackageManager; installed native backends via real UDS. Desktop UI rendering is not included.", "passed":rows.len()-failed,"failed":failed,"temporaryDataRemoved":true,"results":rows, })); ensure!(failed == 0, "Remote source verification failed"); diff --git a/tools/prepare-release.mjs b/tools/prepare-release.mjs index d4e92c2..656965e 100644 --- a/tools/prepare-release.mjs +++ b/tools/prepare-release.mjs @@ -9,12 +9,11 @@ const { values } = parseArgs({ options: { 'base-url': { type: 'string' }, catalog: { type: 'string', default: 'source' }, } }); -if (!values['base-url']) throw new Error('Usage: pnpm run release:prepare --base-url https://downloads.example/source/'); -const base = new URL(values['base-url']); -if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash) { +const base = values['base-url'] ? new URL(values['base-url']) : null; +if (base && (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash)) { throw new Error('Publication base must be an HTTPS directory URL without credentials, query or fragment'); } -if (!base.pathname.endsWith('/')) base.pathname += '/'; +if (base && !base.pathname.endsWith('/')) base.pathname += '/'; const catalogDirectory = path.resolve(root, values.catalog); const publishDirectory = path.join(output, 'release'); const timestamp = new Date().toISOString(); @@ -41,18 +40,36 @@ for (const entry of entries) { const sha256 = createHash('sha256').update(bytes).digest('hex'); if (sha256 !== entry.artifact.sha256 || bytes.length !== entry.artifact.size) throw new Error(`Build metadata doesn't match ${entry.file}; run pack again`); const relative = artifactPath(entry.manifest, entry.artifact.target); - const url = new URL(relative.split('/').map(encodeURIComponent).join('/'), base).href; + const url = base ? new URL(relative.split('/').map(encodeURIComponent).join('/'), base).href : relative; const spec = specifications[0]; const artifact = { ...entry.artifact, url }; catalogs.set(spec.file, appendRelease(catalogs.get(spec.file), entry.manifest, artifact, timestamp)); files.push({ from: entry.file, relative, sha256, size: bytes.length, url }); } +// Git publication keeps immutable packages alongside the canonical index. +if (!base) { + for (const file of files) { + try { + const existing = await readFile(path.join(catalogDirectory, file.relative)); + if (existing.length !== file.size || createHash('sha256').update(existing).digest('hex') !== file.sha256) { + throw new Error(`Published artifact differs: ${file.relative}; bump the plugin version`); + } + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + } +} // Only the current approved publication plan belongs in the generated upload directory. await rm(publishDirectory, { recursive: true, force: true }); for (const file of files) { const destination = path.join(publishDirectory, file.relative); await mkdir(path.dirname(destination), { recursive: true }); await copyFile(file.from, destination); + if (!base) { + const published = path.join(catalogDirectory, file.relative); + await mkdir(path.dirname(published), { recursive: true }); + await copyFile(file.from, published); + } } await mkdir(catalogDirectory, { recursive: true }); await mkdir(publishDirectory, { recursive: true }); @@ -63,7 +80,7 @@ for (const [file, index] of catalogs) { await writeFile(temporary, text); await rename(temporary, path.join(catalogDirectory, file)); } -await writeFile(path.join(publishDirectory, 'upload-plan.json'), JSON.stringify({ baseUrl: base.href, files }, null, 2) + '\n'); +await writeFile(path.join(publishDirectory, 'upload-plan.json'), JSON.stringify({ kind: base ? 'http' : 'git', baseUrl: base?.href, files }, null, 2) + '\n'); console.log(`Prepared ${files.length} immutable artifacts in ${publishDirectory}`); console.log(`Catalog history: ${catalogDirectory}`); -console.log('Next: upload package files without overwrite, verify remote bytes, then atomically publish index.json.'); +console.log(base ? 'Next: upload package files without overwrite, verify remote bytes, then atomically publish index.json.' : 'Next: commit source/index.json and source/packages/ together, push the repository, then verify the Git source.'); diff --git a/tools/verify.mjs b/tools/verify.mjs index 54f7104..d48db5d 100644 --- a/tools/verify.mjs +++ b/tools/verify.mjs @@ -9,7 +9,12 @@ import { assert, equal, expectError, hostChecks, backendChecks, sleep } from '.. import { serve } from './serve.mjs'; // Only this optional integration adapter depends on the app checkout. -const { values: args } = parseArgs({ options: { 'remote-source': { type: 'string' }, 'source-id': { type: 'string', default: 'trove-official' } } }); +const { values: args } = parseArgs({ options: { + 'remote-source': { type: 'string' }, + 'source-id': { type: 'string', default: 'trove-official' }, + 'source-kind': { type: 'string', default: 'http' }, +} }); +if (!['git', 'http'].includes(args['source-kind'])) throw new Error('--source-kind must be git or http'); const reportPath = path.join(output, args['remote-source'] ? 'remote-verification.json' : 'verification.json'); await rm(reportPath, { force: true }); const app = process.env.TROVE_APP_DIR; @@ -41,7 +46,7 @@ const target = process.env.TROVE_QA_TARGET_DIR || path.join(output, 'host-target try { run('cargo', ['build', '--manifest-path', path.join(harness, 'Cargo.toml'), '--target-dir', target]); if (args['remote-source']) { - const result = spawnSync(path.join(target, 'debug/trove-qa-host-check'), ['--remote-source', args['remote-source'], args['source-id']], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], timeout: 180000 }); + const result = spawnSync(path.join(target, 'debug/trove-qa-host-check'), ['--remote-source', args['remote-source'], args['source-id'], args['source-kind']], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], timeout: 180000 }); if (result.error) throw result.error; const report = JSON.parse(result.stdout); report.generatedAt = new Date().toISOString();