getaura 0.1.7 → 0.1.8

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/index.js CHANGED
@@ -9,8 +9,8 @@ var __export = (target, all) => {
9
9
  import { Command, Option } from "commander";
10
10
 
11
11
  // src/commands/apply.ts
12
- import { existsSync as existsSync16, rmSync as rmSync6 } from "fs";
13
- import path18 from "path";
12
+ import { existsSync as existsSync19, rmSync as rmSync6 } from "fs";
13
+ import path20 from "path";
14
14
 
15
15
  // src/core/agents.ts
16
16
  var AGENT_LAYOUT = {
@@ -1009,10 +1009,10 @@ function mergeDefs(...defs) {
1009
1009
  function cloneDef(schema) {
1010
1010
  return mergeDefs(schema._zod.def);
1011
1011
  }
1012
- function getElementAtPath(obj, path25) {
1013
- if (!path25)
1012
+ function getElementAtPath(obj, path27) {
1013
+ if (!path27)
1014
1014
  return obj;
1015
- return path25.reduce((acc, key) => acc?.[key], obj);
1015
+ return path27.reduce((acc, key) => acc?.[key], obj);
1016
1016
  }
1017
1017
  function promiseAllObject(promisesObj) {
1018
1018
  const keys = Object.keys(promisesObj);
@@ -1352,11 +1352,11 @@ function explicitlyAborted(x, startIndex = 0) {
1352
1352
  }
1353
1353
  return false;
1354
1354
  }
1355
- function prefixIssues(path25, issues) {
1355
+ function prefixIssues(path27, issues) {
1356
1356
  return issues.map((iss) => {
1357
1357
  var _a3;
1358
1358
  (_a3 = iss).path ?? (_a3.path = []);
1359
- iss.path.unshift(path25);
1359
+ iss.path.unshift(path27);
1360
1360
  return iss;
1361
1361
  });
1362
1362
  }
@@ -1806,16 +1806,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
1806
1806
  }
1807
1807
  function formatError(error62, mapper = (issue2) => issue2.message) {
1808
1808
  const fieldErrors = { _errors: [] };
1809
- const processError = (error63, path25 = []) => {
1809
+ const processError = (error63, path27 = []) => {
1810
1810
  for (const issue2 of error63.issues) {
1811
1811
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1812
- issue2.errors.map((issues) => processError({ issues }, [...path25, ...issue2.path]));
1812
+ issue2.errors.map((issues) => processError({ issues }, [...path27, ...issue2.path]));
1813
1813
  } else if (issue2.code === "invalid_key") {
1814
- processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1814
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1815
1815
  } else if (issue2.code === "invalid_element") {
1816
- processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1816
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1817
1817
  } else {
1818
- const fullpath = [...path25, ...issue2.path];
1818
+ const fullpath = [...path27, ...issue2.path];
1819
1819
  if (fullpath.length === 0) {
1820
1820
  fieldErrors._errors.push(mapper(issue2));
1821
1821
  } else {
@@ -1854,17 +1854,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
1854
1854
  }
1855
1855
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
1856
1856
  const result = { errors: [] };
1857
- const processError = (error63, path25 = []) => {
1857
+ const processError = (error63, path27 = []) => {
1858
1858
  var _a3;
1859
1859
  for (const issue2 of error63.issues) {
1860
1860
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1861
- issue2.errors.map((issues) => processError({ issues }, [...path25, ...issue2.path]));
1861
+ issue2.errors.map((issues) => processError({ issues }, [...path27, ...issue2.path]));
1862
1862
  } else if (issue2.code === "invalid_key") {
1863
- processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1863
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1864
1864
  } else if (issue2.code === "invalid_element") {
1865
- processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1865
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1866
1866
  } else {
1867
- const fullpath = [...path25, ...issue2.path];
1867
+ const fullpath = [...path27, ...issue2.path];
1868
1868
  if (fullpath.length === 0) {
1869
1869
  result.errors.push(mapper(issue2));
1870
1870
  continue;
@@ -1903,8 +1903,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
1903
1903
  }
1904
1904
  function toDotPath(_path) {
1905
1905
  const segs = [];
1906
- const path25 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1907
- for (const seg of path25) {
1906
+ const path27 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1907
+ for (const seg of path27) {
1908
1908
  if (typeof seg === "number")
1909
1909
  segs.push(`[${seg}]`);
1910
1910
  else if (typeof seg === "symbol")
@@ -2038,18 +2038,18 @@ var validateAsync = async (schema, value, _ctx) => {
2038
2038
  return result.issues.length === 0;
2039
2039
  };
2040
2040
  var _encode = (_Err) => {
2041
- const parse3 = _parse(_Err);
2041
+ const parse5 = _parse(_Err);
2042
2042
  const fn = (schema, value, _ctx, _params) => {
2043
2043
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
2044
- return parse3(schema, value, ctx, finalizeParams(fn, _params));
2044
+ return parse5(schema, value, ctx, finalizeParams(fn, _params));
2045
2045
  };
2046
2046
  return fn;
2047
2047
  };
2048
2048
  var encode = /* @__PURE__ */ _encode($ZodRealError);
2049
2049
  var _decode = (_Err) => {
2050
- const parse3 = _parse(_Err);
2050
+ const parse5 = _parse(_Err);
2051
2051
  const fn = (schema, value, _ctx, _params) => {
2052
- return parse3(schema, value, _ctx, finalizeParams(fn, _params));
2052
+ return parse5(schema, value, _ctx, finalizeParams(fn, _params));
2053
2053
  };
2054
2054
  return fn;
2055
2055
  };
@@ -19006,13 +19006,13 @@ function resolveRef(ref, ctx) {
19006
19006
  if (!ref.startsWith("#")) {
19007
19007
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
19008
19008
  }
19009
- const path25 = ref.slice(1).split("/").filter(Boolean);
19010
- if (path25.length === 0) {
19009
+ const path27 = ref.slice(1).split("/").filter(Boolean);
19010
+ if (path27.length === 0) {
19011
19011
  return ctx.rootSchema;
19012
19012
  }
19013
19013
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
19014
- if (path25[0] === defsKey) {
19015
- const key = path25[1] === void 0 ? void 0 : decodeJSONPointerSegment(path25[1]);
19014
+ if (path27[0] === defsKey) {
19015
+ const key = path27[1] === void 0 ? void 0 : decodeJSONPointerSegment(path27[1]);
19016
19016
  if (!key || !ctx.defs[key]) {
19017
19017
  throw new Error(`Reference not found: ${ref}`);
19018
19018
  }
@@ -19975,6 +19975,7 @@ var FIX_OUTCOMES = [
19975
19975
  "cancelled",
19976
19976
  "task_written",
19977
19977
  "shown",
19978
+ "settings_changed",
19978
19979
  "merged",
19979
19980
  "closed",
19980
19981
  "resolved",
@@ -20001,7 +20002,7 @@ var TelemetryRequest = external_exports.object({
20001
20002
  actionId: external_exports.string().regex(/^[a-z0-9-]{1,80}$/).optional(),
20002
20003
  step: external_exports.enum(SETUP_STEPS).optional(),
20003
20004
  outcome: external_exports.enum(FIX_OUTCOMES).optional(),
20004
- delivery: external_exports.enum(["pr", "agent-fix", "agent-task", "guide", "checklist"]).optional(),
20005
+ delivery: external_exports.enum(["pr", "agent-fix", "agent-task", "guide", "checklist", "settings"]).optional(),
20005
20006
  /** Whether the user picked it from the next-steps checklist or ran the command. */
20006
20007
  via: external_exports.enum(["checklist", "command"]).optional(),
20007
20008
  durationMs: external_exports.number().int().nonnegative().max(864e5).optional(),
@@ -20077,23 +20078,9 @@ function grade(score) {
20077
20078
  return { label: "Generational aura debt", sentence: "You have generational aura debt", color: "red" };
20078
20079
  }
20079
20080
 
20080
- // src/templates.ts
20081
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
20082
- import path4 from "path";
20083
-
20084
- // src/core/config.ts
20085
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
20086
- import os from "os";
20087
- import path2 from "path";
20088
-
20089
- // src/core/repo.ts
20090
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
20091
- import path from "path";
20092
- import { fileURLToPath } from "url";
20093
-
20094
20081
  // src/core/exec.ts
20095
20082
  import { spawn, spawnSync } from "child_process";
20096
- var WINDOWS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "node", "tar", "where"]);
20083
+ var WINDOWS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "node", "tar", "where", "powershell"]);
20097
20084
  function quoteForCmd(arg) {
20098
20085
  return /^[\w@%+=:,./\\-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`;
20099
20086
  }
@@ -20139,7 +20126,14 @@ async function commandExists(cmd) {
20139
20126
  return probe.code === 0;
20140
20127
  }
20141
20128
 
20129
+ // src/core/git.ts
20130
+ import { mkdirSync, rmSync, writeFileSync } from "fs";
20131
+ import path2 from "path";
20132
+
20142
20133
  // src/core/repo.ts
20134
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
20135
+ import path from "path";
20136
+ import { fileURLToPath } from "url";
20143
20137
  var AURA_DIR = ".aura";
20144
20138
  var ALWAYS_IGNORED = ["node_modules", ".next", ".git", "dist", "build", "out", ".vercel", ".turbo", "coverage", ".aura"];
20145
20139
  async function findRepoRoot(cwd = process.cwd()) {
@@ -20204,7 +20198,545 @@ var SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/;
20204
20198
  var isSourceFile = (f) => SOURCE_EXT.test(f) && !f.endsWith(".d.ts");
20205
20199
  var isTestFile = (f) => /(?:^|\/)(?:__tests__|tests?|e2e)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/.test(f);
20206
20200
 
20201
+ // src/core/git.ts
20202
+ async function git(root, ...args) {
20203
+ return run("git", args, { cwd: root });
20204
+ }
20205
+ function ghInstallHint(platform = process.platform) {
20206
+ const how = platform === "win32" ? "Install it with `winget install --id GitHub.cli`" : platform === "darwin" ? "Install it with `brew install gh` (or download it from https://cli.github.com)" : "Install it from https://cli.github.com";
20207
+ return `${how}, open a new terminal, then run \`gh auth login\`.`;
20208
+ }
20209
+ async function ghStatus(root) {
20210
+ const v = await run("gh", ["--version"]);
20211
+ if (v.code !== 0) return { installed: false, authed: false, hasRemote: false };
20212
+ const auth = await run("gh", ["auth", "status"], { cwd: root });
20213
+ const remote = await git(root, "remote", "get-url", "origin");
20214
+ return { installed: true, authed: auth.code === 0, hasRemote: remote.code === 0 && /github\.com/.test(remote.stdout) };
20215
+ }
20216
+ var AURA_OWNED = /^(?:\.aura\/|\.claude\/skills\/aura\/|\.agents\/skills\/aura(?:-(?:scan|next|fix|setup))?\/|\.cursor\/commands\/aura-[a-z]+\.md$|\.pi\/prompts\/aura-[a-z]+\.md$|\.claude\/settings\.json$)/;
20217
+ async function dirtyFiles(root) {
20218
+ const r = await git(root, "status", "--porcelain", "-uall");
20219
+ return r.stdout.split("\n").filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, "").split(" -> ").pop()).filter((f) => !AURA_OWNED.test(f));
20220
+ }
20221
+ async function currentBranch(root) {
20222
+ return (await git(root, "rev-parse", "--abbrev-ref", "HEAD")).stdout.trim();
20223
+ }
20224
+ async function freeBranchName(root, base) {
20225
+ for (let i = 1; i < 50; i++) {
20226
+ const name = i === 1 ? base : `${base}-${i}`;
20227
+ const local = await git(root, "rev-parse", "--verify", "--quiet", `refs/heads/${name}`);
20228
+ const remote = await git(root, "ls-remote", "--exit-code", "--heads", "origin", name);
20229
+ if (local.code !== 0 && remote.code !== 0) return name;
20230
+ }
20231
+ return `${base}-${Date.now()}`;
20232
+ }
20233
+ function writeChanges(root, changes) {
20234
+ for (const c of changes) {
20235
+ const full = path2.join(root, c.path);
20236
+ if (!isInside(root, full)) throw new Error(`Refusing to write outside the repo: ${c.path}`);
20237
+ if (c.content === null) rmSync(full, { force: true });
20238
+ else {
20239
+ mkdirSync(path2.dirname(full), { recursive: true });
20240
+ writeFileSync(full, c.content);
20241
+ }
20242
+ }
20243
+ }
20244
+ async function commitPushPr(root, opts) {
20245
+ const commit = await git(root, "commit", "-m", opts.title, "-m", "Opened by Aura (https://github.com/JohnFazio1/aura).");
20246
+ const base = { branch: opts.branch, url: null };
20247
+ if (commit.code !== 0) {
20248
+ const out = commit.stderr || commit.stdout;
20249
+ const error62 = /tell me who you are|user\.email/i.test(out) ? 'git doesn\u2019t know your name and email yet. Run these two commands:\n git config --global user.name "Your Name"\n git config --global user.email "you@example.com"' : out;
20250
+ return { ...base, committed: false, pushed: false, error: error62 };
20251
+ }
20252
+ if (!opts.openPr) return { ...base, committed: true, pushed: false };
20253
+ const push = await git(root, "push", "-u", "origin", opts.branch);
20254
+ if (push.code !== 0) return { ...base, committed: true, pushed: false, error: push.stderr };
20255
+ const pr = await run("gh", ["pr", "create", "--base", opts.base, "--head", opts.branch, "--title", opts.title, "--body-file", "-"], {
20256
+ cwd: root,
20257
+ input: opts.body
20258
+ });
20259
+ if (pr.code !== 0) return { ...base, committed: true, pushed: true, error: pr.stderr };
20260
+ return { ...base, url: pr.stdout.trim().split("\n").pop() ?? null, committed: true, pushed: true };
20261
+ }
20262
+
20263
+ // src/connectors/github.ts
20264
+ var PAID_SECRETS = "needs GitHub Secret Protection (a paid add-on) on private repositories";
20265
+ var PAID_RULES = "needs a paid GitHub plan (Pro is about $4/month) on private repositories";
20266
+ var PUBLIC_ONLY = "only available on public repositories";
20267
+ var RULESET_NAME = "Protect main (Aura)";
20268
+ var parse3 = (r) => {
20269
+ try {
20270
+ return JSON.parse(r.stdout);
20271
+ } catch {
20272
+ return null;
20273
+ }
20274
+ };
20275
+ var needsUpgrade = (r) => /Upgrade to GitHub Pro|HTTP 403|not available|Advanced Security/i.test(r.stderr + r.stdout);
20276
+ var done = (r) => r.code === 0 ? { ok: true } : { ok: false };
20277
+ async function ciChecks(ctx) {
20278
+ const r = await ctx.gh([
20279
+ "api",
20280
+ `repos/${ctx.repo}/commits/${ctx.defaultBranch}/check-runs`,
20281
+ "--jq",
20282
+ '.check_runs[] | select(.app.slug == "github-actions") | .name'
20283
+ ]);
20284
+ return r.code === 0 ? [
20285
+ ...new Set(
20286
+ r.stdout.split("\n").map((s) => s.trim()).filter(Boolean)
20287
+ )
20288
+ ] : [];
20289
+ }
20290
+ function protectMainRuleset(checks) {
20291
+ return {
20292
+ name: RULESET_NAME,
20293
+ target: "branch",
20294
+ enforcement: "active",
20295
+ conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
20296
+ rules: [
20297
+ { type: "deletion" },
20298
+ { type: "non_fast_forward" },
20299
+ {
20300
+ // No approvals required, so someone working alone can still merge their own pull requests.
20301
+ type: "pull_request",
20302
+ parameters: {
20303
+ required_approving_review_count: 0,
20304
+ dismiss_stale_reviews_on_push: false,
20305
+ require_code_owner_review: false,
20306
+ require_last_push_approval: false,
20307
+ required_review_thread_resolution: false
20308
+ }
20309
+ },
20310
+ ...checks.length ? [
20311
+ {
20312
+ type: "required_status_checks",
20313
+ parameters: { strict_required_status_checks_policy: false, required_status_checks: checks.map((context) => ({ context })) }
20314
+ }
20315
+ ] : []
20316
+ ]
20317
+ };
20318
+ }
20319
+ var secretState = (key) => async (ctx) => {
20320
+ const security = parse3(await ctx.get(`repos/${ctx.repo}`))?.security_and_analysis;
20321
+ if (security?.[key]?.status === "enabled") return { state: "on" };
20322
+ return security?.[key] || !ctx.isPrivate ? { state: "off" } : { state: "unavailable", why: PAID_SECRETS };
20323
+ };
20324
+ var enableSecret = (key) => async (ctx) => {
20325
+ const r = await ctx.gh(
20326
+ ["api", "-X", "PATCH", `repos/${ctx.repo}`, "--input", "-"],
20327
+ JSON.stringify({ security_and_analysis: { [key]: { status: "enabled" } } })
20328
+ );
20329
+ return r.code === 0 ? { ok: true } : { ok: false, unavailable: ctx.isPrivate && needsUpgrade(r) ? PAID_SECRETS : void 0 };
20330
+ };
20331
+ var github = {
20332
+ id: "github",
20333
+ name: "GitHub",
20334
+ actionId: "github-security",
20335
+ // Offered for every repo; connecting finds out whether it's on GitHub.
20336
+ detect: () => true,
20337
+ async connect(root, fake) {
20338
+ const cli = fake;
20339
+ const gh = cli ?? ((args, input2) => run("gh", args, { cwd: root, input: input2, timeoutMs: 3e4 }));
20340
+ if (!cli) {
20341
+ const status = await ghStatus(root);
20342
+ if (!status.installed) return { ok: false, problem: `The GitHub CLI isn\u2019t installed. ${ghInstallHint()}`, errorType: "CliNotInstalled" };
20343
+ if (!status.authed) return { ok: false, problem: "The GitHub CLI isn\u2019t signed in. Run `gh auth login`, then try again.", errorType: "CliNotSignedIn" };
20344
+ if (!status.hasRemote)
20345
+ return { ok: false, problem: "This repo isn\u2019t on GitHub yet. Push it to GitHub first, for example with `gh repo create`.", errorType: "NoProject" };
20346
+ }
20347
+ const info = parse3(await gh(["repo", "view", "--json", "nameWithOwner,isPrivate,defaultBranchRef,viewerPermission"]));
20348
+ if (!info?.nameWithOwner) return { ok: false, problem: "Couldn\u2019t read this repo from GitHub. Check `gh repo view` works here.", errorType: "NoProject" };
20349
+ if (info.viewerPermission !== "ADMIN")
20350
+ return {
20351
+ ok: false,
20352
+ problem: `Changing these settings needs admin access to ${info.nameWithOwner}. Ask the repo\u2019s owner to run \`aura apply github-security\`.`,
20353
+ errorType: "NotAdmin"
20354
+ };
20355
+ const cache = /* @__PURE__ */ new Map();
20356
+ const ctx = {
20357
+ repo: info.nameWithOwner,
20358
+ isPrivate: Boolean(info.isPrivate),
20359
+ defaultBranch: info.defaultBranchRef?.name || "main",
20360
+ gh,
20361
+ get: (path27) => {
20362
+ if (!cache.has(path27)) cache.set(path27, gh(["api", path27]));
20363
+ return cache.get(path27);
20364
+ }
20365
+ };
20366
+ return { ok: true, ctx, target: `${ctx.repo} ${ctx.isPrivate ? "(private)" : "(public)"}` };
20367
+ },
20368
+ settings: [
20369
+ {
20370
+ id: "dependabot-alerts",
20371
+ label: "Dependabot alerts (warns about vulnerable packages)",
20372
+ impact: "GitHub emails you when a package your app uses has a known security hole.",
20373
+ read: async (ctx) => (await ctx.get(`repos/${ctx.repo}/vulnerability-alerts`)).code === 0 ? { state: "on" } : { state: "off" },
20374
+ enable: async (ctx) => done(await ctx.gh(["api", "-X", "PUT", `repos/${ctx.repo}/vulnerability-alerts`]))
20375
+ },
20376
+ {
20377
+ id: "dependabot-fixes",
20378
+ label: "Dependabot security fixes (opens PRs to update them)",
20379
+ impact: "GitHub opens pull requests that update vulnerable packages. Nothing merges without you.",
20380
+ read: async (ctx) => parse3(await ctx.get(`repos/${ctx.repo}/automated-security-fixes`))?.enabled ? { state: "on" } : { state: "off" },
20381
+ enable: async (ctx) => done(await ctx.gh(["api", "-X", "PUT", `repos/${ctx.repo}/automated-security-fixes`]))
20382
+ },
20383
+ {
20384
+ id: "secret-scanning",
20385
+ label: "Secret scanning (finds keys pushed to GitHub)",
20386
+ impact: "GitHub alerts you when a key or password is pushed to the repo.",
20387
+ read: secretState("secret_scanning"),
20388
+ enable: enableSecret("secret_scanning")
20389
+ },
20390
+ {
20391
+ id: "push-protection",
20392
+ label: "Push protection (blocks pushes that contain a key)",
20393
+ impact: "A push that contains a key is refused until you remove it.",
20394
+ read: secretState("secret_scanning_push_protection"),
20395
+ enable: enableSecret("secret_scanning_push_protection")
20396
+ },
20397
+ {
20398
+ id: "private-reporting",
20399
+ label: "Private vulnerability reporting",
20400
+ impact: "Security researchers can report a problem to you privately instead of in a public issue.",
20401
+ read: async (ctx) => ctx.isPrivate ? { state: "unavailable", why: PUBLIC_ONLY } : parse3(await ctx.get(`repos/${ctx.repo}/private-vulnerability-reporting`))?.enabled ? { state: "on" } : { state: "off" },
20402
+ enable: async (ctx) => done(await ctx.gh(["api", "-X", "PUT", `repos/${ctx.repo}/private-vulnerability-reporting`]))
20403
+ },
20404
+ {
20405
+ id: "protect-main",
20406
+ label: "Protect the main branch (pull requests and passing checks required, no force pushes or deletion)",
20407
+ impact: "You can\u2019t push straight to main any more: changes go through pull requests (you can still merge your own) once the checks pass.",
20408
+ async read(ctx) {
20409
+ const rules = parse3(await ctx.get(`repos/${ctx.repo}/rules/branches/${ctx.defaultBranch}`));
20410
+ const protection = await ctx.get(`repos/${ctx.repo}/branches/${ctx.defaultBranch}/protection`);
20411
+ if (Array.isArray(rules) && rules.some((r) => r.type === "pull_request") || protection.code === 0) return { state: "on" };
20412
+ if (ctx.isPrivate && /Upgrade to GitHub Pro/i.test(protection.stderr)) return { state: "unavailable", why: PAID_RULES };
20413
+ return { state: "off" };
20414
+ },
20415
+ async enable(ctx) {
20416
+ const r = await ctx.gh(["api", "-X", "POST", `repos/${ctx.repo}/rulesets`, "--input", "-"], JSON.stringify(protectMainRuleset(await ciChecks(ctx))));
20417
+ return r.code === 0 ? { ok: true } : { ok: false, unavailable: ctx.isPrivate && needsUpgrade(r) ? PAID_RULES : void 0 };
20418
+ }
20419
+ }
20420
+ ],
20421
+ async explain(ctx, toTurnOn) {
20422
+ if (!toTurnOn.includes("protect-main")) return [];
20423
+ const checks = await ciChecks(ctx);
20424
+ return checks.length ? [`Pull requests to main will need these checks to pass: ${checks.join(", ")}.`] : [];
20425
+ },
20426
+ async leftForYou(ctx, results) {
20427
+ const left = ["Turn on two-factor login for your GitHub account: https://github.com/settings/security"];
20428
+ const r = await ctx.gh(["api", `repos/${ctx.repo}/collaborators`, "--jq", '.[] | "\\(.login) (\\(.role_name))"']);
20429
+ const people = r.code === 0 ? r.stdout.split("\n").filter(Boolean) : [];
20430
+ if (people.length)
20431
+ left.push(`People with access: ${people.join(", ")}. Remove anyone who no longer needs it: https://github.com/${ctx.repo}/settings/access`);
20432
+ if (results.some((x) => x.id === "secret-scanning" && x.result === "unavailable"))
20433
+ left.push("Aura\u2019s own secret check still runs on every `aura scan`, and before each commit if you add the pre-commit hook.");
20434
+ return left;
20435
+ }
20436
+ };
20437
+
20438
+ // src/connectors/supabase.ts
20439
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
20440
+ import os from "os";
20441
+ import path3 from "path";
20442
+ var TOKEN = /^sbp_(?:oauth_|v0_)?[a-f0-9]{40}$/;
20443
+ function decodeToken(raw) {
20444
+ const value = raw?.trim() ?? "";
20445
+ const token = value.startsWith("go-keyring-base64:") ? Buffer.from(value.slice("go-keyring-base64:".length), "base64").toString("utf8").trim() : value;
20446
+ return TOKEN.test(token) ? token : null;
20447
+ }
20448
+ function windowsCredentialScript(target) {
20449
+ return `
20450
+ $sig = @'
20451
+ [DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredRead(string target, int type, int flags, out IntPtr cred);
20452
+ [DllImport("advapi32.dll")] public static extern void CredFree(IntPtr cred);
20453
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] public struct CREDENTIAL { public int Flags; public int Type; public string TargetName; public string Comment; public long LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist; public int AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName; }
20454
+ '@
20455
+ Add-Type -MemberDefinition $sig -Namespace AuraCred -Name Win
20456
+ $p = [IntPtr]::Zero
20457
+ if ([AuraCred.Win]::CredRead('${target}', 1, 0, [ref]$p)) {
20458
+ $c = [Runtime.InteropServices.Marshal]::PtrToStructure($p, [type][AuraCred.Win+CREDENTIAL])
20459
+ $b = New-Object byte[] $c.CredentialBlobSize
20460
+ [Runtime.InteropServices.Marshal]::Copy($c.CredentialBlob, $b, 0, $b.Length)
20461
+ [Text.Encoding]::UTF8.GetString($b)
20462
+ [AuraCred.Win]::CredFree($p)
20463
+ }`;
20464
+ }
20465
+ async function supabaseToken() {
20466
+ const fromEnv = decodeToken(process.env.SUPABASE_ACCESS_TOKEN);
20467
+ if (fromEnv) return fromEnv;
20468
+ const home = path3.join(os.homedir(), ".supabase");
20469
+ const profile = process.env.SUPABASE_PROFILE || (existsSync2(path3.join(home, "profile")) ? readFileSync2(path3.join(home, "profile"), "utf8").trim() : "") || "supabase";
20470
+ for (const account of [profile, "access-token"]) {
20471
+ const r = process.platform === "darwin" ? await run("security", ["find-generic-password", "-s", "Supabase CLI", "-a", account, "-w"]) : process.platform === "win32" ? await run("powershell", [
20472
+ "-NoProfile",
20473
+ "-NonInteractive",
20474
+ "-EncodedCommand",
20475
+ Buffer.from(windowsCredentialScript(`Supabase CLI:${account}`), "utf16le").toString("base64")
20476
+ ]) : await run("secret-tool", ["lookup", "service", "Supabase CLI", "username", account]);
20477
+ const token = r.code === 0 ? decodeToken(r.stdout) : null;
20478
+ if (token) return token;
20479
+ }
20480
+ const file2 = path3.join(home, "access-token");
20481
+ return existsSync2(file2) ? decodeToken(readFileSync2(file2, "utf8")) : null;
20482
+ }
20483
+ var realApi = (token) => async (method, endpoint, body) => {
20484
+ const res = await fetch(`https://api.supabase.com${endpoint}`, {
20485
+ method,
20486
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
20487
+ body: body ? JSON.stringify(body) : void 0,
20488
+ signal: AbortSignal.timeout(3e4)
20489
+ });
20490
+ return { status: res.status, json: await res.json().catch(() => null) };
20491
+ };
20492
+ var PRO_ONLY = "needs a paid Supabase plan (Pro or higher)";
20493
+ var authConfig = (ctx) => ctx.get(`/v1/projects/${ctx.ref}/config/auth`);
20494
+ async function patchAuth(ctx, body) {
20495
+ const r = await ctx.api("PATCH", `/v1/projects/${ctx.ref}/config/auth`, body);
20496
+ if (r.status >= 200 && r.status < 300) return { ok: true };
20497
+ return { ok: false, unavailable: /plan|pro\b|upgrade|paid/i.test(JSON.stringify(r.json ?? "")) ? PRO_ONLY : void 0 };
20498
+ }
20499
+ var authFlag = (check2) => async (ctx) => check2(await authConfig(ctx) ?? {}) ? { state: "on" } : { state: "off" };
20500
+ async function advisorProblems(ctx) {
20501
+ const lints = (await ctx.get(`/v1/projects/${ctx.ref}/advisors/security`))?.lints ?? [];
20502
+ return lints.filter((l) => l.level === "ERROR" || l.level === "WARN");
20503
+ }
20504
+ var supabase = {
20505
+ id: "supabase",
20506
+ name: "Supabase",
20507
+ actionId: "supabase-security",
20508
+ detect: (scan) => scan.stack.services.includes("Supabase"),
20509
+ async connect(root, fake) {
20510
+ let api = fake;
20511
+ if (!api) {
20512
+ const token = await supabaseToken();
20513
+ if (!token)
20514
+ return {
20515
+ ok: false,
20516
+ problem: "Aura couldn\u2019t find your Supabase login. Install the Supabase CLI and run `supabase login`, then try again.",
20517
+ errorType: "CliNotSignedIn"
20518
+ };
20519
+ api = realApi(token);
20520
+ }
20521
+ const refFile = path3.join(root, "supabase", ".temp", "project-ref");
20522
+ const ref = process.env.SUPABASE_PROJECT_ID || (existsSync2(refFile) ? readFileSync2(refFile, "utf8").trim() : "");
20523
+ if (!/^[a-z]{20}$/.test(ref))
20524
+ return { ok: false, problem: "This folder isn\u2019t linked to a Supabase project yet. Run `supabase link`, then try again.", errorType: "NoProject" };
20525
+ const cache = /* @__PURE__ */ new Map();
20526
+ const call = api;
20527
+ const ctx = {
20528
+ ref,
20529
+ api: call,
20530
+ get: (endpoint) => {
20531
+ if (!cache.has(endpoint))
20532
+ cache.set(
20533
+ endpoint,
20534
+ call("GET", endpoint).then((r) => r.status === 200 ? r.json : null)
20535
+ );
20536
+ return cache.get(endpoint);
20537
+ }
20538
+ };
20539
+ const project = await ctx.get(`/v1/projects/${ref}`);
20540
+ if (!project?.name) return { ok: false, problem: `Couldn\u2019t read the Supabase project ${ref}. Check your account can open it.`, errorType: "NoProject" };
20541
+ return { ok: true, ctx, target: project.name };
20542
+ },
20543
+ settings: [
20544
+ {
20545
+ id: "email-confirmation",
20546
+ label: "Email confirmation (new users confirm their email before signing in)",
20547
+ impact: "New users must click a link in an email before they can sign in. People already signed up aren\u2019t affected.",
20548
+ read: authFlag((c) => c.mailer_autoconfirm === false),
20549
+ enable: (ctx) => patchAuth(ctx, { mailer_autoconfirm: false })
20550
+ },
20551
+ {
20552
+ id: "password-length",
20553
+ label: "Passwords of at least 8 characters",
20554
+ impact: "New and changed passwords must be at least 8 characters. Existing passwords keep working.",
20555
+ read: authFlag((c) => (c.password_min_length ?? 0) >= 8),
20556
+ enable: (ctx) => patchAuth(ctx, { password_min_length: 8 })
20557
+ },
20558
+ {
20559
+ id: "leaked-passwords",
20560
+ label: "Leaked password protection (refuses passwords found in data breaches)",
20561
+ impact: "Passwords that appear in known data breaches are refused when someone signs up or changes their password.",
20562
+ read: authFlag((c) => c.password_hibp_enabled === true),
20563
+ enable: (ctx) => patchAuth(ctx, { password_hibp_enabled: true })
20564
+ },
20565
+ {
20566
+ id: "app-mfa",
20567
+ label: "Authenticator app sign-in for your users (two-factor, optional for them)",
20568
+ impact: "Your users can protect their account with an authenticator app. It stays optional, and your app needs a screen for it before anyone can use it.",
20569
+ read: authFlag((c) => c.mfa_totp_enroll_enabled === true && c.mfa_totp_verify_enabled === true),
20570
+ enable: (ctx) => patchAuth(ctx, { mfa_totp_enroll_enabled: true, mfa_totp_verify_enabled: true })
20571
+ },
20572
+ {
20573
+ id: "secure-email-change",
20574
+ label: "Secure email change (both the old and new address confirm)",
20575
+ impact: "Changing an account\u2019s email needs confirming from both the old and the new address, so a stolen session can\u2019t take over the account.",
20576
+ read: authFlag((c) => c.mailer_secure_email_change_enabled === true),
20577
+ enable: (ctx) => patchAuth(ctx, { mailer_secure_email_change_enabled: true })
20578
+ },
20579
+ {
20580
+ id: "database-encryption",
20581
+ label: "Encrypted database connections only",
20582
+ impact: "Direct connections to your database must be encrypted. Turning this on restarts the database for a few seconds.",
20583
+ read: async (ctx) => (await ctx.get(`/v1/projects/${ctx.ref}/ssl-enforcement`))?.currentConfig?.database === true ? { state: "on" } : { state: "off" },
20584
+ async enable(ctx) {
20585
+ const r = await ctx.api("PUT", `/v1/projects/${ctx.ref}/ssl-enforcement`, { requestedConfig: { database: true } });
20586
+ return r.status >= 200 && r.status < 300 ? { ok: true } : { ok: false };
20587
+ }
20588
+ },
20589
+ {
20590
+ id: "security-advisor",
20591
+ label: "No problems in Supabase\u2019s security advisor",
20592
+ impact: "Nothing changes on its own: these are fixed in your database code, and Aura lists them below.",
20593
+ read: async (ctx) => (await advisorProblems(ctx)).length ? { state: "off" } : { state: "on" },
20594
+ enable: async () => ({ ok: false, unavailable: "fixed in your database code (see Left for you)" })
20595
+ }
20596
+ ],
20597
+ async leftForYou(ctx, results) {
20598
+ const left = [];
20599
+ const problems = await advisorProblems(ctx);
20600
+ if (problems.length) {
20601
+ const titles = [...new Set(problems.map((p2) => p2.title ?? p2.name ?? "Security problem"))].slice(0, 5);
20602
+ left.push(
20603
+ `Supabase\u2019s security advisor found ${problems.length} problem(s): ${titles.join("; ")}. Fix them in a migration (\`aura fix turn-on-rls\` covers missing row-level security), or see https://supabase.com/dashboard/project/${ctx.ref}/advisors/security`
20604
+ );
20605
+ }
20606
+ if (results.some((r) => r.id === "leaked-passwords" && r.result === "unavailable"))
20607
+ left.push("Leaked password protection needs Supabase Pro. Until then, the 8-character minimum helps.");
20608
+ left.push("Turn on two-factor login for your Supabase account: Supabase dashboard \u2192 Account \u2192 Security.");
20609
+ return left;
20610
+ }
20611
+ };
20612
+
20613
+ // src/connectors/vercel.ts
20614
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
20615
+ import path4 from "path";
20616
+ var PUBLIC_PREFIX = /^(?:NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_|PUBLIC_)/;
20617
+ var SECRET_NAME = /SECRET|SERVICE_ROLE|PRIVATE|PASSWORD|_TOKEN$|API_KEY$/;
20618
+ var parse4 = (r) => {
20619
+ try {
20620
+ return r.code === 0 ? JSON.parse(r.stdout) : null;
20621
+ } catch {
20622
+ return null;
20623
+ }
20624
+ };
20625
+ async function exposedSecrets(ctx) {
20626
+ const envs = (await ctx.get(`/v10/projects/${ctx.projectId}/env?decrypt=true${ctx.team.replace("?", "&")}`))?.envs ?? [];
20627
+ return [...new Set(envs.filter((e) => PUBLIC_PREFIX.test(e.key) && (SECRET_NAME.test(e.key) || e.value && containsSecret(e.value))).map((e) => e.key))];
20628
+ }
20629
+ async function patchProject(ctx, body) {
20630
+ const r = await ctx.vercel(["api", `/v9/projects/${ctx.projectId}${ctx.team}`, "-X", "PATCH", "--input", "-", "--raw"], JSON.stringify(body));
20631
+ if (r.code === 0) return { ok: true };
20632
+ return { ok: false, unavailable: /plan|upgrade|pro\b|enterprise/i.test(r.stderr + r.stdout) ? "not available on your Vercel plan" : void 0 };
20633
+ }
20634
+ var vercel = {
20635
+ id: "vercel",
20636
+ name: "Vercel",
20637
+ actionId: "vercel-security",
20638
+ detect: (scan) => scan.stack.services.includes("Vercel"),
20639
+ async connect(root, fake) {
20640
+ const cli = fake ?? ((args, input2) => run("vercel", args, { cwd: root, input: input2, timeoutMs: 6e4 }));
20641
+ if (!fake) {
20642
+ if (!await commandExists("vercel"))
20643
+ return {
20644
+ ok: false,
20645
+ problem: "The Vercel CLI isn\u2019t installed. Install it with `npm install -g vercel`, then run `vercel login`.",
20646
+ errorType: "CliNotInstalled"
20647
+ };
20648
+ if ((await cli(["whoami"])).code !== 0)
20649
+ return { ok: false, problem: "The Vercel CLI isn\u2019t signed in. Run `vercel login`, then try again.", errorType: "CliNotSignedIn" };
20650
+ }
20651
+ const linked = [path4.join(root, ".vercel", "project.json"), ...["apps", "packages"].flatMap((d) => findLinks(path4.join(root, d)))].find(
20652
+ (f) => existsSync3(f)
20653
+ );
20654
+ let project = null;
20655
+ try {
20656
+ project = linked ? JSON.parse(readFileSync3(linked, "utf8")) : null;
20657
+ } catch {
20658
+ project = null;
20659
+ }
20660
+ if (!project?.projectId)
20661
+ return { ok: false, problem: "This folder isn\u2019t linked to a Vercel project yet. Run `vercel link`, then try again.", errorType: "NoProject" };
20662
+ const cache = /* @__PURE__ */ new Map();
20663
+ const ctx = {
20664
+ projectId: project.projectId,
20665
+ team: project.orgId?.startsWith("team_") ? `?teamId=${project.orgId}` : "",
20666
+ vercel: cli,
20667
+ get: (endpoint) => {
20668
+ if (!cache.has(endpoint)) cache.set(endpoint, cli(["api", endpoint, "--raw"]).then(parse4));
20669
+ return cache.get(endpoint);
20670
+ }
20671
+ };
20672
+ const name = (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.name;
20673
+ if (!name)
20674
+ return { ok: false, problem: "Couldn\u2019t read this project from Vercel. Check you have access to it with `vercel project ls`.", errorType: "NoProject" };
20675
+ return { ok: true, ctx, target: name };
20676
+ },
20677
+ settings: [
20678
+ {
20679
+ id: "preview-protection",
20680
+ label: "Protect preview links (only your Vercel team can open them)",
20681
+ impact: "Preview and deployment links need a Vercel login to open. Your live site on your own domain stays public.",
20682
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.ssoProtection ? { state: "on" } : { state: "off" },
20683
+ enable: (ctx) => patchProject(ctx, { ssoProtection: { deploymentType: "all_except_custom_domains" } })
20684
+ },
20685
+ {
20686
+ id: "git-fork-protection",
20687
+ label: "Fork protection (pull requests from forks don\u2019t deploy automatically)",
20688
+ impact: "A pull request from someone else\u2019s copy of your repo only deploys after you approve it, so it can\u2019t read your settings.",
20689
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.gitForkProtection ? { state: "on" } : { state: "off" },
20690
+ enable: (ctx) => patchProject(ctx, { gitForkProtection: true })
20691
+ },
20692
+ {
20693
+ id: "protected-sourcemaps",
20694
+ label: "Protected source maps (your original code isn\u2019t served to the public)",
20695
+ impact: "Source maps, which reveal your original code, are only served to people signed in to your Vercel team.",
20696
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.protectedSourcemaps ? { state: "on" } : { state: "off" },
20697
+ enable: (ctx) => patchProject(ctx, { protectedSourcemaps: true })
20698
+ },
20699
+ {
20700
+ id: "no-public-secrets",
20701
+ label: "No secret keys in browser-visible settings (NEXT_PUBLIC_\u2026 and similar)",
20702
+ impact: "Nothing changes on its own: Aura can\u2019t move a key for you, and lists what to do below.",
20703
+ read: async (ctx) => (await exposedSecrets(ctx)).length ? { state: "off" } : { state: "on" },
20704
+ enable: async () => ({ ok: false, unavailable: "needs you to move the key (see Left for you)" })
20705
+ }
20706
+ ],
20707
+ async leftForYou(ctx) {
20708
+ const left = [];
20709
+ const exposed = await exposedSecrets(ctx);
20710
+ if (exposed.length)
20711
+ left.push(
20712
+ `These settings are visible to anyone using your site: ${exposed.join(", ")}. Rename each without the public prefix, read it only in server code, then rotate the key in its dashboard (\`aura guide rotate-generic-key\`).`
20713
+ );
20714
+ left.push("Turn on two-factor login for your Vercel account: Vercel \u2192 Account Settings \u2192 Authentication.");
20715
+ return left;
20716
+ }
20717
+ };
20718
+ function findLinks(dir) {
20719
+ try {
20720
+ return readdirSync2(dir).map((d) => path4.join(dir, d, ".vercel", "project.json"));
20721
+ } catch {
20722
+ return [];
20723
+ }
20724
+ }
20725
+
20726
+ // src/connectors/index.ts
20727
+ var CONNECTORS = [github, supabase, vercel];
20728
+ function connectorForAction(actionId) {
20729
+ return CONNECTORS.find((c) => c.actionId === actionId);
20730
+ }
20731
+
20732
+ // src/templates.ts
20733
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
20734
+ import path7 from "path";
20735
+
20207
20736
  // src/core/config.ts
20737
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
20738
+ import os2 from "os";
20739
+ import path5 from "path";
20208
20740
  var DEFAULT_CONFIG = {
20209
20741
  version: 1,
20210
20742
  agents: [],
@@ -20213,45 +20745,45 @@ var DEFAULT_CONFIG = {
20213
20745
  ciMaxDrop: 0
20214
20746
  };
20215
20747
  function auraPath(root, ...parts) {
20216
- return path2.join(root, AURA_DIR, ...parts);
20748
+ return path5.join(root, AURA_DIR, ...parts);
20217
20749
  }
20218
20750
  function ensureAuraDir(root) {
20219
- mkdirSync(auraPath(root), { recursive: true });
20751
+ mkdirSync2(auraPath(root), { recursive: true });
20220
20752
  }
20221
20753
  function loadConfig(root) {
20222
20754
  const file2 = auraPath(root, "config.json");
20223
- if (!existsSync2(file2)) return { ...DEFAULT_CONFIG };
20755
+ if (!existsSync4(file2)) return { ...DEFAULT_CONFIG };
20224
20756
  try {
20225
- return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync2(file2, "utf8")) };
20757
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync4(file2, "utf8")) };
20226
20758
  } catch {
20227
20759
  return { ...DEFAULT_CONFIG };
20228
20760
  }
20229
20761
  }
20230
20762
  function saveConfig(root, config2) {
20231
20763
  ensureAuraDir(root);
20232
- writeFileSync(auraPath(root, "config.json"), JSON.stringify(config2, null, 2) + "\n");
20764
+ writeFileSync2(auraPath(root, "config.json"), JSON.stringify(config2, null, 2) + "\n");
20233
20765
  }
20234
20766
  function writeAuraFile(root, name, content) {
20235
20767
  ensureAuraDir(root);
20236
20768
  const file2 = auraPath(root, name);
20237
- mkdirSync(path2.dirname(file2), { recursive: true });
20238
- writeFileSync(file2, content);
20769
+ mkdirSync2(path5.dirname(file2), { recursive: true });
20770
+ writeFileSync2(file2, content);
20239
20771
  return file2;
20240
20772
  }
20241
- var homeAuraDir = () => path2.join(os.homedir(), ".aura");
20242
- var credentialsFile = () => path2.join(homeAuraDir(), "credentials.json");
20773
+ var homeAuraDir = () => path5.join(os2.homedir(), ".aura");
20774
+ var credentialsFile = () => path5.join(homeAuraDir(), "credentials.json");
20243
20775
  function getApiKey() {
20244
20776
  if (process.env.AURA_KEY) return process.env.AURA_KEY.trim();
20245
20777
  try {
20246
- const data = JSON.parse(readFileSync2(credentialsFile(), "utf8"));
20778
+ const data = JSON.parse(readFileSync4(credentialsFile(), "utf8"));
20247
20779
  return typeof data.key === "string" ? data.key : null;
20248
20780
  } catch {
20249
20781
  return null;
20250
20782
  }
20251
20783
  }
20252
20784
  function saveApiKey(key) {
20253
- mkdirSync(homeAuraDir(), { recursive: true });
20254
- writeFileSync(credentialsFile(), JSON.stringify({ key }, null, 2) + "\n", { mode: 384 });
20785
+ mkdirSync2(homeAuraDir(), { recursive: true });
20786
+ writeFileSync2(credentialsFile(), JSON.stringify({ key }, null, 2) + "\n", { mode: 384 });
20255
20787
  try {
20256
20788
  chmodSync(credentialsFile(), 384);
20257
20789
  } catch {
@@ -20261,7 +20793,7 @@ function getApiUrl(config2) {
20261
20793
  return (process.env.AURA_API_URL || config2?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
20262
20794
  }
20263
20795
  function auraBinDir() {
20264
- return path2.join(homeAuraDir(), "bin");
20796
+ return path5.join(homeAuraDir(), "bin");
20265
20797
  }
20266
20798
  var AURA_GITIGNORE = `# Aura local files: they can describe security weaknesses, so they stay on your machine.
20267
20799
  report.md
@@ -20273,15 +20805,15 @@ tasks/
20273
20805
  `;
20274
20806
  function ensureGitignore(root) {
20275
20807
  const file2 = auraPath(root, ".gitignore");
20276
- if (existsSync2(file2) && readFileSync2(file2, "utf8") === AURA_GITIGNORE) return false;
20808
+ if (existsSync4(file2) && readFileSync4(file2, "utf8") === AURA_GITIGNORE) return false;
20277
20809
  ensureAuraDir(root);
20278
- writeFileSync(file2, AURA_GITIGNORE);
20810
+ writeFileSync2(file2, AURA_GITIGNORE);
20279
20811
  return true;
20280
20812
  }
20281
20813
 
20282
20814
  // src/core/stack.ts
20283
- import { existsSync as existsSync3 } from "fs";
20284
- import path3 from "path";
20815
+ import { existsSync as existsSync5 } from "fs";
20816
+ import path6 from "path";
20285
20817
  function allDeps(pkg) {
20286
20818
  return { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
20287
20819
  }
@@ -20405,7 +20937,7 @@ function findEnvRefs(files, read2) {
20405
20937
  }
20406
20938
  function detectStack(root, files, pkg, read2) {
20407
20939
  const deps2 = allDeps(pkg);
20408
- const has = (f) => files.includes(f) || existsSync3(path3.join(root, f));
20940
+ const has = (f) => files.includes(f) || existsSync5(path6.join(root, f));
20409
20941
  const hasDir = (d) => files.some((f) => f.startsWith(`${d}/`));
20410
20942
  let packageManager = null;
20411
20943
  const pmField = pkg?.packageManager?.split("@")[0];
@@ -20430,6 +20962,7 @@ function detectStack(root, files, pkg, read2) {
20430
20962
  }
20431
20963
  if (has("vercel.json") || has(".vercel/project.json")) services.add("Vercel");
20432
20964
  if (hasDir("supabase")) services.add("Supabase");
20965
+ if (has("vercel.json") || has(".vercel/project.json")) services.add("Vercel");
20433
20966
  services.add("GitHub");
20434
20967
  let nextjs = null;
20435
20968
  if (deps2.next) {
@@ -20439,9 +20972,9 @@ function detectStack(root, files, pkg, read2) {
20439
20972
  const pages = hasDir(`${base}pages`);
20440
20973
  nextjs = { version: deps2.next.replace(/^[^\d]*/, ""), router: app && pages ? "both" : app ? "app" : pages ? "pages" : null, srcDir };
20441
20974
  }
20442
- let supabase = null;
20975
+ let supabase2 = null;
20443
20976
  if (services.has("Supabase")) {
20444
- supabase = { migrationsDir: hasDir("supabase/migrations") ? "supabase/migrations" : null, hasConfig: has("supabase/config.toml") };
20977
+ supabase2 = { migrationsDir: hasDir("supabase/migrations") ? "supabase/migrations" : null, hasConfig: has("supabase/config.toml") };
20445
20978
  }
20446
20979
  let testRunner = null;
20447
20980
  if (deps2.vitest || files.some((f) => /^vitest\.config\./.test(f))) testRunner = "vitest";
@@ -20456,7 +20989,7 @@ function detectStack(root, files, pkg, read2) {
20456
20989
  frameworks,
20457
20990
  services: [...services].sort(),
20458
20991
  nextjs,
20459
- supabase,
20992
+ supabase: supabase2,
20460
20993
  typescript: has("tsconfig.json"),
20461
20994
  testRunner,
20462
20995
  envVars,
@@ -20500,7 +21033,7 @@ function pmCommands(pm) {
20500
21033
  // package.json
20501
21034
  var package_default = {
20502
21035
  name: "getaura",
20503
- version: "0.1.7",
21036
+ version: "0.1.8",
20504
21037
  description: "Aura helps you and your coding agent follow best practices while you vibe code.",
20505
21038
  type: "module",
20506
21039
  bin: {
@@ -20581,19 +21114,19 @@ function fill(text, vars) {
20581
21114
  return text.replace(/\{\{([a-z_]+)\}\}/g, (m, name) => KNOWN.has(name) ? vars[name] ?? "" : m);
20582
21115
  }
20583
21116
  function readTemplate(rel) {
20584
- return readFileSync3(path4.join(templatesDir(), rel), "utf8");
21117
+ return readFileSync5(path7.join(templatesDir(), rel), "utf8");
20585
21118
  }
20586
21119
  function render(rel, vars) {
20587
21120
  return fill(readTemplate(rel), vars);
20588
21121
  }
20589
21122
  function templateExists(rel) {
20590
- return existsSync4(path4.join(templatesDir(), rel));
21123
+ return existsSync6(path7.join(templatesDir(), rel));
20591
21124
  }
20592
21125
  function briefSummary(root, pkg) {
20593
- const fallbackName = pkg?.name ? pkg.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : path4.basename(root);
21126
+ const fallbackName = pkg?.name ? pkg.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : path7.basename(root);
20594
21127
  const file2 = auraPath(root, "brief.md");
20595
- if (!existsSync4(file2)) return { name: fallbackName, summary: pkg?.description ?? "See `.aura/brief.md` once the product brief is added." };
20596
- const text = readFileSync3(file2, "utf8");
21128
+ if (!existsSync6(file2)) return { name: fallbackName, summary: pkg?.description ?? "See `.aura/brief.md` once the product brief is added." };
21129
+ const text = readFileSync5(file2, "utf8");
20597
21130
  const title = text.match(/^#\s+(.+)$/m)?.[1]?.replace(/[:\-–—]?\s*product brief$/i, "").trim();
20598
21131
  const paragraph = text.split(/\n\s*\n/).map((p2) => p2.trim()).find((p2) => p2 && !p2.startsWith("#") && !p2.startsWith(">") && !p2.startsWith("-") && !p2.startsWith("|") && p2.length > 40) ?? "";
20599
21132
  const sentences = paragraph.replace(/\s+/g, " ").match(/[^.!?]+[.!?]+/g) ?? [paragraph];
@@ -20758,7 +21291,6 @@ var ACTIONS = [
20758
21291
  },
20759
21292
  // Accounts and ownership
20760
21293
  { id: "inventory", title: "Complete the keys and accounts inventory", category: "security", delivery: "checklist", resolves: [] },
20761
- { id: "github-security", title: "Enable GitHub security features", category: "security", delivery: "guide", resolves: [] },
20762
21294
  { id: "transfer-ownership", title: "Move accounts to a company owner", category: "security", delivery: "guide", resolves: [] }
20763
21295
  ];
20764
21296
  var FOUNDATION_BUNDLE = ["add-brief", "add-agent-rules", "install-skills", "token-efficiency", "add-ci", "add-env-example", "add-aura-workflow"];
@@ -20773,6 +21305,15 @@ function getAction(id) {
20773
21305
  };
20774
21306
  }
20775
21307
  if (/^rotate-[a-z]+-(?:key|token)$/.test(id)) return { id, title: "Rotate an exposed key", category: "security", delivery: "guide", resolves: [] };
21308
+ const connector = connectorForAction(id);
21309
+ if (connector)
21310
+ return {
21311
+ id,
21312
+ title: `Turn on ${connector.name} security features`,
21313
+ category: "security",
21314
+ delivery: "guide",
21315
+ resolves: [`security.${connector.id}-settings`]
21316
+ };
20776
21317
  return ACTIONS.find((a) => a.id === id);
20777
21318
  }
20778
21319
  var CATEGORY_PRIORITY = ["security", "agent-setup", "foundations", "structure"];
@@ -20808,15 +21349,16 @@ function nextSteps(scan, opts = {}) {
20808
21349
  if (!action2) continue;
20809
21350
  const { delivery, fellBack } = effectiveDelivery(action2, scan);
20810
21351
  const worst = fs.map((f) => f.severity).sort((a, b) => SEVERITY_ORDER.indexOf(a) - SEVERITY_ORDER.indexOf(b))[0];
21352
+ const connected = Boolean(connectorForAction(actionId));
20811
21353
  steps.push({
20812
21354
  actionId,
20813
21355
  title: action2.title,
20814
21356
  delivery,
20815
- effort: EFFORT[delivery],
21357
+ effort: connected ? "one-click" : EFFORT[delivery],
20816
21358
  category: action2.category,
20817
21359
  why: fs[0].why,
20818
21360
  impact: impactOf(scan.checks, action2.resolves),
20819
- command: commandFor(actionId, delivery),
21361
+ command: connected ? `aura apply ${actionId}` : commandFor(actionId, delivery),
20820
21362
  findingIds: fs.map((f) => f.id),
20821
21363
  fellBack,
20822
21364
  severity: worst
@@ -20841,16 +21383,16 @@ function nextSteps(scan, opts = {}) {
20841
21383
  severity: "critical"
20842
21384
  });
20843
21385
  }
20844
- if (opts.accounts && !opts.accounts.githubSecurity) {
21386
+ for (const connector of opts.accounts ? CONNECTORS.filter((c) => c.detect(scan) && !opts.accounts.secured[c.id] && !steps.some((s) => s.actionId === c.actionId)) : []) {
20845
21387
  steps.push({
20846
- actionId: "github-security",
20847
- title: "Enable GitHub security features",
21388
+ actionId: connector.actionId,
21389
+ title: `Turn on ${connector.name} security features`,
20848
21390
  delivery: "guide",
20849
- effort: "follow-guide",
21391
+ effort: "one-click",
20850
21392
  category: "security",
20851
- why: "Branch protection and secret scanning stop mistakes (and leaked keys) before they reach your main branch.",
21393
+ why: `Aura checks ${connector.name}\u2019s security settings and, once you agree, turns on the ones that are off.`,
20852
21394
  impact: 0,
20853
- command: "aura guide github-security",
21395
+ command: `aura apply ${connector.actionId}`,
20854
21396
  findingIds: [],
20855
21397
  severity: "low"
20856
21398
  });
@@ -20899,12 +21441,12 @@ function nextSteps(scan, opts = {}) {
20899
21441
  }
20900
21442
 
20901
21443
  // src/actions/generators.ts
20902
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
20903
- import path9 from "path";
21444
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
21445
+ import path12 from "path";
20904
21446
 
20905
21447
  // src/checks/agent-setup.ts
20906
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
20907
- import path5 from "path";
21448
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
21449
+ import path8 from "path";
20908
21450
 
20909
21451
  // src/checks/types.ts
20910
21452
  import { createHash } from "crypto";
@@ -20938,18 +21480,18 @@ function lineOf(text, index) {
20938
21480
 
20939
21481
  // src/checks/agent-setup.ts
20940
21482
  function exists(ctx, file2) {
20941
- return existsSync5(path5.join(ctx.root, file2));
21483
+ return existsSync7(path8.join(ctx.root, file2));
20942
21484
  }
20943
21485
  function readDisk(ctx, file2) {
20944
21486
  try {
20945
- return readFileSync4(path5.join(ctx.root, file2), "utf8");
21487
+ return readFileSync6(path8.join(ctx.root, file2), "utf8");
20946
21488
  } catch {
20947
21489
  return "";
20948
21490
  }
20949
21491
  }
20950
21492
  function skillsIn(ctx, dir) {
20951
21493
  try {
20952
- return readdirSync2(path5.join(ctx.root, dir), { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync5(path5.join(ctx.root, dir, d.name, "SKILL.md"))).map((d) => d.name);
21494
+ return readdirSync3(path8.join(ctx.root, dir), { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync7(path8.join(ctx.root, dir, d.name, "SKILL.md"))).map((d) => d.name);
20953
21495
  } catch {
20954
21496
  return [];
20955
21497
  }
@@ -20966,7 +21508,7 @@ function rulesFileFor(ctx, agent) {
20966
21508
  if (agent === "cursor") {
20967
21509
  if (exists(ctx, ".cursorrules")) return ".cursorrules";
20968
21510
  try {
20969
- const rules = readdirSync2(path5.join(ctx.root, ".cursor/rules")).filter((f) => /\.mdc?$/.test(f));
21511
+ const rules = readdirSync3(path8.join(ctx.root, ".cursor/rules")).filter((f) => /\.mdc?$/.test(f));
20970
21512
  if (rules.length) return `.cursor/rules/${rules[0]}`;
20971
21513
  } catch {
20972
21514
  }
@@ -21111,7 +21653,7 @@ var brief = {
21111
21653
  var agentChecks = [rulesFile, rulesCurrent, skills, auraSkill, brief];
21112
21654
 
21113
21655
  // src/core/graph.ts
21114
- import path6 from "path";
21656
+ import path9 from "path";
21115
21657
  var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
21116
21658
  var IMPORT_RE = /(?:import|export)\s+(?:type\s+)?(?:[\w*{}\s,$]+?\s+from\s+)?(['"])([^'"\n]+)\1|import\s*\(\s*(['"])([^'"\n]+)\3\s*\)|require\s*\(\s*(['"])([^'"\n]+)\5\s*\)/g;
21117
21659
  function parseImports(text) {
@@ -21139,7 +21681,7 @@ function readAliases(read2) {
21139
21681
  for (const [key, targets] of Object.entries(json2.compilerOptions?.paths ?? {})) {
21140
21682
  const target = targets[0];
21141
21683
  if (!key.endsWith("/*") || !target?.endsWith("/*")) continue;
21142
- const resolved = path6.posix.normalize(path6.posix.join(base || ".", target.slice(0, -1)));
21684
+ const resolved = path9.posix.normalize(path9.posix.join(base || ".", target.slice(0, -1)));
21143
21685
  entries.push([key.slice(0, -1), resolved === "." || resolved === "./" ? "" : resolved.replace(/\/?$/, "/")]);
21144
21686
  }
21145
21687
  } catch {
@@ -21150,10 +21692,10 @@ function readAliases(read2) {
21150
21692
  }
21151
21693
  function resolveImport(from, spec, files, aliases) {
21152
21694
  let base = null;
21153
- if (spec.startsWith(".")) base = path6.posix.normalize(path6.posix.join(path6.posix.dirname(from), spec));
21695
+ if (spec.startsWith(".")) base = path9.posix.normalize(path9.posix.join(path9.posix.dirname(from), spec));
21154
21696
  else {
21155
21697
  const alias = aliases.entries.find(([prefix]) => spec.startsWith(prefix));
21156
- if (alias) base = path6.posix.normalize(alias[1] + spec.slice(alias[0].length));
21698
+ if (alias) base = path9.posix.normalize(alias[1] + spec.slice(alias[0].length));
21157
21699
  }
21158
21700
  if (base === null) return null;
21159
21701
  const withoutJs = base.replace(/\.(?:js|jsx|mjs|cjs)$/, "");
@@ -21199,14 +21741,14 @@ function unreachableFiles(files, read2, pkgScripts) {
21199
21741
  }
21200
21742
 
21201
21743
  // src/actions/agent-commands.ts
21202
- import { readFileSync as readFileSync5 } from "fs";
21203
- import path7 from "path";
21744
+ import { readFileSync as readFileSync7 } from "fs";
21745
+ import path10 from "path";
21204
21746
  var AURA_COMMANDS = ["scan", "next", "fix", "setup"];
21205
21747
  function invocation(agent, command) {
21206
21748
  return agent === "claude" ? `/aura:${command}` : agent === "codex" ? `$aura-${command}` : `/aura-${command}`;
21207
21749
  }
21208
21750
  function pluginCommand(command) {
21209
- const text = readFileSync5(path7.join(path7.dirname(templatesDir()), "plugin", "skills", command, "SKILL.md"), "utf8").replace(/\r\n/g, "\n");
21751
+ const text = readFileSync7(path10.join(path10.dirname(templatesDir()), "plugin", "skills", command, "SKILL.md"), "utf8").replace(/\r\n/g, "\n");
21210
21752
  const [, frontmatter = "", body = ""] = text.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/) ?? [];
21211
21753
  const description = frontmatter.match(/^description: (.+)$/m)?.[1] ?? "";
21212
21754
  return { description, body };
@@ -21251,11 +21793,11 @@ function commandList(agent) {
21251
21793
  }
21252
21794
 
21253
21795
  // src/actions/agent-files.ts
21254
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
21255
- import path8 from "path";
21796
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
21797
+ import path11 from "path";
21256
21798
  function current(root, file2) {
21257
- const full = path8.join(root, file2);
21258
- return existsSync6(full) ? readFileSync6(full, "utf8") : null;
21799
+ const full = path11.join(root, file2);
21800
+ return existsSync8(full) ? readFileSync8(full, "utf8") : null;
21259
21801
  }
21260
21802
  function upsertBlock(existing, body) {
21261
21803
  const block = `${AURA_BLOCK_START}
@@ -21299,8 +21841,8 @@ function skillChanges(agents, names) {
21299
21841
 
21300
21842
  // src/actions/generators.ts
21301
21843
  function read(root, file2) {
21302
- const full = path9.join(root, file2);
21303
- return existsSync7(full) ? readFileSync7(full, "utf8") : null;
21844
+ const full = path12.join(root, file2);
21845
+ return existsSync9(full) ? readFileSync9(full, "utf8") : null;
21304
21846
  }
21305
21847
  function withScripts(root, scripts, overwrite = false) {
21306
21848
  const text = read(root, "package.json");
@@ -21321,13 +21863,13 @@ function findingMeta(scan, checkId, key) {
21321
21863
  return scan.checks.find((c) => c.checkId === checkId)?.findings[0]?.meta?.[key];
21322
21864
  }
21323
21865
  var addBrief = ({ root }) => {
21324
- const brief2 = BRIEF_PATHS.find((f) => existsSync7(path9.join(root, f)));
21866
+ const brief2 = BRIEF_PATHS.find((f) => existsSync9(path12.join(root, f)));
21325
21867
  if (!brief2) return { skip: "There is no product brief yet. Run `aura init` to draft and confirm it first." };
21326
21868
  const changes = [
21327
21869
  { path: brief2, content: read(root, brief2) },
21328
21870
  { path: ".aura/.gitignore", content: AURA_GITIGNORE }
21329
21871
  ];
21330
- if (existsSync7(auraPath(root, "config.json"))) changes.push({ path: ".aura/config.json", content: read(root, ".aura/config.json") });
21872
+ if (existsSync9(auraPath(root, "config.json"))) changes.push({ path: ".aura/config.json", content: read(root, ".aura/config.json") });
21331
21873
  return { changes, details: [`- Adds \`${brief2}\`, \`.aura/config.json\` and \`.aura/.gitignore\` (keeps Aura's local reports out of git)`] };
21332
21874
  };
21333
21875
  var addAgentRules = ({ root, agents, vars }) => {
@@ -21346,14 +21888,14 @@ var installSkills = ({ agents, skills: skills2 }) => {
21346
21888
  };
21347
21889
  var tokenEfficiency = (g) => {
21348
21890
  const changes = [];
21349
- if (!existsSync7(path9.join(g.root, "docs/index.md"))) changes.push({ path: "docs/index.md", content: render("docs/index.md", g.vars) });
21891
+ if (!existsSync9(path12.join(g.root, "docs/index.md"))) changes.push({ path: "docs/index.md", content: render("docs/index.md", g.vars) });
21350
21892
  changes.push(...rulesChanges(g.root, g.agents, g.vars));
21351
21893
  return { changes, details: ["- `docs/index.md`: a map of the repo your agent reads first", "- Lean-context rules in the agent rules file"] };
21352
21894
  };
21353
21895
  var addCi = ({ root, ctx, vars }) => {
21354
21896
  const existing = ctx.files.filter((f) => /^\.github\/workflows\//.test(f));
21355
21897
  const file2 = existing.includes(".github/workflows/ci.yml") ? ".github/workflows/checks.yml" : ".github/workflows/ci.yml";
21356
- if (existsSync7(path9.join(root, file2))) return { skip: `${file2} already exists.` };
21898
+ if (existsSync9(path12.join(root, file2))) return { skip: `${file2} already exists.` };
21357
21899
  return {
21358
21900
  changes: [{ path: file2, content: render("github/workflows/ci.yml", vars) }],
21359
21901
  details: [`- \`${file2}\`: lint, type check, tests and build on every pull request`]
@@ -21377,7 +21919,7 @@ var setupTesting = ({ root, ctx, vars }) => {
21377
21919
  };
21378
21920
  };
21379
21921
  var addEnvExample = ({ root, scan }) => {
21380
- const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync7(path9.join(root, f))) ?? ".env.example";
21922
+ const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync9(path12.join(root, f))) ?? ".env.example";
21381
21923
  const existing = read(root, file2);
21382
21924
  const declared = new Set(
21383
21925
  (existing ?? "").split(/\r?\n/).map((l) => l.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1]).filter(Boolean)
@@ -21434,7 +21976,7 @@ var addLinting = ({ root, ctx }) => {
21434
21976
  return { changes, devDeps, details };
21435
21977
  };
21436
21978
  var addPreCommit = ({ root, ctx, vars }) => {
21437
- if (existsSync7(path9.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21979
+ if (existsSync9(path12.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21438
21980
  const changes = [{ path: ".husky/pre-commit", content: render("configs/husky-pre-commit", vars) }];
21439
21981
  const scripts = withScripts(root, { prepare: "husky" }, false);
21440
21982
  if (scripts) changes.push(scripts);
@@ -21464,7 +22006,7 @@ ${current2.trim()}
21464
22006
  };
21465
22007
  };
21466
22008
  var enableDependencyUpdates = ({ root, vars }) => {
21467
- if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync7(path9.join(root, f)))) {
22009
+ if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync9(path12.join(root, f)))) {
21468
22010
  return { skip: "Dependabot or Renovate is already configured." };
21469
22011
  }
21470
22012
  return {
@@ -21544,7 +22086,7 @@ function planMoves(root, ctx, moves) {
21544
22086
  const alias = aliasFor(toFile);
21545
22087
  if (alias) return alias[0] + noExt.slice(alias[1].length);
21546
22088
  }
21547
- let rel = path9.posix.relative(path9.posix.dirname(fromFile), noExt);
22089
+ let rel = path12.posix.relative(path12.posix.dirname(fromFile), noExt);
21548
22090
  if (!rel.startsWith(".")) rel = `./${rel}`;
21549
22091
  return rel;
21550
22092
  };
@@ -21578,7 +22120,7 @@ function planMoves(root, ctx, moves) {
21578
22120
  }
21579
22121
  var moveMisplacedFiles = ({ root, ctx, scan }) => {
21580
22122
  const raw = findingMeta(scan, "structure.layout", "moves") ?? [];
21581
- const moves = raw.map((m) => m.split("=>")).filter(([, to]) => !existsSync7(path9.join(root, to))).slice(0, MAX_MOVES);
22123
+ const moves = raw.map((m) => m.split("=>")).filter(([, to]) => !existsSync9(path12.join(root, to))).slice(0, MAX_MOVES);
21582
22124
  if (moves.length === 0) return { skip: "No files to move (or their targets already exist)." };
21583
22125
  return { changes: planMoves(root, ctx, moves), details: moves.map(([from, to]) => `- \`${from}\` \u2192 \`${to}\``), verifyTypes: true };
21584
22126
  };
@@ -21669,7 +22211,7 @@ var AuraClient = class _AuraClient {
21669
22211
  get hasKey() {
21670
22212
  return Boolean(this.key);
21671
22213
  }
21672
- async request(method, path25, body, timeoutMs = 6e4) {
22214
+ async request(method, path27, body, timeoutMs = 6e4) {
21673
22215
  if (!this.key) throw new AuraApiError("no_key", "No pilot key");
21674
22216
  let payload;
21675
22217
  if (body !== void 0) {
@@ -21679,7 +22221,7 @@ var AuraClient = class _AuraClient {
21679
22221
  }
21680
22222
  let res;
21681
22223
  try {
21682
- res = await fetch(`${this.baseUrl}/v1/${path25}`, {
22224
+ res = await fetch(`${this.baseUrl}/v1/${path27}`, {
21683
22225
  method,
21684
22226
  headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json", "user-agent": `getaura/${AURA_VERSION}` },
21685
22227
  body: payload,
@@ -21743,13 +22285,13 @@ var say = {
21743
22285
  };
21744
22286
 
21745
22287
  // src/fix-tracking.ts
21746
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
22288
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
21747
22289
  var FILE = "fixes.json";
21748
22290
  var MAX_AGE_MS = 60 * 24 * 60 * 60 * 1e3;
21749
22291
  var MAX_CHECKS_PER_SCAN = 5;
21750
22292
  function loadTrackedFixes(root) {
21751
22293
  try {
21752
- return existsSync8(auraPath(root, FILE)) ? JSON.parse(readFileSync8(auraPath(root, FILE), "utf8")) : [];
22294
+ return existsSync10(auraPath(root, FILE)) ? JSON.parse(readFileSync10(auraPath(root, FILE), "utf8")) : [];
21753
22295
  } catch {
21754
22296
  return [];
21755
22297
  }
@@ -21857,70 +22399,6 @@ async function recordAction(root, config2, actionId, report, startedAt, via) {
21857
22399
  }
21858
22400
  }
21859
22401
 
21860
- // src/core/git.ts
21861
- import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
21862
- import path10 from "path";
21863
- async function git(root, ...args) {
21864
- return run("git", args, { cwd: root });
21865
- }
21866
- function ghInstallHint(platform = process.platform) {
21867
- const how = platform === "win32" ? "Install it with `winget install --id GitHub.cli`" : platform === "darwin" ? "Install it with `brew install gh` (or download it from https://cli.github.com)" : "Install it from https://cli.github.com";
21868
- return `${how}, open a new terminal, then run \`gh auth login\`.`;
21869
- }
21870
- async function ghStatus(root) {
21871
- const v = await run("gh", ["--version"]);
21872
- if (v.code !== 0) return { installed: false, authed: false, hasRemote: false };
21873
- const auth = await run("gh", ["auth", "status"], { cwd: root });
21874
- const remote = await git(root, "remote", "get-url", "origin");
21875
- return { installed: true, authed: auth.code === 0, hasRemote: remote.code === 0 && /github\.com/.test(remote.stdout) };
21876
- }
21877
- var AURA_OWNED = /^(?:\.aura\/|\.claude\/skills\/aura\/|\.agents\/skills\/aura(?:-(?:scan|next|fix|setup))?\/|\.cursor\/commands\/aura-[a-z]+\.md$|\.pi\/prompts\/aura-[a-z]+\.md$|\.claude\/settings\.json$)/;
21878
- async function dirtyFiles(root) {
21879
- const r = await git(root, "status", "--porcelain", "-uall");
21880
- return r.stdout.split("\n").filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, "").split(" -> ").pop()).filter((f) => !AURA_OWNED.test(f));
21881
- }
21882
- async function currentBranch(root) {
21883
- return (await git(root, "rev-parse", "--abbrev-ref", "HEAD")).stdout.trim();
21884
- }
21885
- async function freeBranchName(root, base) {
21886
- for (let i = 1; i < 50; i++) {
21887
- const name = i === 1 ? base : `${base}-${i}`;
21888
- const local = await git(root, "rev-parse", "--verify", "--quiet", `refs/heads/${name}`);
21889
- const remote = await git(root, "ls-remote", "--exit-code", "--heads", "origin", name);
21890
- if (local.code !== 0 && remote.code !== 0) return name;
21891
- }
21892
- return `${base}-${Date.now()}`;
21893
- }
21894
- function writeChanges(root, changes) {
21895
- for (const c of changes) {
21896
- const full = path10.join(root, c.path);
21897
- if (!isInside(root, full)) throw new Error(`Refusing to write outside the repo: ${c.path}`);
21898
- if (c.content === null) rmSync(full, { force: true });
21899
- else {
21900
- mkdirSync2(path10.dirname(full), { recursive: true });
21901
- writeFileSync2(full, c.content);
21902
- }
21903
- }
21904
- }
21905
- async function commitPushPr(root, opts) {
21906
- const commit = await git(root, "commit", "-m", opts.title, "-m", "Opened by Aura (https://github.com/JohnFazio1/aura).");
21907
- const base = { branch: opts.branch, url: null };
21908
- if (commit.code !== 0) {
21909
- const out = commit.stderr || commit.stdout;
21910
- const error62 = /tell me who you are|user\.email/i.test(out) ? 'git doesn\u2019t know your name and email yet. Run these two commands:\n git config --global user.name "Your Name"\n git config --global user.email "you@example.com"' : out;
21911
- return { ...base, committed: false, pushed: false, error: error62 };
21912
- }
21913
- if (!opts.openPr) return { ...base, committed: true, pushed: false };
21914
- const push = await git(root, "push", "-u", "origin", opts.branch);
21915
- if (push.code !== 0) return { ...base, committed: true, pushed: false, error: push.stderr };
21916
- const pr = await run("gh", ["pr", "create", "--base", opts.base, "--head", opts.branch, "--title", opts.title, "--body-file", "-"], {
21917
- cwd: root,
21918
- input: opts.body
21919
- });
21920
- if (pr.code !== 0) return { ...base, committed: true, pushed: true, error: pr.stderr };
21921
- return { ...base, url: pr.stdout.trim().split("\n").pop() ?? null, committed: true, pushed: true };
21922
- }
21923
-
21924
22402
  // src/checks/foundations.ts
21925
22403
  import YAML from "yaml";
21926
22404
  var ciWorkflow = {
@@ -22120,10 +22598,10 @@ var typecheck = {
22120
22598
  applies: (ctx) => ctx.stack.typescript,
22121
22599
  async run(ctx) {
22122
22600
  if (ctx.options.fast) return { status: "unknown", summary: "Skipped in fast mode." };
22123
- const { existsSync: existsSync18 } = await import("fs");
22124
- const path25 = await import("path");
22125
- const tsc = path25.join(ctx.root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
22126
- if (!existsSync18(tsc)) return { status: "unknown", summary: "Dependencies not installed, so the type check was skipped." };
22601
+ const { existsSync: existsSync21 } = await import("fs");
22602
+ const path27 = await import("path");
22603
+ const tsc = path27.join(ctx.root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
22604
+ if (!existsSync21(tsc)) return { status: "unknown", summary: "Dependencies not installed, so the type check was skipped." };
22127
22605
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: ctx.root, timeoutMs: 18e4 });
22128
22606
  if (r.timedOut) return { status: "unknown", summary: "Type check timed out." };
22129
22607
  if (r.code === 0) return { status: "pass", summary: "No type errors.", engine: "tsc" };
@@ -22143,6 +22621,56 @@ var typecheck = {
22143
22621
  };
22144
22622
  var foundationChecks = [ciWorkflow, tests, readme, envExample, lockfile, lintFormat, typecheck];
22145
22623
 
22624
+ // src/connectors/snapshot.ts
22625
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
22626
+ var FILE2 = "cache/connectors.json";
22627
+ function loadSnapshots(root) {
22628
+ try {
22629
+ return existsSync11(auraPath(root, FILE2)) ? JSON.parse(readFileSync11(auraPath(root, FILE2), "utf8")) : {};
22630
+ } catch {
22631
+ return {};
22632
+ }
22633
+ }
22634
+ function saveSnapshot(root, toolId, states) {
22635
+ const all = loadSnapshots(root);
22636
+ all[toolId] = { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), settings: Object.fromEntries(Object.entries(states).map(([id, s]) => [id, s.state])) };
22637
+ writeAuraFile(root, FILE2, `${JSON.stringify(all, null, 2)}
22638
+ `);
22639
+ }
22640
+ var isStale = (snap, now = Date.now()) => !snap || now - Date.parse(snap.checkedAt) > 24 * 60 * 60 * 1e3;
22641
+
22642
+ // src/checks/security/tool-settings.ts
22643
+ function toolSettingsCheck(connector) {
22644
+ return {
22645
+ id: `security.${connector.id}-settings`,
22646
+ category: "security",
22647
+ title: `${connector.name} security settings`,
22648
+ weight: 3,
22649
+ applies: (ctx) => {
22650
+ const snap = loadSnapshots(ctx.root)[connector.id];
22651
+ return Boolean(snap && Object.values(snap.settings).some((s) => s !== "unavailable"));
22652
+ },
22653
+ run(ctx) {
22654
+ const snap = loadSnapshots(ctx.root)[connector.id];
22655
+ const available = connector.settings.filter((s) => snap.settings[s.id] === "on" || snap.settings[s.id] === "off");
22656
+ const off = available.filter((s) => snap.settings[s.id] === "off");
22657
+ const engine = `${connector.name}, checked ${snap.checkedAt.slice(0, 10)}`;
22658
+ if (off.length === 0) return { status: "pass", summary: `All ${available.length} available ${connector.name} security settings are on.`, engine };
22659
+ const finding = makeFinding(this, {
22660
+ severity: off.some((s) => /protect|push|secret/.test(s.id)) ? "medium" : "low",
22661
+ title: `${off.length} ${connector.name} security setting(s) off`,
22662
+ detail: `Off in ${connector.name}: ${off.map((s) => s.label).join("; ")}.`,
22663
+ why: "These settings catch leaked keys, vulnerable packages and risky changes before they cause harm, and they cost nothing to turn on.",
22664
+ locations: [],
22665
+ actionId: connector.actionId,
22666
+ key: `${connector.id}-settings`
22667
+ });
22668
+ const score = (available.length - off.length) / available.length;
22669
+ return { status: score === 0 ? "fail" : "partial", score, summary: finding.title, findings: [finding], engine };
22670
+ }
22671
+ };
22672
+ }
22673
+
22146
22674
  // src/checks/security/api-routes.ts
22147
22675
  var AUTH_SIGNALS = [
22148
22676
  /\bauth\.getUser\s*\(/,
@@ -22249,9 +22777,9 @@ var apiRoutes = {
22249
22777
 
22250
22778
  // src/core/tools.ts
22251
22779
  import { createHash as createHash3 } from "crypto";
22252
- import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync3, mkdtempSync, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
22253
- import os2 from "os";
22254
- import path11 from "path";
22780
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync3, mkdtempSync, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
22781
+ import os3 from "os";
22782
+ import path13 from "path";
22255
22783
  var GITLEAKS_SHA = {
22256
22784
  darwin_arm64: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
22257
22785
  darwin_x64: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
@@ -22269,7 +22797,7 @@ var OSV_SHA = {
22269
22797
  windows_arm64: "ca1379da0e408279ef2c85cde0846a903495738a91c7b065a208048437c1686c"
22270
22798
  };
22271
22799
  function tarCommand(platform = process.platform, systemRoot = process.env.SystemRoot) {
22272
- return platform === "win32" ? path11.win32.join(systemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
22800
+ return platform === "win32" ? path13.win32.join(systemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
22273
22801
  }
22274
22802
  var TOOLS = {
22275
22803
  gitleaks: {
@@ -22305,8 +22833,8 @@ function ensureTool(name, noDownload = false) {
22305
22833
  async function resolveTool(name, noDownload) {
22306
22834
  const spec = TOOLS[name];
22307
22835
  const exe = process.platform === "win32" ? `${name}.exe` : name;
22308
- const cached2 = path11.join(auraBinDir(), `${name}-${spec.version}`, exe);
22309
- if (existsSync9(cached2)) return { path: cached2, version: spec.version };
22836
+ const cached2 = path13.join(auraBinDir(), `${name}-${spec.version}`, exe);
22837
+ if (existsSync12(cached2)) return { path: cached2, version: spec.version };
22310
22838
  if (await commandExists(name)) {
22311
22839
  const v = await run(name, ["version"]);
22312
22840
  return { path: name, version: (v.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? "system").trim() };
@@ -22326,20 +22854,20 @@ async function download(name, spec, target) {
22326
22854
  const buf = Buffer.from(await res.arrayBuffer());
22327
22855
  const digest = createHash3("sha256").update(buf).digest("hex");
22328
22856
  if (digest !== asset.sha256) throw new Error(`Checksum mismatch for ${asset.file}`);
22329
- const dir = path11.dirname(target);
22857
+ const dir = path13.dirname(target);
22330
22858
  mkdirSync3(dir, { recursive: true });
22331
- const tmp = mkdtempSync(path11.join(os2.tmpdir(), `aura-${name}-`));
22859
+ const tmp = mkdtempSync(path13.join(os3.tmpdir(), `aura-${name}-`));
22332
22860
  try {
22333
22861
  if (asset.archive === null) {
22334
22862
  writeFileSync3(target, buf);
22335
22863
  } else {
22336
- const archivePath = path11.join(tmp, asset.file);
22864
+ const archivePath = path13.join(tmp, asset.file);
22337
22865
  writeFileSync3(archivePath, buf);
22338
22866
  const r = await run(tarCommand(), ["-xf", archivePath, "-C", tmp]);
22339
22867
  if (r.code !== 0) return null;
22340
- const exe = readdirSync3(tmp).find((f) => f === name || f === `${name}.exe`);
22868
+ const exe = readdirSync4(tmp).find((f) => f === name || f === `${name}.exe`);
22341
22869
  if (!exe) return null;
22342
- renameSync(path11.join(tmp, exe), target);
22870
+ renameSync(path13.join(tmp, exe), target);
22343
22871
  }
22344
22872
  chmodSync2(target, 493);
22345
22873
  return { path: target, version: spec.version };
@@ -22610,9 +23138,9 @@ var rls = {
22610
23138
  };
22611
23139
 
22612
23140
  // src/checks/security/secrets.ts
22613
- import { existsSync as existsSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
22614
- import os3 from "os";
22615
- import path12 from "path";
23141
+ import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync3 } from "fs";
23142
+ import os4 from "os";
23143
+ import path14 from "path";
22616
23144
 
22617
23145
  // src/core/ignore.ts
22618
23146
  function globToRegex(glob) {
@@ -22650,15 +23178,15 @@ async function gitleaksHistory(ctx) {
22650
23178
  if (!ctx.tracked) return null;
22651
23179
  const bin = await ensureTool("gitleaks", ctx.options.noDownload);
22652
23180
  if (!bin) return null;
22653
- const dir = mkdtempSync2(path12.join(os3.tmpdir(), "aura-gitleaks-"));
22654
- const report = path12.join(dir, "report.json");
23181
+ const dir = mkdtempSync2(path14.join(os4.tmpdir(), "aura-gitleaks-"));
23182
+ const report = path14.join(dir, "report.json");
22655
23183
  try {
22656
23184
  const r = await run(bin.path, ["git", "--no-banner", "--redact", "--report-format", "json", "--report-path", report, "--exit-code", "0", "."], {
22657
23185
  cwd: ctx.root,
22658
23186
  timeoutMs: 18e4
22659
23187
  });
22660
- if (r.code !== 0 || !existsSync10(report)) return null;
22661
- return { hits: JSON.parse(readFileSync9(report, "utf8")), engine: `gitleaks ${bin.version}` };
23188
+ if (r.code !== 0 || !existsSync13(report)) return null;
23189
+ return { hits: JSON.parse(readFileSync12(report, "utf8")), engine: `gitleaks ${bin.version}` };
22662
23190
  } catch {
22663
23191
  return null;
22664
23192
  } finally {
@@ -22770,10 +23298,10 @@ var envFiles = {
22770
23298
  };
22771
23299
 
22772
23300
  // src/checks/security.ts
22773
- var securityChecks = [secrets, envFiles, deps, rls, apiRoutes, headers];
23301
+ var securityChecks = [secrets, envFiles, deps, rls, apiRoutes, headers, ...CONNECTORS.map(toolSettingsCheck)];
22774
23302
 
22775
23303
  // src/checks/structure.ts
22776
- import path13 from "path";
23304
+ import path15 from "path";
22777
23305
  var GENERATED = /(?:^|\/)(?:supabase\/migrations|generated|__generated__|\.generated)\/|\.(?:d\.ts|min\.js)$|(?:^|\/)(?:database|supabase)\.types\.ts$|(?:^|\/)components\/ui\//;
22778
23306
  function appSource(ctx) {
22779
23307
  return ctx.files.filter((f) => isSourceFile(f) && !GENERATED.test(f));
@@ -22862,7 +23390,7 @@ var duplicates = {
22862
23390
  const findings = pairs.map(
22863
23391
  ([a, b, sim]) => makeFinding(this, {
22864
23392
  severity: "low",
22865
- title: `${path13.basename(a)} and ${path13.basename(b)} are near-duplicates`,
23393
+ title: `${path15.basename(a)} and ${path15.basename(b)} are near-duplicates`,
22866
23394
  detail: `${a} and ${b} are ${Math.round(sim * 100)}% the same.`,
22867
23395
  why: "Duplicates drift apart over time: a bug gets fixed in one copy and not the other, and your agent can\u2019t tell which one is right.",
22868
23396
  locations: [{ file: a }, { file: b }],
@@ -22876,7 +23404,7 @@ var duplicates = {
22876
23404
  };
22877
23405
  var ROOT_ALLOWED = /^(?:[^/]+\.config\.[cm]?[jt]s|middleware\.[cm]?[jt]s|proxy\.[cm]?[jt]s|instrumentation(?:-client)?\.[cm]?[jt]s|next-env\.d\.ts|global\.d\.ts|env\.d\.ts|sentry\.[\w.]+\.[cm]?[jt]s)$/;
22878
23406
  function suggestTarget(file2, srcDir) {
22879
- const base = path13.posix.basename(file2);
23407
+ const base = path15.posix.basename(file2);
22880
23408
  const prefix = srcDir ? "src/" : "";
22881
23409
  const isComponent = /^[A-Z][A-Za-z0-9]*\.(?:tsx|jsx)$/.test(base);
22882
23410
  if (isComponent) return `${prefix}components/${base}`;
@@ -22989,21 +23517,21 @@ async function runScan(root, options, onProgress) {
22989
23517
  }
22990
23518
 
22991
23519
  // src/commands/fix.ts
22992
- import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
22993
- import path17 from "path";
23520
+ import { existsSync as existsSync18, readFileSync as readFileSync19, writeFileSync as writeFileSync6 } from "fs";
23521
+ import path19 from "path";
22994
23522
 
22995
23523
  // src/agent/tools.ts
22996
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
22997
- import path14 from "path";
23524
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync13, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
23525
+ import path16 from "path";
22998
23526
  var MAX_OUTPUT = 12e3;
22999
23527
  var MAX_FILE_LINES = 800;
23000
23528
  var FORBIDDEN = /^(?:\.git|node_modules|\.next|\.vercel)(?:\/|$)|(?:^|\/)\.env(?:\.(?!example$|sample$|template$)[^/]+)?$/;
23001
23529
  function safePath(root, rel) {
23002
23530
  if (typeof rel !== "string" || !rel.trim()) throw new Error("A file path is required.");
23003
- const normalized = path14.posix.normalize(rel.replace(/\\/g, "/").replace(/^\.\//, ""));
23004
- if (normalized.startsWith("../") || normalized === ".." || path14.isAbsolute(normalized)) throw new Error(`Path is outside the repo: ${rel}`);
23531
+ const normalized = path16.posix.normalize(rel.replace(/\\/g, "/").replace(/^\.\//, ""));
23532
+ if (normalized.startsWith("../") || normalized === ".." || path16.isAbsolute(normalized)) throw new Error(`Path is outside the repo: ${rel}`);
23005
23533
  if (FORBIDDEN.test(normalized)) throw new Error(`Aura doesn\u2019t let the agent touch ${normalized}.`);
23006
- const full = path14.join(root, normalized);
23534
+ const full = path16.join(root, normalized);
23007
23535
  if (!isInside(root, full)) throw new Error(`Path is outside the repo: ${rel}`);
23008
23536
  return normalized;
23009
23537
  }
@@ -23024,10 +23552,10 @@ async function runTool(root, call, allowedCommands2) {
23024
23552
  }
23025
23553
  case "read_file": {
23026
23554
  const rel = safePath(root, input2.path);
23027
- const full = path14.join(root, rel);
23028
- if (!existsSync11(full)) return { output: `File not found: ${rel}` };
23555
+ const full = path16.join(root, rel);
23556
+ if (!existsSync14(full)) return { output: `File not found: ${rel}` };
23029
23557
  if (statSync2(full).size > 1e6) return { output: `${rel} is too large to read.` };
23030
- const lines = readFileSync10(full, "utf8").split("\n");
23558
+ const lines = readFileSync13(full, "utf8").split("\n");
23031
23559
  const start = Math.max(1, Number(input2.startLine) || 1);
23032
23560
  const end = Math.min(lines.length, Number(input2.endLine) || start + MAX_FILE_LINES - 1);
23033
23561
  const body = lines.slice(start - 1, end).map((l, i) => `${start + i}: ${l}`).join("\n");
@@ -23044,7 +23572,7 @@ async function runTool(root, call, allowedCommands2) {
23044
23572
  if (FORBIDDEN.test(f) || glob && !glob.test(f)) continue;
23045
23573
  let text;
23046
23574
  try {
23047
- text = readFileSync10(path14.join(root, f), "utf8");
23575
+ text = readFileSync13(path16.join(root, f), "utf8");
23048
23576
  } catch {
23049
23577
  continue;
23050
23578
  }
@@ -23063,14 +23591,14 @@ async function runTool(root, call, allowedCommands2) {
23063
23591
  output: "Refused: the content contains a [REDACTED:\u2026] placeholder. Secrets are hidden from you on purpose. Read the value from an environment variable (process.env.NAME) instead of writing it."
23064
23592
  };
23065
23593
  }
23066
- mkdirSync4(path14.dirname(path14.join(root, rel)), { recursive: true });
23067
- writeFileSync4(path14.join(root, rel), content);
23594
+ mkdirSync4(path16.dirname(path16.join(root, rel)), { recursive: true });
23595
+ writeFileSync4(path16.join(root, rel), content);
23068
23596
  return { output: `Wrote ${rel} (${content.split("\n").length} lines).`, touched: rel };
23069
23597
  }
23070
23598
  case "delete_file": {
23071
23599
  const rel = safePath(root, input2.path);
23072
- if (!existsSync11(path14.join(root, rel))) return { output: `File not found: ${rel}` };
23073
- rmSync4(path14.join(root, rel));
23600
+ if (!existsSync14(path16.join(root, rel))) return { output: `File not found: ${rel}` };
23601
+ rmSync4(path16.join(root, rel));
23074
23602
  return { output: `Deleted ${rel}.`, touched: rel };
23075
23603
  }
23076
23604
  case "run_command": {
@@ -23092,7 +23620,7 @@ ${(r.stdout + "\n" + r.stderr).trim().slice(-6e3)}`;
23092
23620
 
23093
23621
  // src/api/explain.ts
23094
23622
  import { createHash as createHash4 } from "crypto";
23095
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
23623
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
23096
23624
 
23097
23625
  // src/report.ts
23098
23626
  import pc2 from "picocolors";
@@ -23281,7 +23809,7 @@ var CACHE_FILE = "cache/explanations.json";
23281
23809
  var MAX_EXPLAIN = 25;
23282
23810
  function loadCache(root) {
23283
23811
  try {
23284
- return JSON.parse(readFileSync11(auraPath(root, CACHE_FILE), "utf8"));
23812
+ return JSON.parse(readFileSync14(auraPath(root, CACHE_FILE), "utf8"));
23285
23813
  } catch {
23286
23814
  return {};
23287
23815
  }
@@ -23302,8 +23830,8 @@ function excerptFor(root, f, context = 4) {
23302
23830
  }
23303
23831
  function productContext(root) {
23304
23832
  const file2 = auraPath(root, "brief.md");
23305
- if (!existsSync12(file2)) return void 0;
23306
- return redact(readFileSync11(file2, "utf8")).slice(0, 1800);
23833
+ if (!existsSync15(file2)) return void 0;
23834
+ return redact(readFileSync14(file2, "utf8")).slice(0, 1800);
23307
23835
  }
23308
23836
  async function addExplanations(root, scan, client) {
23309
23837
  const cache = loadCache(root);
@@ -23347,19 +23875,22 @@ async function addExplanations(root, scan, client) {
23347
23875
 
23348
23876
  // src/commands/simple.ts
23349
23877
  import { execFileSync } from "child_process";
23350
- import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync15 } from "fs";
23351
- import path16 from "path";
23878
+ import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync18 } from "fs";
23879
+ import path18 from "path";
23352
23880
  import YAML2 from "yaml";
23353
23881
 
23354
23882
  // src/commands/inventory.ts
23355
- import { readFileSync as readFileSync12 } from "fs";
23883
+ import { readFileSync as readFileSync15 } from "fs";
23356
23884
  function accountState(root, services) {
23357
23885
  const inv = loadInventory(root);
23358
- return { inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg), githubSecurity: Boolean(inv.githubSecurity) };
23886
+ return {
23887
+ inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg),
23888
+ secured: { ...inv.secured, github: Boolean(inv.githubSecurity || inv.secured?.github) }
23889
+ };
23359
23890
  }
23360
23891
  function loadInventory(root) {
23361
23892
  try {
23362
- return JSON.parse(readFileSync12(auraPath(root, "inventory.json"), "utf8"));
23893
+ return JSON.parse(readFileSync15(auraPath(root, "inventory.json"), "utf8"));
23363
23894
  } catch {
23364
23895
  return { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), services: {} };
23365
23896
  }
@@ -23446,10 +23977,10 @@ async function inventoryCommand(flags) {
23446
23977
  }
23447
23978
 
23448
23979
  // src/history.ts
23449
- import { readFileSync as readFileSync13 } from "fs";
23980
+ import { readFileSync as readFileSync16 } from "fs";
23450
23981
  function loadHistory(root) {
23451
23982
  try {
23452
- const data = JSON.parse(readFileSync13(auraPath(root, "history.json"), "utf8"));
23983
+ const data = JSON.parse(readFileSync16(auraPath(root, "history.json"), "utf8"));
23453
23984
  return Array.isArray(data) ? data : [];
23454
23985
  } catch {
23455
23986
  return [];
@@ -23497,10 +24028,10 @@ function banner(colors = pc3.isColorSupported) {
23497
24028
  // src/api/connect.ts
23498
24029
  import { spawn as spawn2 } from "child_process";
23499
24030
  import { constants, generateKeyPairSync, privateDecrypt } from "crypto";
23500
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
23501
- import os4 from "os";
23502
- import path15 from "path";
23503
- var pendingFile = () => path15.join(os4.homedir(), ".aura", "pending-connect.json");
24031
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync17, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
24032
+ import os5 from "os";
24033
+ import path17 from "path";
24034
+ var pendingFile = () => path17.join(os5.homedir(), ".aura", "pending-connect.json");
23504
24035
  async function postJson(apiUrl, route, body) {
23505
24036
  let res;
23506
24037
  try {
@@ -23519,14 +24050,14 @@ async function postJson(apiUrl, route, body) {
23519
24050
  }
23520
24051
  function loadPending(apiUrl, now) {
23521
24052
  try {
23522
- const p2 = JSON.parse(readFileSync14(pendingFile(), "utf8"));
24053
+ const p2 = JSON.parse(readFileSync17(pendingFile(), "utf8"));
23523
24054
  if (p2.apiUrl === apiUrl && Date.parse(p2.expiresAt) - now > 6e4) return p2;
23524
24055
  } catch {
23525
24056
  }
23526
24057
  return null;
23527
24058
  }
23528
24059
  function clearPendingConnect() {
23529
- if (existsSync13(pendingFile())) rmSync5(pendingFile(), { force: true });
24060
+ if (existsSync16(pendingFile())) rmSync5(pendingFile(), { force: true });
23530
24061
  }
23531
24062
  async function startOrResumeConnect(apiUrl, now = Date.now()) {
23532
24063
  const existing = loadPending(apiUrl, now);
@@ -23538,7 +24069,7 @@ async function startOrResumeConnect(apiUrl, now = Date.now()) {
23538
24069
  });
23539
24070
  const res = ConnectStartResponse.parse(await postJson(apiUrl, "connect", { publicKey }));
23540
24071
  const pending = { apiUrl, privateKey, ...res };
23541
- mkdirSync5(path15.dirname(pendingFile()), { recursive: true });
24072
+ mkdirSync5(path17.dirname(pendingFile()), { recursive: true });
23542
24073
  writeFileSync5(pendingFile(), JSON.stringify(pending, null, 2) + "\n", { mode: 384 });
23543
24074
  return { ...pending, resumed: false };
23544
24075
  }
@@ -23631,7 +24162,7 @@ function stepRun(step, hasKey) {
23631
24162
  }
23632
24163
  function hint(step, hasKey) {
23633
24164
  const run2 = stepRun(step, hasKey);
23634
- const what = run2.kind === "apply" ? "opens a pull request" : run2.kind === "fix" ? run2.withOwnAgent ? "writes a task for your coding agent" : "Aura\u2019s agent fixes it in a pull request" : run2.kind === "guide" ? "shows step-by-step instructions" : "a few questions";
24165
+ const what = connectorForAction(step.actionId) ? `turns on ${connectorForAction(step.actionId).name}\u2019s security settings, after you agree` : run2.kind === "apply" ? "opens a pull request" : run2.kind === "fix" ? run2.withOwnAgent ? "writes a task for your coding agent" : "Aura\u2019s agent fixes it in a pull request" : run2.kind === "guide" ? "shows step-by-step instructions" : "a few questions";
23635
24166
  return [what, step.impact > 0 ? `+${step.impact} points` : "", effortLabel(step.effort)].filter(Boolean).join(" \xB7 ");
23636
24167
  }
23637
24168
  async function pickAndRunSteps(steps, limit = 10) {
@@ -23657,7 +24188,10 @@ async function pickAndRunSteps(steps, limit = 10) {
23657
24188
  console.log(`
23658
24189
  ${pc.magenta("\u2726")} ${pc.bold(step.title)}`);
23659
24190
  const run2 = stepRun(step, hasKey);
23660
- const code = run2.kind === "apply" ? await applyCommand(run2.actionId, { yes: true, via: "checklist" }) : run2.kind === "fix" ? await fixCommand(run2.actionId, run2.withOwnAgent ? { agent: true, via: "checklist" } : { yes: true, via: "checklist" }) : run2.kind === "guide" ? await guideCommand(run2.topic, {}) : await inventoryCommand({});
24191
+ const code = run2.kind === "apply" ? (
24192
+ // Settings changes always ask first, even when picked here; pull requests don't need to.
24193
+ await applyCommand(run2.actionId, { yes: !connectorForAction(run2.actionId), via: "checklist" })
24194
+ ) : run2.kind === "fix" ? await fixCommand(run2.actionId, run2.withOwnAgent ? { agent: true, via: "checklist" } : { yes: true, via: "checklist" }) : run2.kind === "guide" ? await guideCommand(run2.topic, {}) : await inventoryCommand({});
23661
24195
  if (run2.kind === "guide" || run2.kind === "inventory") {
23662
24196
  const root = await findRepoRoot();
23663
24197
  await recordUsage(root, loadConfig(root), {
@@ -23681,9 +24215,9 @@ ${pc.bold(" Done")}`);
23681
24215
  // src/commands/simple.ts
23682
24216
  async function latestScan(root) {
23683
24217
  const file2 = auraPath(root, "scan.json");
23684
- if (existsSync14(file2)) {
24218
+ if (existsSync17(file2)) {
23685
24219
  try {
23686
- const scan = JSON.parse(readFileSync15(file2, "utf8"));
24220
+ const scan = JSON.parse(readFileSync18(file2, "utf8"));
23687
24221
  const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23688
24222
  const dirty = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23689
24223
  if (scan.commit === head && !dirty && Date.now() - Date.parse(scan.scannedAt) < 24 * 36e5) return scan;
@@ -23798,9 +24332,9 @@ async function configCommand(key, value) {
23798
24332
  return 0;
23799
24333
  }
23800
24334
  function loadGuides() {
23801
- const dir = path16.join(templatesDir(), "guides");
23802
- return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => {
23803
- const text = readFileSync15(path16.join(dir, f), "utf8");
24335
+ const dir = path18.join(templatesDir(), "guides");
24336
+ return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((f) => {
24337
+ const text = readFileSync18(path18.join(dir, f), "utf8");
23804
24338
  const fm = text.match(/^---\n([\s\S]*?)\n---\n?/);
23805
24339
  const meta3 = fm ? YAML2.parse(fm[1]) : {};
23806
24340
  return { id: f.replace(/\.md$/, ""), title: meta3.title ?? f, summary: meta3.summary ?? "", body: fm ? text.slice(fm[0].length) : text };
@@ -23868,7 +24402,7 @@ function allowedCommands(root, pm, typescript) {
23868
24402
  const pkg = readJson(root, "package.json");
23869
24403
  const run2 = pmCommands(pm).run;
23870
24404
  const cmds = ["test", "lint", "typecheck", "build"].filter((s) => pkg?.scripts?.[s]).map((s) => `${run2} ${s}`);
23871
- if (typescript && !pkg?.scripts?.typecheck && existsSync15(path17.join(root, "node_modules/.bin/tsc"))) cmds.push(`${pmCommands(pm).exec} tsc --noEmit`);
24405
+ if (typescript && !pkg?.scripts?.typecheck && existsSync18(path19.join(root, "node_modules/.bin/tsc"))) cmds.push(`${pmCommands(pm).exec} tsc --noEmit`);
23872
24406
  return cmds;
23873
24407
  }
23874
24408
  function taskFor(root, scan, action2, findings) {
@@ -23919,8 +24453,8 @@ async function untrackEnvFiles(root, findings) {
23919
24453
  const files = findings.filter((f) => f.checkId === "security.env-files").map((f) => f.locations[0].file);
23920
24454
  for (const f of files) await git(root, "rm", "-q", "--cached", "--", f);
23921
24455
  if (files.length) {
23922
- const gi = path17.join(root, ".gitignore");
23923
- const current2 = existsSync15(gi) ? readFileSync16(gi, "utf8") : "";
24456
+ const gi = path19.join(root, ".gitignore");
24457
+ const current2 = existsSync18(gi) ? readFileSync19(gi, "utf8") : "";
23924
24458
  if (!/^\.env\*?$/m.test(current2)) writeFileSync6(gi, `${current2.trimEnd()}
23925
24459
 
23926
24460
  # Environment files hold secrets
@@ -24097,6 +24631,121 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
24097
24631
  }
24098
24632
  }
24099
24633
 
24634
+ // src/connectors/engine.ts
24635
+ async function readSettings(connector, ctx) {
24636
+ const states = await Promise.all(connector.settings.map((s) => s.read(ctx).catch(() => ({ state: "off" }))));
24637
+ return Object.fromEntries(connector.settings.map((s, i) => [s.id, states[i]]));
24638
+ }
24639
+ async function enableSettings(connector, ctx, states) {
24640
+ const results = [];
24641
+ for (const setting of connector.settings) {
24642
+ const s = states[setting.id];
24643
+ if (s.state === "on") results.push({ id: setting.id, result: "already-on" });
24644
+ else if (s.state === "unavailable") results.push({ id: setting.id, result: "unavailable", why: s.why });
24645
+ else {
24646
+ const out = await setting.enable(ctx).catch(() => ({ ok: false }));
24647
+ results.push(
24648
+ out.ok ? { id: setting.id, result: "turned-on" } : out.unavailable ? { id: setting.id, result: "unavailable", why: out.unavailable } : { id: setting.id, result: "failed", why: `${connector.name} refused the change` }
24649
+ );
24650
+ }
24651
+ }
24652
+ return results;
24653
+ }
24654
+ var securityDone = (results) => results.every((r) => r.result !== "failed");
24655
+ async function secureCommand(connector, flags, report = {}, fake) {
24656
+ const root = await findRepoRoot();
24657
+ report.delivery = "settings";
24658
+ const connection = await connector.connect(root, fake);
24659
+ if (!connection.ok) {
24660
+ say.error(connection.problem);
24661
+ Object.assign(report, { outcome: "failed", errorType: connection.errorType });
24662
+ return 1;
24663
+ }
24664
+ const { ctx, target } = connection;
24665
+ const states = await readSettings(connector, ctx);
24666
+ saveSnapshot(root, connector.id, states);
24667
+ const toTurnOn = connector.settings.filter((s) => states[s.id].state === "off").map((s) => s.id);
24668
+ console.log(`
24669
+ ${pc.magenta("\u2726")} ${pc.bold(`${connector.name} security for ${target}`)}`);
24670
+ for (const setting of connector.settings) {
24671
+ const s = states[setting.id];
24672
+ const line = s.state === "on" ? pc.green("\u2714 already on") : s.state === "off" ? pc.cyan("\u2192 Aura will turn this on") : pc.dim(`\u2013 not available: ${s.why}`);
24673
+ console.log(` ${setting.label}
24674
+ ${line}`);
24675
+ }
24676
+ for (const line of await connector.explain?.(ctx, toTurnOn) ?? []) say.dim(` ${line}`);
24677
+ const label = (id) => connector.settings.find((s) => s.id === id).label;
24678
+ const impact = (id) => connector.settings.find((s) => s.id === id).impact;
24679
+ if (toTurnOn.length && !flags.dryRun) {
24680
+ console.log(`
24681
+ ${pc.bold(" What changes for you")}`);
24682
+ for (const id of toTurnOn) console.log(` \u2022 ${impact(id)}`);
24683
+ }
24684
+ if (flags.dryRun) {
24685
+ say.dim(toTurnOn.length ? "\nDry run: nothing was changed." : "\nEverything Aura can turn on here is already on.");
24686
+ return 0;
24687
+ }
24688
+ let chosen = toTurnOn;
24689
+ if (toTurnOn.length && !flags.yes) {
24690
+ if (!interactive()) {
24691
+ say.info(`
24692
+ Nothing was changed. Show the user the changes above, and once they agree run: aura apply ${connector.actionId} --yes`);
24693
+ report.outcome = "shown";
24694
+ return 0;
24695
+ }
24696
+ chosen = unwrap(
24697
+ await p.multiselect({
24698
+ message: `Which should Aura turn on in ${connector.name}? (untick any you don\u2019t want)`,
24699
+ options: toTurnOn.map((id) => ({ value: id, label: label(id) })),
24700
+ initialValues: toTurnOn,
24701
+ required: false
24702
+ })
24703
+ );
24704
+ if (chosen.length === 0) {
24705
+ say.dim("Nothing was changed.");
24706
+ report.outcome = "cancelled";
24707
+ return 0;
24708
+ }
24709
+ }
24710
+ const skipped = toTurnOn.filter((id) => !chosen.includes(id));
24711
+ let results;
24712
+ if (toTurnOn.length === 0) {
24713
+ say.ok("Everything Aura can turn on here is already on.");
24714
+ results = await enableSettings(connector, ctx, states);
24715
+ report.outcome = "no_changes";
24716
+ } else {
24717
+ const attempt = Object.fromEntries(
24718
+ Object.entries(states).map(([id, s]) => [id, skipped.includes(id) ? { state: "unavailable", why: "you chose to leave it off" } : s])
24719
+ );
24720
+ results = await enableSettings(connector, ctx, attempt);
24721
+ console.log("");
24722
+ for (const r of results.filter((x) => chosen.includes(x.id))) {
24723
+ if (r.result === "turned-on") say.ok(`${label(r.id)}: on`);
24724
+ else if (r.result === "unavailable") say.dim(`\u2013 ${label(r.id)}: ${r.why}`);
24725
+ else say.warn(`${label(r.id)}: ${r.why}.`);
24726
+ }
24727
+ report.outcome = results.some((r) => r.result === "failed") ? "failed" : "settings_changed";
24728
+ }
24729
+ saveSnapshot(
24730
+ root,
24731
+ connector.id,
24732
+ Object.fromEntries(Object.entries(states).map(([id, s]) => [id, results.find((r) => r.id === id)?.result === "turned-on" ? { state: "on" } : s]))
24733
+ );
24734
+ const left = await connector.leftForYou(ctx, results);
24735
+ if (left.length) {
24736
+ console.log(`
24737
+ ${pc.bold(" Left for you")}`);
24738
+ for (const line of left) console.log(` \u2022 ${line}`);
24739
+ }
24740
+ const inv = loadInventory(root);
24741
+ inv.secured = { ...inv.secured, [connector.id]: securityDone(results) };
24742
+ if (connector.id === "github") inv.githubSecurity = inv.secured.github;
24743
+ inv.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
24744
+ writeAuraFile(root, "inventory.json", `${JSON.stringify(inv, null, 2)}
24745
+ `);
24746
+ return results.some((r) => r.result === "failed") ? 1 : 0;
24747
+ }
24748
+
24100
24749
  // src/commands/apply.ts
24101
24750
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
24102
24751
  async function applyCommand(actionId, flags) {
@@ -24116,6 +24765,8 @@ async function applyAction(actionId, flags, report) {
24116
24765
  }
24117
24766
  const scan = await latestScan(root);
24118
24767
  const { delivery, fellBack } = effectiveDelivery(action2, scan);
24768
+ const connector = connectorForAction(actionId);
24769
+ if (connector) return secureCommand(connector, flags, report);
24119
24770
  if (delivery === "guide") return guideCommand(actionId, {});
24120
24771
  if (delivery === "agent-fix") {
24121
24772
  if (fellBack) say.info(pc.dim(`Aura only opens template PRs on stacks it recognizes (Next.js). Handing "${actionId}" to Aura\u2019s agent instead.`));
@@ -24149,7 +24800,7 @@ async function applyAction(actionId, flags, report) {
24149
24800
  console.log(`
24150
24801
  ${pc.bold(action2.title)}`);
24151
24802
  for (const c of plan.changes)
24152
- console.log(` ${c.content === null ? pc.red("delete") : existsSync16(path18.join(root, c.path)) ? pc.yellow("update") : pc.green("add ")} ${c.path}`);
24803
+ console.log(` ${c.content === null ? pc.red("delete") : existsSync19(path20.join(root, c.path)) ? pc.yellow("update") : pc.green("add ")} ${c.path}`);
24153
24804
  const pm = pmCommands(scan.stack.packageManager);
24154
24805
  if (plan.devDeps?.length) console.log(` ${pc.cyan("install")} ${plan.devDeps.join(", ")} ${pc.dim(`(${pm.addDev})`)}`);
24155
24806
  for (const cmd of plan.commands ?? []) console.log(` ${pc.cyan("run ")} ${cmd.join(" ")}`);
@@ -24181,8 +24832,8 @@ ${pc.bold(action2.title)}`);
24181
24832
  report.outcome = "cancelled";
24182
24833
  return 0;
24183
24834
  }
24184
- const tsc = path18.join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
24185
- const checkTypes = plan.verifyTypes && scan.stack.typescript && existsSync16(tsc);
24835
+ const tsc = path20.join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
24836
+ const checkTypes = plan.verifyTypes && scan.stack.typescript && existsSync19(tsc);
24186
24837
  const typeErrors = async () => {
24187
24838
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: root, timeoutMs: 3e5 });
24188
24839
  return (r.stdout.match(/error TS\d+/g) ?? []).length;
@@ -24291,17 +24942,17 @@ async function undoPlan(root, plan) {
24291
24942
  for (const file2 of touched) {
24292
24943
  const inHead = (await git(root, "cat-file", "-e", `HEAD:${file2}`)).code === 0;
24293
24944
  if (inHead) await git(root, "checkout", "HEAD", "--", file2);
24294
- else if (plan.changes.some((c) => c.path === file2)) rmSync6(path18.join(root, file2), { force: true });
24945
+ else if (plan.changes.some((c) => c.path === file2)) rmSync6(path20.join(root, file2), { force: true });
24295
24946
  }
24296
24947
  }
24297
24948
 
24298
24949
  // src/commands/init.ts
24299
- import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
24950
+ import { existsSync as existsSync20, readFileSync as readFileSync21 } from "fs";
24300
24951
  import { spawnSync as spawnSync3 } from "child_process";
24301
- import path22 from "path";
24952
+ import path24 from "path";
24302
24953
 
24303
24954
  // src/brief.ts
24304
- import path19 from "path";
24955
+ import path21 from "path";
24305
24956
  function repoSummary(ctx) {
24306
24957
  const routes = ctx.files.filter((f) => /^(?:src\/)?app\/(?:.+\/)?(?:page|route)\.[cm]?[jt]sx?$/.test(f) || /^(?:src\/)?pages\/.+\.[cm]?[jt]sx?$/.test(f)).map(
24307
24958
  (f) => "/" + f.replace(/^(?:src\/)?(?:app|pages)\//, "").replace(/(?:^|\/)(?:page|route|index)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/, "").replace(/\([^)]+\)\//g, "")
@@ -24317,7 +24968,7 @@ function repoSummary(ctx) {
24317
24968
  const readme2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f));
24318
24969
  const readmeText = readme2 ? ctx.read(readme2) ?? "" : "";
24319
24970
  return {
24320
- name: ctx.pkg?.name ?? path19.basename(ctx.root),
24971
+ name: ctx.pkg?.name ?? path21.basename(ctx.root),
24321
24972
  packageJson: ctx.pkg ? {
24322
24973
  name: ctx.pkg.name,
24323
24974
  description: ctx.pkg.description,
@@ -24410,10 +25061,10 @@ function installFallback(result, platform = process.platform) {
24410
25061
 
24411
25062
  // src/core/claude-plugin.ts
24412
25063
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
24413
- import os5 from "os";
24414
- import path20 from "path";
25064
+ import os6 from "os";
25065
+ import path22 from "path";
24415
25066
  var CLAUDE_PLUGIN_ID = "aura@aura";
24416
- var marketplaceDir = () => path20.join(os5.homedir(), ".aura", "claude-marketplace");
25067
+ var marketplaceDir = () => path22.join(os6.homedir(), ".aura", "claude-marketplace");
24417
25068
  var MARKETPLACE = {
24418
25069
  name: "aura",
24419
25070
  description: "Aura for Claude Code",
@@ -24428,8 +25079,8 @@ var MARKETPLACE = {
24428
25079
  };
24429
25080
  function writeMarketplace() {
24430
25081
  const dir = marketplaceDir();
24431
- mkdirSync6(path20.join(dir, ".claude-plugin"), { recursive: true });
24432
- writeFileSync7(path20.join(dir, ".claude-plugin", "marketplace.json"), `${JSON.stringify(MARKETPLACE, null, 2)}
25082
+ mkdirSync6(path22.join(dir, ".claude-plugin"), { recursive: true });
25083
+ writeFileSync7(path22.join(dir, ".claude-plugin", "marketplace.json"), `${JSON.stringify(MARKETPLACE, null, 2)}
24433
25084
  `);
24434
25085
  return dir;
24435
25086
  }
@@ -24532,11 +25183,30 @@ function describeInferred(inferred) {
24532
25183
 
24533
25184
  // src/commands/scan.ts
24534
25185
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
24535
- import os6 from "os";
24536
- import path21 from "path";
25186
+ import os7 from "os";
25187
+ import path23 from "path";
25188
+
25189
+ // src/connectors/refresh.ts
25190
+ import { readFileSync as readFileSync20 } from "fs";
25191
+ async function refreshToolSettings(root) {
25192
+ if (process.env.AURA_NO_CONNECTORS) return;
25193
+ let previous = null;
25194
+ try {
25195
+ previous = JSON.parse(readFileSync20(auraPath(root, "scan.json"), "utf8"));
25196
+ } catch {
25197
+ }
25198
+ const snapshots = loadSnapshots(root);
25199
+ for (const connector of CONNECTORS) {
25200
+ if (!(previous ? connector.detect(previous) : connector.id === "github") || !isStale(snapshots[connector.id])) continue;
25201
+ const connection = await connector.connect(root).catch(() => null);
25202
+ if (connection?.ok) saveSnapshot(root, connector.id, await readSettings(connector, connection.ctx));
25203
+ }
25204
+ }
25205
+
25206
+ // src/commands/scan.ts
24537
25207
  async function scanRef(root, ref, opts) {
24538
- const dir = mkdtempSync3(path21.join(os6.tmpdir(), "aura-base-"));
24539
- const wt = path21.join(dir, "tree");
25208
+ const dir = mkdtempSync3(path23.join(os7.tmpdir(), "aura-base-"));
25209
+ const wt = path23.join(dir, "tree");
24540
25210
  const add = await run("git", ["worktree", "add", "--detach", wt, ref], { cwd: root });
24541
25211
  if (add.code !== 0) {
24542
25212
  rmSync7(dir, { recursive: true, force: true });
@@ -24557,6 +25227,7 @@ async function scanCommand(flags) {
24557
25227
  const opts = { fast: Boolean(flags.fast || flags.base), noDownload: flags.download === false };
24558
25228
  const s = spin(!quiet && !flags.ci);
24559
25229
  s.start("Scanning");
25230
+ if (!flags.ci) await refreshToolSettings(root);
24560
25231
  const scan = await runScan(root, opts, (m) => s.message(`Checking: ${m}`));
24561
25232
  s.stop(`Scanned ${scan.checks.filter((c) => c.status !== "not-applicable").length} checks`);
24562
25233
  const client = AuraClient.from(config2);
@@ -24625,7 +25296,7 @@ async function scanCommand(flags) {
24625
25296
  // src/commands/init.ts
24626
25297
  var AGENT_IDS = ["claude", "codex", "cursor", "pi"];
24627
25298
  function detectAgents(root) {
24628
- return detectAgentsFrom((rel) => existsSync17(path22.join(root, rel)));
25299
+ return detectAgentsFrom((rel) => existsSync20(path24.join(root, rel)));
24629
25300
  }
24630
25301
  function openEditor(file2) {
24631
25302
  const fallback = process.platform === "win32" ? "notepad" : spawnSync3("sh", ["-c", "command -v nano"]).status === 0 ? "nano" : "vi";
@@ -24740,13 +25411,13 @@ async function initCommand(flags) {
24740
25411
  let brief2 = null;
24741
25412
  let keptBrief = false;
24742
25413
  if (flags.briefFile) {
24743
- brief2 = readFileSync17(flags.briefFile, "utf8");
24744
- } else if (!flags.answers && existsSync17(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
25414
+ brief2 = readFileSync21(flags.briefFile, "utf8");
25415
+ } else if (!flags.answers && existsSync20(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
24745
25416
  keptBrief = true;
24746
25417
  } else {
24747
25418
  let answers = null;
24748
25419
  if (flags.answers) {
24749
- answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync17(flags.answers, "utf8")));
25420
+ answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync21(flags.answers, "utf8")));
24750
25421
  } else if (tty) {
24751
25422
  const inferred = inferAnswers(ctx);
24752
25423
  p.note(describeInferred(inferred).join("\n"), "Here\u2019s what Aura found about your app");
@@ -24878,7 +25549,7 @@ ${pc.magenta("\u2726")} ${pc.bold("Give the user the aura command")}`);
24878
25549
  }
24879
25550
 
24880
25551
  // src/commands/summary.ts
24881
- import path23 from "path";
25552
+ import path25 from "path";
24882
25553
  async function activity(root, since) {
24883
25554
  const log = await git(root, "log", `--since=${since.toISOString()}`, "--no-merges", "--format=%H%x09%an%x09%s");
24884
25555
  const commits = log.stdout.split("\n").filter(Boolean).slice(0, 300).map((l) => {
@@ -24935,7 +25606,7 @@ async function summaryCommand(flags) {
24935
25606
  const beforeIds = new Set(before ? allFindings(before).map((f) => f.id) : []);
24936
25607
  const nowIds = new Set(allFindings(now).map((f) => f.id));
24937
25608
  const req = {
24938
- repo: path23.basename(root),
25609
+ repo: path25.basename(root),
24939
25610
  since: since.toISOString(),
24940
25611
  until: until.toISOString(),
24941
25612
  product: productContext(root),
@@ -24983,11 +25654,11 @@ ${body}`);
24983
25654
  import pc4 from "picocolors";
24984
25655
 
24985
25656
  // src/core/update-check.ts
24986
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
24987
- import os7 from "os";
24988
- import path24 from "path";
25657
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "fs";
25658
+ import os8 from "os";
25659
+ import path26 from "path";
24989
25660
  var DAY_MS = 24 * 60 * 60 * 1e3;
24990
- var cacheFile = () => path24.join(os7.homedir(), ".aura", "update-check.json");
25661
+ var cacheFile = () => path26.join(os8.homedir(), ".aura", "update-check.json");
24991
25662
  function isNewer(a, b) {
24992
25663
  if (!/^\d+\.\d+\.\d+$/.test(a) || !/^\d+\.\d+\.\d+$/.test(b)) return false;
24993
25664
  const [x, y] = [a, b].map((v) => v.split(".").map(Number));
@@ -25007,13 +25678,13 @@ async function newerVersion(opts = {}) {
25007
25678
  const now = opts.now ?? Date.now();
25008
25679
  let latest = null;
25009
25680
  try {
25010
- const cached2 = JSON.parse(readFileSync18(cacheFile(), "utf8"));
25681
+ const cached2 = JSON.parse(readFileSync22(cacheFile(), "utf8"));
25011
25682
  if (now - cached2.checkedAt < DAY_MS) latest = cached2.latest;
25012
25683
  else throw new Error("stale");
25013
25684
  } catch {
25014
25685
  latest = await (opts.fetchLatest ?? fetchLatest)();
25015
25686
  try {
25016
- mkdirSync7(path24.dirname(cacheFile()), { recursive: true });
25687
+ mkdirSync7(path26.dirname(cacheFile()), { recursive: true });
25017
25688
  writeFileSync9(cacheFile(), JSON.stringify({ checkedAt: now, latest }));
25018
25689
  } catch {
25019
25690
  }