@percepta/create 3.4.1 → 3.4.3
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/package.json +1 -1
- package/templates/webapp/AGENTS.md +1 -1
- package/templates/webapp/agent-skills/deploy.md +1 -1
- package/templates/webapp/agent-skills/inngest.md +5 -5
- package/templates/webapp/agent-skills/langfuse.md +4 -4
- package/templates/webapp/agent-skills/llm.md +1 -1
- package/templates/webapp/deploy/README.md +1 -1
- package/templates/webapp/package.json.template +8 -14
- package/templates/webapp/src/app/api/inngest/route.ts +12 -22
- package/templates/webapp/src/instrumentation.ts +2 -63
- package/templates/webapp/src/server/trpc.ts +6 -18
- package/templates/webapp/src/services/AuthContextService.ts +7 -59
- package/templates/webapp/src/services/inngest/InngestService.ts +14 -62
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +9 -77
- package/templates/webapp/src/services/llm/LLMService.ts +10 -88
- package/templates/webapp/src/services/logger/AppLogger.ts +3 -48
- package/templates/webapp/src/utils/syncInngestApp.ts +4 -56
- package/templates/webapp/scripts/deploy-percepta-test.ts +0 -1112
- package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +0 -497
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +0 -5
- package/templates/webapp/src/services/llm/LlmProviderService.ts +0 -85
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/* eslint-disable n/no-process-env */
|
|
3
|
-
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
9
|
-
import { createInterface } from "node:readline/promises";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
|
|
12
|
-
const APP_NAME = "__APP_NAME__";
|
|
13
|
-
const DATABASE_SCHEMA = "__APP_NAME_SNAKE__";
|
|
14
|
-
const ENVIRONMENT = "percepta-test";
|
|
15
|
-
const INFRA_REMOTE = "git@github.com:Percepta-Core/infra.git";
|
|
16
|
-
|
|
17
|
-
type DeployPhase = "service" | "installation";
|
|
18
|
-
|
|
19
|
-
interface Options {
|
|
20
|
-
infraPath?: string;
|
|
21
|
-
yes: boolean;
|
|
22
|
-
noClone: boolean;
|
|
23
|
-
phase: DeployPhase;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parseArgs(argv: string[]): Options {
|
|
27
|
-
const options: Options = {
|
|
28
|
-
yes: false,
|
|
29
|
-
noClone: false,
|
|
30
|
-
phase: "service",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
for (let index = 0; index < argv.length; index++) {
|
|
34
|
-
const arg = argv[index];
|
|
35
|
-
if (arg === "--") {
|
|
36
|
-
continue;
|
|
37
|
-
} else if (arg === "--yes" || arg === "-y") {
|
|
38
|
-
options.yes = true;
|
|
39
|
-
} else if (arg === "--no-clone") {
|
|
40
|
-
options.noClone = true;
|
|
41
|
-
} else if (arg === "--service") {
|
|
42
|
-
options.phase = "service";
|
|
43
|
-
} else if (arg === "--installation" || arg === "--install") {
|
|
44
|
-
options.phase = "installation";
|
|
45
|
-
} else if (arg === "--phase") {
|
|
46
|
-
const value = argv[index + 1];
|
|
47
|
-
if (!value) throw new Error("--phase requires service or installation");
|
|
48
|
-
if (value === "service") {
|
|
49
|
-
options.phase = "service";
|
|
50
|
-
} else if (value === "installation" || value === "install") {
|
|
51
|
-
options.phase = "installation";
|
|
52
|
-
} else {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Invalid --phase value "${value}". Use service or installation.`,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
index++;
|
|
58
|
-
} else if (arg === "--infra") {
|
|
59
|
-
const value = argv[index + 1];
|
|
60
|
-
if (!value) throw new Error("--infra requires a path");
|
|
61
|
-
options.infraPath = path.resolve(value);
|
|
62
|
-
index++;
|
|
63
|
-
} else {
|
|
64
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return options;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function run(
|
|
72
|
-
command: string,
|
|
73
|
-
args: string[],
|
|
74
|
-
cwd: string,
|
|
75
|
-
stdio: "inherit" | "pipe" = "inherit",
|
|
76
|
-
): string {
|
|
77
|
-
const result = execFileSync(command, args, {
|
|
78
|
-
cwd,
|
|
79
|
-
encoding: "utf8",
|
|
80
|
-
stdio,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
return typeof result === "string" ? result.trim() : "";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function assertCommand(command: string): void {
|
|
87
|
-
try {
|
|
88
|
-
run(command, ["--version"], process.cwd(), "pipe");
|
|
89
|
-
} catch {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`Required command not found or not authenticated: ${command}`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function findUp(startDir: string, fileName: string): string | null {
|
|
97
|
-
let dir = startDir;
|
|
98
|
-
while (true) {
|
|
99
|
-
if (existsSync(path.join(dir, fileName))) return dir;
|
|
100
|
-
const parent = path.dirname(dir);
|
|
101
|
-
if (parent === dir) return null;
|
|
102
|
-
dir = parent;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isGitRepo(dir: string): boolean {
|
|
107
|
-
if (!existsSync(dir)) return false;
|
|
108
|
-
try {
|
|
109
|
-
run("git", ["rev-parse", "--is-inside-work-tree"], dir, "pipe");
|
|
110
|
-
return true;
|
|
111
|
-
} catch {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function confirm(message: string): Promise<boolean> {
|
|
117
|
-
const rl = createInterface({ input, output });
|
|
118
|
-
try {
|
|
119
|
-
const answer = (await rl.question(`${message} [Y/n] `))
|
|
120
|
-
.trim()
|
|
121
|
-
.toLowerCase();
|
|
122
|
-
return answer === "" || answer === "y" || answer === "yes";
|
|
123
|
-
} finally {
|
|
124
|
-
rl.close();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function resolveInfraRepo(
|
|
129
|
-
options: Options,
|
|
130
|
-
defaultInfraPath: string,
|
|
131
|
-
): Promise<string> {
|
|
132
|
-
const infraPath =
|
|
133
|
-
options.infraPath ?? process.env.INFRA_REPO ?? defaultInfraPath;
|
|
134
|
-
|
|
135
|
-
if (isGitRepo(infraPath)) {
|
|
136
|
-
console.log(`Using infra repo: ${infraPath}`);
|
|
137
|
-
return infraPath;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (existsSync(infraPath)) {
|
|
141
|
-
throw new Error(`${infraPath} exists but is not a git repository`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (options.noClone) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
`Infra repo not found at ${infraPath}. Re-run with --infra <path>.`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!options.yes) {
|
|
151
|
-
if (!process.stdin.isTTY) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`Infra repo not found at ${infraPath}. Re-run with --yes to clone or --infra <path>.`,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const shouldClone = await confirm(
|
|
158
|
-
`Infra repo not found. Clone Percepta-Core/infra to ${infraPath}?`,
|
|
159
|
-
);
|
|
160
|
-
if (!shouldClone) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
"Skipped cloning infra repo. Re-run with --infra <path>.",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
await mkdir(path.dirname(infraPath), { recursive: true });
|
|
168
|
-
console.log(`Cloning ${INFRA_REMOTE} to ${infraPath}...`);
|
|
169
|
-
run("git", ["clone", INFRA_REMOTE, infraPath], process.cwd());
|
|
170
|
-
console.log(`Cloned infra repo: ${infraPath}`);
|
|
171
|
-
return infraPath;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function assertCleanGitWorktree(repoPath: string): void {
|
|
175
|
-
const status = run("git", ["status", "--porcelain"], repoPath, "pipe");
|
|
176
|
-
if (status.length === 0) return;
|
|
177
|
-
|
|
178
|
-
throw new Error(
|
|
179
|
-
`Infra repo has local changes. Commit/stash them first or use --infra with a clean checkout.\n${status}`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function getDeployBranchName(phase: DeployPhase): string {
|
|
184
|
-
return phase === "service"
|
|
185
|
-
? `deploy/${APP_NAME}-${ENVIRONMENT}-service`
|
|
186
|
-
: `deploy/${APP_NAME}-${ENVIRONMENT}-installation`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function switchDeployBranch(repoPath: string, phase: DeployPhase): string {
|
|
190
|
-
const branchName = getDeployBranchName(phase);
|
|
191
|
-
const currentBranch = run(
|
|
192
|
-
"git",
|
|
193
|
-
["branch", "--show-current"],
|
|
194
|
-
repoPath,
|
|
195
|
-
"pipe",
|
|
196
|
-
);
|
|
197
|
-
if (currentBranch === branchName) return branchName;
|
|
198
|
-
|
|
199
|
-
run("git", ["fetch", "origin", "main"], repoPath);
|
|
200
|
-
|
|
201
|
-
let branchExists = false;
|
|
202
|
-
try {
|
|
203
|
-
run("git", ["rev-parse", "--verify", branchName], repoPath, "pipe");
|
|
204
|
-
branchExists = true;
|
|
205
|
-
} catch {
|
|
206
|
-
branchExists = false;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (branchExists) {
|
|
210
|
-
run("git", ["switch", branchName], repoPath);
|
|
211
|
-
} else {
|
|
212
|
-
run("git", ["switch", "-c", branchName, "origin/main"], repoPath);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return branchName;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function writeIfChanged(
|
|
219
|
-
filePath: string,
|
|
220
|
-
content: string,
|
|
221
|
-
): Promise<boolean> {
|
|
222
|
-
if (existsSync(filePath)) {
|
|
223
|
-
const existing = await readFile(filePath, "utf8");
|
|
224
|
-
if (existing === content) return false;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
228
|
-
await writeFile(filePath, content);
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function copyDeployYaml(
|
|
233
|
-
packageDir: string,
|
|
234
|
-
infraRepo: string,
|
|
235
|
-
phase: DeployPhase,
|
|
236
|
-
): Promise<string[]> {
|
|
237
|
-
const serviceSource = path.join(
|
|
238
|
-
packageDir,
|
|
239
|
-
"deploy",
|
|
240
|
-
"ryvn",
|
|
241
|
-
`${APP_NAME}.service.yaml`,
|
|
242
|
-
);
|
|
243
|
-
const installationSource = path.join(
|
|
244
|
-
packageDir,
|
|
245
|
-
"deploy",
|
|
246
|
-
"ryvn",
|
|
247
|
-
"environments",
|
|
248
|
-
ENVIRONMENT,
|
|
249
|
-
"installations",
|
|
250
|
-
`${APP_NAME}.env.${ENVIRONMENT}.serviceinstallation.yaml`,
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
const serviceTarget = path.join(
|
|
254
|
-
infraRepo,
|
|
255
|
-
"ryvn",
|
|
256
|
-
`${APP_NAME}.service.yaml`,
|
|
257
|
-
);
|
|
258
|
-
const installationTarget = path.join(
|
|
259
|
-
infraRepo,
|
|
260
|
-
"ryvn",
|
|
261
|
-
"environments",
|
|
262
|
-
ENVIRONMENT,
|
|
263
|
-
"installations",
|
|
264
|
-
`${APP_NAME}.env.${ENVIRONMENT}.serviceinstallation.yaml`,
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const changed: string[] = [];
|
|
268
|
-
if (
|
|
269
|
-
phase === "service" &&
|
|
270
|
-
(await writeIfChanged(serviceTarget, await readFile(serviceSource, "utf8")))
|
|
271
|
-
) {
|
|
272
|
-
changed.push(path.relative(infraRepo, serviceTarget));
|
|
273
|
-
}
|
|
274
|
-
if (
|
|
275
|
-
phase === "installation" &&
|
|
276
|
-
(await writeIfChanged(
|
|
277
|
-
installationTarget,
|
|
278
|
-
await readFile(installationSource, "utf8"),
|
|
279
|
-
))
|
|
280
|
-
) {
|
|
281
|
-
changed.push(path.relative(infraRepo, installationTarget));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return changed;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function ensureDatabaseSchema(infraRepo: string): Promise<string | null> {
|
|
288
|
-
const databasesPath = path.join(
|
|
289
|
-
infraRepo,
|
|
290
|
-
"terraform",
|
|
291
|
-
"percepta-internal",
|
|
292
|
-
"databases.tf",
|
|
293
|
-
);
|
|
294
|
-
const content = await readFile(databasesPath, "utf8");
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
content.includes(`resource "postgresql_schema" "${DATABASE_SCHEMA}"`) ||
|
|
298
|
-
content.includes(`name = "${DATABASE_SCHEMA}"`)
|
|
299
|
-
) {
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const block = `resource "postgresql_schema" "${DATABASE_SCHEMA}" {
|
|
304
|
-
provider = postgresql.percepta_internal
|
|
305
|
-
database = postgresql_database.demos.name
|
|
306
|
-
name = "${DATABASE_SCHEMA}"
|
|
307
|
-
}
|
|
308
|
-
`;
|
|
309
|
-
|
|
310
|
-
await appendFile(
|
|
311
|
-
databasesPath,
|
|
312
|
-
`${content.endsWith("\n") ? "\n" : "\n\n"}${block}`,
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
return path.relative(infraRepo, databasesPath);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function hasStagedChanges(repoPath: string): boolean {
|
|
319
|
-
try {
|
|
320
|
-
run("git", ["diff", "--cached", "--quiet"], repoPath, "pipe");
|
|
321
|
-
return false;
|
|
322
|
-
} catch (error) {
|
|
323
|
-
const status =
|
|
324
|
-
typeof error === "object" && error !== null && "status" in error
|
|
325
|
-
? (error as { status?: number }).status
|
|
326
|
-
: undefined;
|
|
327
|
-
if (status === 1) return true;
|
|
328
|
-
throw error;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function getPrUrl(repoPath: string, branchName: string): string | null {
|
|
333
|
-
try {
|
|
334
|
-
return run(
|
|
335
|
-
"gh",
|
|
336
|
-
["pr", "view", branchName, "--json", "url", "--jq", ".url"],
|
|
337
|
-
repoPath,
|
|
338
|
-
"pipe",
|
|
339
|
-
);
|
|
340
|
-
} catch {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function getPrTitle(phase: DeployPhase): string {
|
|
346
|
-
return phase === "service"
|
|
347
|
-
? `Add ${APP_NAME} ${ENVIRONMENT} service`
|
|
348
|
-
: `Install ${APP_NAME} in ${ENVIRONMENT}`;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function getPrBody(phase: DeployPhase): string {
|
|
352
|
-
if (phase === "service") {
|
|
353
|
-
return [
|
|
354
|
-
`Adds the Ryvn service for ${APP_NAME}.`,
|
|
355
|
-
"",
|
|
356
|
-
`Also creates the ${DATABASE_SCHEMA} schema in the shared demos database.`,
|
|
357
|
-
"",
|
|
358
|
-
"After merge:",
|
|
359
|
-
"1. Let GitOps import the service.",
|
|
360
|
-
"2. Approve/apply the percepta-internal Terraform task if one is created.",
|
|
361
|
-
"3. Push the app to main or run the release workflow so Ryvn has a first release.",
|
|
362
|
-
`4. Open the installation PR with pnpm deploy:percepta-test:pr -- --phase installation --yes.`,
|
|
363
|
-
].join("\n");
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return [
|
|
367
|
-
`Adds the ${ENVIRONMENT} ServiceInstallation for ${APP_NAME}.`,
|
|
368
|
-
"",
|
|
369
|
-
"Pre-merge checklist:",
|
|
370
|
-
"1. The Ryvn Service exists from the service/schema PR.",
|
|
371
|
-
"2. At least one Ryvn release exists for this service.",
|
|
372
|
-
"",
|
|
373
|
-
"After merge, import deploy/ryvn/percepta-test.secrets.env in the Ryvn UI for the installation secrets.",
|
|
374
|
-
].join("\n");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function printNextSteps(phase: DeployPhase): void {
|
|
378
|
-
if (phase === "service") {
|
|
379
|
-
console.log("");
|
|
380
|
-
console.log("Next:");
|
|
381
|
-
console.log("1. Merge the service/schema PR.");
|
|
382
|
-
console.log("2. Wait for GitOps to import the service.");
|
|
383
|
-
console.log(
|
|
384
|
-
"3. Approve/apply the percepta-internal Terraform task if one is created.",
|
|
385
|
-
);
|
|
386
|
-
console.log("4. Push the app to main or run the release workflow.");
|
|
387
|
-
console.log(
|
|
388
|
-
"5. Run: pnpm deploy:percepta-test:pr -- --phase installation --yes",
|
|
389
|
-
);
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
console.log("");
|
|
394
|
-
console.log("Next:");
|
|
395
|
-
console.log("1. Merge the installation PR.");
|
|
396
|
-
console.log("2. Let GitOps import the ServiceInstallation.");
|
|
397
|
-
console.log(
|
|
398
|
-
"3. Import deploy/ryvn/percepta-test.secrets.env in the Ryvn UI.",
|
|
399
|
-
);
|
|
400
|
-
console.log(
|
|
401
|
-
"4. Verify health with: ryvn get installation __APP_NAME__ -e percepta-test",
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function confirmInstallationPrerequisites(
|
|
406
|
-
options: Options,
|
|
407
|
-
): Promise<void> {
|
|
408
|
-
if (options.phase !== "installation" || options.yes) return;
|
|
409
|
-
|
|
410
|
-
const message =
|
|
411
|
-
"Before opening the installation PR, confirm the service exists, " +
|
|
412
|
-
"and a first Ryvn release exists. Continue?";
|
|
413
|
-
|
|
414
|
-
if (!process.stdin.isTTY) {
|
|
415
|
-
throw new Error(`${message} Re-run with --yes to confirm.`);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (!(await confirm(message))) {
|
|
419
|
-
throw new Error("Skipped installation PR.");
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async function main(): Promise<void> {
|
|
424
|
-
const options = parseArgs(process.argv.slice(2));
|
|
425
|
-
assertCommand("git");
|
|
426
|
-
assertCommand("gh");
|
|
427
|
-
await confirmInstallationPrerequisites(options);
|
|
428
|
-
|
|
429
|
-
const packageDir = path.resolve(
|
|
430
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
431
|
-
"..",
|
|
432
|
-
);
|
|
433
|
-
const monorepoRoot = findUp(packageDir, "pnpm-workspace.yaml") ?? packageDir;
|
|
434
|
-
const defaultInfraPath = path.resolve(monorepoRoot, "..", "infra");
|
|
435
|
-
const infraRepo = await resolveInfraRepo(options, defaultInfraPath);
|
|
436
|
-
|
|
437
|
-
assertCleanGitWorktree(infraRepo);
|
|
438
|
-
const branchName = switchDeployBranch(infraRepo, options.phase);
|
|
439
|
-
|
|
440
|
-
const changedPaths = await copyDeployYaml(
|
|
441
|
-
packageDir,
|
|
442
|
-
infraRepo,
|
|
443
|
-
options.phase,
|
|
444
|
-
);
|
|
445
|
-
if (options.phase === "service") {
|
|
446
|
-
const schemaPath = await ensureDatabaseSchema(infraRepo);
|
|
447
|
-
if (schemaPath) changedPaths.push(schemaPath);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (changedPaths.length === 0) {
|
|
451
|
-
console.log(
|
|
452
|
-
options.phase === "service"
|
|
453
|
-
? "Infra repo already has the generated Ryvn service and database schema."
|
|
454
|
-
: "Infra repo already has the generated Ryvn installation.",
|
|
455
|
-
);
|
|
456
|
-
printNextSteps(options.phase);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
run("git", ["add", ...changedPaths], infraRepo);
|
|
461
|
-
if (!hasStagedChanges(infraRepo)) {
|
|
462
|
-
console.log("No infra changes to commit.");
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
run("git", ["commit", "-m", getPrTitle(options.phase)], infraRepo);
|
|
467
|
-
run("git", ["push", "-u", "origin", branchName], infraRepo);
|
|
468
|
-
|
|
469
|
-
const existingPr = getPrUrl(infraRepo, branchName);
|
|
470
|
-
if (existingPr) {
|
|
471
|
-
console.log(`Infra PR: ${existingPr}`);
|
|
472
|
-
printNextSteps(options.phase);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const prUrl = run(
|
|
477
|
-
"gh",
|
|
478
|
-
[
|
|
479
|
-
"pr",
|
|
480
|
-
"create",
|
|
481
|
-
"--title",
|
|
482
|
-
getPrTitle(options.phase),
|
|
483
|
-
"--body",
|
|
484
|
-
getPrBody(options.phase),
|
|
485
|
-
],
|
|
486
|
-
infraRepo,
|
|
487
|
-
"pipe",
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
console.log(`Infra PR: ${prUrl}`);
|
|
491
|
-
printNextSteps(options.phase);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
void main().catch((error) => {
|
|
495
|
-
console.error(error instanceof Error ? error.message : error);
|
|
496
|
-
process.exit(1);
|
|
497
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
|
-
import { type LanguageModel } from "ai";
|
|
4
|
-
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
5
|
-
|
|
6
|
-
export type LlmProviderName = "anthropic" | "openai";
|
|
7
|
-
|
|
8
|
-
export interface LanguageModelSelection {
|
|
9
|
-
model: LanguageModel;
|
|
10
|
-
modelId: string;
|
|
11
|
-
provider: LlmProviderName;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface GetLanguageModelOptions {
|
|
15
|
-
modelId?: string;
|
|
16
|
-
provider?: LlmProviderName;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5-20250929";
|
|
20
|
-
const DEFAULT_OPENAI_MODEL = "gpt-4.1";
|
|
21
|
-
|
|
22
|
-
export class LlmProviderService {
|
|
23
|
-
private static SINGLETON: LlmProviderService | undefined;
|
|
24
|
-
|
|
25
|
-
public static create(): LlmProviderService {
|
|
26
|
-
if (LlmProviderService.SINGLETON == null) {
|
|
27
|
-
LlmProviderService.SINGLETON = new LlmProviderService();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return LlmProviderService.SINGLETON;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private constructor() {}
|
|
34
|
-
|
|
35
|
-
public getLanguageModel(
|
|
36
|
-
options: GetLanguageModelOptions = {},
|
|
37
|
-
): LanguageModelSelection {
|
|
38
|
-
const config = getEnvConfig();
|
|
39
|
-
const provider =
|
|
40
|
-
options.provider ??
|
|
41
|
-
config.LLM_PROVIDER ??
|
|
42
|
-
(config.ANTHROPIC_API_KEY ? "anthropic" : undefined) ??
|
|
43
|
-
(config.OPENAI_API_KEY ? "openai" : undefined);
|
|
44
|
-
|
|
45
|
-
if (provider === "anthropic") {
|
|
46
|
-
if (!config.ANTHROPIC_API_KEY) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
"LLM_PROVIDER=anthropic but ANTHROPIC_API_KEY is not configured.",
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const modelId =
|
|
53
|
-
options.modelId ?? config.LLM_MODEL ?? DEFAULT_ANTHROPIC_MODEL;
|
|
54
|
-
return {
|
|
55
|
-
provider,
|
|
56
|
-
modelId,
|
|
57
|
-
model: createAnthropic({
|
|
58
|
-
apiKey: config.ANTHROPIC_API_KEY,
|
|
59
|
-
}).languageModel(modelId),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (provider === "openai") {
|
|
64
|
-
if (!config.OPENAI_API_KEY) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
"LLM_PROVIDER=openai but OPENAI_API_KEY is not configured.",
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const modelId =
|
|
71
|
-
options.modelId ?? config.LLM_MODEL ?? DEFAULT_OPENAI_MODEL;
|
|
72
|
-
return {
|
|
73
|
-
provider,
|
|
74
|
-
modelId,
|
|
75
|
-
model: createOpenAI({
|
|
76
|
-
apiKey: config.OPENAI_API_KEY,
|
|
77
|
-
}).languageModel(modelId),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
throw new Error(
|
|
82
|
-
"No LLM provider is configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY locally, or deploy to an environment with a shared LLM variable group.",
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|