everything-dev 2.0.0-rc.8 → 2.0.0-rc.9
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.
- package/dist/apps/upload/src/client.cjs +7 -1
- package/dist/apps/upload/src/client.cjs.map +1 -1
- package/dist/apps/upload/src/client.mjs +7 -1
- package/dist/apps/upload/src/client.mjs.map +1 -1
- package/dist/auth-login.cjs +24 -1
- package/dist/auth-login.cjs.map +1 -1
- package/dist/auth-login.mjs +24 -1
- package/dist/auth-login.mjs.map +1 -1
- package/dist/auth-session.cjs +4 -2
- package/dist/auth-session.cjs.map +1 -1
- package/dist/auth-session.mjs +4 -2
- package/dist/auth-session.mjs.map +1 -1
- package/dist/cdn-deploy.cjs +11 -7
- package/dist/cdn-deploy.cjs.map +1 -1
- package/dist/cdn-deploy.mjs +11 -7
- package/dist/cdn-deploy.mjs.map +1 -1
- package/dist/cli/import.cjs +1140 -0
- package/dist/cli/import.cjs.map +1 -0
- package/dist/cli/import.mjs +1126 -0
- package/dist/cli/import.mjs.map +1 -0
- package/dist/cli/init.cjs +11 -4
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +8 -2
- package/dist/cli/init.d.mts +8 -2
- package/dist/cli/init.mjs +11 -5
- package/dist/cli/init.mjs.map +1 -1
- package/dist/cli.cjs +55 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +55 -0
- package/dist/cli.mjs.map +1 -1
- package/dist/commands/import.cjs +49 -0
- package/dist/commands/import.cjs.map +1 -0
- package/dist/commands/import.mjs +46 -0
- package/dist/commands/import.mjs.map +1 -0
- package/dist/commands/keys.cjs +24 -3
- package/dist/commands/keys.cjs.map +1 -1
- package/dist/commands/keys.mjs +24 -3
- package/dist/commands/keys.mjs.map +1 -1
- package/dist/config.d.cts +1 -0
- package/dist/config.d.mts +1 -0
- package/dist/contract.cjs +89 -0
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +190 -1
- package/dist/contract.d.mts +190 -1
- package/dist/contract.meta.cjs +16 -0
- package/dist/contract.meta.cjs.map +1 -1
- package/dist/contract.meta.d.cts +26 -0
- package/dist/contract.meta.d.mts +26 -0
- package/dist/contract.meta.mjs +16 -0
- package/dist/contract.meta.mjs.map +1 -1
- package/dist/contract.mjs +84 -1
- package/dist/contract.mjs.map +1 -1
- package/dist/custody-signer.cjs +70 -0
- package/dist/custody-signer.cjs.map +1 -0
- package/dist/custody-signer.mjs +68 -0
- package/dist/custody-signer.mjs.map +1 -0
- package/dist/descriptor/schema.cjs +8 -5
- package/dist/descriptor/schema.cjs.map +1 -1
- package/dist/descriptor/schema.d.cts +1 -0
- package/dist/descriptor/schema.d.mts +1 -0
- package/dist/descriptor/schema.mjs +8 -5
- package/dist/descriptor/schema.mjs.map +1 -1
- package/dist/descriptor/serialize.cjs +1 -1
- package/dist/descriptor/serialize.cjs.map +1 -1
- package/dist/descriptor/serialize.mjs +1 -1
- package/dist/descriptor/serialize.mjs.map +1 -1
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/outlayer-login.cjs +54 -0
- package/dist/outlayer-login.cjs.map +1 -0
- package/dist/outlayer-login.mjs +54 -0
- package/dist/outlayer-login.mjs.map +1 -0
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +74 -0
- package/dist/plugin.d.mts +74 -0
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/publish.cjs +145 -22
- package/dist/publish.cjs.map +1 -1
- package/dist/publish.mjs +147 -24
- package/dist/publish.mjs.map +1 -1
- package/dist/source-archive.cjs +51 -0
- package/dist/source-archive.cjs.map +1 -0
- package/dist/source-archive.mjs +50 -0
- package/dist/source-archive.mjs.map +1 -0
- package/dist/storage-upload.cjs +6 -1
- package/dist/storage-upload.cjs.map +1 -1
- package/dist/storage-upload.mjs +6 -1
- package/dist/storage-upload.mjs.map +1 -1
- package/dist/types.cjs +5 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +5 -1
- package/dist/types.mjs.map +1 -1
- package/dist/utils/workspace-subtree.cjs +37 -0
- package/dist/utils/workspace-subtree.cjs.map +1 -0
- package/dist/utils/workspace-subtree.mjs +36 -0
- package/dist/utils/workspace-subtree.mjs.map +1 -0
- package/dist/version-manifest-deploy.cjs +4 -2
- package/dist/version-manifest-deploy.cjs.map +1 -1
- package/dist/version-manifest-deploy.mjs +4 -2
- package/dist/version-manifest-deploy.mjs.map +1 -1
- package/dist/version-manifest-resolve.cjs +2 -1
- package/dist/version-manifest-resolve.cjs.map +1 -1
- package/dist/version-manifest-resolve.d.cts +3 -0
- package/dist/version-manifest-resolve.d.mts +3 -0
- package/dist/version-manifest-resolve.mjs +2 -1
- package/dist/version-manifest-resolve.mjs.map +1 -1
- package/package.json +3 -2
- package/skills/cli-reference/SKILL.md +17 -1
- package/skills/init-upgrade/SKILL.md +76 -3
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import { fetchResponse } from "../http-client.mjs";
|
|
2
|
+
import { resolveExtendsRef } from "../merge.mjs";
|
|
3
|
+
import { parseBosUrl } from "../fastkv.mjs";
|
|
4
|
+
import { localConfigEntryPath } from "../config.mjs";
|
|
5
|
+
import { computeSnapshotHash } from "../utils/snapshot-hash.mjs";
|
|
6
|
+
import { readSnapshot, writeSnapshot } from "./snapshot.mjs";
|
|
7
|
+
import { downloadTarball, parseGitHubUrl, runPnpmInstall, runTypesGen } from "./init.mjs";
|
|
8
|
+
import { walkExtendsChain } from "../resolution/session.mjs";
|
|
9
|
+
import { computeSriHash } from "../integrity.mjs";
|
|
10
|
+
import { findWorkspaceRoot } from "../workspace.mjs";
|
|
11
|
+
import { listWorkspaceSubtreeFiles } from "../utils/workspace-subtree.mjs";
|
|
12
|
+
import { mergePackageJson } from "./sync.mjs";
|
|
13
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
15
|
+
import { WorkspaceVersionManifestSchema } from "every-plugin/version-manifest";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { glob } from "glob";
|
|
18
|
+
import { extract } from "tar";
|
|
19
|
+
|
|
20
|
+
//#region src/cli/import.ts
|
|
21
|
+
const IMPORT_SNAPSHOT_FILE = "import-snapshot.json";
|
|
22
|
+
const IMPORT_REPORT_FILE = "import-report.md";
|
|
23
|
+
const IMPORT_REPORT_PATH = `.bos/${IMPORT_REPORT_FILE}`;
|
|
24
|
+
function isExistingDir(path) {
|
|
25
|
+
try {
|
|
26
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function normalizeGithubRepo(spec) {
|
|
32
|
+
if (parseGitHubUrl(spec)) return spec;
|
|
33
|
+
const match = spec.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/);
|
|
34
|
+
if (!match?.[1] || !match[2]) return null;
|
|
35
|
+
if (match[1] === "." || match[1] === ".." || match[2] === "." || match[2] === "..") return null;
|
|
36
|
+
return `https://github.com/${match[1]}/${match[2]}`;
|
|
37
|
+
}
|
|
38
|
+
function parseFromSpec(spec) {
|
|
39
|
+
const trimmed = spec.trim();
|
|
40
|
+
if (!trimmed) throw new Error("--from is required: bos://<account>/<gateway>, <owner>/<repo>[@<ref>], a GitHub URL, or a local directory");
|
|
41
|
+
if (trimmed.startsWith("bos://")) {
|
|
42
|
+
const hashIndex = trimmed.indexOf("#");
|
|
43
|
+
const bosPart = hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex);
|
|
44
|
+
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.`);
|
|
45
|
+
const parsed = parseBosUrl(bosPart);
|
|
46
|
+
if (parsed.pathSegments.length > 0) throw new Error(`Unsupported bos:// path — expected bos://<account>/<gateway>: ${spec}`);
|
|
47
|
+
return {
|
|
48
|
+
kind: "bos",
|
|
49
|
+
bosUrl: `bos://${parsed.accountId}/${parsed.gatewayId}`,
|
|
50
|
+
account: parsed.accountId,
|
|
51
|
+
gateway: parsed.gatewayId
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../")) {
|
|
55
|
+
if (!isExistingDir(trimmed)) throw new Error(`Local directory not found: ${trimmed}`);
|
|
56
|
+
return {
|
|
57
|
+
kind: "local",
|
|
58
|
+
dir: resolve(trimmed)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const at = trimmed.indexOf("@");
|
|
62
|
+
if (at > 0) {
|
|
63
|
+
const head = trimmed.slice(0, at);
|
|
64
|
+
const ref = trimmed.slice(at + 1);
|
|
65
|
+
const repo = normalizeGithubRepo(head);
|
|
66
|
+
if (repo && ref) return {
|
|
67
|
+
kind: "github",
|
|
68
|
+
repo,
|
|
69
|
+
ref
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const repo = normalizeGithubRepo(trimmed);
|
|
73
|
+
if (repo) return {
|
|
74
|
+
kind: "github",
|
|
75
|
+
repo
|
|
76
|
+
};
|
|
77
|
+
if (isExistingDir(trimmed)) return {
|
|
78
|
+
kind: "local",
|
|
79
|
+
dir: resolve(trimmed)
|
|
80
|
+
};
|
|
81
|
+
throw new Error(`Cannot parse --from source: ${spec}. Expected bos://<account>/<gateway>, <owner>/<repo>[@<ref>], a GitHub URL, or a local directory.`);
|
|
82
|
+
}
|
|
83
|
+
function normalizeWorkspacePath(input) {
|
|
84
|
+
const trimmed = input.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
85
|
+
if (isAbsolute(input)) throw new Error(`Workspace path must be repo-relative: ${input}`);
|
|
86
|
+
if (!trimmed || trimmed === "." || trimmed === "..") throw new Error(`Invalid workspace path: ${input}`);
|
|
87
|
+
if (trimmed.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`Invalid workspace path: ${input}`);
|
|
88
|
+
return trimmed;
|
|
89
|
+
}
|
|
90
|
+
function classifyImportFile(input) {
|
|
91
|
+
const { localHash, upstreamHash, baselineHash, mergeable } = input;
|
|
92
|
+
if (localHash === null) return { kind: "added" };
|
|
93
|
+
if (localHash === upstreamHash) return { kind: "unchanged" };
|
|
94
|
+
if (baselineHash === void 0) return mergeable ? { kind: "merged" } : {
|
|
95
|
+
kind: "conflict",
|
|
96
|
+
reason: "no-baseline"
|
|
97
|
+
};
|
|
98
|
+
if (localHash === baselineHash) return mergeable ? { kind: "merged" } : { kind: "updated" };
|
|
99
|
+
if (upstreamHash === baselineHash) return { kind: "preserved-local-edit" };
|
|
100
|
+
return mergeable ? { kind: "merged" } : {
|
|
101
|
+
kind: "conflict",
|
|
102
|
+
reason: "both-changed"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function extractContractRoutes(source) {
|
|
106
|
+
const routes = /* @__PURE__ */ new Set();
|
|
107
|
+
const marker = ".route(";
|
|
108
|
+
let cursor = source.indexOf(marker);
|
|
109
|
+
while (cursor !== -1) {
|
|
110
|
+
const open = cursor + 7 - 1;
|
|
111
|
+
let depth = 0;
|
|
112
|
+
let end = open;
|
|
113
|
+
for (; end < source.length; end++) {
|
|
114
|
+
const char = source[end];
|
|
115
|
+
if (char === "(") depth++;
|
|
116
|
+
else if (char === ")") {
|
|
117
|
+
depth--;
|
|
118
|
+
if (depth === 0) break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const arg = source.slice(open + 1, end);
|
|
122
|
+
const method = arg.match(/\bmethod:\s*["']([A-Za-z]+)["']/)?.[1];
|
|
123
|
+
const path = arg.match(/\bpath:\s*["']([^"']+)["']/)?.[1];
|
|
124
|
+
if (method && path) routes.add(`${method} ${path}`);
|
|
125
|
+
cursor = source.indexOf(marker, cursor + 1);
|
|
126
|
+
}
|
|
127
|
+
return [...routes].sort();
|
|
128
|
+
}
|
|
129
|
+
function computeRouteDelta(ours, theirs) {
|
|
130
|
+
const oursSet = new Set(ours);
|
|
131
|
+
const theirsSet = new Set(theirs);
|
|
132
|
+
return {
|
|
133
|
+
oursOnly: ours.filter((route) => !theirsSet.has(route)),
|
|
134
|
+
theirsOnly: theirs.filter((route) => !oursSet.has(route))
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function collectContractRouteSources(root, workspace) {
|
|
138
|
+
const sources = [];
|
|
139
|
+
const mainPath = join(root, workspace, "src", "contract.ts");
|
|
140
|
+
if (existsSync(mainPath)) sources.push(readFileSync(mainPath, "utf-8"));
|
|
141
|
+
const splitDir = join(root, workspace, "src", "contract");
|
|
142
|
+
if (existsSync(splitDir) && lstatSync(splitDir).isDirectory()) {
|
|
143
|
+
const matches = await glob("**/*.{ts,tsx}", {
|
|
144
|
+
cwd: splitDir,
|
|
145
|
+
nodir: true,
|
|
146
|
+
absolute: false
|
|
147
|
+
});
|
|
148
|
+
for (const match of matches.sort()) sources.push(readFileSync(join(splitDir, match), "utf-8"));
|
|
149
|
+
}
|
|
150
|
+
return sources.length > 0 ? sources : null;
|
|
151
|
+
}
|
|
152
|
+
function extractWorkspaceRoutes(sources) {
|
|
153
|
+
if (sources === null) return null;
|
|
154
|
+
const routes = /* @__PURE__ */ new Set();
|
|
155
|
+
for (const source of sources) for (const route of extractContractRoutes(source)) routes.add(route);
|
|
156
|
+
return [...routes].sort();
|
|
157
|
+
}
|
|
158
|
+
function loadMigrationEntries(root, workspace) {
|
|
159
|
+
const journalPath = journalPathFor(root, workspace);
|
|
160
|
+
if (!existsSync(journalPath)) return null;
|
|
161
|
+
let journal;
|
|
162
|
+
try {
|
|
163
|
+
journal = JSON.parse(readFileSync(journalPath, "utf-8"));
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const entries = journal.entries;
|
|
168
|
+
if (!Array.isArray(entries)) return null;
|
|
169
|
+
const migrationsDir = join(root, workspace, "src", "db", "migrations");
|
|
170
|
+
const result = [];
|
|
171
|
+
for (const raw of entries) {
|
|
172
|
+
const entry = raw;
|
|
173
|
+
if (typeof entry.idx !== "number" || typeof entry.tag !== "string") continue;
|
|
174
|
+
const sqlPath = join(migrationsDir, `${entry.tag}.sql`);
|
|
175
|
+
let sha = null;
|
|
176
|
+
if (existsSync(sqlPath)) try {
|
|
177
|
+
sha = computeSnapshotHash(readFileSync(sqlPath));
|
|
178
|
+
} catch {
|
|
179
|
+
sha = null;
|
|
180
|
+
}
|
|
181
|
+
result.push({
|
|
182
|
+
idx: entry.idx,
|
|
183
|
+
tag: entry.tag,
|
|
184
|
+
sha
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return result.sort((a, b) => a.idx - b.idx);
|
|
188
|
+
}
|
|
189
|
+
function journalPathFor(root, workspace) {
|
|
190
|
+
return join(root, workspace, "src", "db", "migrations", "meta", "_journal.json");
|
|
191
|
+
}
|
|
192
|
+
function loadRawJournal(root, workspace) {
|
|
193
|
+
const journalPath = journalPathFor(root, workspace);
|
|
194
|
+
if (!existsSync(journalPath)) return null;
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(readFileSync(journalPath, "utf-8"));
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
202
|
+
const entries = parsed.entries;
|
|
203
|
+
if (!Array.isArray(entries)) return null;
|
|
204
|
+
return {
|
|
205
|
+
journal: parsed,
|
|
206
|
+
entries
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function buildMergedJournal(localRoot, upstreamRoot, workspace, appended) {
|
|
210
|
+
const localRaw = loadRawJournal(localRoot, workspace);
|
|
211
|
+
const upstreamRaw = loadRawJournal(upstreamRoot, workspace);
|
|
212
|
+
if (!localRaw || !upstreamRaw) return null;
|
|
213
|
+
const upstreamEntryByTag = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const entry of upstreamRaw.entries) if (typeof entry.tag === "string") upstreamEntryByTag.set(entry.tag, entry);
|
|
215
|
+
let appendedWhen = localRaw.entries.reduce((max, entry) => Math.max(max, typeof entry.when === "number" ? entry.when : 0), 0);
|
|
216
|
+
const appendedEntries = appended.map((append) => {
|
|
217
|
+
const raw = upstreamEntryByTag.get(append.fromTag);
|
|
218
|
+
const when = typeof raw?.when === "number" && raw.when > appendedWhen ? raw.when : ++appendedWhen;
|
|
219
|
+
return {
|
|
220
|
+
...raw ?? {},
|
|
221
|
+
idx: append.idx,
|
|
222
|
+
tag: append.toTag,
|
|
223
|
+
when
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
const merged = {
|
|
227
|
+
...localRaw.journal,
|
|
228
|
+
entries: [...localRaw.entries, ...appendedEntries]
|
|
229
|
+
};
|
|
230
|
+
return `${JSON.stringify(merged, null, 2)}\n`;
|
|
231
|
+
}
|
|
232
|
+
const MIGRATION_TAG_PATTERN = /^(\d+)_(.+)$/;
|
|
233
|
+
function migrationTagFor(idx, fromTag) {
|
|
234
|
+
const suffix = fromTag.match(MIGRATION_TAG_PATTERN)?.[2];
|
|
235
|
+
return suffix ? `${String(idx).padStart(4, "0")}_${suffix}` : fromTag;
|
|
236
|
+
}
|
|
237
|
+
function snapshotPrefixOf(tag) {
|
|
238
|
+
const match = tag.match(MIGRATION_TAG_PATTERN);
|
|
239
|
+
return match?.[1] ? match[1].padStart(4, "0") : null;
|
|
240
|
+
}
|
|
241
|
+
function reconcileMigrationChains(local, upstream) {
|
|
242
|
+
const localTags = new Set(local.map((entry) => entry.tag));
|
|
243
|
+
const unresolvable = [];
|
|
244
|
+
const appended = [];
|
|
245
|
+
const contentMatched = [];
|
|
246
|
+
let nextIdx = local.reduce((max, entry) => Math.max(max, entry.idx), -1) + 1;
|
|
247
|
+
for (const upstreamEntry of upstream) {
|
|
248
|
+
if (localTags.has(upstreamEntry.tag)) {
|
|
249
|
+
const localEntry = local.find((candidate) => candidate.tag === upstreamEntry.tag);
|
|
250
|
+
if (localEntry?.sha && upstreamEntry.sha && localEntry.sha !== upstreamEntry.sha) unresolvable.push({
|
|
251
|
+
kind: "tag-mismatch",
|
|
252
|
+
idx: upstreamEntry.idx,
|
|
253
|
+
localTag: localEntry.tag,
|
|
254
|
+
upstreamTag: upstreamEntry.tag,
|
|
255
|
+
message: `${upstreamEntry.tag} has different content locally than upstream — already-applied hashes would drift`
|
|
256
|
+
});
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (upstreamEntry.sha && local.some((candidate) => candidate.sha === upstreamEntry.sha)) {
|
|
260
|
+
contentMatched.push(upstreamEntry.tag);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
appended.push({
|
|
264
|
+
idx: nextIdx,
|
|
265
|
+
fromTag: upstreamEntry.tag,
|
|
266
|
+
toTag: migrationTagFor(nextIdx, upstreamEntry.tag)
|
|
267
|
+
});
|
|
268
|
+
nextIdx++;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
kept: local.map((entry) => entry.tag),
|
|
272
|
+
appended,
|
|
273
|
+
contentMatched,
|
|
274
|
+
unresolvable
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function hashFile(root, path) {
|
|
278
|
+
const fullPath = join(root, path);
|
|
279
|
+
if (!existsSync(fullPath)) return null;
|
|
280
|
+
try {
|
|
281
|
+
return computeSnapshotHash(readFileSync(fullPath));
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function safeProjectPath(projectDir, path) {
|
|
287
|
+
const dest = join(projectDir, path);
|
|
288
|
+
const rel = relative(projectDir, dest);
|
|
289
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`Refusing to write outside the project: ${path}`);
|
|
290
|
+
return dest;
|
|
291
|
+
}
|
|
292
|
+
function readJson(path) {
|
|
293
|
+
try {
|
|
294
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
295
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
296
|
+
return null;
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function readImportSnapshot(projectDir) {
|
|
302
|
+
const path = join(projectDir, ".bos", IMPORT_SNAPSHOT_FILE);
|
|
303
|
+
if (!existsSync(path)) return { workspaces: {} };
|
|
304
|
+
try {
|
|
305
|
+
const workspaces = JSON.parse(readFileSync(path, "utf-8")).workspaces;
|
|
306
|
+
if (workspaces && typeof workspaces === "object" && !Array.isArray(workspaces)) return { workspaces };
|
|
307
|
+
return { workspaces: {} };
|
|
308
|
+
} catch {
|
|
309
|
+
return { workspaces: {} };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function writeImportSnapshot(projectDir, snapshot) {
|
|
313
|
+
const dir = join(projectDir, ".bos");
|
|
314
|
+
mkdirSync(dir, { recursive: true });
|
|
315
|
+
writeFileSync(join(dir, IMPORT_SNAPSHOT_FILE), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
316
|
+
}
|
|
317
|
+
function writeImportReport(projectDir, content) {
|
|
318
|
+
const dir = join(projectDir, ".bos");
|
|
319
|
+
mkdirSync(dir, { recursive: true });
|
|
320
|
+
writeFileSync(join(dir, IMPORT_REPORT_FILE), content);
|
|
321
|
+
return IMPORT_REPORT_PATH;
|
|
322
|
+
}
|
|
323
|
+
function backupImportFiles(projectDir, paths) {
|
|
324
|
+
const existing = paths.filter((path) => existsSync(join(projectDir, path)));
|
|
325
|
+
if (existing.length === 0) return;
|
|
326
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
327
|
+
const backupDir = join(projectDir, ".bos", "import-backup", stamp);
|
|
328
|
+
for (const path of existing) {
|
|
329
|
+
const dest = safeProjectPath(backupDir, path);
|
|
330
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
331
|
+
copyFileSync(join(projectDir, path), dest);
|
|
332
|
+
}
|
|
333
|
+
return backupDir;
|
|
334
|
+
}
|
|
335
|
+
function listSection(title, items, held) {
|
|
336
|
+
if (items.length === 0) return [
|
|
337
|
+
`## ${title}`,
|
|
338
|
+
"",
|
|
339
|
+
"(none)",
|
|
340
|
+
""
|
|
341
|
+
];
|
|
342
|
+
return [
|
|
343
|
+
`## ${title}`,
|
|
344
|
+
"",
|
|
345
|
+
...items.map((item) => held?.has(item) ? `- \`${item}\` (held)` : `- \`${item}\``),
|
|
346
|
+
""
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
function buildImportReport(analysis, outcome) {
|
|
350
|
+
const lines = [];
|
|
351
|
+
lines.push(`# bos import report — \`${analysis.workspace}\``);
|
|
352
|
+
lines.push("");
|
|
353
|
+
if (analysis.chain) {
|
|
354
|
+
lines.push(`- **Source:** \`${analysis.chain.extendsRef}\` (extends chain)`);
|
|
355
|
+
if (analysis.chain.sourceArchive) lines.push(`- **Source archive:** \`${analysis.chain.sourceArchive.url}\``);
|
|
356
|
+
if (analysis.chain.repository) lines.push(`- **Upstream:** \`${analysis.chain.repository}\` @ \`${analysis.ref}\`${analysis.chain.pinnedSha ? ` (pinned git sha \`${analysis.chain.pinnedSha}\`)` : ""}`);
|
|
357
|
+
for (const warning of analysis.chain.warnings) lines.push(`- **Note:** ${warning}`);
|
|
358
|
+
} else {
|
|
359
|
+
const source = analysis.sourceKind === "local" ? `\`${analysis.from}\` (local directory)` : `\`${analysis.from}\`${analysis.ref ? ` @ \`${analysis.ref}\`` : ""}`;
|
|
360
|
+
lines.push(`- **Source:** ${source}`);
|
|
361
|
+
}
|
|
362
|
+
if (analysis.resolvedSha) lines.push(`- **Resolved commit:** \`${analysis.resolvedSha}\``);
|
|
363
|
+
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";
|
|
364
|
+
lines.push(`- **Mode:** ${modeLabel}`);
|
|
365
|
+
lines.push(`- **Generated:** ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
366
|
+
lines.push("");
|
|
367
|
+
lines.push("## Summary");
|
|
368
|
+
lines.push("");
|
|
369
|
+
lines.push("| bucket | count |");
|
|
370
|
+
lines.push("| --- | --- |");
|
|
371
|
+
lines.push(`| added (upstream-only) | ${analysis.added.length} |`);
|
|
372
|
+
lines.push(`| updated (local unchanged, upstream changed) | ${analysis.updated.length} |`);
|
|
373
|
+
lines.push(`| merged (structured package.json merge) | ${analysis.merged.length} |`);
|
|
374
|
+
lines.push(`| conflicts (unresolvable) | ${analysis.conflicts.length} |`);
|
|
375
|
+
lines.push(`| migration collisions (unresolvable) | ${analysis.migrationCollisions.length} |`);
|
|
376
|
+
lines.push(`| migrations appended (renumbered from upstream) | ${analysis.migrationReconciliation?.appended.length ?? 0} |`);
|
|
377
|
+
lines.push(`| migration chain held back | ${analysis.heldBackMigrations.length} |`);
|
|
378
|
+
lines.push(`| local-only (preserved) | ${analysis.preservedLocal.length} |`);
|
|
379
|
+
lines.push(`| local edits preserved (upstream unchanged) | ${analysis.preservedLocalEdit.length} |`);
|
|
380
|
+
lines.push(`| unchanged | ${analysis.unchanged.length} |`);
|
|
381
|
+
lines.push("");
|
|
382
|
+
if (analysis.conflicts.length > 0) {
|
|
383
|
+
lines.push("## Conflicts — manual-port checklist");
|
|
384
|
+
lines.push("");
|
|
385
|
+
lines.push("| file | reason |");
|
|
386
|
+
lines.push("| --- | --- |");
|
|
387
|
+
for (const conflict of analysis.conflicts) {
|
|
388
|
+
const reason = conflict.reason === "no-baseline" ? "no import baseline (first import or snapshot gap)" : "local and upstream both changed since the baseline";
|
|
389
|
+
lines.push(`| \`${conflict.path}\` | ${reason}${conflict.held ? " — held (migration chain)" : ""} |`);
|
|
390
|
+
}
|
|
391
|
+
lines.push("");
|
|
392
|
+
const appliedConflicts = analysis.conflicts.filter((conflict) => !conflict.held);
|
|
393
|
+
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.");
|
|
394
|
+
else if (outcome.mode === "applied") {
|
|
395
|
+
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.`);
|
|
396
|
+
if (analysis.heldBackMigrations.length > 0) lines.push("Migration-chain conflicts were held back and NOT written — see the held-back section below.");
|
|
397
|
+
} 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.");
|
|
398
|
+
lines.push("");
|
|
399
|
+
}
|
|
400
|
+
if (analysis.migrationReconciliation) {
|
|
401
|
+
const reconciliation = analysis.migrationReconciliation;
|
|
402
|
+
lines.push("## Migration chain reconciliation");
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push("Applied locally — kept verbatim, never renumbered or rehashed:");
|
|
405
|
+
lines.push("");
|
|
406
|
+
if (reconciliation.kept.length === 0) {
|
|
407
|
+
lines.push("(no local journal entries)");
|
|
408
|
+
lines.push("");
|
|
409
|
+
} else {
|
|
410
|
+
for (const tag of reconciliation.kept) lines.push(`- \`${tag}\``);
|
|
411
|
+
lines.push("");
|
|
412
|
+
}
|
|
413
|
+
if (reconciliation.appended.length > 0) {
|
|
414
|
+
lines.push("Appended — colliding upstream migrations renumbered after the local tail:");
|
|
415
|
+
lines.push("");
|
|
416
|
+
lines.push("| upstream tag | new tag | new idx |");
|
|
417
|
+
lines.push("| --- | --- | --- |");
|
|
418
|
+
for (const append of reconciliation.appended) lines.push(`| \`${append.fromTag}\` | \`${append.toTag}\` | ${append.idx} |`);
|
|
419
|
+
lines.push("");
|
|
420
|
+
lines.push(`Next boot will apply the ${reconciliation.appended.length} appended migration(s) above; the kept set is already applied to the database.`);
|
|
421
|
+
lines.push("");
|
|
422
|
+
} else {
|
|
423
|
+
lines.push("Nothing to append: every upstream migration is already applied (by tag or by content).");
|
|
424
|
+
lines.push("");
|
|
425
|
+
}
|
|
426
|
+
if (reconciliation.contentMatched.length > 0) {
|
|
427
|
+
lines.push("Already imported under another tag (matched by SQL content hash):");
|
|
428
|
+
lines.push("");
|
|
429
|
+
for (const tag of reconciliation.contentMatched) lines.push(`- \`${tag}\``);
|
|
430
|
+
lines.push("");
|
|
431
|
+
}
|
|
432
|
+
if (reconciliation.appended.length > 0) {
|
|
433
|
+
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.");
|
|
434
|
+
lines.push("");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (analysis.migrationCollisions.length > 0) {
|
|
438
|
+
lines.push("## Migration chain collisions — unresolvable");
|
|
439
|
+
lines.push("");
|
|
440
|
+
lines.push("| kind | idx | local | upstream | detail |");
|
|
441
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
442
|
+
for (const collision of analysis.migrationCollisions) lines.push(`| ${collision.kind} | ${collision.idx} | \`${collision.localTag ?? "—"}\` | \`${collision.upstreamTag ?? "—"}\` | ${collision.message} |`);
|
|
443
|
+
lines.push("");
|
|
444
|
+
}
|
|
445
|
+
if (analysis.heldBackMigrations.length > 0) {
|
|
446
|
+
lines.push("## Migration chain held back");
|
|
447
|
+
lines.push("");
|
|
448
|
+
for (const path of analysis.heldBackMigrations) lines.push(`- \`${path}\``);
|
|
449
|
+
lines.push("");
|
|
450
|
+
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.");
|
|
451
|
+
lines.push("");
|
|
452
|
+
}
|
|
453
|
+
if (analysis.routeDelta) {
|
|
454
|
+
lines.push("## Contract route-set delta");
|
|
455
|
+
lines.push("");
|
|
456
|
+
if (analysis.routeDelta.oursOnly.length === 0 && analysis.routeDelta.theirsOnly.length === 0) {
|
|
457
|
+
lines.push("Route sets are identical.");
|
|
458
|
+
lines.push("");
|
|
459
|
+
} else {
|
|
460
|
+
lines.push(`Ours-only: ${analysis.routeDelta.oursOnly.length}, theirs-only: ${analysis.routeDelta.theirsOnly.length}.`);
|
|
461
|
+
lines.push("");
|
|
462
|
+
for (const route of analysis.routeDelta.oursOnly) lines.push(`- ours only: \`${route}\``);
|
|
463
|
+
for (const route of analysis.routeDelta.theirsOnly) lines.push(`- theirs only: \`${route}\``);
|
|
464
|
+
lines.push("");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const heldSet = new Set(analysis.heldBackMigrations);
|
|
468
|
+
lines.push(...listSection("Added (upstream-only)", analysis.added, heldSet));
|
|
469
|
+
lines.push(...listSection("Updated (would be overwritten with upstream)", analysis.updated, heldSet));
|
|
470
|
+
lines.push(...listSection("Merged (package.json — local-only keys preserved)", analysis.merged, heldSet));
|
|
471
|
+
lines.push(...listSection("Local-only (preserved, never deleted)", analysis.preservedLocal));
|
|
472
|
+
lines.push(...listSection("Local edits preserved (upstream unchanged)", analysis.preservedLocalEdit));
|
|
473
|
+
lines.push(`## Unchanged`, "", `${analysis.unchanged.length} file(s) identical on both sides.`, "");
|
|
474
|
+
lines.push("## Next steps");
|
|
475
|
+
lines.push("");
|
|
476
|
+
lines.push("- `bos types gen` (and a build) regenerates `*.gen.ts` and `types/contract.d.ts` against the imported contract.");
|
|
477
|
+
lines.push("- If preserved local changes should live upstream, open a PR against the source repo rather than keeping them only locally.");
|
|
478
|
+
lines.push("");
|
|
479
|
+
return lines.join("\n");
|
|
480
|
+
}
|
|
481
|
+
const LOCAL_PREFIX = "local:";
|
|
482
|
+
function localWorkspaceSlotOf(config, workspace) {
|
|
483
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return null;
|
|
484
|
+
const node = config;
|
|
485
|
+
const matches = (ref) => typeof ref === "string" && ref.startsWith(LOCAL_PREFIX) && ref.slice(6) === workspace;
|
|
486
|
+
const app = node.app;
|
|
487
|
+
if (app && typeof app === "object") for (const key of [
|
|
488
|
+
"host",
|
|
489
|
+
"ui",
|
|
490
|
+
"api",
|
|
491
|
+
"auth"
|
|
492
|
+
]) {
|
|
493
|
+
const entry = app[key];
|
|
494
|
+
if (entry && typeof entry === "object" && matches(entry.development)) return { slot: `app.${key}` };
|
|
495
|
+
}
|
|
496
|
+
const plugins = node.plugins;
|
|
497
|
+
if (plugins && typeof plugins === "object") for (const key of Object.keys(plugins)) {
|
|
498
|
+
const entry = plugins[key];
|
|
499
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry) && matches(entry.development)) return { slot: `plugins.${key}` };
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
function configEntryAt(config, slot) {
|
|
504
|
+
if (!config || typeof config !== "object") return null;
|
|
505
|
+
const dot = slot.indexOf(".");
|
|
506
|
+
if (dot === -1) return null;
|
|
507
|
+
const container = config[slot.slice(0, dot)];
|
|
508
|
+
const key = slot.slice(dot + 1);
|
|
509
|
+
const entry = typeof container === "object" && container !== null && !Array.isArray(container) ? container[key] : void 0;
|
|
510
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
511
|
+
return entry;
|
|
512
|
+
}
|
|
513
|
+
const HEAD_FALLBACK_NOTE = "importing the default branch (HEAD)";
|
|
514
|
+
async function defaultVersionManifestFetch(url) {
|
|
515
|
+
const response = await fetchResponse(url, { headers: { accept: "application/json" } });
|
|
516
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
517
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
518
|
+
return {
|
|
519
|
+
bytes,
|
|
520
|
+
manifest: JSON.parse(new TextDecoder().decode(bytes))
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function manifestSourceOf(manifest) {
|
|
524
|
+
const parsed = WorkspaceVersionManifestSchema.pick({ source: true }).safeParse(manifest);
|
|
525
|
+
return parsed.success ? parsed.data.source ?? null : null;
|
|
526
|
+
}
|
|
527
|
+
async function resolvePinnedSource(entry, fetchManifest = defaultVersionManifestFetch) {
|
|
528
|
+
if (!entry) return { warning: `no config entry found for the pinned slot — ${HEAD_FALLBACK_NOTE}` };
|
|
529
|
+
const pin = entry.pin;
|
|
530
|
+
const manifest = typeof pin?.manifest === "string" ? pin.manifest : void 0;
|
|
531
|
+
const production = typeof entry.production === "string" ? entry.production : void 0;
|
|
532
|
+
if (!manifest || !production) return { warning: `no pinned version manifest on the published config — ${HEAD_FALLBACK_NOTE}` };
|
|
533
|
+
const url = `${production.replace(/\/+$/, "")}/${manifest}`;
|
|
534
|
+
let body;
|
|
535
|
+
try {
|
|
536
|
+
body = await fetchManifest(url);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
return { warning: `could not read the version manifest (${error instanceof Error ? error.message : String(error)}) — ${HEAD_FALLBACK_NOTE}` };
|
|
539
|
+
}
|
|
540
|
+
const pinIntegrity = typeof pin?.integrity === "string" ? pin.integrity : void 0;
|
|
541
|
+
if (pinIntegrity) {
|
|
542
|
+
const computed = computeSriHash(Buffer.from(body.bytes));
|
|
543
|
+
if (computed !== pinIntegrity) throw new Error(`Version manifest integrity mismatch for ${url} — expected ${pinIntegrity}, computed ${computed}. Refusing to fall back to GitHub.`);
|
|
544
|
+
}
|
|
545
|
+
const source = manifestSourceOf(body.manifest);
|
|
546
|
+
if (typeof body.manifest.gitSha !== "string" || body.manifest.gitSha.length === 0) return {
|
|
547
|
+
warning: `the version manifest records no git sha (it pins content hashes only) — ${HEAD_FALLBACK_NOTE}; future deploys pin the exact commit`,
|
|
548
|
+
...source ? { source } : {}
|
|
549
|
+
};
|
|
550
|
+
return {
|
|
551
|
+
sha: body.manifest.gitSha,
|
|
552
|
+
...source ? { source } : {}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const FROM_REMEDIES = "Pass the upstream explicitly: --from bos://<account>/<gateway> or --from <owner>/<repo>";
|
|
556
|
+
async function resolveImportChain(input) {
|
|
557
|
+
const fromBosSpec = input.entrySpec !== void 0;
|
|
558
|
+
const entry = fromBosSpec ? `bos://${input.entrySpec.account}/${input.entrySpec.gateway}` : localConfigEntryPath(input.projectDir) ?? null;
|
|
559
|
+
if (!entry) throw new Error(`No authored config (bos.app.ts) found — ${FROM_REMEDIES}`);
|
|
560
|
+
const links = [];
|
|
561
|
+
await walkExtendsChain(entry, {
|
|
562
|
+
env: "production",
|
|
563
|
+
io: input.io,
|
|
564
|
+
visit: async (link) => {
|
|
565
|
+
links.push(link);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
links.reverse();
|
|
569
|
+
if (links.length === 0) throw new Error(`Empty extends chain from ${entry}`);
|
|
570
|
+
const declaredIndex = links.findIndex((link) => localWorkspaceSlotOf(link.config, input.workspace) !== null);
|
|
571
|
+
if (declaredIndex === -1) {
|
|
572
|
+
const chainRefs = links.map((link) => link.ref).join(" -> ");
|
|
573
|
+
throw new Error(`No runtime in the extends chain declares workspace "${input.workspace}" (${chainRefs}). ${FROM_REMEDIES}.`);
|
|
574
|
+
}
|
|
575
|
+
if (!fromBosSpec && declaredIndex === 0) throw new Error(`Workspace "${input.workspace}" is declared locally in your authored config — bos import targets an upstream ancestor`);
|
|
576
|
+
const ancestor = links[declaredIndex];
|
|
577
|
+
const ancestorRef = ancestor.ref.startsWith("bos://") ? ancestor.ref : `authored config at ${ancestor.ref}`;
|
|
578
|
+
let repository;
|
|
579
|
+
for (let i = declaredIndex; i < links.length && repository === void 0; i++) {
|
|
580
|
+
const value = links[i].config?.repository;
|
|
581
|
+
if (typeof value === "string" && value.trim().length > 0) repository = value;
|
|
582
|
+
}
|
|
583
|
+
const warnings = [];
|
|
584
|
+
const slot = localWorkspaceSlotOf(ancestor.config, input.workspace).slot;
|
|
585
|
+
const ancestorEntry = configEntryAt(ancestor.config, slot);
|
|
586
|
+
const pin = await resolvePinnedSource(ancestorEntry, input.fetchVersionManifest);
|
|
587
|
+
if (pin.warning) warnings.push(pin.warning);
|
|
588
|
+
const production = typeof ancestorEntry?.production === "string" ? ancestorEntry.production : void 0;
|
|
589
|
+
const sourceArchive = pin.source && production ? {
|
|
590
|
+
file: pin.source.file,
|
|
591
|
+
url: `${production.replace(/\/+$/, "")}/${pin.source.file}`,
|
|
592
|
+
integrity: pin.source.integrity
|
|
593
|
+
} : void 0;
|
|
594
|
+
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)`);
|
|
595
|
+
const extendsRef = fromBosSpec ? entry : (() => {
|
|
596
|
+
const extendsValue = links[0].config?.extends;
|
|
597
|
+
if (extendsValue === void 0) return "authored config";
|
|
598
|
+
try {
|
|
599
|
+
return resolveExtendsRef(extendsValue, "production") ?? "authored config";
|
|
600
|
+
} catch {
|
|
601
|
+
return "authored config";
|
|
602
|
+
}
|
|
603
|
+
})();
|
|
604
|
+
if (sourceArchive) return {
|
|
605
|
+
spec: {
|
|
606
|
+
kind: "archive",
|
|
607
|
+
url: sourceArchive.url,
|
|
608
|
+
integrity: sourceArchive.integrity
|
|
609
|
+
},
|
|
610
|
+
provenance: {
|
|
611
|
+
extendsRef,
|
|
612
|
+
ancestorRef,
|
|
613
|
+
...repository ? { repository } : {},
|
|
614
|
+
...pin.sha ? { pinnedSha: pin.sha } : {},
|
|
615
|
+
sourceArchive,
|
|
616
|
+
warnings
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
return {
|
|
620
|
+
spec: {
|
|
621
|
+
kind: "github",
|
|
622
|
+
repo: repository,
|
|
623
|
+
...pin.sha ? { ref: pin.sha } : {}
|
|
624
|
+
},
|
|
625
|
+
provenance: {
|
|
626
|
+
extendsRef,
|
|
627
|
+
ancestorRef,
|
|
628
|
+
...pin.sha ? { pinnedSha: pin.sha } : {},
|
|
629
|
+
repository,
|
|
630
|
+
warnings
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
async function resolveImportPlan(input) {
|
|
635
|
+
const from = input.from;
|
|
636
|
+
if (!from) {
|
|
637
|
+
const resolved = await resolveImportChain({
|
|
638
|
+
workspace: input.workspace,
|
|
639
|
+
projectDir: input.projectDir
|
|
640
|
+
});
|
|
641
|
+
return {
|
|
642
|
+
fromLabel: resolved.provenance.extendsRef,
|
|
643
|
+
spec: resolved.spec,
|
|
644
|
+
chain: resolved.provenance
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const spec = parseFromSpec(from);
|
|
648
|
+
if (spec.kind === "bos") {
|
|
649
|
+
const resolved = await resolveImportChain({
|
|
650
|
+
workspace: input.workspace,
|
|
651
|
+
projectDir: input.projectDir,
|
|
652
|
+
entrySpec: spec
|
|
653
|
+
});
|
|
654
|
+
return {
|
|
655
|
+
fromLabel: spec.bosUrl,
|
|
656
|
+
spec: resolved.spec,
|
|
657
|
+
chain: resolved.provenance
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
fromLabel: from,
|
|
662
|
+
spec
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async function defaultArchiveFetch(url) {
|
|
666
|
+
const response = await fetchResponse(url, {
|
|
667
|
+
redirect: "follow",
|
|
668
|
+
timeout: "60 seconds"
|
|
669
|
+
});
|
|
670
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
671
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
672
|
+
}
|
|
673
|
+
async function extractArchive(bytes) {
|
|
674
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "bos-import-archive-"));
|
|
675
|
+
const archivePath = join(tmpDir, "source.tar.gz");
|
|
676
|
+
const extractDir = mkdtempSync(join(tmpdir(), "bos-import-archive-src-"));
|
|
677
|
+
try {
|
|
678
|
+
writeFileSync(archivePath, bytes);
|
|
679
|
+
await extract({
|
|
680
|
+
cwd: extractDir,
|
|
681
|
+
file: archivePath,
|
|
682
|
+
strip: 0
|
|
683
|
+
});
|
|
684
|
+
} finally {
|
|
685
|
+
rmSync(tmpDir, {
|
|
686
|
+
recursive: true,
|
|
687
|
+
force: true
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
root: extractDir,
|
|
692
|
+
cleanup: async () => {
|
|
693
|
+
rmSync(extractDir, {
|
|
694
|
+
recursive: true,
|
|
695
|
+
force: true
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
async function fetchGithubSource(input) {
|
|
701
|
+
const pinned = input.pinnedSha !== void 0 && input.ref === input.pinnedSha;
|
|
702
|
+
try {
|
|
703
|
+
const tarball = await input.download(input.repo, input.ref);
|
|
704
|
+
return { source: {
|
|
705
|
+
root: tarball.dir,
|
|
706
|
+
cleanup: tarball.cleanup,
|
|
707
|
+
ref: input.ref ?? tarball.resolvedRef ?? "main",
|
|
708
|
+
resolvedSha: tarball.resolvedSha
|
|
709
|
+
} };
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (!pinned) throw error;
|
|
712
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
713
|
+
const tarball = await input.download(input.repo, void 0);
|
|
714
|
+
return {
|
|
715
|
+
source: {
|
|
716
|
+
root: tarball.dir,
|
|
717
|
+
cleanup: tarball.cleanup,
|
|
718
|
+
ref: tarball.resolvedRef ?? "main",
|
|
719
|
+
resolvedSha: tarball.resolvedSha
|
|
720
|
+
},
|
|
721
|
+
fallbackWarning: `could not fetch the pinned commit ${input.pinnedSha} (${message}) — importing the default branch`
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function fetchSourceWithPinFallback(input) {
|
|
726
|
+
if (input.spec.kind === "local") return { source: {
|
|
727
|
+
root: input.spec.dir,
|
|
728
|
+
cleanup: async () => {},
|
|
729
|
+
ref: "local"
|
|
730
|
+
} };
|
|
731
|
+
const download = input.download ?? downloadTarball;
|
|
732
|
+
if (input.spec.kind === "archive") {
|
|
733
|
+
const fetchArchive = input.fetchArchive ?? defaultArchiveFetch;
|
|
734
|
+
try {
|
|
735
|
+
const bytes = await fetchArchive(input.spec.url);
|
|
736
|
+
const computed = computeSriHash(Buffer.from(bytes));
|
|
737
|
+
if (computed !== input.spec.integrity) throw new Error(`integrity mismatch for ${input.spec.url} — expected ${input.spec.integrity}, computed ${computed}`);
|
|
738
|
+
return { source: {
|
|
739
|
+
...await extractArchive(bytes),
|
|
740
|
+
ref: "source archive"
|
|
741
|
+
} };
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (!input.fallback) throw error;
|
|
744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
745
|
+
return {
|
|
746
|
+
...await fetchGithubSource({
|
|
747
|
+
repo: input.fallback.repo,
|
|
748
|
+
ref: input.fallback.ref,
|
|
749
|
+
pinnedSha: input.pinnedSha,
|
|
750
|
+
download
|
|
751
|
+
}),
|
|
752
|
+
fallbackWarning: `could not fetch the source archive (${message}) — importing from ${input.fallback.repo} instead`
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return fetchGithubSource({
|
|
757
|
+
repo: input.spec.repo,
|
|
758
|
+
ref: input.spec.ref,
|
|
759
|
+
pinnedSha: input.pinnedSha,
|
|
760
|
+
download
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
async function resolveImportSource(spec, chain) {
|
|
764
|
+
const { source, fallbackWarning } = await fetchSourceWithPinFallback({
|
|
765
|
+
spec,
|
|
766
|
+
pinnedSha: chain?.pinnedSha,
|
|
767
|
+
fallback: chain?.repository ? {
|
|
768
|
+
repo: chain.repository,
|
|
769
|
+
...chain.pinnedSha ? { ref: chain.pinnedSha } : {}
|
|
770
|
+
} : void 0
|
|
771
|
+
});
|
|
772
|
+
return {
|
|
773
|
+
...source,
|
|
774
|
+
fallbackWarning
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async function importWorkspace(projectDir, options, deps) {
|
|
778
|
+
const errorResult = (workspace, ref, message) => ({
|
|
779
|
+
status: "error",
|
|
780
|
+
workspace,
|
|
781
|
+
from: options.from ?? "",
|
|
782
|
+
ref,
|
|
783
|
+
added: [],
|
|
784
|
+
updated: [],
|
|
785
|
+
unchanged: [],
|
|
786
|
+
preservedLocal: [],
|
|
787
|
+
merged: [],
|
|
788
|
+
conflicts: [],
|
|
789
|
+
heldBackMigrations: [],
|
|
790
|
+
migrationCollisions: [],
|
|
791
|
+
warnings: [],
|
|
792
|
+
error: message
|
|
793
|
+
});
|
|
794
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
795
|
+
let workspace = options.workspace;
|
|
796
|
+
let fromLabel = options.from ?? "";
|
|
797
|
+
let ref = "";
|
|
798
|
+
let chain;
|
|
799
|
+
try {
|
|
800
|
+
workspace = normalizeWorkspacePath(options.workspace);
|
|
801
|
+
const plan = await resolveImportPlan({
|
|
802
|
+
workspace,
|
|
803
|
+
from: options.from,
|
|
804
|
+
projectDir
|
|
805
|
+
});
|
|
806
|
+
fromLabel = plan.fromLabel;
|
|
807
|
+
chain = plan.chain;
|
|
808
|
+
const spec = plan.spec;
|
|
809
|
+
const source = await resolveImportSource(spec, chain);
|
|
810
|
+
if (source.fallbackWarning && chain) {
|
|
811
|
+
const { sourceArchive: _dropped, ...rest } = chain;
|
|
812
|
+
chain = {
|
|
813
|
+
...rest,
|
|
814
|
+
pinnedSha: void 0,
|
|
815
|
+
warnings: [...chain.warnings, source.fallbackWarning]
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
ref = source.ref;
|
|
819
|
+
try {
|
|
820
|
+
const upstreamWorkspaceDir = join(source.root, workspace);
|
|
821
|
+
if (!existsSync(upstreamWorkspaceDir) || !lstatSync(upstreamWorkspaceDir).isDirectory()) throw new Error(`Workspace "${workspace}" not found in ${fromLabel}${source.ref !== "local" ? ` at ref ${source.ref}` : ""}`);
|
|
822
|
+
const upstreamFiles = await listWorkspaceSubtreeFiles(source.root, workspace);
|
|
823
|
+
const localWorkspaceDir = join(projectDir, workspace);
|
|
824
|
+
const localFiles = existsSync(localWorkspaceDir) ? await listWorkspaceSubtreeFiles(projectDir, workspace) : [];
|
|
825
|
+
const upstreamSet = new Set(upstreamFiles);
|
|
826
|
+
const localSet = new Set(localFiles);
|
|
827
|
+
const importSnapshot = readImportSnapshot(projectDir);
|
|
828
|
+
const workspaceSnapshot = importSnapshot.workspaces[workspace];
|
|
829
|
+
const syncSnapshot = await readSnapshot(projectDir);
|
|
830
|
+
const baselineFor = (path) => workspaceSnapshot?.files[path] ?? syncSnapshot?.files[path];
|
|
831
|
+
const packageJsonPath = `${workspace}/package.json`;
|
|
832
|
+
const mergeablePackageJson = (() => {
|
|
833
|
+
if (!localSet.has(packageJsonPath) || !upstreamSet.has(packageJsonPath)) return false;
|
|
834
|
+
return readJson(join(projectDir, packageJsonPath)) !== null && readJson(join(source.root, packageJsonPath)) !== null;
|
|
835
|
+
})();
|
|
836
|
+
const chainPrefix = `${workspace}/src/db/migrations/`;
|
|
837
|
+
const chainJournalPath = `${chainPrefix}meta/_journal.json`;
|
|
838
|
+
const localMigrations = loadMigrationEntries(projectDir, workspace);
|
|
839
|
+
const upstreamMigrations = loadMigrationEntries(source.root, workspace);
|
|
840
|
+
const localJournalExists = existsSync(join(projectDir, chainJournalPath));
|
|
841
|
+
const reconciliation = localMigrations && upstreamMigrations ? reconcileMigrationChains(localMigrations, upstreamMigrations) : null;
|
|
842
|
+
let chainUnsafe;
|
|
843
|
+
if (reconciliation) chainUnsafe = reconciliation.unresolvable.length > 0;
|
|
844
|
+
else if (localMigrations === null && localJournalExists) chainUnsafe = true;
|
|
845
|
+
else if (localMigrations !== null && upstreamMigrations === null) chainUnsafe = true;
|
|
846
|
+
else chainUnsafe = false;
|
|
847
|
+
const migrationRemap = /* @__PURE__ */ new Map();
|
|
848
|
+
const migrationSkipped = /* @__PURE__ */ new Set();
|
|
849
|
+
if (reconciliation && !chainUnsafe) {
|
|
850
|
+
const appendedByFromTag = new Map(reconciliation.appended.map((append) => [append.fromTag, append]));
|
|
851
|
+
const contentMatchedTags = new Set(reconciliation.contentMatched);
|
|
852
|
+
const upstreamTagBySnapshotPrefix = /* @__PURE__ */ new Map();
|
|
853
|
+
for (const entry of upstreamMigrations ?? []) {
|
|
854
|
+
const prefix = snapshotPrefixOf(entry.tag);
|
|
855
|
+
if (prefix) upstreamTagBySnapshotPrefix.set(prefix, entry.tag);
|
|
856
|
+
}
|
|
857
|
+
for (const path of upstreamFiles) {
|
|
858
|
+
if (!path.startsWith(chainPrefix) || path === chainJournalPath) continue;
|
|
859
|
+
const relInMigrations = path.slice(chainPrefix.length);
|
|
860
|
+
if (relInMigrations.endsWith(".sql")) {
|
|
861
|
+
const tag = relInMigrations.slice(0, -4);
|
|
862
|
+
const append = appendedByFromTag.get(tag);
|
|
863
|
+
if (append) migrationRemap.set(path, `${chainPrefix}${append.toTag}.sql`);
|
|
864
|
+
else if (contentMatchedTags.has(tag)) migrationSkipped.add(path);
|
|
865
|
+
} else if (relInMigrations.startsWith("meta/") && relInMigrations.endsWith("_snapshot.json")) {
|
|
866
|
+
const prefix = relInMigrations.slice(5, relInMigrations.length - 14);
|
|
867
|
+
const tag = upstreamTagBySnapshotPrefix.get(prefix);
|
|
868
|
+
const append = tag ? appendedByFromTag.get(tag) : void 0;
|
|
869
|
+
if (append) migrationRemap.set(path, `${chainPrefix}meta/${String(append.idx).padStart(4, "0")}_snapshot.json`);
|
|
870
|
+
else if (tag && contentMatchedTags.has(tag)) migrationSkipped.add(path);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const added = [];
|
|
875
|
+
const updated = [];
|
|
876
|
+
const unchanged = [];
|
|
877
|
+
const preservedLocal = [];
|
|
878
|
+
const preservedLocalEdit = [];
|
|
879
|
+
const merged = [];
|
|
880
|
+
const conflicts = [];
|
|
881
|
+
const upstreamHashByPath = /* @__PURE__ */ new Map();
|
|
882
|
+
const allPaths = [.../* @__PURE__ */ new Set([...localFiles, ...upstreamFiles])].sort();
|
|
883
|
+
for (const path of allPaths) {
|
|
884
|
+
if (!upstreamSet.has(path)) {
|
|
885
|
+
preservedLocal.push(path);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (reconciliation && path === chainJournalPath) continue;
|
|
889
|
+
if (migrationRemap.has(path) || migrationSkipped.has(path)) continue;
|
|
890
|
+
const localHash = localSet.has(path) ? hashFile(projectDir, path) : null;
|
|
891
|
+
const upstreamHash = hashFile(source.root, path);
|
|
892
|
+
if (upstreamHash === null) throw new Error(`Failed to read upstream file: ${path}`);
|
|
893
|
+
upstreamHashByPath.set(path, upstreamHash);
|
|
894
|
+
const classification = classifyImportFile({
|
|
895
|
+
localHash,
|
|
896
|
+
upstreamHash,
|
|
897
|
+
baselineHash: baselineFor(path),
|
|
898
|
+
mergeable: path === packageJsonPath && mergeablePackageJson
|
|
899
|
+
});
|
|
900
|
+
switch (classification.kind) {
|
|
901
|
+
case "added":
|
|
902
|
+
added.push(path);
|
|
903
|
+
break;
|
|
904
|
+
case "unchanged":
|
|
905
|
+
unchanged.push(path);
|
|
906
|
+
break;
|
|
907
|
+
case "updated":
|
|
908
|
+
updated.push(path);
|
|
909
|
+
break;
|
|
910
|
+
case "preserved-local":
|
|
911
|
+
preservedLocal.push(path);
|
|
912
|
+
break;
|
|
913
|
+
case "preserved-local-edit":
|
|
914
|
+
preservedLocalEdit.push(path);
|
|
915
|
+
break;
|
|
916
|
+
case "merged":
|
|
917
|
+
merged.push(path);
|
|
918
|
+
break;
|
|
919
|
+
case "conflict": conflicts.push({
|
|
920
|
+
path,
|
|
921
|
+
reason: classification.reason,
|
|
922
|
+
held: false
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
let routeDelta;
|
|
927
|
+
const localRoutes = extractWorkspaceRoutes(await collectContractRouteSources(projectDir, workspace));
|
|
928
|
+
const upstreamRoutes = extractWorkspaceRoutes(await collectContractRouteSources(source.root, workspace));
|
|
929
|
+
if (localRoutes !== null || upstreamRoutes !== null) routeDelta = computeRouteDelta(localRoutes ?? [], upstreamRoutes ?? []);
|
|
930
|
+
const migrationCollisions = reconciliation?.unresolvable ?? [];
|
|
931
|
+
const writeCandidates = /* @__PURE__ */ new Set([
|
|
932
|
+
...added,
|
|
933
|
+
...updated,
|
|
934
|
+
...merged,
|
|
935
|
+
...conflicts.map((conflict) => conflict.path)
|
|
936
|
+
]);
|
|
937
|
+
const heldSet = new Set(chainUnsafe ? [...writeCandidates].filter((path) => path.startsWith(chainPrefix)) : []);
|
|
938
|
+
if (chainUnsafe && reconciliation) heldSet.add(chainJournalPath);
|
|
939
|
+
const heldBackMigrations = [...heldSet].sort();
|
|
940
|
+
for (const conflict of conflicts) conflict.held = heldSet.has(conflict.path);
|
|
941
|
+
const analysis = {
|
|
942
|
+
workspace,
|
|
943
|
+
from: fromLabel,
|
|
944
|
+
ref: source.ref,
|
|
945
|
+
resolvedSha: source.resolvedSha,
|
|
946
|
+
sourceKind: spec.kind,
|
|
947
|
+
chain,
|
|
948
|
+
added,
|
|
949
|
+
updated,
|
|
950
|
+
unchanged,
|
|
951
|
+
preservedLocal,
|
|
952
|
+
preservedLocalEdit,
|
|
953
|
+
merged,
|
|
954
|
+
conflicts,
|
|
955
|
+
routeDelta,
|
|
956
|
+
migrationCollisions,
|
|
957
|
+
migrationReconciliation: reconciliation,
|
|
958
|
+
heldBackMigrations
|
|
959
|
+
};
|
|
960
|
+
const resultBase = {
|
|
961
|
+
workspace,
|
|
962
|
+
from: fromLabel,
|
|
963
|
+
ref: source.ref,
|
|
964
|
+
resolvedSha: source.resolvedSha,
|
|
965
|
+
sourceRepository: chain?.repository,
|
|
966
|
+
warnings: chain ? chain.warnings : [],
|
|
967
|
+
added,
|
|
968
|
+
updated,
|
|
969
|
+
unchanged,
|
|
970
|
+
preservedLocal,
|
|
971
|
+
merged,
|
|
972
|
+
conflicts: conflicts.map((conflict) => conflict.path),
|
|
973
|
+
heldBackMigrations,
|
|
974
|
+
routeDelta,
|
|
975
|
+
migrationCollisions,
|
|
976
|
+
migrationReconciliation: reconciliation ?? void 0
|
|
977
|
+
};
|
|
978
|
+
if (options.dryRun) {
|
|
979
|
+
const reportPath = writeImportReport(projectDir, buildImportReport(analysis, { mode: "dry-run" }));
|
|
980
|
+
return {
|
|
981
|
+
status: "dry-run",
|
|
982
|
+
...resultBase,
|
|
983
|
+
reportPath
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
const appendedCount = reconciliation?.appended.length ?? 0;
|
|
987
|
+
if ((conflicts.length > 0 || heldBackMigrations.length > 0 || appendedCount > 0) && !options.yes) {
|
|
988
|
+
let approved = false;
|
|
989
|
+
if (deps?.confirm) approved = await deps.confirm({
|
|
990
|
+
workspace,
|
|
991
|
+
conflictCount: conflicts.length,
|
|
992
|
+
migrationCollisionCount: migrationCollisions.length,
|
|
993
|
+
heldCount: heldBackMigrations.length,
|
|
994
|
+
appendedCount
|
|
995
|
+
});
|
|
996
|
+
if (!approved) {
|
|
997
|
+
const reportPath = writeImportReport(projectDir, buildImportReport(analysis, { mode: "refused" }));
|
|
998
|
+
return {
|
|
999
|
+
status: "conflicts",
|
|
1000
|
+
...resultBase,
|
|
1001
|
+
reportPath
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const isHeld = (path) => heldSet.has(path);
|
|
1006
|
+
const mergeContents = /* @__PURE__ */ new Map();
|
|
1007
|
+
for (const path of merged) {
|
|
1008
|
+
const local = readJson(join(projectDir, path));
|
|
1009
|
+
const upstream = readJson(join(source.root, path));
|
|
1010
|
+
if (!local || !upstream) throw new Error(`Failed to merge ${path}: unreadable package.json`);
|
|
1011
|
+
mergeContents.set(path, `${JSON.stringify(mergePackageJson(path, local, upstream), null, 2)}\n`);
|
|
1012
|
+
}
|
|
1013
|
+
const reconciliationActive = reconciliation !== null && !chainUnsafe;
|
|
1014
|
+
const journalRebuildContent = reconciliationActive && reconciliation.appended.length > 0 ? buildMergedJournal(projectDir, source.root, workspace, reconciliation.appended) : null;
|
|
1015
|
+
const journalChanged = journalRebuildContent !== null && journalRebuildContent !== readFileSync(join(projectDir, chainJournalPath), "utf-8");
|
|
1016
|
+
const reconciliationOverwrites = [];
|
|
1017
|
+
if (reconciliationActive) for (const [upstreamPath, localPath] of migrationRemap) {
|
|
1018
|
+
const upstreamContent = readFileSync(join(source.root, upstreamPath), "utf-8");
|
|
1019
|
+
if (!existsSync(join(projectDir, localPath)) || readFileSync(join(projectDir, localPath), "utf-8") !== upstreamContent) reconciliationOverwrites.push({
|
|
1020
|
+
upstreamPath,
|
|
1021
|
+
localPath
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
const backupDir = backupImportFiles(projectDir, [
|
|
1025
|
+
...updated.filter((path) => !isHeld(path)),
|
|
1026
|
+
...conflicts.map((conflict) => conflict.path).filter((path) => !isHeld(path)),
|
|
1027
|
+
...merged.filter((path) => mergeContents.get(path) !== readFileSync(join(projectDir, path), "utf-8")),
|
|
1028
|
+
...journalChanged ? [chainJournalPath] : [],
|
|
1029
|
+
...reconciliationOverwrites.map((write) => write.localPath)
|
|
1030
|
+
]);
|
|
1031
|
+
const writeUpstream = (path) => {
|
|
1032
|
+
const dest = safeProjectPath(projectDir, path);
|
|
1033
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1034
|
+
copyFileSync(join(source.root, path), dest);
|
|
1035
|
+
};
|
|
1036
|
+
for (const path of added) if (!isHeld(path)) writeUpstream(path);
|
|
1037
|
+
for (const path of updated) if (!isHeld(path)) writeUpstream(path);
|
|
1038
|
+
for (const conflict of conflicts) if (!isHeld(conflict.path)) writeUpstream(conflict.path);
|
|
1039
|
+
for (const path of merged) {
|
|
1040
|
+
const content = mergeContents.get(path);
|
|
1041
|
+
if (content === void 0 || isHeld(path)) continue;
|
|
1042
|
+
if (content === readFileSync(join(projectDir, path), "utf-8")) continue;
|
|
1043
|
+
const dest = safeProjectPath(projectDir, path);
|
|
1044
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1045
|
+
writeFileSync(dest, content);
|
|
1046
|
+
}
|
|
1047
|
+
for (const { upstreamPath, localPath } of reconciliationOverwrites) {
|
|
1048
|
+
const dest = safeProjectPath(projectDir, localPath);
|
|
1049
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1050
|
+
copyFileSync(join(source.root, upstreamPath), dest);
|
|
1051
|
+
}
|
|
1052
|
+
if (journalChanged && journalRebuildContent) {
|
|
1053
|
+
const dest = safeProjectPath(projectDir, chainJournalPath);
|
|
1054
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1055
|
+
writeFileSync(dest, journalRebuildContent);
|
|
1056
|
+
}
|
|
1057
|
+
const writtenPaths = [
|
|
1058
|
+
...added,
|
|
1059
|
+
...updated,
|
|
1060
|
+
...merged,
|
|
1061
|
+
...conflicts.map((conflict) => conflict.path)
|
|
1062
|
+
].filter((path) => !isHeld(path));
|
|
1063
|
+
const recorded = {};
|
|
1064
|
+
for (const path of writtenPaths) {
|
|
1065
|
+
const hash = hashFile(projectDir, path);
|
|
1066
|
+
if (hash) recorded[path] = hash;
|
|
1067
|
+
}
|
|
1068
|
+
if (journalChanged && journalRebuildContent) recorded[chainJournalPath] = computeSnapshotHash(journalRebuildContent);
|
|
1069
|
+
for (const { localPath } of reconciliationOverwrites) {
|
|
1070
|
+
const hash = hashFile(projectDir, localPath);
|
|
1071
|
+
if (hash) recorded[localPath] = hash;
|
|
1072
|
+
}
|
|
1073
|
+
for (const path of [...unchanged, ...preservedLocalEdit]) {
|
|
1074
|
+
const hash = upstreamHashByPath.get(path);
|
|
1075
|
+
if (hash) recorded[path] = hash;
|
|
1076
|
+
}
|
|
1077
|
+
writeImportSnapshot(projectDir, { workspaces: {
|
|
1078
|
+
...importSnapshot.workspaces,
|
|
1079
|
+
[workspace]: {
|
|
1080
|
+
from: fromLabel,
|
|
1081
|
+
ref: source.ref,
|
|
1082
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1083
|
+
files: recorded
|
|
1084
|
+
}
|
|
1085
|
+
} });
|
|
1086
|
+
if (syncSnapshot) await writeSnapshot(projectDir, {
|
|
1087
|
+
parentRef: syncSnapshot.parentRef,
|
|
1088
|
+
files: {
|
|
1089
|
+
...syncSnapshot.files,
|
|
1090
|
+
...recorded
|
|
1091
|
+
},
|
|
1092
|
+
starter: syncSnapshot.starter
|
|
1093
|
+
});
|
|
1094
|
+
const reportPath = writeImportReport(projectDir, buildImportReport(analysis, {
|
|
1095
|
+
mode: "applied",
|
|
1096
|
+
backupDir
|
|
1097
|
+
}));
|
|
1098
|
+
if (!options.noInstall) try {
|
|
1099
|
+
await runPnpmInstall(findWorkspaceRoot(projectDir)?.dir ?? projectDir);
|
|
1100
|
+
await runTypesGen(projectDir);
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
return {
|
|
1103
|
+
status: "error",
|
|
1104
|
+
...resultBase,
|
|
1105
|
+
backupDir,
|
|
1106
|
+
reportPath,
|
|
1107
|
+
error: `Import applied, but the post-install step failed: ${errorMessage(error)}`
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
status: "imported",
|
|
1112
|
+
...resultBase,
|
|
1113
|
+
backupDir,
|
|
1114
|
+
reportPath
|
|
1115
|
+
};
|
|
1116
|
+
} finally {
|
|
1117
|
+
await source.cleanup();
|
|
1118
|
+
}
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
return errorResult(workspace, ref, errorMessage(error));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
//#endregion
|
|
1125
|
+
export { IMPORT_REPORT_PATH, buildImportReport, classifyImportFile, computeRouteDelta, configEntryAt, extractContractRoutes, fetchSourceWithPinFallback, importWorkspace, loadMigrationEntries, localWorkspaceSlotOf, normalizeWorkspacePath, parseFromSpec, reconcileMigrationChains, resolveImportChain, resolvePinnedSource };
|
|
1126
|
+
//# sourceMappingURL=import.mjs.map
|