@growthagent/ci 0.3.0 → 0.5.0
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/cli.js +90 -27
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
-
import { cpSync, existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import
|
|
5
|
+
import { cpSync, existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6
|
+
import path5 from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
// ../github/src/app.ts
|
|
@@ -100,6 +100,7 @@ var FORBIDDEN_PATTERNS = [
|
|
|
100
100
|
{ pattern: /^\.github\//i, why: "workflow and CI configuration" },
|
|
101
101
|
{ pattern: /^\.git(\/|$)/i, why: "git internals" },
|
|
102
102
|
{ pattern: /(^|\/)\.env(\.|$)/i, why: "environment files" },
|
|
103
|
+
// except the examples below
|
|
103
104
|
{ pattern: /(^|\/)\.npmrc$|(^|\/)\.yarnrc(\.yml)?$|(^|\/)\.pypirc$/i, why: "registry credentials" },
|
|
104
105
|
{ pattern: /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|poetry\.lock|Gemfile\.lock|composer\.lock)$/i, why: "dependency lockfiles" },
|
|
105
106
|
{ pattern: /(^|\/)(Dockerfile|docker-compose\.ya?ml|Procfile)$/i, why: "build and deployment configuration" },
|
|
@@ -111,6 +112,44 @@ var FORBIDDEN_PATTERNS = [
|
|
|
111
112
|
{ pattern: /\.(pem|p12|pfx|key|keystore|jks)$/i, why: "key material" },
|
|
112
113
|
{ pattern: /(^|\/)node_modules\//i, why: "installed dependencies" }
|
|
113
114
|
];
|
|
115
|
+
var EXAMPLE_ENV = /(^|\/)\.env(\.[\w-]+)*\.(example|sample|template)$/i;
|
|
116
|
+
var CREDENTIAL_PREFIXES = [
|
|
117
|
+
"sk-",
|
|
118
|
+
"sk_live",
|
|
119
|
+
"sk_test",
|
|
120
|
+
"pk_live",
|
|
121
|
+
"rk_live",
|
|
122
|
+
"whsec_",
|
|
123
|
+
"phc_",
|
|
124
|
+
"ghp_",
|
|
125
|
+
"gho_",
|
|
126
|
+
"ghs_",
|
|
127
|
+
"github_pat_",
|
|
128
|
+
"AKIA",
|
|
129
|
+
"AIza",
|
|
130
|
+
"xox",
|
|
131
|
+
"eyJ",
|
|
132
|
+
"-----BEGIN"
|
|
133
|
+
];
|
|
134
|
+
function secretishEnvLine(line) {
|
|
135
|
+
const text = line.trim();
|
|
136
|
+
if (!text || text.startsWith("#")) return null;
|
|
137
|
+
const eq = text.indexOf("=");
|
|
138
|
+
if (eq < 0) return null;
|
|
139
|
+
const key = text.slice(0, eq).replace(/^export\s+/, "").trim();
|
|
140
|
+
const value = text.slice(eq + 1).trim().replace(/^["']|["']$/g, "").trim();
|
|
141
|
+
if (!value) return null;
|
|
142
|
+
if (/^https?:\/\//i.test(value)) return null;
|
|
143
|
+
for (const prefix of CREDENTIAL_PREFIXES) {
|
|
144
|
+
if (value.startsWith(prefix)) {
|
|
145
|
+
return `${key} carries what looks like a real credential`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (value.length >= 20 && /^[A-Za-z0-9_\-.]+$/.test(value)) {
|
|
149
|
+
return `${key} carries a ${value.length}-character opaque value, not a placeholder`;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
114
153
|
var SYMLINK_MODE = "120000";
|
|
115
154
|
var EXECUTABLE_MODE = "100755";
|
|
116
155
|
function parsePatch(patch) {
|
|
@@ -183,6 +222,7 @@ function forbiddenReason(filePath) {
|
|
|
183
222
|
return "an absolute path";
|
|
184
223
|
}
|
|
185
224
|
if (normalised.split("/").includes("..")) return "a parent-directory escape";
|
|
225
|
+
if (EXAMPLE_ENV.test(normalised)) return null;
|
|
186
226
|
for (const { pattern, why } of FORBIDDEN_PATTERNS) {
|
|
187
227
|
if (pattern.test(normalised)) return why;
|
|
188
228
|
}
|
|
@@ -210,6 +250,20 @@ function validatePatch(patch, limits = DEFAULT_PATCH_LIMITS) {
|
|
|
210
250
|
return { ok: false, reason: `the patch changes ${candidate}, which is ${why} \u2014 never modifiable by the agent` };
|
|
211
251
|
}
|
|
212
252
|
}
|
|
253
|
+
if (EXAMPLE_ENV.test(file.path)) {
|
|
254
|
+
if (file.addedLines > file.added.length) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
reason: `the patch adds ${file.addedLines} lines to ${file.path}, more than can be checked for credentials`
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
for (const line of file.added) {
|
|
261
|
+
const why = secretishEnvLine(line);
|
|
262
|
+
if (why) {
|
|
263
|
+
return { ok: false, reason: `the patch writes a secret into ${file.path}: ${why}` };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
213
267
|
if (/(^|\/)package\.json$/i.test(file.path)) {
|
|
214
268
|
const why = manifestChangeReason(file);
|
|
215
269
|
if (why) {
|
|
@@ -235,6 +289,28 @@ function validatePatch(patch, limits = DEFAULT_PATCH_LIMITS) {
|
|
|
235
289
|
return { ok: true, report: { files, bytes, addedLines } };
|
|
236
290
|
}
|
|
237
291
|
|
|
292
|
+
// src/provider-config.ts
|
|
293
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync } from "node:fs";
|
|
294
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
295
|
+
import path4 from "node:path";
|
|
296
|
+
var RUN_TOKEN_ENV = "GX_RUN_TOKEN";
|
|
297
|
+
function writeGoogleProviderConfig(model) {
|
|
298
|
+
const gateway = (process.env.GX_GATEWAY_URL ?? "").replace(/\/$/, "");
|
|
299
|
+
const dir = mkdtempSync2(path4.join(tmpdir3(), "growth-agent-pi-"));
|
|
300
|
+
const config = {
|
|
301
|
+
providers: {
|
|
302
|
+
"growthagent-google": {
|
|
303
|
+
baseUrl: `${gateway}/v1beta`,
|
|
304
|
+
api: "google-generative-ai",
|
|
305
|
+
apiKey: `$${RUN_TOKEN_ENV}`,
|
|
306
|
+
models: [{ id: model, name: model, input: ["text"], contextWindow: 1e6 }]
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
writeFileSync(path4.join(dir, "models.json"), JSON.stringify(config, null, 2));
|
|
311
|
+
return dir;
|
|
312
|
+
}
|
|
313
|
+
|
|
238
314
|
// src/trust.ts
|
|
239
315
|
var TRUSTED_APP_LOGIN = "growth-agent[bot]";
|
|
240
316
|
function authorIsTrusted(issue, extraLogins = []) {
|
|
@@ -340,11 +416,11 @@ function buildPrompt(fields, briefing) {
|
|
|
340
416
|
}
|
|
341
417
|
|
|
342
418
|
// src/cli.ts
|
|
343
|
-
var HERE =
|
|
419
|
+
var HERE = path5.dirname(fileURLToPath(import.meta.url));
|
|
344
420
|
var PATCH_FILE = "growth-agent.patch";
|
|
345
421
|
function assetsDir() {
|
|
346
|
-
const packaged =
|
|
347
|
-
return existsSync2(packaged) ? packaged :
|
|
422
|
+
const packaged = path5.join(HERE, "assets", "skills");
|
|
423
|
+
return existsSync2(packaged) ? packaged : path5.join(HERE, "..", "assets", "skills");
|
|
348
424
|
}
|
|
349
425
|
function sh(cmd, args, opts = {}) {
|
|
350
426
|
const r = spawnSync(cmd, args, { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] });
|
|
@@ -452,20 +528,6 @@ async function openSession() {
|
|
|
452
528
|
};
|
|
453
529
|
}
|
|
454
530
|
}
|
|
455
|
-
function writeGoogleProviderConfig(piDir, model, runToken) {
|
|
456
|
-
const gateway = (process.env.GX_GATEWAY_URL ?? "").replace(/\/$/, "");
|
|
457
|
-
const config = {
|
|
458
|
-
providers: {
|
|
459
|
-
"growthagent-google": {
|
|
460
|
-
baseUrl: `${gateway}/v1beta`,
|
|
461
|
-
api: "google-generative-ai",
|
|
462
|
-
apiKey: runToken,
|
|
463
|
-
models: [{ id: model, name: model, input: ["text"], contextWindow: 1e6 }]
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
writeFileSync(path4.join(piDir, "..", "models.json"), JSON.stringify(config, null, 2));
|
|
468
|
-
}
|
|
469
531
|
async function implement() {
|
|
470
532
|
const issue = readIssue();
|
|
471
533
|
const refusal = refuseUntrustedAuthor(issue);
|
|
@@ -485,7 +547,7 @@ async function implement() {
|
|
|
485
547
|
console.log(
|
|
486
548
|
meta.kind === "instrumentation" ? `\u25B8 instrumentation request #${issue.number}` : `\u25B8 change request #${issue.number} \u2014 flag ${meta.flagKey} on ${meta.surface}`
|
|
487
549
|
);
|
|
488
|
-
const skillsDir =
|
|
550
|
+
const skillsDir = path5.resolve(process.cwd(), ".pi", "skills");
|
|
489
551
|
mkdirSync(skillsDir, { recursive: true });
|
|
490
552
|
cpSync(assetsDir(), skillsDir, { recursive: true });
|
|
491
553
|
const prompt = buildPrompt(meta, extractBriefing(issue.body));
|
|
@@ -497,9 +559,7 @@ async function implement() {
|
|
|
497
559
|
return 1;
|
|
498
560
|
}
|
|
499
561
|
const piProvider = provider === "google" ? "growthagent-google" : provider;
|
|
500
|
-
|
|
501
|
-
writeGoogleProviderConfig(skillsDir, model, session.token);
|
|
502
|
-
}
|
|
562
|
+
const piConfigDir = provider === "google" ? writeGoogleProviderConfig(model) : null;
|
|
503
563
|
console.log(`\u25B8 running pi (${piProvider}/${model})`);
|
|
504
564
|
let piStatus;
|
|
505
565
|
try {
|
|
@@ -517,7 +577,9 @@ async function implement() {
|
|
|
517
577
|
// environment, whatever else it might read there. Nor may a raw
|
|
518
578
|
// provider key: everything is spent through the run token.
|
|
519
579
|
GROWTH_AGENT_TOKEN: "",
|
|
520
|
-
GEMINI_API_KEY: ""
|
|
580
|
+
GEMINI_API_KEY: "",
|
|
581
|
+
// Only set for the Google path; pi keeps its own default otherwise.
|
|
582
|
+
...piConfigDir ? { PI_CODING_AGENT_DIR: piConfigDir, [RUN_TOKEN_ENV]: session.token } : {}
|
|
521
583
|
}
|
|
522
584
|
}
|
|
523
585
|
);
|
|
@@ -529,15 +591,16 @@ async function implement() {
|
|
|
529
591
|
console.error("pi exited non-zero \u2014 no patch produced");
|
|
530
592
|
return 1;
|
|
531
593
|
}
|
|
532
|
-
sh("git", ["
|
|
533
|
-
sh("git", ["
|
|
594
|
+
const piTracked = sh("git", ["ls-files", "--", ".pi"], { allowFail: true });
|
|
595
|
+
if (piTracked) sh("git", ["checkout", "--", ".pi"], { allowFail: true });
|
|
596
|
+
sh("git", ["clean", "-fdq", ".pi"], { allowFail: true });
|
|
534
597
|
sh("git", ["add", "-A"]);
|
|
535
598
|
const patch = sh("git", ["diff", "--staged"]);
|
|
536
599
|
if (!patch.trim()) {
|
|
537
600
|
console.error("pi made no changes \u2014 nothing to open a PR for");
|
|
538
601
|
return 2;
|
|
539
602
|
}
|
|
540
|
-
|
|
603
|
+
writeFileSync2(PATCH_FILE, patch + "\n", "utf8");
|
|
541
604
|
const verdict = validatePatch(patch);
|
|
542
605
|
if (!verdict.ok) {
|
|
543
606
|
console.error(`The produced patch was refused: ${verdict.reason}`);
|