UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

1,137 lines (1,135 loc) • 47.6 kB
const require_merge = require('../resolution/merge.cjs'); const require_http_client = require('../../utils/http-client.cjs'); const require_services_registry_fastkv = require('../registry/fastkv.cjs'); const require_load = require('../resolution/load.cjs'); const require_init_source = require('../resolution/init-source.cjs'); const require_services_resolution_session = require('../resolution/session.cjs'); const require_services_deploy_integrity = require('../deploy/integrity.cjs'); const require_workspace = require('../workspace/workspace.cjs'); const require_workspace_subtree = require('../../utils/workspace-subtree.cjs'); const require_snapshot_hash = require('../../utils/snapshot-hash.cjs'); const require_snapshot = require('./snapshot.cjs'); const require_init = require('./init.cjs'); const require_sync = require('./sync.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); let every_plugin_version_manifest = require("every-plugin/version-manifest"); let node_os = require("node:os"); let tar = require("tar"); let glob = require("glob"); //#region src/services/scaffold/import.ts const IMPORT_SNAPSHOT_FILE = "import-snapshot.json"; const IMPORT_REPORT_FILE = "import-report.md"; const IMPORT_REPORT_PATH = `.bos/${IMPORT_REPORT_FILE}`; function isExistingDir(path) { try { return (0, node_fs.existsSync)(path) && (0, node_fs.statSync)(path).isDirectory(); } catch { return false; } } function normalizeGithubRepo(spec) { if (require_init_source.parseGitHubUrl(spec)) return spec; const match = spec.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/); if (!match?.[1] || !match[2]) return null; if (match[1] === "." || match[1] === ".." || match[2] === "." || match[2] === "..") return null; return `https://github.com/${match[1]}/${match[2]}`; } function parseFromSpec(spec) { const trimmed = spec.trim(); if (!trimmed) throw new Error("--from is required: bos://<account>/<gateway>, <owner>/<repo>[@<ref>], a GitHub URL, or a local directory"); if (trimmed.startsWith("bos://")) { const hashIndex = trimmed.indexOf("#"); const bosPart = hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex); if (hashIndex === -1 ? void 0 : trimmed.slice(hashIndex + 1).trim()) throw new Error(`Targeted extends refs are not supported for import sources: ${spec}. Omit the #target and pass the workspace as the positional argument.`); const parsed = require_services_registry_fastkv.parseBosUrl(bosPart); if (parsed.pathSegments.length > 0) throw new Error(`Unsupported bos:// path — expected bos://<account>/<gateway>: ${spec}`); return { kind: "bos", bosUrl: `bos://${parsed.accountId}/${parsed.gatewayId}`, account: parsed.accountId, gateway: parsed.gatewayId }; } if (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../")) { if (!isExistingDir(trimmed)) throw new Error(`Local directory not found: ${trimmed}`); return { kind: "local", dir: (0, node_path.resolve)(trimmed) }; } const at = trimmed.indexOf("@"); if (at > 0) { const head = trimmed.slice(0, at); const ref = trimmed.slice(at + 1); const repo = normalizeGithubRepo(head); if (repo && ref) return { kind: "github", repo, ref }; } const repo = normalizeGithubRepo(trimmed); if (repo) return { kind: "github", repo }; if (isExistingDir(trimmed)) return { kind: "local", dir: (0, node_path.resolve)(trimmed) }; throw new Error(`Cannot parse --from source: ${spec}. Expected bos://<account>/<gateway>, <owner>/<repo>[@<ref>], a GitHub URL, or a local directory.`); } function normalizeWorkspacePath(input) { const trimmed = input.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, ""); if ((0, node_path.isAbsolute)(input)) throw new Error(`Workspace path must be repo-relative: ${input}`); if (!trimmed || trimmed === "." || trimmed === "..") throw new Error(`Invalid workspace path: ${input}`); if (trimmed.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`Invalid workspace path: ${input}`); return trimmed; } function classifyImportFile(input) { const { localHash, upstreamHash, baselineHash, mergeable } = input; if (localHash === null) return { kind: "added" }; if (localHash === upstreamHash) return { kind: "unchanged" }; if (baselineHash === void 0) return mergeable ? { kind: "merged" } : { kind: "conflict", reason: "no-baseline" }; if (localHash === baselineHash) return mergeable ? { kind: "merged" } : { kind: "updated" }; if (upstreamHash === baselineHash) return { kind: "preserved-local-edit" }; return mergeable ? { kind: "merged" } : { kind: "conflict", reason: "both-changed" }; } function extractContractRoutes(source) { const routes = /* @__PURE__ */ new Set(); const marker = ".route("; let cursor = source.indexOf(marker); while (cursor !== -1) { const open = cursor + 7 - 1; let depth = 0; let end = open; for (; end < source.length; end++) { const char = source[end]; if (char === "(") depth++; else if (char === ")") { depth--; if (depth === 0) break; } } const arg = source.slice(open + 1, end); const method = arg.match(/\bmethod:\s*["']([A-Za-z]+)["']/)?.[1]; const path = arg.match(/\bpath:\s*["']([^"']+)["']/)?.[1]; if (method && path) routes.add(`${method} ${path}`); cursor = source.indexOf(marker, cursor + 1); } return [...routes].sort(); } function computeRouteDelta(ours, theirs) { const oursSet = new Set(ours); const theirsSet = new Set(theirs); return { oursOnly: ours.filter((route) => !theirsSet.has(route)), theirsOnly: theirs.filter((route) => !oursSet.has(route)) }; } async function collectContractRouteSources(root, workspace) { const sources = []; const mainPath = (0, node_path.join)(root, workspace, "src", "contract.ts"); if ((0, node_fs.existsSync)(mainPath)) sources.push((0, node_fs.readFileSync)(mainPath, "utf-8")); const splitDir = (0, node_path.join)(root, workspace, "src", "contract"); if ((0, node_fs.existsSync)(splitDir) && (0, node_fs.lstatSync)(splitDir).isDirectory()) { const matches = await (0, glob.glob)("**/*.{ts,tsx}", { cwd: splitDir, nodir: true, absolute: false }); for (const match of matches.sort()) sources.push((0, node_fs.readFileSync)((0, node_path.join)(splitDir, match), "utf-8")); } return sources.length > 0 ? sources : null; } function extractWorkspaceRoutes(sources) { if (sources === null) return null; const routes = /* @__PURE__ */ new Set(); for (const source of sources) for (const route of extractContractRoutes(source)) routes.add(route); return [...routes].sort(); } function loadMigrationEntries(root, workspace) { const journalPath = journalPathFor(root, workspace); if (!(0, node_fs.existsSync)(journalPath)) return null; let journal; try { journal = JSON.parse((0, node_fs.readFileSync)(journalPath, "utf-8")); } catch { return null; } const entries = journal.entries; if (!Array.isArray(entries)) return null; const migrationsDir = (0, node_path.join)(root, workspace, "src", "db", "migrations"); const result = []; for (const raw of entries) { const entry = raw; if (typeof entry.idx !== "number" || typeof entry.tag !== "string") continue; const sqlPath = (0, node_path.join)(migrationsDir, `${entry.tag}.sql`); let sha = null; if ((0, node_fs.existsSync)(sqlPath)) try { sha = require_snapshot_hash.computeSnapshotHash((0, node_fs.readFileSync)(sqlPath)); } catch { sha = null; } result.push({ idx: entry.idx, tag: entry.tag, sha }); } return result.sort((a, b) => a.idx - b.idx); } function journalPathFor(root, workspace) { return (0, node_path.join)(root, workspace, "src", "db", "migrations", "meta", "_journal.json"); } function loadRawJournal(root, workspace) { const journalPath = journalPathFor(root, workspace); if (!(0, node_fs.existsSync)(journalPath)) return null; let parsed; try { parsed = JSON.parse((0, node_fs.readFileSync)(journalPath, "utf-8")); } catch { return null; } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; const entries = parsed.entries; if (!Array.isArray(entries)) return null; return { journal: parsed, entries }; } function buildMergedJournal(localRoot, upstreamRoot, workspace, appended) { const localRaw = loadRawJournal(localRoot, workspace); const upstreamRaw = loadRawJournal(upstreamRoot, workspace); if (!localRaw || !upstreamRaw) return null; const upstreamEntryByTag = /* @__PURE__ */ new Map(); for (const entry of upstreamRaw.entries) if (typeof entry.tag === "string") upstreamEntryByTag.set(entry.tag, entry); let appendedWhen = localRaw.entries.reduce((max, entry) => Math.max(max, typeof entry.when === "number" ? entry.when : 0), 0); const appendedEntries = appended.map((append) => { const raw = upstreamEntryByTag.get(append.fromTag); const when = typeof raw?.when === "number" && raw.when > appendedWhen ? raw.when : ++appendedWhen; return { ...raw ?? {}, idx: append.idx, tag: append.toTag, when }; }); const merged = { ...localRaw.journal, entries: [...localRaw.entries, ...appendedEntries] }; return `${JSON.stringify(merged, null, 2)}\n`; } const MIGRATION_TAG_PATTERN = /^(\d+)_(.+)$/; function migrationTagFor(idx, fromTag) { const suffix = fromTag.match(MIGRATION_TAG_PATTERN)?.[2]; return suffix ? `${String(idx).padStart(4, "0")}_${suffix}` : fromTag; } function snapshotPrefixOf(tag) { const match = tag.match(MIGRATION_TAG_PATTERN); return match?.[1] ? match[1].padStart(4, "0") : null; } function reconcileMigrationChains(local, upstream) { const localTags = new Set(local.map((entry) => entry.tag)); const unresolvable = []; const appended = []; const contentMatched = []; let nextIdx = local.reduce((max, entry) => Math.max(max, entry.idx), -1) + 1; for (const upstreamEntry of upstream) { if (localTags.has(upstreamEntry.tag)) { const localEntry = local.find((candidate) => candidate.tag === upstreamEntry.tag); if (localEntry?.sha && upstreamEntry.sha && localEntry.sha !== upstreamEntry.sha) unresolvable.push({ kind: "tag-mismatch", idx: upstreamEntry.idx, localTag: localEntry.tag, upstreamTag: upstreamEntry.tag, message: `${upstreamEntry.tag} has different content locally than upstream — already-applied hashes would drift` }); continue; } if (upstreamEntry.sha && local.some((candidate) => candidate.sha === upstreamEntry.sha)) { contentMatched.push(upstreamEntry.tag); continue; } appended.push({ idx: nextIdx, fromTag: upstreamEntry.tag, toTag: migrationTagFor(nextIdx, upstreamEntry.tag) }); nextIdx++; } return { kept: local.map((entry) => entry.tag), appended, contentMatched, unresolvable }; } function hashFile(root, path) { const fullPath = (0, node_path.join)(root, path); if (!(0, node_fs.existsSync)(fullPath)) return null; try { return require_snapshot_hash.computeSnapshotHash((0, node_fs.readFileSync)(fullPath)); } catch { return null; } } function safeProjectPath(projectDir, path) { const dest = (0, node_path.join)(projectDir, path); const rel = (0, node_path.relative)(projectDir, dest); if (rel.startsWith("..") || (0, node_path.isAbsolute)(rel)) throw new Error(`Refusing to write outside the project: ${path}`); return dest; } function readJson(path) { try { const parsed = JSON.parse((0, node_fs.readFileSync)(path, "utf-8")); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed; return null; } catch { return null; } } function readImportSnapshot(projectDir) { const path = (0, node_path.join)(projectDir, ".bos", IMPORT_SNAPSHOT_FILE); if (!(0, node_fs.existsSync)(path)) return { workspaces: {} }; try { const workspaces = JSON.parse((0, node_fs.readFileSync)(path, "utf-8")).workspaces; if (workspaces && typeof workspaces === "object" && !Array.isArray(workspaces)) return { workspaces }; return { workspaces: {} }; } catch { return { workspaces: {} }; } } function writeImportSnapshot(projectDir, snapshot) { const dir = (0, node_path.join)(projectDir, ".bos"); (0, node_fs.mkdirSync)(dir, { recursive: true }); (0, node_fs.writeFileSync)((0, node_path.join)(dir, IMPORT_SNAPSHOT_FILE), `${JSON.stringify(snapshot, null, 2)}\n`); } function writeImportReport(projectDir, content) { const dir = (0, node_path.join)(projectDir, ".bos"); (0, node_fs.mkdirSync)(dir, { recursive: true }); (0, node_fs.writeFileSync)((0, node_path.join)(dir, IMPORT_REPORT_FILE), content); return IMPORT_REPORT_PATH; } function backupImportFiles(projectDir, paths) { const existing = paths.filter((path) => (0, node_fs.existsSync)((0, node_path.join)(projectDir, path))); if (existing.length === 0) return; const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); const backupDir = (0, node_path.join)(projectDir, ".bos", "import-backup", stamp); for (const path of existing) { const dest = safeProjectPath(backupDir, path); (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true }); (0, node_fs.copyFileSync)((0, node_path.join)(projectDir, path), dest); } return backupDir; } function listSection(title, items, held) { if (items.length === 0) return [ `## ${title}`, "", "(none)", "" ]; return [ `## ${title}`, "", ...items.map((item) => held?.has(item) ? `- \`${item}\` (held)` : `- \`${item}\``), "" ]; } function buildImportReport(analysis, outcome) { const lines = []; lines.push(`# bos import report — \`${analysis.workspace}\``); lines.push(""); if (analysis.chain) { lines.push(`- **Source:** \`${analysis.chain.extendsRef}\` (extends chain)`); if (analysis.chain.sourceArchive) lines.push(`- **Source archive:** \`${analysis.chain.sourceArchive.url}\``); if (analysis.chain.repository) lines.push(`- **Upstream:** \`${analysis.chain.repository}\` @ \`${analysis.ref}\`${analysis.chain.pinnedSha ? ` (pinned git sha \`${analysis.chain.pinnedSha}\`)` : ""}`); for (const warning of analysis.chain.warnings) lines.push(`- **Note:** ${warning}`); } else { const source = analysis.sourceKind === "local" ? `\`${analysis.from}\` (local directory)` : `\`${analysis.from}\`${analysis.ref ? ` @ \`${analysis.ref}\`` : ""}`; lines.push(`- **Source:** ${source}`); } if (analysis.resolvedSha) lines.push(`- **Resolved commit:** \`${analysis.resolvedSha}\``); const modeLabel = outcome.mode === "dry-run" ? "Dry run — no files written" : outcome.mode === "applied" ? "Applied — upstream written over shared files" : "Refused — unresolvable conflicts exist, nothing was written"; lines.push(`- **Mode:** ${modeLabel}`); lines.push(`- **Generated:** ${(/* @__PURE__ */ new Date()).toISOString()}`); lines.push(""); lines.push("## Summary"); lines.push(""); lines.push("| bucket | count |"); lines.push("| --- | --- |"); lines.push(`| added (upstream-only) | ${analysis.added.length} |`); lines.push(`| updated (local unchanged, upstream changed) | ${analysis.updated.length} |`); lines.push(`| merged (structured package.json merge) | ${analysis.merged.length} |`); lines.push(`| conflicts (unresolvable) | ${analysis.conflicts.length} |`); lines.push(`| migration collisions (unresolvable) | ${analysis.migrationCollisions.length} |`); lines.push(`| migrations appended (renumbered from upstream) | ${analysis.migrationReconciliation?.appended.length ?? 0} |`); lines.push(`| migration chain held back | ${analysis.heldBackMigrations.length} |`); lines.push(`| local-only (preserved) | ${analysis.preservedLocal.length} |`); lines.push(`| local edits preserved (upstream unchanged) | ${analysis.preservedLocalEdit.length} |`); lines.push(`| unchanged | ${analysis.unchanged.length} |`); lines.push(""); if (analysis.conflicts.length > 0) { lines.push("## Conflicts — manual-port checklist"); lines.push(""); lines.push("| file | reason |"); lines.push("| --- | --- |"); for (const conflict of analysis.conflicts) { const reason = conflict.reason === "no-baseline" ? "no import baseline (first import or snapshot gap)" : "local and upstream both changed since the baseline"; lines.push(`| \`${conflict.path}\` | ${reason}${conflict.held ? " — held (migration chain)" : ""} |`); } lines.push(""); const appliedConflicts = analysis.conflicts.filter((conflict) => !conflict.held); if (outcome.mode === "refused") lines.push("Nothing was written. Re-run with `--yes` to apply upstream over these files (local copies are backed up first), then port local-only content back onto the new upstream versions."); else if (outcome.mode === "applied") { if (appliedConflicts.length > 0) lines.push(`Upstream was applied over ${appliedConflicts.length} of these file(s)${outcome.backupDir ? ` — local copies backed up at \`${outcome.backupDir}\`` : ""}. Port local-only content back onto the new upstream versions.`); if (analysis.heldBackMigrations.length > 0) lines.push("Migration-chain conflicts were held back and NOT written — see the held-back section below."); } else lines.push("A real run with `--yes` would overwrite these with upstream (backing up local copies first); migration-chain files would be held back."); lines.push(""); } if (analysis.migrationReconciliation) { const reconciliation = analysis.migrationReconciliation; lines.push("## Migration chain reconciliation"); lines.push(""); lines.push("Applied locally — kept verbatim, never renumbered or rehashed:"); lines.push(""); if (reconciliation.kept.length === 0) { lines.push("(no local journal entries)"); lines.push(""); } else { for (const tag of reconciliation.kept) lines.push(`- \`${tag}\``); lines.push(""); } if (reconciliation.appended.length > 0) { lines.push("Appended — colliding upstream migrations renumbered after the local tail:"); lines.push(""); lines.push("| upstream tag | new tag | new idx |"); lines.push("| --- | --- | --- |"); for (const append of reconciliation.appended) lines.push(`| \`${append.fromTag}\` | \`${append.toTag}\` | ${append.idx} |`); lines.push(""); lines.push(`Next boot will apply the ${reconciliation.appended.length} appended migration(s) above; the kept set is already applied to the database.`); lines.push(""); } else { lines.push("Nothing to append: every upstream migration is already applied (by tag or by content)."); lines.push(""); } if (reconciliation.contentMatched.length > 0) { lines.push("Already imported under another tag (matched by SQL content hash):"); lines.push(""); for (const tag of reconciliation.contentMatched) lines.push(`- \`${tag}\``); lines.push(""); } if (reconciliation.appended.length > 0) { lines.push("Snapshot-chain note: renamed snapshot files record upstream intermediate schema states; review `drizzle-kit generate` output against the merged chain before generating new migrations."); lines.push(""); } } if (analysis.migrationCollisions.length > 0) { lines.push("## Migration chain collisions — unresolvable"); lines.push(""); lines.push("| kind | idx | local | upstream | detail |"); lines.push("| --- | --- | --- | --- | --- |"); for (const collision of analysis.migrationCollisions) lines.push(`| ${collision.kind} | ${collision.idx} | \`${collision.localTag ?? "—"}\` | \`${collision.upstreamTag ?? "—"}\` | ${collision.message} |`); lines.push(""); } if (analysis.heldBackMigrations.length > 0) { lines.push("## Migration chain held back"); lines.push(""); for (const path of analysis.heldBackMigrations) lines.push(`- \`${path}\``); lines.push(""); lines.push("`src/db/migrations/**` was not modified: reconciliation could not produce a safe merged chain (content drift on an already-applied migration, or an unreadable journal). Resolve the collisions above, then re-import. The rest of the workspace imported normally."); lines.push(""); } if (analysis.routeDelta) { lines.push("## Contract route-set delta"); lines.push(""); if (analysis.routeDelta.oursOnly.length === 0 && analysis.routeDelta.theirsOnly.length === 0) { lines.push("Route sets are identical."); lines.push(""); } else { lines.push(`Ours-only: ${analysis.routeDelta.oursOnly.length}, theirs-only: ${analysis.routeDelta.theirsOnly.length}.`); lines.push(""); for (const route of analysis.routeDelta.oursOnly) lines.push(`- ours only: \`${route}\``); for (const route of analysis.routeDelta.theirsOnly) lines.push(`- theirs only: \`${route}\``); lines.push(""); } } const heldSet = new Set(analysis.heldBackMigrations); lines.push(...listSection("Added (upstream-only)", analysis.added, heldSet)); lines.push(...listSection("Updated (would be overwritten with upstream)", analysis.updated, heldSet)); lines.push(...listSection("Merged (package.json — local-only keys preserved)", analysis.merged, heldSet)); lines.push(...listSection("Local-only (preserved, never deleted)", analysis.preservedLocal)); lines.push(...listSection("Local edits preserved (upstream unchanged)", analysis.preservedLocalEdit)); lines.push(`## Unchanged`, "", `${analysis.unchanged.length} file(s) identical on both sides.`, ""); lines.push("## Next steps"); lines.push(""); lines.push("- `bos types gen` (and a build) regenerates `*.gen.ts` and `types/contract.d.ts` against the imported contract."); lines.push("- If preserved local changes should live upstream, open a PR against the source repo rather than keeping them only locally."); lines.push(""); return lines.join("\n"); } const LOCAL_PREFIX = "local:"; function localWorkspaceSlotOf(config, workspace) { if (!config || typeof config !== "object" || Array.isArray(config)) return null; const node = config; const matches = (ref) => typeof ref === "string" && ref.startsWith(LOCAL_PREFIX) && ref.slice(6) === workspace; const app = node.app; if (app && typeof app === "object") for (const key of [ "host", "ui", "api", "auth" ]) { const entry = app[key]; if (entry && typeof entry === "object" && matches(entry.development)) return { slot: `app.${key}` }; } const plugins = node.plugins; if (plugins && typeof plugins === "object") for (const key of Object.keys(plugins)) { const entry = plugins[key]; if (entry && typeof entry === "object" && !Array.isArray(entry) && matches(entry.development)) return { slot: `plugins.${key}` }; } return null; } function configEntryAt(config, slot) { if (!config || typeof config !== "object") return null; const dot = slot.indexOf("."); if (dot === -1) return null; const container = config[slot.slice(0, dot)]; const key = slot.slice(dot + 1); const entry = typeof container === "object" && container !== null && !Array.isArray(container) ? container[key] : void 0; if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null; return entry; } const HEAD_FALLBACK_NOTE = "importing the default branch (HEAD)"; async function defaultVersionManifestFetch(url) { const response = await require_http_client.fetchResponse(url, { headers: { accept: "application/json" } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const bytes = new Uint8Array(await response.arrayBuffer()); return { bytes, manifest: JSON.parse(new TextDecoder().decode(bytes)) }; } function manifestSourceOf(manifest) { const parsed = every_plugin_version_manifest.WorkspaceVersionManifestSchema.pick({ source: true }).safeParse(manifest); return parsed.success ? parsed.data.source ?? null : null; } async function resolvePinnedSource(entry, fetchManifest = defaultVersionManifestFetch) { if (!entry) return { warning: `no config entry found for the pinned slot — ${HEAD_FALLBACK_NOTE}` }; const pin = entry.pin; const manifest = typeof pin?.manifest === "string" ? pin.manifest : void 0; const production = typeof entry.production === "string" ? entry.production : void 0; if (!manifest || !production) return { warning: `no pinned version manifest on the published config — ${HEAD_FALLBACK_NOTE}` }; const url = `${production.replace(/\/+$/, "")}/${manifest}`; let body; try { body = await fetchManifest(url); } catch (error) { return { warning: `could not read the version manifest (${error instanceof Error ? error.message : String(error)}) — ${HEAD_FALLBACK_NOTE}` }; } const pinIntegrity = typeof pin?.integrity === "string" ? pin.integrity : void 0; if (pinIntegrity) { const computed = require_services_deploy_integrity.computeSriHash(Buffer.from(body.bytes)); if (computed !== pinIntegrity) throw new Error(`Version manifest integrity mismatch for ${url} — expected ${pinIntegrity}, computed ${computed}. Refusing to fall back to GitHub.`); } const source = manifestSourceOf(body.manifest); if (typeof body.manifest.gitSha !== "string" || body.manifest.gitSha.length === 0) return { warning: `the version manifest records no git sha (it pins content hashes only) — ${HEAD_FALLBACK_NOTE}; future deploys pin the exact commit`, ...source ? { source } : {} }; return { sha: body.manifest.gitSha, ...source ? { source } : {} }; } const FROM_REMEDIES = "Pass the upstream explicitly: --from bos://<account>/<gateway> or --from <owner>/<repo>"; async function resolveImportChain(input) { const fromBosSpec = input.entrySpec !== void 0; const entry = fromBosSpec ? `bos://${input.entrySpec.account}/${input.entrySpec.gateway}` : require_load.localConfigEntryPath(input.projectDir) ?? null; if (!entry) throw new Error(`No authored config (bos.app.ts) found — ${FROM_REMEDIES}`); const links = []; await require_services_resolution_session.walkExtendsChain(entry, { env: "production", io: input.io, visit: async (link) => { links.push(link); } }); links.reverse(); if (links.length === 0) throw new Error(`Empty extends chain from ${entry}`); const declaredIndex = links.findIndex((link) => localWorkspaceSlotOf(link.config, input.workspace) !== null); if (declaredIndex === -1) { const chainRefs = links.map((link) => link.ref).join(" -> "); throw new Error(`No runtime in the extends chain declares workspace "${input.workspace}" (${chainRefs}). ${FROM_REMEDIES}.`); } if (!fromBosSpec && declaredIndex === 0) throw new Error(`Workspace "${input.workspace}" is declared locally in your authored config — bos import targets an upstream ancestor`); const ancestor = links[declaredIndex]; const ancestorRef = ancestor.ref.startsWith("bos://") ? ancestor.ref : `authored config at ${ancestor.ref}`; let repository; for (let i = declaredIndex; i < links.length && repository === void 0; i++) { const value = links[i].config?.repository; if (typeof value === "string" && value.trim().length > 0) repository = value; } const warnings = []; const slot = localWorkspaceSlotOf(ancestor.config, input.workspace).slot; const ancestorEntry = configEntryAt(ancestor.config, slot); const pin = await resolvePinnedSource(ancestorEntry, input.fetchVersionManifest); if (pin.warning) warnings.push(pin.warning); const production = typeof ancestorEntry?.production === "string" ? ancestorEntry.production : void 0; const sourceArchive = pin.source && production ? { file: pin.source.file, url: `${production.replace(/\/+$/, "")}/${pin.source.file}`, integrity: pin.source.integrity } : void 0; if (!sourceArchive && !repository) throw new Error(`No repository found along the extends chain above ${ancestorRef} — the declaring runtime must publish its source (repository field in bos.app.ts)`); const extendsRef = fromBosSpec ? entry : (() => { const extendsValue = links[0].config?.extends; if (extendsValue === void 0) return "authored config"; try { return require_merge.resolveExtendsRef(extendsValue, "production") ?? "authored config"; } catch { return "authored config"; } })(); if (sourceArchive) return { spec: { kind: "archive", url: sourceArchive.url, integrity: sourceArchive.integrity }, provenance: { extendsRef, ancestorRef, ...repository ? { repository } : {}, ...pin.sha ? { pinnedSha: pin.sha } : {}, sourceArchive, warnings } }; return { spec: { kind: "github", repo: repository, ...pin.sha ? { ref: pin.sha } : {} }, provenance: { extendsRef, ancestorRef, ...pin.sha ? { pinnedSha: pin.sha } : {}, repository, warnings } }; } async function resolveImportPlan(input) { const from = input.from; if (!from) { const resolved = await resolveImportChain({ workspace: input.workspace, projectDir: input.projectDir }); return { fromLabel: resolved.provenance.extendsRef, spec: resolved.spec, chain: resolved.provenance }; } const spec = parseFromSpec(from); if (spec.kind === "bos") { const resolved = await resolveImportChain({ workspace: input.workspace, projectDir: input.projectDir, entrySpec: spec }); return { fromLabel: spec.bosUrl, spec: resolved.spec, chain: resolved.provenance }; } return { fromLabel: from, spec }; } async function defaultArchiveFetch(url) { const response = await require_http_client.fetchResponse(url, { redirect: "follow", timeout: "60 seconds" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return new Uint8Array(await response.arrayBuffer()); } async function extractArchive(bytes) { const tmpDir = (0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), "bos-import-archive-")); const archivePath = (0, node_path.join)(tmpDir, "source.tar.gz"); const extractDir = (0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), "bos-import-archive-src-")); try { (0, node_fs.writeFileSync)(archivePath, bytes); await (0, tar.extract)({ cwd: extractDir, file: archivePath, strip: 0 }); } finally { (0, node_fs.rmSync)(tmpDir, { recursive: true, force: true }); } return { root: extractDir, cleanup: async () => { (0, node_fs.rmSync)(extractDir, { recursive: true, force: true }); } }; } async function fetchGithubSource(input) { const pinned = input.pinnedSha !== void 0 && input.ref === input.pinnedSha; try { const tarball = await input.download(input.repo, input.ref); return { source: { root: tarball.dir, cleanup: tarball.cleanup, ref: input.ref ?? tarball.resolvedRef ?? "main", resolvedSha: tarball.resolvedSha } }; } catch (error) { if (!pinned) throw error; const message = error instanceof Error ? error.message : String(error); const tarball = await input.download(input.repo, void 0); return { source: { root: tarball.dir, cleanup: tarball.cleanup, ref: tarball.resolvedRef ?? "main", resolvedSha: tarball.resolvedSha }, fallbackWarning: `could not fetch the pinned commit ${input.pinnedSha} (${message}) — importing the default branch` }; } } async function fetchSourceWithPinFallback(input) { if (input.spec.kind === "local") return { source: { root: input.spec.dir, cleanup: async () => {}, ref: "local" } }; const download = input.download ?? require_init_source.downloadTarball; if (input.spec.kind === "archive") { const fetchArchive = input.fetchArchive ?? defaultArchiveFetch; try { const bytes = await fetchArchive(input.spec.url); const computed = require_services_deploy_integrity.computeSriHash(Buffer.from(bytes)); if (computed !== input.spec.integrity) throw new Error(`integrity mismatch for ${input.spec.url} — expected ${input.spec.integrity}, computed ${computed}`); return { source: { ...await extractArchive(bytes), ref: "source archive" } }; } catch (error) { if (!input.fallback) throw error; const message = error instanceof Error ? error.message : String(error); return { ...await fetchGithubSource({ repo: input.fallback.repo, ref: input.fallback.ref, pinnedSha: input.pinnedSha, download }), fallbackWarning: `could not fetch the source archive (${message}) — importing from ${input.fallback.repo} instead` }; } } return fetchGithubSource({ repo: input.spec.repo, ref: input.spec.ref, pinnedSha: input.pinnedSha, download }); } async function resolveImportSource(spec, chain) { const { source, fallbackWarning } = await fetchSourceWithPinFallback({ spec, pinnedSha: chain?.pinnedSha, fallback: chain?.repository ? { repo: chain.repository, ...chain.pinnedSha ? { ref: chain.pinnedSha } : {} } : void 0 }); return { ...source, fallbackWarning }; } async function importWorkspace(projectDir, options, deps) { const errorResult = (workspace, ref, message) => ({ status: "error", workspace, from: options.from ?? "", ref, added: [], updated: [], unchanged: [], preservedLocal: [], merged: [], conflicts: [], heldBackMigrations: [], migrationCollisions: [], warnings: [], error: message }); const errorMessage = (error) => error instanceof Error ? error.message : String(error); let workspace = options.workspace; let fromLabel = options.from ?? ""; let ref = ""; let chain; try { workspace = normalizeWorkspacePath(options.workspace); const plan = await resolveImportPlan({ workspace, from: options.from, projectDir }); fromLabel = plan.fromLabel; chain = plan.chain; const spec = plan.spec; const source = await resolveImportSource(spec, chain); if (source.fallbackWarning && chain) { const { sourceArchive: _dropped, ...rest } = chain; chain = { ...rest, pinnedSha: void 0, warnings: [...chain.warnings, source.fallbackWarning] }; } ref = source.ref; try { const upstreamWorkspaceDir = (0, node_path.join)(source.root, workspace); if (!(0, node_fs.existsSync)(upstreamWorkspaceDir) || !(0, node_fs.lstatSync)(upstreamWorkspaceDir).isDirectory()) throw new Error(`Workspace "${workspace}" not found in ${fromLabel}${source.ref !== "local" ? ` at ref ${source.ref}` : ""}`); const upstreamFiles = await require_workspace_subtree.listWorkspaceSubtreeFiles(source.root, workspace); const localWorkspaceDir = (0, node_path.join)(projectDir, workspace); const localFiles = (0, node_fs.existsSync)(localWorkspaceDir) ? await require_workspace_subtree.listWorkspaceSubtreeFiles(projectDir, workspace) : []; const upstreamSet = new Set(upstreamFiles); const localSet = new Set(localFiles); const importSnapshot = readImportSnapshot(projectDir); const workspaceSnapshot = importSnapshot.workspaces[workspace]; const syncSnapshot = await require_snapshot.readSnapshot(projectDir); const baselineFor = (path) => workspaceSnapshot?.files[path] ?? syncSnapshot?.files[path]; const packageJsonPath = `${workspace}/package.json`; const mergeablePackageJson = (() => { if (!localSet.has(packageJsonPath) || !upstreamSet.has(packageJsonPath)) return false; return readJson((0, node_path.join)(projectDir, packageJsonPath)) !== null && readJson((0, node_path.join)(source.root, packageJsonPath)) !== null; })(); const chainPrefix = `${workspace}/src/db/migrations/`; const chainJournalPath = `${chainPrefix}meta/_journal.json`; const localMigrations = loadMigrationEntries(projectDir, workspace); const upstreamMigrations = loadMigrationEntries(source.root, workspace); const localJournalExists = (0, node_fs.existsSync)((0, node_path.join)(projectDir, chainJournalPath)); const reconciliation = localMigrations && upstreamMigrations ? reconcileMigrationChains(localMigrations, upstreamMigrations) : null; let chainUnsafe; if (reconciliation) chainUnsafe = reconciliation.unresolvable.length > 0; else if (localMigrations === null && localJournalExists) chainUnsafe = true; else if (localMigrations !== null && upstreamMigrations === null) chainUnsafe = true; else chainUnsafe = false; const migrationRemap = /* @__PURE__ */ new Map(); const migrationSkipped = /* @__PURE__ */ new Set(); if (reconciliation && !chainUnsafe) { const appendedByFromTag = new Map(reconciliation.appended.map((append) => [append.fromTag, append])); const contentMatchedTags = new Set(reconciliation.contentMatched); const upstreamTagBySnapshotPrefix = /* @__PURE__ */ new Map(); for (const entry of upstreamMigrations ?? []) { const prefix = snapshotPrefixOf(entry.tag); if (prefix) upstreamTagBySnapshotPrefix.set(prefix, entry.tag); } for (const path of upstreamFiles) { if (!path.startsWith(chainPrefix) || path === chainJournalPath) continue; const relInMigrations = path.slice(chainPrefix.length); if (relInMigrations.endsWith(".sql")) { const tag = relInMigrations.slice(0, -4); const append = appendedByFromTag.get(tag); if (append) migrationRemap.set(path, `${chainPrefix}${append.toTag}.sql`); else if (contentMatchedTags.has(tag)) migrationSkipped.add(path); } else if (relInMigrations.startsWith("meta/") && relInMigrations.endsWith("_snapshot.json")) { const prefix = relInMigrations.slice(5, relInMigrations.length - 14); const tag = upstreamTagBySnapshotPrefix.get(prefix); const append = tag ? appendedByFromTag.get(tag) : void 0; if (append) migrationRemap.set(path, `${chainPrefix}meta/${String(append.idx).padStart(4, "0")}_snapshot.json`); else if (tag && contentMatchedTags.has(tag)) migrationSkipped.add(path); } } } const added = []; const updated = []; const unchanged = []; const preservedLocal = []; const preservedLocalEdit = []; const merged = []; const conflicts = []; const upstreamHashByPath = /* @__PURE__ */ new Map(); const allPaths = [.../* @__PURE__ */ new Set([...localFiles, ...upstreamFiles])].sort(); for (const path of allPaths) { if (!upstreamSet.has(path)) { preservedLocal.push(path); continue; } if (reconciliation && path === chainJournalPath) continue; if (migrationRemap.has(path) || migrationSkipped.has(path)) continue; const localHash = localSet.has(path) ? hashFile(projectDir, path) : null; const upstreamHash = hashFile(source.root, path); if (upstreamHash === null) throw new Error(`Failed to read upstream file: ${path}`); upstreamHashByPath.set(path, upstreamHash); const classification = classifyImportFile({ localHash, upstreamHash, baselineHash: baselineFor(path), mergeable: path === packageJsonPath && mergeablePackageJson }); switch (classification.kind) { case "added": added.push(path); break; case "unchanged": unchanged.push(path); break; case "updated": updated.push(path); break; case "preserved-local": preservedLocal.push(path); break; case "preserved-local-edit": preservedLocalEdit.push(path); break; case "merged": merged.push(path); break; case "conflict": conflicts.push({ path, reason: classification.reason, held: false }); } } let routeDelta; const localRoutes = extractWorkspaceRoutes(await collectContractRouteSources(projectDir, workspace)); const upstreamRoutes = extractWorkspaceRoutes(await collectContractRouteSources(source.root, workspace)); if (localRoutes !== null || upstreamRoutes !== null) routeDelta = computeRouteDelta(localRoutes ?? [], upstreamRoutes ?? []); const migrationCollisions = reconciliation?.unresolvable ?? []; const writeCandidates = /* @__PURE__ */ new Set([ ...added, ...updated, ...merged, ...conflicts.map((conflict) => conflict.path) ]); const heldSet = new Set(chainUnsafe ? [...writeCandidates].filter((path) => path.startsWith(chainPrefix)) : []); if (chainUnsafe && reconciliation) heldSet.add(chainJournalPath); const heldBackMigrations = [...heldSet].sort(); for (const conflict of conflicts) conflict.held = heldSet.has(conflict.path); const analysis = { workspace, from: fromLabel, ref: source.ref, resolvedSha: source.resolvedSha, sourceKind: spec.kind, chain, added, updated, unchanged, preservedLocal, preservedLocalEdit, merged, conflicts, routeDelta, migrationCollisions, migrationReconciliation: reconciliation, heldBackMigrations }; const resultBase = { workspace, from: fromLabel, ref: source.ref, resolvedSha: source.resolvedSha, sourceRepository: chain?.repository, warnings: chain ? chain.warnings : [], added, updated, unchanged, preservedLocal, merged, conflicts: conflicts.map((conflict) => conflict.path), heldBackMigrations, routeDelta, migrationCollisions, migrationReconciliation: reconciliation ?? void 0 }; if (options.dryRun) { const reportPath = writeImportReport(projectDir, buildImportReport(analysis, { mode: "dry-run" })); return { status: "dry-run", ...resultBase, reportPath }; } const appendedCount = reconciliation?.appended.length ?? 0; if ((conflicts.length > 0 || heldBackMigrations.length > 0 || appendedCount > 0) && !options.yes) { let approved = false; if (deps?.confirm) approved = await deps.confirm({ workspace, conflictCount: conflicts.length, migrationCollisionCount: migrationCollisions.length, heldCount: heldBackMigrations.length, appendedCount }); if (!approved) { const reportPath = writeImportReport(projectDir, buildImportReport(analysis, { mode: "refused" })); return { status: "conflicts", ...resultBase, reportPath }; } } const isHeld = (path) => heldSet.has(path); const mergeContents = /* @__PURE__ */ new Map(); for (const path of merged) { const local = readJson((0, node_path.join)(projectDir, path)); const upstream = readJson((0, node_path.join)(source.root, path)); if (!local || !upstream) throw new Error(`Failed to merge ${path}: unreadable package.json`); mergeContents.set(path, `${JSON.stringify(require_sync.mergePackageJson(path, local, upstream), null, 2)}\n`); } const reconciliationActive = reconciliation !== null && !chainUnsafe; const journalRebuildContent = reconciliationActive && reconciliation.appended.length > 0 ? buildMergedJournal(projectDir, source.root, workspace, reconciliation.appended) : null; const journalChanged = journalRebuildContent !== null && journalRebuildContent !== (0, node_fs.readFileSync)((0, node_path.join)(projectDir, chainJournalPath), "utf-8"); const reconciliationOverwrites = []; if (reconciliationActive) for (const [upstreamPath, localPath] of migrationRemap) { const upstreamContent = (0, node_fs.readFileSync)((0, node_path.join)(source.root, upstreamPath), "utf-8"); if (!(0, node_fs.existsSync)((0, node_path.join)(projectDir, localPath)) || (0, node_fs.readFileSync)((0, node_path.join)(projectDir, localPath), "utf-8") !== upstreamContent) reconciliationOverwrites.push({ upstreamPath, localPath }); } const backupDir = backupImportFiles(projectDir, [ ...updated.filter((path) => !isHeld(path)), ...conflicts.map((conflict) => conflict.path).filter((path) => !isHeld(path)), ...merged.filter((path) => mergeContents.get(path) !== (0, node_fs.readFileSync)((0, node_path.join)(projectDir, path), "utf-8")), ...journalChanged ? [chainJournalPath] : [], ...reconciliationOverwrites.map((write) => write.localPath) ]); const writeUpstream = (path) => { const dest = safeProjectPath(projectDir, path); (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true }); (0, node_fs.copyFileSync)((0, node_path.join)(source.root, path), dest); }; for (const path of added) if (!isHeld(path)) writeUpstream(path); for (const path of updated) if (!isHeld(path)) writeUpstream(path); for (const conflict of conflicts) if (!isHeld(conflict.path)) writeUpstream(conflict.path); for (const path of merged) { const content = mergeContents.get(path); if (content === void 0 || isHeld(path)) continue; if (content === (0, node_fs.readFileSync)((0, node_path.join)(projectDir, path), "utf-8")) continue; const dest = safeProjectPath(projectDir, path); (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true }); (0, node_fs.writeFileSync)(dest, content); } for (const { upstreamPath, localPath } of reconciliationOverwrites) { const dest = safeProjectPath(projectDir, localPath); (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true }); (0, node_fs.copyFileSync)((0, node_path.join)(source.root, upstreamPath), dest); } if (journalChanged && journalRebuildContent) { const dest = safeProjectPath(projectDir, chainJournalPath); (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true }); (0, node_fs.writeFileSync)(dest, journalRebuildContent); } const writtenPaths = [ ...added, ...updated, ...merged, ...conflicts.map((conflict) => conflict.path) ].filter((path) => !isHeld(path)); const recorded = {}; for (const path of writtenPaths) { const hash = hashFile(projectDir, path); if (hash) recorded[path] = hash; } if (journalChanged && journalRebuildContent) recorded[chainJournalPath] = require_snapshot_hash.computeSnapshotHash(journalRebuildContent); for (const { localPath } of reconciliationOverwrites) { const hash = hashFile(projectDir, localPath); if (hash) recorded[localPath] = hash; } for (const path of [...unchanged, ...preservedLocalEdit]) { const hash = upstreamHashByPath.get(path); if (hash) recorded[path] = hash; } writeImportSnapshot(projectDir, { workspaces: { ...importSnapshot.workspaces, [workspace]: { from: fromLabel, ref: source.ref, timestamp: (/* @__PURE__ */ new Date()).toISOString(), files: recorded } } }); if (syncSnapshot) await require_snapshot.writeSnapshot(projectDir, { files: { ...syncSnapshot.files, ...recorded } }); const reportPath = writeImportReport(projectDir, buildImportReport(analysis, { mode: "applied", backupDir })); if (!options.noInstall) try { await require_init.runPnpmInstall(require_workspace.findWorkspaceRoot(projectDir)?.dir ?? projectDir); await require_init.runTypesGen(projectDir); } catch (error) { return { status: "error", ...resultBase, backupDir, reportPath, error: `Import applied, but the post-install step failed: ${errorMessage(error)}` }; } return { status: "imported", ...resultBase, backupDir, reportPath }; } finally { await source.cleanup(); } } catch (error) { return errorResult(workspace, ref, errorMessage(error)); } } //#endregion exports.IMPORT_REPORT_PATH = IMPORT_REPORT_PATH; exports.buildImportReport = buildImportReport; exports.classifyImportFile = classifyImportFile; exports.computeRouteDelta = computeRouteDelta; exports.configEntryAt = configEntryAt; exports.extractContractRoutes = extractContractRoutes; exports.fetchSourceWithPinFallback = fetchSourceWithPinFallback; exports.importWorkspace = importWorkspace; exports.loadMigrationEntries = loadMigrationEntries; exports.localWorkspaceSlotOf = localWorkspaceSlotOf; exports.normalizeWorkspacePath = normalizeWorkspacePath; exports.parseFromSpec = parseFromSpec; exports.reconcileMigrationChains = reconcileMigrationChains; exports.resolveImportChain = resolveImportChain; exports.resolvePinnedSource = resolvePinnedSource; //# sourceMappingURL=import.cjs.map