@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.
Files changed (2) hide show
  1. package/dist/cli.js +90 -27
  2. 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 path4 from "node:path";
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 = path4.dirname(fileURLToPath(import.meta.url));
419
+ var HERE = path5.dirname(fileURLToPath(import.meta.url));
344
420
  var PATCH_FILE = "growth-agent.patch";
345
421
  function assetsDir() {
346
- const packaged = path4.join(HERE, "assets", "skills");
347
- return existsSync2(packaged) ? packaged : path4.join(HERE, "..", "assets", "skills");
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 = path4.resolve(process.cwd(), ".pi", "skills");
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
- if (provider === "google") {
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", ["checkout", "--", ".pi"], { allowFail: true });
533
- sh("git", ["clean", "-fd", ".pi"], { allowFail: true });
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
- writeFileSync(PATCH_FILE, patch + "\n", "utf8");
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growthagent/ci",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {