@zuplo/cli 7.9.11 → 7.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/integration/source-upgrade.integration.test.d.ts +2 -0
- package/dist/__tests__/integration/source-upgrade.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/source-upgrade.integration.test.js +100 -0
- package/dist/__tests__/integration/source-upgrade.integration.test.js.map +1 -0
- package/dist/cmds/source/upgrade.d.ts.map +1 -1
- package/dist/cmds/source/upgrade.js +45 -2
- package/dist/cmds/source/upgrade.js.map +1 -1
- package/dist/source/ai-gateway/catalog.json +411 -0
- package/dist/source/ai-gateway/files.d.ts +16 -0
- package/dist/source/ai-gateway/files.d.ts.map +1 -0
- package/dist/source/ai-gateway/files.js +174 -0
- package/dist/source/ai-gateway/files.js.map +1 -0
- package/dist/source/ai-gateway/files.test.d.ts +2 -0
- package/dist/source/ai-gateway/files.test.d.ts.map +1 -0
- package/dist/source/ai-gateway/files.test.js +135 -0
- package/dist/source/ai-gateway/files.test.js.map +1 -0
- package/dist/source/ai-gateway/handler.d.ts +31 -0
- package/dist/source/ai-gateway/handler.d.ts.map +1 -0
- package/dist/source/ai-gateway/handler.js +140 -0
- package/dist/source/ai-gateway/handler.js.map +1 -0
- package/dist/source/ai-gateway/handler.test.d.ts +2 -0
- package/dist/source/ai-gateway/handler.test.d.ts.map +1 -0
- package/dist/source/ai-gateway/handler.test.js +113 -0
- package/dist/source/ai-gateway/handler.test.js.map +1 -0
- package/dist/source/ai-gateway/package.test.d.ts +2 -0
- package/dist/source/ai-gateway/package.test.d.ts.map +1 -0
- package/dist/source/ai-gateway/package.test.js +30 -0
- package/dist/source/ai-gateway/package.test.js.map +1 -0
- package/dist/source/ai-gateway/planner.d.ts +132 -0
- package/dist/source/ai-gateway/planner.d.ts.map +1 -0
- package/dist/source/ai-gateway/planner.js +365 -0
- package/dist/source/ai-gateway/planner.js.map +1 -0
- package/dist/source/ai-gateway/planner.test.d.ts +2 -0
- package/dist/source/ai-gateway/planner.test.d.ts.map +1 -0
- package/dist/source/ai-gateway/planner.test.js +282 -0
- package/dist/source/ai-gateway/planner.test.js.map +1 -0
- package/dist/source/ai-gateway/release.d.ts +19 -0
- package/dist/source/ai-gateway/release.d.ts.map +1 -0
- package/dist/source/ai-gateway/release.js +75 -0
- package/dist/source/ai-gateway/release.js.map +1 -0
- package/dist/source/ai-gateway/release.test.d.ts +2 -0
- package/dist/source/ai-gateway/release.test.d.ts.map +1 -0
- package/dist/source/ai-gateway/release.test.js +62 -0
- package/dist/source/ai-gateway/release.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@posthog/core/dist/featureFlagUtils.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/featureFlagUtils.js +2 -1
- package/node_modules/@posthog/core/dist/featureFlagUtils.mjs +2 -1
- package/node_modules/@posthog/core/dist/index.d.ts +1 -0
- package/node_modules/@posthog/core/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/index.js +66 -10
- package/node_modules/@posthog/core/dist/index.mjs +1 -0
- package/node_modules/@posthog/core/dist/privacy.d.ts +35 -0
- package/node_modules/@posthog/core/dist/privacy.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/privacy.js +366 -0
- package/node_modules/@posthog/core/dist/privacy.mjs +319 -0
- package/node_modules/@posthog/core/package.json +2 -2
- package/node_modules/@posthog/core/src/featureFlagUtils.ts +2 -0
- package/node_modules/@posthog/core/src/index.ts +1 -0
- package/node_modules/@posthog/core/src/privacy.ts +696 -0
- package/node_modules/@posthog/types/dist/index.d.ts +1 -1
- package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/posthog-config.d.ts +39 -8
- package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
- package/node_modules/@posthog/types/package.json +1 -1
- package/node_modules/@posthog/types/src/index.ts +1 -0
- package/node_modules/@posthog/types/src/posthog-config.ts +41 -8
- package/node_modules/@zuplo/core/customer.cli.minified.js +215 -215
- package/node_modules/@zuplo/core/index.minified.js +220 -220
- package/node_modules/@zuplo/core/package.json +1 -1
- package/node_modules/@zuplo/graphql/package.json +1 -1
- package/node_modules/@zuplo/openapi-tools/package.json +1 -1
- package/node_modules/@zuplo/otel/package.json +1 -1
- package/node_modules/@zuplo/runtime/out/esm/chunk-2XSPVUOE.js +411 -0
- package/node_modules/@zuplo/runtime/out/esm/chunk-2XSPVUOE.js.map +1 -0
- package/node_modules/@zuplo/runtime/out/esm/chunk-PBDT4FDO.js +37 -0
- package/node_modules/@zuplo/runtime/out/esm/chunk-PBDT4FDO.js.map +1 -0
- package/node_modules/@zuplo/runtime/out/esm/{chunk-QTXBMT6O.js → chunk-VTO3F6DK.js} +2 -2
- package/node_modules/@zuplo/runtime/out/esm/{chunk-7AHIER5R.js → chunk-YJ2BHVUH.js} +2 -2
- package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/index.js.map +1 -1
- package/node_modules/@zuplo/runtime/out/esm/internal/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/sdks/aws/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/types/index.d.ts +152 -54
- package/node_modules/@zuplo/runtime/out/types/internal/index.d.ts +32 -0
- package/node_modules/@zuplo/runtime/package.json +1 -1
- package/node_modules/express-rate-limit/dist/index.cjs +9 -7
- package/node_modules/express-rate-limit/dist/index.mjs +9 -7
- package/node_modules/express-rate-limit/package.json +9 -9
- package/package.json +11 -11
- package/node_modules/@zuplo/runtime/out/esm/chunk-DDUIJEX3.js +0 -411
- package/node_modules/@zuplo/runtime/out/esm/chunk-DDUIJEX3.js.map +0 -1
- package/node_modules/@zuplo/runtime/out/esm/chunk-KESHY2P3.js +0 -37
- package/node_modules/@zuplo/runtime/out/esm/chunk-KESHY2P3.js.map +0 -1
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-DDUIJEX3.js.LEGAL.txt → chunk-2XSPVUOE.js.LEGAL.txt} +0 -0
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-KESHY2P3.js.LEGAL.txt → chunk-PBDT4FDO.js.LEGAL.txt} +0 -0
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-QTXBMT6O.js.map → chunk-VTO3F6DK.js.map} +0 -0
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-7AHIER5R.js.map → chunk-YJ2BHVUH.js.map} +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Files, type UpgradePlan } from "./planner.js";
|
|
2
|
+
export declare function git(dir: string, args: string[]): Promise<string>;
|
|
3
|
+
export declare function readProject(dir: string): Promise<Files>;
|
|
4
|
+
export declare function requireCleanProject(
|
|
5
|
+
dir: string,
|
|
6
|
+
plan: UpgradePlan
|
|
7
|
+
): Promise<void>;
|
|
8
|
+
export declare function applyPlan(
|
|
9
|
+
dir: string,
|
|
10
|
+
plan: UpgradePlan,
|
|
11
|
+
backupRoot: string,
|
|
12
|
+
hooks?: {
|
|
13
|
+
beforeReplace?: (file: string, index: number) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
): Promise<string>;
|
|
16
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../src/source/ai-gateway/files.ts"],"names":[],"mappings":"AAeA,OAAO,EAAe,KAAK,KAAK,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAGzE,wBAAsB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAItE;AAgBD,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAsC7D;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,IAAI,CAAC,CAaf;AAGD,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE;IAAE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAO,GAC7E,OAAO,CAAC,MAAM,CAAC,CAgHjB"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { lstat, mkdir, mkdtemp, open, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { isDeepStrictEqual, promisify } from "node:util";
|
|
7
|
+
import { contentHash } from "./planner.js";
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
export async function git(dir, args) {
|
|
10
|
+
return (await exec("git", ["-C", dir, ...args], { maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
11
|
+
}
|
|
12
|
+
async function readRegularFile(file) {
|
|
13
|
+
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
14
|
+
try {
|
|
15
|
+
const stat = await handle.stat();
|
|
16
|
+
if (!stat.isFile() || stat.size > 5 * 1024 * 1024)
|
|
17
|
+
throw new Error(`${file}: expected a regular file no larger than 5 MB.`);
|
|
18
|
+
return await handle.readFile("utf8");
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await handle.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function readProject(dir) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
async function read(relative, optional = false) {
|
|
27
|
+
const absolute = path.join(dir, relative);
|
|
28
|
+
let stat;
|
|
29
|
+
try {
|
|
30
|
+
stat = await lstat(absolute);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (optional && error.code === "ENOENT")
|
|
34
|
+
return;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
if (stat.isSymbolicLink())
|
|
38
|
+
throw new Error(`${relative}: symlinks require manual review.`);
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
for (const name of (await readdir(absolute)).sort())
|
|
41
|
+
await read(`${relative}/${name}`);
|
|
42
|
+
}
|
|
43
|
+
else if (stat.isFile()) {
|
|
44
|
+
if (stat.size > 5 * 1024 * 1024)
|
|
45
|
+
throw new Error(`${relative}: file is too large for a conservative upgrade.`);
|
|
46
|
+
entries.push([relative, await readRegularFile(absolute)]);
|
|
47
|
+
}
|
|
48
|
+
else
|
|
49
|
+
throw new Error(`${relative}: expected a regular file.`);
|
|
50
|
+
}
|
|
51
|
+
for (const file of [
|
|
52
|
+
"config",
|
|
53
|
+
"modules",
|
|
54
|
+
"zuplo.jsonc",
|
|
55
|
+
"package.json",
|
|
56
|
+
"package-lock.json",
|
|
57
|
+
"npm-shrinkwrap.json",
|
|
58
|
+
"pnpm-lock.yaml",
|
|
59
|
+
"yarn.lock",
|
|
60
|
+
".zuplo-template.json",
|
|
61
|
+
])
|
|
62
|
+
await read(file, true);
|
|
63
|
+
return Object.fromEntries(entries);
|
|
64
|
+
}
|
|
65
|
+
export async function requireCleanProject(dir, plan) {
|
|
66
|
+
if ((await git(dir, ["status", "--porcelain", "--untracked-files=all"])).trim()) {
|
|
67
|
+
throw new Error("Commit or stash your changes before applying an AI Gateway upgrade. Preview is available without a clean working tree.");
|
|
68
|
+
}
|
|
69
|
+
for (const file of Object.keys(plan.after).filter((f) => plan.before[f] !== undefined)) {
|
|
70
|
+
await git(dir, ["ls-files", "--error-unmatch", "--", file]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function applyPlan(dir, plan, backupRoot, hooks = {}) {
|
|
74
|
+
if (plan.blockers.length)
|
|
75
|
+
throw new Error("Blocked upgrade plans cannot be applied.");
|
|
76
|
+
await mkdir(backupRoot, { recursive: true, mode: 0o700 });
|
|
77
|
+
const lock = path.join(backupRoot, `${contentHash(path.resolve(dir))}.lock`);
|
|
78
|
+
try {
|
|
79
|
+
await writeFile(lock, String(process.pid), { flag: "wx", mode: 0o600 });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code !== "EEXIST")
|
|
83
|
+
throw error;
|
|
84
|
+
throw new Error(`Another upgrade may be running. Inspect ${lock} before removing a stale lock.`);
|
|
85
|
+
}
|
|
86
|
+
let backup;
|
|
87
|
+
const written = [];
|
|
88
|
+
const temporary = [];
|
|
89
|
+
try {
|
|
90
|
+
await requireCleanProject(dir, plan);
|
|
91
|
+
if (!isDeepStrictEqual(await readProject(dir), plan.before))
|
|
92
|
+
throw new Error("Project files changed after preview. Run the preview again.");
|
|
93
|
+
backup = await mkdtemp(path.join(backupRoot, "ai-gateway-"));
|
|
94
|
+
const journal = Object.keys(plan.after).map((file) => ({
|
|
95
|
+
file,
|
|
96
|
+
existed: plan.before[file] !== undefined,
|
|
97
|
+
before: plan.before[file] === undefined ? null : contentHash(plan.before[file]),
|
|
98
|
+
after: contentHash(plan.after[file]),
|
|
99
|
+
}));
|
|
100
|
+
await writeFile(path.join(backup, "journal.json"), JSON.stringify({
|
|
101
|
+
project: dir,
|
|
102
|
+
release: plan.release,
|
|
103
|
+
target: plan.target,
|
|
104
|
+
files: journal,
|
|
105
|
+
}, null, 2), { mode: 0o600, flush: true });
|
|
106
|
+
for (const file of Object.keys(plan.after)) {
|
|
107
|
+
if (plan.before[file] !== undefined) {
|
|
108
|
+
await mkdir(path.dirname(path.join(backup, file)), {
|
|
109
|
+
recursive: true,
|
|
110
|
+
mode: 0o700,
|
|
111
|
+
});
|
|
112
|
+
await writeFile(path.join(backup, file), plan.before[file], {
|
|
113
|
+
mode: 0o600,
|
|
114
|
+
flush: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const expected = { ...plan.before };
|
|
119
|
+
for (const [index, file] of Object.keys(plan.after).entries()) {
|
|
120
|
+
await hooks.beforeReplace?.(file, index);
|
|
121
|
+
if (!isDeepStrictEqual(await readProject(dir), expected))
|
|
122
|
+
throw new Error("Project files changed while applying the upgrade. Stopping.");
|
|
123
|
+
const destination = path.join(dir, file);
|
|
124
|
+
const mode = plan.before[file] === undefined
|
|
125
|
+
? 0o644
|
|
126
|
+
: (await lstat(destination)).mode;
|
|
127
|
+
const temp = `${destination}.zuplo-upgrade-${randomUUID()}.tmp`;
|
|
128
|
+
temporary.push(temp);
|
|
129
|
+
await writeFile(temp, plan.after[file], {
|
|
130
|
+
mode,
|
|
131
|
+
flag: "wx",
|
|
132
|
+
flush: true,
|
|
133
|
+
});
|
|
134
|
+
await rename(temp, destination);
|
|
135
|
+
written.push(file);
|
|
136
|
+
expected[file] = plan.after[file];
|
|
137
|
+
}
|
|
138
|
+
return backup;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const recovery = [];
|
|
142
|
+
for (const file of written.reverse()) {
|
|
143
|
+
try {
|
|
144
|
+
const destination = path.join(dir, file);
|
|
145
|
+
if ((await readRegularFile(destination)) !== plan.after[file]) {
|
|
146
|
+
recovery.push(file);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (plan.before[file] === undefined)
|
|
150
|
+
await rm(destination);
|
|
151
|
+
else {
|
|
152
|
+
const temp = `${destination}.zuplo-upgrade-${randomUUID()}.tmp`;
|
|
153
|
+
temporary.push(temp);
|
|
154
|
+
await writeFile(temp, plan.before[file], {
|
|
155
|
+
mode: (await lstat(destination)).mode,
|
|
156
|
+
flag: "wx",
|
|
157
|
+
flush: true,
|
|
158
|
+
});
|
|
159
|
+
await rename(temp, destination);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
recovery.push(file);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}${backup ? ` Originals and recovery journal: ${backup}.` : ""}${recovery.length ? ` Restore manually after reviewing concurrent edits: ${recovery.join(", ")}.` : ""}`);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
for (const temp of temporary)
|
|
170
|
+
await rm(temp, { force: true }).catch(() => { });
|
|
171
|
+
await rm(lock, { force: true }).catch(() => { });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/source/ai-gateway/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EACL,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,EACJ,OAAO,EACP,MAAM,EACN,EAAE,EACF,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,WAAW,EAAgC,MAAM,cAAc,CAAC;AAEzE,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AACjC,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,GAAW,EAAE,IAAc;IACnD,OAAO,CACL,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CACxE,CAAC,MAAM,CAAC;AACX,CAAC;AAGD,KAAK,UAAU,eAAe,CAAC,IAAY;IACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IAC3E,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI;YAC/C,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,gDAAgD,CAAC,CAAC;QAC3E,OAAO,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AAGD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW;IAC3C,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,KAAK,UAAU,IAAI,CAAC,QAAgB,EAAE,QAAQ,GAAG,KAAK;QACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC;QACT,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,QAAQ,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAChE,OAAO;YACT,MAAM,KAAK,CAAC;QACd,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,EAAE;YACvB,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,mCAAmC,CAAC,CAAC;QAClE,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACvB,KAAK,MAAM,IAAI,IAAI,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE;gBACjD,MAAM,IAAI,CAAC,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC,CAAC;QACtC,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI;gBAC7B,MAAM,IAAI,KAAK,CACb,GAAG,QAAQ,iDAAiD,CAC7D,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5D,CAAC;;YAAM,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,4BAA4B,CAAC,CAAC;IAClE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI;QACjB,QAAQ;QACR,SAAS;QACT,aAAa;QACb,cAAc;QACd,mBAAmB;QACnB,qBAAqB;QACrB,gBAAgB;QAChB,WAAW;QACX,sBAAsB;KACvB;QACC,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACzB,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAW,EACX,IAAiB;IAEjB,IACE,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAC3E,CAAC;QACD,MAAM,IAAI,KAAK,CACb,wHAAwH,CACzH,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAC/C,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,SAAS,CACpC,EAAE,CAAC;QACF,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAGD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAW,EACX,IAAiB,EACjB,UAAkB,EAClB,QAA4E,EAAE;IAE9E,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM;QACtB,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,MAAM,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAC7E,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAC;QACpE,MAAM,IAAI,KAAK,CACb,2CAA2C,IAAI,gCAAgC,CAChF,CAAC;IACJ,CAAC;IACD,IAAI,MAA0B,CAAC;IAC/B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,iBAAiB,CAAC,MAAM,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC;YACzD,MAAM,IAAI,KAAK,CACb,6DAA6D,CAC9D,CAAC;QACJ,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,CAAC;QAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACrD,IAAI;YACJ,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS;YACxC,MAAM,EACJ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACzE,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;SACrC,CAAC,CAAC,CAAC;QACJ,MAAM,SAAS,CACb,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EACjC,IAAI,CAAC,SAAS,CACZ;YACE,OAAO,EAAE,GAAG;YACZ,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,OAAO;SACf,EACD,IAAI,EACJ,CAAC,CACF,EACD,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAC7B,CAAC;QACF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3C,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;gBACpC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE;oBACjD,SAAS,EAAE,IAAI;oBACf,IAAI,EAAE,KAAK;iBACZ,CAAC,CAAC;gBACH,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;oBAC1D,IAAI,EAAE,KAAK;oBACX,KAAK,EAAE,IAAI;iBACZ,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,MAAM,QAAQ,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACpC,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;YAC9D,MAAM,KAAK,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACzC,IAAI,CAAC,iBAAiB,CAAC,MAAM,WAAW,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC;gBACtD,MAAM,IAAI,KAAK,CACb,6DAA6D,CAC9D,CAAC;YACJ,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACzC,MAAM,IAAI,GACR,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS;gBAC7B,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;YACtC,MAAM,IAAI,GAAG,GAAG,WAAW,kBAAkB,UAAU,EAAE,MAAM,CAAC;YAChE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrB,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;gBACtC,IAAI;gBACJ,IAAI,EAAE,IAAI;gBACV,KAAK,EAAE,IAAI;aACZ,CAAC,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YACrC,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACzC,IAAI,CAAC,MAAM,eAAe,CAAC,WAAW,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9D,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACpB,SAAS;gBACX,CAAC;gBACD,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS;oBAAE,MAAM,EAAE,CAAC,WAAW,CAAC,CAAC;qBACtD,CAAC;oBACJ,MAAM,IAAI,GAAG,GAAG,WAAW,kBAAkB,UAAU,EAAE,MAAM,CAAC;oBAChE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACrB,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;wBACvC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI;wBACrC,IAAI,EAAE,IAAI;wBACV,KAAK,EAAE,IAAI;qBACZ,CAAC,CAAC;oBACH,MAAM,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;gBAClC,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,oCAAoC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,uDAAuD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACjO,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,KAAK,MAAM,IAAI,IAAI,SAAS;YAC1B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAClD,CAAC;AACH,CAAC","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { constants } from \"node:fs\";\nimport {\n lstat,\n mkdir,\n mkdtemp,\n open,\n readdir,\n rename,\n rm,\n writeFile,\n} from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { isDeepStrictEqual, promisify } from \"node:util\";\nimport { contentHash, type Files, type UpgradePlan } from \"./planner.js\";\n\nconst exec = promisify(execFile);\nexport async function git(dir: string, args: string[]): Promise<string> {\n return (\n await exec(\"git\", [\"-C\", dir, ...args], { maxBuffer: 8 * 1024 * 1024 })\n ).stdout;\n}\n\n/** Read and validate the same open file; never follow a final-component symlink. */\nasync function readRegularFile(file: string): Promise<string> {\n const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);\n try {\n const stat = await handle.stat();\n if (!stat.isFile() || stat.size > 5 * 1024 * 1024)\n throw new Error(`${file}: expected a regular file no larger than 5 MB.`);\n return await handle.readFile(\"utf8\");\n } finally {\n await handle.close();\n }\n}\n\n/** Capture the full routing/configuration inventory, including files not changed by the upgrade. */\nexport async function readProject(dir: string): Promise<Files> {\n const entries: [string, string][] = [];\n async function read(relative: string, optional = false): Promise<void> {\n const absolute = path.join(dir, relative);\n let stat;\n try {\n stat = await lstat(absolute);\n } catch (error) {\n if (optional && (error as NodeJS.ErrnoException).code === \"ENOENT\")\n return;\n throw error;\n }\n if (stat.isSymbolicLink())\n throw new Error(`${relative}: symlinks require manual review.`);\n if (stat.isDirectory()) {\n for (const name of (await readdir(absolute)).sort())\n await read(`${relative}/${name}`);\n } else if (stat.isFile()) {\n if (stat.size > 5 * 1024 * 1024)\n throw new Error(\n `${relative}: file is too large for a conservative upgrade.`\n );\n entries.push([relative, await readRegularFile(absolute)]);\n } else throw new Error(`${relative}: expected a regular file.`);\n }\n for (const file of [\n \"config\",\n \"modules\",\n \"zuplo.jsonc\",\n \"package.json\",\n \"package-lock.json\",\n \"npm-shrinkwrap.json\",\n \"pnpm-lock.yaml\",\n \"yarn.lock\",\n \".zuplo-template.json\",\n ])\n await read(file, true);\n return Object.fromEntries(entries);\n}\n\nexport async function requireCleanProject(\n dir: string,\n plan: UpgradePlan\n): Promise<void> {\n if (\n (await git(dir, [\"status\", \"--porcelain\", \"--untracked-files=all\"])).trim()\n ) {\n throw new Error(\n \"Commit or stash your changes before applying an AI Gateway upgrade. Preview is available without a clean working tree.\"\n );\n }\n for (const file of Object.keys(plan.after).filter(\n (f) => plan.before[f] !== undefined\n )) {\n await git(dir, [\"ls-files\", \"--error-unmatch\", \"--\", file]);\n }\n}\n\n/** Atomic per-file replacement, durable originals, and rollback that never overwrites a concurrent edit. */\nexport async function applyPlan(\n dir: string,\n plan: UpgradePlan,\n backupRoot: string,\n hooks: { beforeReplace?: (file: string, index: number) => Promise<void> } = {}\n): Promise<string> {\n if (plan.blockers.length)\n throw new Error(\"Blocked upgrade plans cannot be applied.\");\n await mkdir(backupRoot, { recursive: true, mode: 0o700 });\n const lock = path.join(backupRoot, `${contentHash(path.resolve(dir))}.lock`);\n try {\n await writeFile(lock, String(process.pid), { flag: \"wx\", mode: 0o600 });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n throw new Error(\n `Another upgrade may be running. Inspect ${lock} before removing a stale lock.`\n );\n }\n let backup: string | undefined;\n const written: string[] = [];\n const temporary: string[] = [];\n try {\n await requireCleanProject(dir, plan);\n if (!isDeepStrictEqual(await readProject(dir), plan.before))\n throw new Error(\n \"Project files changed after preview. Run the preview again.\"\n );\n backup = await mkdtemp(path.join(backupRoot, \"ai-gateway-\"));\n const journal = Object.keys(plan.after).map((file) => ({\n file,\n existed: plan.before[file] !== undefined,\n before:\n plan.before[file] === undefined ? null : contentHash(plan.before[file]),\n after: contentHash(plan.after[file]),\n }));\n await writeFile(\n path.join(backup, \"journal.json\"),\n JSON.stringify(\n {\n project: dir,\n release: plan.release,\n target: plan.target,\n files: journal,\n },\n null,\n 2\n ),\n { mode: 0o600, flush: true }\n );\n for (const file of Object.keys(plan.after)) {\n if (plan.before[file] !== undefined) {\n await mkdir(path.dirname(path.join(backup, file)), {\n recursive: true,\n mode: 0o700,\n });\n await writeFile(path.join(backup, file), plan.before[file], {\n mode: 0o600,\n flush: true,\n });\n }\n }\n const expected = { ...plan.before };\n for (const [index, file] of Object.keys(plan.after).entries()) {\n await hooks.beforeReplace?.(file, index);\n if (!isDeepStrictEqual(await readProject(dir), expected))\n throw new Error(\n \"Project files changed while applying the upgrade. Stopping.\"\n );\n const destination = path.join(dir, file);\n const mode =\n plan.before[file] === undefined\n ? 0o644\n : (await lstat(destination)).mode;\n const temp = `${destination}.zuplo-upgrade-${randomUUID()}.tmp`;\n temporary.push(temp);\n await writeFile(temp, plan.after[file], {\n mode,\n flag: \"wx\",\n flush: true,\n });\n await rename(temp, destination);\n written.push(file);\n expected[file] = plan.after[file];\n }\n return backup;\n } catch (error) {\n const recovery: string[] = [];\n for (const file of written.reverse()) {\n try {\n const destination = path.join(dir, file);\n if ((await readRegularFile(destination)) !== plan.after[file]) {\n recovery.push(file);\n continue;\n }\n if (plan.before[file] === undefined) await rm(destination);\n else {\n const temp = `${destination}.zuplo-upgrade-${randomUUID()}.tmp`;\n temporary.push(temp);\n await writeFile(temp, plan.before[file], {\n mode: (await lstat(destination)).mode,\n flag: \"wx\",\n flush: true,\n });\n await rename(temp, destination);\n }\n } catch {\n recovery.push(file);\n }\n }\n throw new Error(\n `${error instanceof Error ? error.message : String(error)}${backup ? ` Originals and recovery journal: ${backup}.` : \"\"}${recovery.length ? ` Restore manually after reviewing concurrent edits: ${recovery.join(\", \")}.` : \"\"}`\n );\n } finally {\n for (const temp of temporary)\n await rm(temp, { force: true }).catch(() => {});\n await rm(lock, { force: true }).catch(() => {});\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.test.d.ts","sourceRoot":"","sources":["../../../src/source/ai-gateway/files.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmod, lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
6
|
+
import { applyPlan, git, readProject } from "./files.js";
|
|
7
|
+
import { AI_OAS, catalog, POLICIES, planUpgrade } from "./planner.js";
|
|
8
|
+
const dirs = [];
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
for (const dir of dirs.splice(0))
|
|
11
|
+
await rm(dir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
async function fixture() {
|
|
14
|
+
const root = await mkdtemp(path.join(tmpdir(), "gateway-upgrade-test-"));
|
|
15
|
+
dirs.push(root);
|
|
16
|
+
const dir = path.join(root, "project");
|
|
17
|
+
await mkdir(dir);
|
|
18
|
+
for (const [file, content] of Object.entries(catalog.snapshots[1].files)) {
|
|
19
|
+
await mkdir(path.dirname(path.join(dir, file)), { recursive: true });
|
|
20
|
+
await writeFile(path.join(dir, file), content);
|
|
21
|
+
}
|
|
22
|
+
await git(dir, ["init"]);
|
|
23
|
+
await git(dir, ["add", "."]);
|
|
24
|
+
await git(dir, [
|
|
25
|
+
"-c",
|
|
26
|
+
"user.name=Test",
|
|
27
|
+
"-c",
|
|
28
|
+
"user.email=test@example.com",
|
|
29
|
+
"-c",
|
|
30
|
+
"commit.gpgsign=false",
|
|
31
|
+
"commit",
|
|
32
|
+
"-m",
|
|
33
|
+
"baseline",
|
|
34
|
+
]);
|
|
35
|
+
const before = await readProject(dir);
|
|
36
|
+
const plan = planUpgrade(before, "1.0.0");
|
|
37
|
+
assert.deepEqual(plan.blockers, []);
|
|
38
|
+
return { dir, backupRoot: path.join(root, "backups"), before, plan };
|
|
39
|
+
}
|
|
40
|
+
describe("AI Gateway upgrade transaction", () => {
|
|
41
|
+
it("applies additions and retains originals with a recovery journal", async () => {
|
|
42
|
+
const { dir, backupRoot, before, plan } = await fixture();
|
|
43
|
+
const backup = await applyPlan(dir, plan, backupRoot);
|
|
44
|
+
assert.equal(await readFile(path.join(backup, AI_OAS), "utf8"), before[AI_OAS]);
|
|
45
|
+
assert.ok(JSON.parse(await readFile(path.join(backup, "journal.json"), "utf8"))
|
|
46
|
+
.files.length);
|
|
47
|
+
assert.equal(await readFile(path.join(dir, AI_OAS), "utf8"), plan.after[AI_OAS]);
|
|
48
|
+
assert.deepEqual(planUpgrade(await readProject(dir), "1.0.0").additions, []);
|
|
49
|
+
});
|
|
50
|
+
it("reports lock permission failures without claiming another upgrade is running", {
|
|
51
|
+
skip: process.platform === "win32" || process.getuid?.() === 0,
|
|
52
|
+
}, async () => {
|
|
53
|
+
const { dir, backupRoot, before, plan } = await fixture();
|
|
54
|
+
await mkdir(backupRoot, { mode: 0o500 });
|
|
55
|
+
try {
|
|
56
|
+
await assert.rejects(applyPlan(dir, plan, backupRoot), {
|
|
57
|
+
code: "EACCES",
|
|
58
|
+
});
|
|
59
|
+
assert.deepEqual(await readProject(dir), before);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await chmod(backupRoot, 0o700);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
it("writes nothing for dirty projects", async () => {
|
|
66
|
+
const { dir, backupRoot, before, plan } = await fixture();
|
|
67
|
+
await writeFile(path.join(dir, "scratch.txt"), "uncommitted");
|
|
68
|
+
await assert.rejects(applyPlan(dir, plan, backupRoot), /Commit or stash/);
|
|
69
|
+
assert.deepEqual(await readProject(dir), before);
|
|
70
|
+
});
|
|
71
|
+
it("rejects stale plans even when intervening edits were committed", async () => {
|
|
72
|
+
const { dir, backupRoot, plan } = await fixture();
|
|
73
|
+
await writeFile(path.join(dir, AI_OAS), `${plan.before[AI_OAS]}\n`);
|
|
74
|
+
await git(dir, ["add", "."]);
|
|
75
|
+
await git(dir, [
|
|
76
|
+
"-c",
|
|
77
|
+
"user.name=Test",
|
|
78
|
+
"-c",
|
|
79
|
+
"user.email=test@example.com",
|
|
80
|
+
"-c",
|
|
81
|
+
"commit.gpgsign=false",
|
|
82
|
+
"commit",
|
|
83
|
+
"-m",
|
|
84
|
+
"changed",
|
|
85
|
+
]);
|
|
86
|
+
await assert.rejects(applyPlan(dir, plan, backupRoot), /changed after preview/);
|
|
87
|
+
assert.equal(await readFile(path.join(dir, POLICIES), "utf8"), plan.before[POLICIES]);
|
|
88
|
+
});
|
|
89
|
+
it("rolls back a failure after the first replacement", async () => {
|
|
90
|
+
const { dir, backupRoot, before, plan } = await fixture();
|
|
91
|
+
await assert.rejects(applyPlan(dir, plan, backupRoot, {
|
|
92
|
+
beforeReplace: async (_file, index) => {
|
|
93
|
+
if (index === 1)
|
|
94
|
+
throw new Error("simulated disk failure");
|
|
95
|
+
},
|
|
96
|
+
}), /simulated disk failure.*Originals/s);
|
|
97
|
+
assert.deepEqual(await readProject(dir), before);
|
|
98
|
+
});
|
|
99
|
+
it("does not overwrite a concurrent edit during rollback", async () => {
|
|
100
|
+
const { dir, backupRoot, plan } = await fixture();
|
|
101
|
+
const first = Object.keys(plan.after)[0];
|
|
102
|
+
await assert.rejects(applyPlan(dir, plan, backupRoot, {
|
|
103
|
+
beforeReplace: async (_file, index) => {
|
|
104
|
+
if (index === 1)
|
|
105
|
+
await writeFile(path.join(dir, first), "concurrent edit");
|
|
106
|
+
},
|
|
107
|
+
}), /Restore manually/);
|
|
108
|
+
assert.equal(await readFile(path.join(dir, first), "utf8"), "concurrent edit");
|
|
109
|
+
});
|
|
110
|
+
it("preserves a file replaced with a symlink during rollback", async () => {
|
|
111
|
+
const { dir, backupRoot, plan } = await fixture();
|
|
112
|
+
const first = Object.keys(plan.after)[0];
|
|
113
|
+
const destination = path.join(dir, first);
|
|
114
|
+
const external = path.join(path.dirname(dir), "external.json");
|
|
115
|
+
await writeFile(external, "external contents");
|
|
116
|
+
await assert.rejects(applyPlan(dir, plan, backupRoot, {
|
|
117
|
+
beforeReplace: async (_file, index) => {
|
|
118
|
+
if (index === 1) {
|
|
119
|
+
await rm(destination);
|
|
120
|
+
await symlink(external, destination);
|
|
121
|
+
throw new Error("concurrent replacement");
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}), /Restore manually/);
|
|
125
|
+
assert.ok((await lstat(destination)).isSymbolicLink());
|
|
126
|
+
assert.equal(await readFile(external, "utf8"), "external contents");
|
|
127
|
+
});
|
|
128
|
+
it("rejects symlinked configuration and blocked plans", async () => {
|
|
129
|
+
const { dir, backupRoot, plan } = await fixture();
|
|
130
|
+
await assert.rejects(applyPlan(dir, { ...plan, blockers: ["conflict"] }, backupRoot), /Blocked upgrade/);
|
|
131
|
+
await symlink(path.join(dir, AI_OAS), path.join(dir, "config/other.oas.json"));
|
|
132
|
+
await assert.rejects(readProject(dir), /symlinks/);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
//# sourceMappingURL=files.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.test.js","sourceRoot":"","sources":["../../../src/source/ai-gateway/files.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport {\n chmod,\n lstat,\n mkdir,\n mkdtemp,\n readFile,\n rm,\n symlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { afterEach, describe, it } from \"node:test\";\nimport { applyPlan, git, readProject } from \"./files.js\";\nimport { AI_OAS, catalog, POLICIES, planUpgrade } from \"./planner.js\";\n\nconst dirs: string[] = [];\nafterEach(async () => {\n for (const dir of dirs.splice(0))\n await rm(dir, { recursive: true, force: true });\n});\nasync function fixture() {\n const root = await mkdtemp(path.join(tmpdir(), \"gateway-upgrade-test-\"));\n dirs.push(root);\n const dir = path.join(root, \"project\");\n await mkdir(dir);\n for (const [file, content] of Object.entries(catalog.snapshots[1].files)) {\n await mkdir(path.dirname(path.join(dir, file)), { recursive: true });\n await writeFile(path.join(dir, file), content);\n }\n await git(dir, [\"init\"]);\n await git(dir, [\"add\", \".\"]);\n await git(dir, [\n \"-c\",\n \"user.name=Test\",\n \"-c\",\n \"user.email=test@example.com\",\n \"-c\",\n \"commit.gpgsign=false\",\n \"commit\",\n \"-m\",\n \"baseline\",\n ]);\n const before = await readProject(dir);\n const plan = planUpgrade(before, \"1.0.0\");\n assert.deepEqual(plan.blockers, []);\n return { dir, backupRoot: path.join(root, \"backups\"), before, plan };\n}\n\ndescribe(\"AI Gateway upgrade transaction\", () => {\n it(\"applies additions and retains originals with a recovery journal\", async () => {\n const { dir, backupRoot, before, plan } = await fixture();\n const backup = await applyPlan(dir, plan, backupRoot);\n assert.equal(\n await readFile(path.join(backup, AI_OAS), \"utf8\"),\n before[AI_OAS]\n );\n assert.ok(\n JSON.parse(await readFile(path.join(backup, \"journal.json\"), \"utf8\"))\n .files.length\n );\n assert.equal(\n await readFile(path.join(dir, AI_OAS), \"utf8\"),\n plan.after[AI_OAS]\n );\n assert.deepEqual(\n planUpgrade(await readProject(dir), \"1.0.0\").additions,\n []\n );\n });\n it(\n \"reports lock permission failures without claiming another upgrade is running\",\n {\n skip: process.platform === \"win32\" || process.getuid?.() === 0,\n },\n async () => {\n const { dir, backupRoot, before, plan } = await fixture();\n await mkdir(backupRoot, { mode: 0o500 });\n try {\n await assert.rejects(applyPlan(dir, plan, backupRoot), {\n code: \"EACCES\",\n });\n assert.deepEqual(await readProject(dir), before);\n } finally {\n await chmod(backupRoot, 0o700);\n }\n }\n );\n it(\"writes nothing for dirty projects\", async () => {\n const { dir, backupRoot, before, plan } = await fixture();\n await writeFile(path.join(dir, \"scratch.txt\"), \"uncommitted\");\n await assert.rejects(applyPlan(dir, plan, backupRoot), /Commit or stash/);\n assert.deepEqual(await readProject(dir), before);\n });\n it(\"rejects stale plans even when intervening edits were committed\", async () => {\n const { dir, backupRoot, plan } = await fixture();\n await writeFile(path.join(dir, AI_OAS), `${plan.before[AI_OAS]}\\n`);\n await git(dir, [\"add\", \".\"]);\n await git(dir, [\n \"-c\",\n \"user.name=Test\",\n \"-c\",\n \"user.email=test@example.com\",\n \"-c\",\n \"commit.gpgsign=false\",\n \"commit\",\n \"-m\",\n \"changed\",\n ]);\n await assert.rejects(\n applyPlan(dir, plan, backupRoot),\n /changed after preview/\n );\n assert.equal(\n await readFile(path.join(dir, POLICIES), \"utf8\"),\n plan.before[POLICIES]\n );\n });\n it(\"rolls back a failure after the first replacement\", async () => {\n const { dir, backupRoot, before, plan } = await fixture();\n await assert.rejects(\n applyPlan(dir, plan, backupRoot, {\n beforeReplace: async (_file, index) => {\n if (index === 1) throw new Error(\"simulated disk failure\");\n },\n }),\n /simulated disk failure.*Originals/s\n );\n assert.deepEqual(await readProject(dir), before);\n });\n it(\"does not overwrite a concurrent edit during rollback\", async () => {\n const { dir, backupRoot, plan } = await fixture();\n const first = Object.keys(plan.after)[0];\n await assert.rejects(\n applyPlan(dir, plan, backupRoot, {\n beforeReplace: async (_file, index) => {\n if (index === 1)\n await writeFile(path.join(dir, first), \"concurrent edit\");\n },\n }),\n /Restore manually/\n );\n assert.equal(\n await readFile(path.join(dir, first), \"utf8\"),\n \"concurrent edit\"\n );\n });\n it(\"preserves a file replaced with a symlink during rollback\", async () => {\n const { dir, backupRoot, plan } = await fixture();\n const first = Object.keys(plan.after)[0];\n const destination = path.join(dir, first);\n const external = path.join(path.dirname(dir), \"external.json\");\n await writeFile(external, \"external contents\");\n await assert.rejects(\n applyPlan(dir, plan, backupRoot, {\n beforeReplace: async (_file, index) => {\n if (index === 1) {\n await rm(destination);\n await symlink(external, destination);\n throw new Error(\"concurrent replacement\");\n }\n },\n }),\n /Restore manually/\n );\n assert.ok((await lstat(destination)).isSymbolicLink());\n assert.equal(await readFile(external, \"utf8\"), \"external contents\");\n });\n it(\"rejects symlinked configuration and blocked plans\", async () => {\n const { dir, backupRoot, plan } = await fixture();\n await assert.rejects(\n applyPlan(dir, { ...plan, blockers: [\"conflict\"] }, backupRoot),\n /Blocked upgrade/\n );\n await symlink(\n path.join(dir, AI_OAS),\n path.join(dir, \"config/other.oas.json\")\n );\n await assert.rejects(readProject(dir), /symlinks/);\n });\n});\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isInteractiveTerminal } from "../../common/terminal.js";
|
|
2
|
+
import { checkRelease, installedRuntime } from "./release.js";
|
|
3
|
+
export interface GatewayUpgradeArgs {
|
|
4
|
+
dir: string;
|
|
5
|
+
prompt: boolean;
|
|
6
|
+
apply?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const defaultServices: {
|
|
9
|
+
version: () => Promise<string>;
|
|
10
|
+
release: typeof checkRelease;
|
|
11
|
+
runtime: typeof installedRuntime;
|
|
12
|
+
confirm: import("@inquirer/type").Prompt<
|
|
13
|
+
boolean,
|
|
14
|
+
{
|
|
15
|
+
message: string;
|
|
16
|
+
default?: boolean;
|
|
17
|
+
transformer?: (value: boolean) => string;
|
|
18
|
+
theme?: import("@inquirer/type").PartialDeep<
|
|
19
|
+
import("@inquirer/core").Theme
|
|
20
|
+
>;
|
|
21
|
+
}
|
|
22
|
+
>;
|
|
23
|
+
interactive: typeof isInteractiveTerminal;
|
|
24
|
+
backupRoot: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function upgradeGateway(
|
|
27
|
+
args: GatewayUpgradeArgs,
|
|
28
|
+
overrides?: Partial<typeof defaultServices>
|
|
29
|
+
): Promise<number>;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/source/ai-gateway/handler.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAIjE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAkB,MAAM,cAAc,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAiDD,QAAA,MAAM,eAAe;mBACA,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;CASnC,CAAC;AAEF,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,EACxB,SAAS,GAAE,OAAO,CAAC,OAAO,eAAe,CAAM,GAC9C,OAAO,CAAC,MAAM,CAAC,CAmGjB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { confirm } from "@inquirer/prompts";
|
|
5
|
+
import { printDiagnosticsToConsole, printResultToConsole, } from "../../common/output.js";
|
|
6
|
+
import { isInteractiveTerminal } from "../../common/terminal.js";
|
|
7
|
+
import { ZUPLO_XDG_DATA_HOME } from "../../common/xdg/lib.js";
|
|
8
|
+
import { applyPlan, git, readProject, requireCleanProject } from "./files.js";
|
|
9
|
+
import { catalog, planUpgrade } from "./planner.js";
|
|
10
|
+
import { checkRelease, installedRuntime, runtimeBlocker } from "./release.js";
|
|
11
|
+
async function showDiff(plan) {
|
|
12
|
+
const temp = await mkdtemp(path.join(tmpdir(), "zuplo-upgrade-preview-"));
|
|
13
|
+
try {
|
|
14
|
+
for (const [file, text] of Object.entries(plan.after)) {
|
|
15
|
+
for (const side of ["before", "after"]) {
|
|
16
|
+
if (side === "before" && plan.before[file] === undefined)
|
|
17
|
+
continue;
|
|
18
|
+
const destination = path.join(temp, side, file);
|
|
19
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
20
|
+
await writeFile(destination, side === "before" ? (plan.before[file] ?? "") : text, { mode: 0o600 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (Object.keys(plan.after).length) {
|
|
24
|
+
try {
|
|
25
|
+
printResultToConsole(await git(temp, [
|
|
26
|
+
"diff",
|
|
27
|
+
"--no-index",
|
|
28
|
+
"--no-ext-diff",
|
|
29
|
+
"--no-textconv",
|
|
30
|
+
"--",
|
|
31
|
+
"before",
|
|
32
|
+
"after",
|
|
33
|
+
]));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error &&
|
|
37
|
+
typeof error === "object" &&
|
|
38
|
+
"code" in error &&
|
|
39
|
+
error.code === 1 &&
|
|
40
|
+
"stdout" in error &&
|
|
41
|
+
typeof error.stdout === "string")
|
|
42
|
+
printResultToConsole(error.stdout);
|
|
43
|
+
else
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await rm(temp, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const defaultServices = {
|
|
53
|
+
version: async () => JSON.parse(await readFile(new URL("../../../package.json", import.meta.url), "utf8")).version,
|
|
54
|
+
release: checkRelease,
|
|
55
|
+
runtime: installedRuntime,
|
|
56
|
+
confirm,
|
|
57
|
+
interactive: isInteractiveTerminal,
|
|
58
|
+
backupRoot: path.join(ZUPLO_XDG_DATA_HOME, "template-upgrade-backups"),
|
|
59
|
+
};
|
|
60
|
+
export async function upgradeGateway(args, overrides = {}) {
|
|
61
|
+
const dir = await realpath(path.resolve(args.dir));
|
|
62
|
+
const services = { ...defaultServices, ...overrides };
|
|
63
|
+
const version = await services.version();
|
|
64
|
+
const release = await services.release(version);
|
|
65
|
+
printResultToConsole(`AI Gateway template: bundled release ${version}, revision ${catalog.target}. Published stable: ${release.latest ?? "unverified"}.`);
|
|
66
|
+
let plan;
|
|
67
|
+
try {
|
|
68
|
+
plan = planUpgrade(await readProject(dir), version);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
plan = planUpgrade({}, version);
|
|
72
|
+
plan.blockers = [error instanceof Error ? error.message : String(error)];
|
|
73
|
+
}
|
|
74
|
+
let runtime;
|
|
75
|
+
if (args.apply && plan.additions.length) {
|
|
76
|
+
try {
|
|
77
|
+
runtime = await services.runtime(dir);
|
|
78
|
+
const blocker = runtimeBlocker(runtime.version, plan);
|
|
79
|
+
if (blocker)
|
|
80
|
+
plan.blockers.push(blocker);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
plan.blockers.push("Could not verify the project's installed runtime. Install project dependencies and preview again.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const note of plan.notes)
|
|
87
|
+
printDiagnosticsToConsole(note);
|
|
88
|
+
for (const addition of plan.additions) {
|
|
89
|
+
printResultToConsole(`${addition.file}: add ${addition.key}\n${addition.step.instructions}\n${JSON.stringify(addition.value, null, 2)}`);
|
|
90
|
+
}
|
|
91
|
+
if (release.blocker)
|
|
92
|
+
printDiagnosticsToConsole(release.blocker);
|
|
93
|
+
for (const blocker of plan.blockers)
|
|
94
|
+
printDiagnosticsToConsole(`Manual review required: ${blocker}`);
|
|
95
|
+
if (plan.blockers.length || (args.apply && release.blocker)) {
|
|
96
|
+
printDiagnosticsToConsole("No files changed. Use the definitions above for a manual upgrade; preserve your authentication and policy ordering.");
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
if (!plan.additions.length) {
|
|
100
|
+
printResultToConsole("No eligible additions are missing. Existing definitions were not rewritten or certified as matching the latest template.");
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
await showDiff(plan);
|
|
104
|
+
if (!args.apply) {
|
|
105
|
+
printResultToConsole("No files changed. To apply this target after review, rerun with --type ai-gateway --apply.");
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await requireCleanProject(dir, plan);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
printDiagnosticsToConsole(error instanceof Error ? error.message : String(error));
|
|
113
|
+
return 2;
|
|
114
|
+
}
|
|
115
|
+
if (args.prompt !== false) {
|
|
116
|
+
if (!services.interactive()) {
|
|
117
|
+
printDiagnosticsToConsole("Noninteractive application requires --apply and --prompt=false. No files changed.");
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
if (!(await services.confirm({
|
|
121
|
+
message: "Apply these additions to your local source files?",
|
|
122
|
+
default: false,
|
|
123
|
+
})))
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
const finalRelease = await services.release(version);
|
|
127
|
+
if (finalRelease.blocker) {
|
|
128
|
+
printDiagnosticsToConsole(`${finalRelease.blocker} No files changed.`);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const currentRuntime = await services.runtime(dir);
|
|
132
|
+
if (!runtime ||
|
|
133
|
+
currentRuntime.manifest !== runtime.manifest ||
|
|
134
|
+
currentRuntime.contents !== runtime.contents)
|
|
135
|
+
throw new Error("Installed runtime changed after preview. Run the upgrade again.");
|
|
136
|
+
const backup = await applyPlan(dir, plan, services.backupRoot);
|
|
137
|
+
printResultToConsole(`Applied ${plan.additions.length} additions. Originals and recovery journal: ${backup}. Review, test, and commit the changes before deploying.`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../../src/source/ai-gateway/handler.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n mkdir,\n mkdtemp,\n readFile,\n realpath,\n rm,\n writeFile,\n} from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { confirm } from \"@inquirer/prompts\";\nimport {\n printDiagnosticsToConsole,\n printResultToConsole,\n} from \"../../common/output.js\";\nimport { isInteractiveTerminal } from \"../../common/terminal.js\";\nimport { ZUPLO_XDG_DATA_HOME } from \"../../common/xdg/lib.js\";\nimport { applyPlan, git, readProject, requireCleanProject } from \"./files.js\";\nimport { catalog, planUpgrade, type UpgradePlan } from \"./planner.js\";\nimport { checkRelease, installedRuntime, runtimeBlocker } from \"./release.js\";\n\nexport interface GatewayUpgradeArgs {\n dir: string;\n prompt: boolean;\n apply?: boolean;\n}\n\nasync function showDiff(plan: UpgradePlan): Promise<void> {\n const temp = await mkdtemp(path.join(tmpdir(), \"zuplo-upgrade-preview-\"));\n try {\n for (const [file, text] of Object.entries(plan.after)) {\n for (const side of [\"before\", \"after\"]) {\n if (side === \"before\" && plan.before[file] === undefined) continue;\n const destination = path.join(temp, side, file);\n await mkdir(path.dirname(destination), { recursive: true });\n await writeFile(\n destination,\n side === \"before\" ? (plan.before[file] ?? \"\") : text,\n { mode: 0o600 }\n );\n }\n }\n if (Object.keys(plan.after).length) {\n try {\n printResultToConsole(\n await git(temp, [\n \"diff\",\n \"--no-index\",\n \"--no-ext-diff\",\n \"--no-textconv\",\n \"--\",\n \"before\",\n \"after\",\n ])\n );\n } catch (error) {\n // git diff exits 1 when there are changes; other exits are genuine errors.\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === 1 &&\n \"stdout\" in error &&\n typeof error.stdout === \"string\"\n )\n printResultToConsole(error.stdout);\n else throw error;\n }\n }\n } finally {\n await rm(temp, { recursive: true, force: true });\n }\n}\n\nconst defaultServices = {\n version: async (): Promise<string> =>\n JSON.parse(\n await readFile(new URL(\"../../../package.json\", import.meta.url), \"utf8\")\n ).version,\n release: checkRelease,\n runtime: installedRuntime,\n confirm,\n interactive: isInteractiveTerminal,\n backupRoot: path.join(ZUPLO_XDG_DATA_HOME, \"template-upgrade-backups\"),\n};\n\nexport async function upgradeGateway(\n args: GatewayUpgradeArgs,\n overrides: Partial<typeof defaultServices> = {}\n): Promise<number> {\n const dir = await realpath(path.resolve(args.dir));\n const services = { ...defaultServices, ...overrides };\n const version = await services.version();\n const release = await services.release(version);\n printResultToConsole(\n `AI Gateway template: bundled release ${version}, revision ${catalog.target}. Published stable: ${release.latest ?? \"unverified\"}.`\n );\n let plan: UpgradePlan;\n try {\n plan = planUpgrade(await readProject(dir), version);\n } catch (error) {\n // Still supply target definitions when a filesystem safety check prevents reading customer input.\n plan = planUpgrade({}, version);\n plan.blockers = [error instanceof Error ? error.message : String(error)];\n }\n let runtime: Awaited<ReturnType<typeof installedRuntime>> | undefined;\n if (args.apply && plan.additions.length) {\n try {\n runtime = await services.runtime(dir);\n const blocker = runtimeBlocker(runtime.version, plan);\n if (blocker) plan.blockers.push(blocker);\n } catch {\n plan.blockers.push(\n \"Could not verify the project's installed runtime. Install project dependencies and preview again.\"\n );\n }\n }\n for (const note of plan.notes) printDiagnosticsToConsole(note);\n for (const addition of plan.additions) {\n printResultToConsole(\n `${addition.file}: add ${addition.key}\\n${addition.step.instructions}\\n${JSON.stringify(addition.value, null, 2)}`\n );\n }\n if (release.blocker) printDiagnosticsToConsole(release.blocker);\n for (const blocker of plan.blockers)\n printDiagnosticsToConsole(`Manual review required: ${blocker}`);\n if (plan.blockers.length || (args.apply && release.blocker)) {\n printDiagnosticsToConsole(\n \"No files changed. Use the definitions above for a manual upgrade; preserve your authentication and policy ordering.\"\n );\n return 2;\n }\n if (!plan.additions.length) {\n printResultToConsole(\n \"No eligible additions are missing. Existing definitions were not rewritten or certified as matching the latest template.\"\n );\n return 0;\n }\n await showDiff(plan);\n if (!args.apply) {\n printResultToConsole(\n \"No files changed. To apply this target after review, rerun with --type ai-gateway --apply.\"\n );\n return 0;\n }\n try {\n await requireCleanProject(dir, plan);\n } catch (error) {\n printDiagnosticsToConsole(\n error instanceof Error ? error.message : String(error)\n );\n return 2;\n }\n if (args.prompt !== false) {\n if (!services.interactive()) {\n printDiagnosticsToConsole(\n \"Noninteractive application requires --apply and --prompt=false. No files changed.\"\n );\n return 2;\n }\n if (\n !(await services.confirm({\n message: \"Apply these additions to your local source files?\",\n default: false,\n }))\n )\n return 0;\n }\n // The selected target never changes underneath a displayed preview.\n const finalRelease = await services.release(version);\n if (finalRelease.blocker) {\n printDiagnosticsToConsole(`${finalRelease.blocker} No files changed.`);\n return 2;\n }\n const currentRuntime = await services.runtime(dir);\n if (\n !runtime ||\n currentRuntime.manifest !== runtime.manifest ||\n currentRuntime.contents !== runtime.contents\n )\n throw new Error(\n \"Installed runtime changed after preview. Run the upgrade again.\"\n );\n const backup = await applyPlan(dir, plan, services.backupRoot);\n printResultToConsole(\n `Applied ${plan.additions.length} additions. Originals and recovery journal: ${backup}. Review, test, and commit the changes before deploying.`\n );\n return 0;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.test.d.ts","sourceRoot":"","sources":["../../../src/source/ai-gateway/handler.test.ts"],"names":[],"mappings":""}
|