getaura 0.1.7 → 0.1.9

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,558 @@ 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 read2 = await call("GET", `/v1/projects/${ref}`);
20540
+ if (read2.status === 401)
20541
+ return { ok: false, problem: "Your Supabase login has expired. Run `npx supabase login`, then try again.", errorType: "CliNotSignedIn" };
20542
+ if (read2.status !== 200 || !read2.json?.name)
20543
+ return {
20544
+ ok: false,
20545
+ // Projects created through Vercel's Supabase integration live in a Supabase account linked to Vercel.
20546
+ problem: [
20547
+ `The Supabase account you\u2019re signed in to can\u2019t open this project (${ref}).`,
20548
+ "If you created the database through Vercel: in Vercel open Storage, click the database, then Open in Supabase. In that same browser, run `npx supabase login` and approve it. Then try again.",
20549
+ "Otherwise run `npx supabase login` with the Supabase account that owns the project."
20550
+ ].join("\n "),
20551
+ errorType: "NoAccess"
20552
+ };
20553
+ const project = read2.json;
20554
+ return { ok: true, ctx, target: project.name };
20555
+ },
20556
+ settings: [
20557
+ {
20558
+ id: "email-confirmation",
20559
+ label: "Email confirmation (new users confirm their email before signing in)",
20560
+ impact: "New users must click a link in an email before they can sign in. People already signed up aren\u2019t affected.",
20561
+ read: authFlag((c) => c.mailer_autoconfirm === false),
20562
+ enable: (ctx) => patchAuth(ctx, { mailer_autoconfirm: false })
20563
+ },
20564
+ {
20565
+ id: "password-length",
20566
+ label: "Passwords of at least 8 characters",
20567
+ impact: "New and changed passwords must be at least 8 characters. Existing passwords keep working.",
20568
+ read: authFlag((c) => (c.password_min_length ?? 0) >= 8),
20569
+ enable: (ctx) => patchAuth(ctx, { password_min_length: 8 })
20570
+ },
20571
+ {
20572
+ id: "leaked-passwords",
20573
+ label: "Leaked password protection (refuses passwords found in data breaches)",
20574
+ impact: "Passwords that appear in known data breaches are refused when someone signs up or changes their password.",
20575
+ read: authFlag((c) => c.password_hibp_enabled === true),
20576
+ enable: (ctx) => patchAuth(ctx, { password_hibp_enabled: true })
20577
+ },
20578
+ {
20579
+ id: "app-mfa",
20580
+ label: "Authenticator app sign-in for your users (two-factor, optional for them)",
20581
+ 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.",
20582
+ read: authFlag((c) => c.mfa_totp_enroll_enabled === true && c.mfa_totp_verify_enabled === true),
20583
+ enable: (ctx) => patchAuth(ctx, { mfa_totp_enroll_enabled: true, mfa_totp_verify_enabled: true })
20584
+ },
20585
+ {
20586
+ id: "secure-email-change",
20587
+ label: "Secure email change (both the old and new address confirm)",
20588
+ 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.",
20589
+ read: authFlag((c) => c.mailer_secure_email_change_enabled === true),
20590
+ enable: (ctx) => patchAuth(ctx, { mailer_secure_email_change_enabled: true })
20591
+ },
20592
+ {
20593
+ id: "database-encryption",
20594
+ label: "Encrypted database connections only",
20595
+ impact: "Direct connections to your database must be encrypted. Turning this on restarts the database for a few seconds.",
20596
+ read: async (ctx) => (await ctx.get(`/v1/projects/${ctx.ref}/ssl-enforcement`))?.currentConfig?.database === true ? { state: "on" } : { state: "off" },
20597
+ async enable(ctx) {
20598
+ const r = await ctx.api("PUT", `/v1/projects/${ctx.ref}/ssl-enforcement`, { requestedConfig: { database: true } });
20599
+ return r.status >= 200 && r.status < 300 ? { ok: true } : { ok: false };
20600
+ }
20601
+ },
20602
+ {
20603
+ id: "security-advisor",
20604
+ label: "No problems in Supabase\u2019s security advisor",
20605
+ impact: "Nothing changes on its own: these are fixed in your database code, and Aura lists them below.",
20606
+ read: async (ctx) => (await advisorProblems(ctx)).length ? { state: "off" } : { state: "on" },
20607
+ enable: async () => ({ ok: false, unavailable: "fixed in your database code (see Left for you)" })
20608
+ }
20609
+ ],
20610
+ async leftForYou(ctx, results) {
20611
+ const left = [];
20612
+ const problems = await advisorProblems(ctx);
20613
+ if (problems.length) {
20614
+ const titles = [...new Set(problems.map((p2) => p2.title ?? p2.name ?? "Security problem"))].slice(0, 5);
20615
+ left.push(
20616
+ `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`
20617
+ );
20618
+ }
20619
+ if (results.some((r) => r.id === "leaked-passwords" && r.result === "unavailable"))
20620
+ left.push("Leaked password protection needs Supabase Pro. Until then, the 8-character minimum helps.");
20621
+ left.push("Turn on two-factor login for your Supabase account: Supabase dashboard \u2192 Account \u2192 Security.");
20622
+ return left;
20623
+ }
20624
+ };
20625
+
20626
+ // src/connectors/vercel.ts
20627
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
20628
+ import path4 from "path";
20629
+ var PUBLIC_PREFIX = /^(?:NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_|PUBLIC_)/;
20630
+ var SECRET_NAME = /SECRET|SERVICE_ROLE|PRIVATE|PASSWORD|_TOKEN$|API_KEY$/;
20631
+ var parse4 = (r) => {
20632
+ try {
20633
+ return r.code === 0 ? JSON.parse(r.stdout) : null;
20634
+ } catch {
20635
+ return null;
20636
+ }
20637
+ };
20638
+ async function exposedSecrets(ctx) {
20639
+ const envs = (await ctx.get(`/v10/projects/${ctx.projectId}/env?decrypt=true${ctx.team.replace("?", "&")}`))?.envs ?? [];
20640
+ 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))];
20641
+ }
20642
+ async function patchProject(ctx, body) {
20643
+ const r = await ctx.vercel(["api", `/v9/projects/${ctx.projectId}${ctx.team}`, "-X", "PATCH", "--input", "-", "--raw"], JSON.stringify(body));
20644
+ if (r.code === 0) return { ok: true };
20645
+ return { ok: false, unavailable: /plan|upgrade|pro\b|enterprise/i.test(r.stderr + r.stdout) ? "not available on your Vercel plan" : void 0 };
20646
+ }
20647
+ var vercel = {
20648
+ id: "vercel",
20649
+ name: "Vercel",
20650
+ actionId: "vercel-security",
20651
+ detect: (scan) => scan.stack.services.includes("Vercel"),
20652
+ async connect(root, fake) {
20653
+ const cli = fake ?? ((args, input2) => run("vercel", args, { cwd: root, input: input2, timeoutMs: 6e4 }));
20654
+ if (!fake) {
20655
+ if (!await commandExists("vercel"))
20656
+ return {
20657
+ ok: false,
20658
+ problem: "The Vercel CLI isn\u2019t installed. Install it with `npm install -g vercel`, then run `vercel login`.",
20659
+ errorType: "CliNotInstalled"
20660
+ };
20661
+ if ((await cli(["whoami"])).code !== 0)
20662
+ return { ok: false, problem: "The Vercel CLI isn\u2019t signed in. Run `vercel login`, then try again.", errorType: "CliNotSignedIn" };
20663
+ }
20664
+ const linked = [path4.join(root, ".vercel", "project.json"), ...["apps", "packages"].flatMap((d) => findLinks(path4.join(root, d)))].find(
20665
+ (f) => existsSync3(f)
20666
+ );
20667
+ let project = null;
20668
+ try {
20669
+ project = linked ? JSON.parse(readFileSync3(linked, "utf8")) : null;
20670
+ } catch {
20671
+ project = null;
20672
+ }
20673
+ if (!project?.projectId)
20674
+ return { ok: false, problem: "This folder isn\u2019t linked to a Vercel project yet. Run `vercel link`, then try again.", errorType: "NoProject" };
20675
+ const cache = /* @__PURE__ */ new Map();
20676
+ const ctx = {
20677
+ projectId: project.projectId,
20678
+ team: project.orgId?.startsWith("team_") ? `?teamId=${project.orgId}` : "",
20679
+ vercel: cli,
20680
+ get: (endpoint) => {
20681
+ if (!cache.has(endpoint)) cache.set(endpoint, cli(["api", endpoint, "--raw"]).then(parse4));
20682
+ return cache.get(endpoint);
20683
+ }
20684
+ };
20685
+ const name = (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.name;
20686
+ if (!name)
20687
+ return { ok: false, problem: "Couldn\u2019t read this project from Vercel. Check you have access to it with `vercel project ls`.", errorType: "NoProject" };
20688
+ return { ok: true, ctx, target: name };
20689
+ },
20690
+ settings: [
20691
+ {
20692
+ id: "preview-protection",
20693
+ label: "Protect preview links (only your Vercel team can open them)",
20694
+ impact: "Preview and deployment links need a Vercel login to open. Your live site on your own domain stays public.",
20695
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.ssoProtection ? { state: "on" } : { state: "off" },
20696
+ enable: (ctx) => patchProject(ctx, { ssoProtection: { deploymentType: "all_except_custom_domains" } })
20697
+ },
20698
+ {
20699
+ id: "git-fork-protection",
20700
+ label: "Fork protection (pull requests from forks don\u2019t deploy automatically)",
20701
+ 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.",
20702
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.gitForkProtection ? { state: "on" } : { state: "off" },
20703
+ enable: (ctx) => patchProject(ctx, { gitForkProtection: true })
20704
+ },
20705
+ {
20706
+ id: "protected-sourcemaps",
20707
+ label: "Protected source maps (your original code isn\u2019t served to the public)",
20708
+ impact: "Source maps, which reveal your original code, are only served to people signed in to your Vercel team.",
20709
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.protectedSourcemaps ? { state: "on" } : { state: "off" },
20710
+ enable: (ctx) => patchProject(ctx, { protectedSourcemaps: true })
20711
+ },
20712
+ {
20713
+ id: "no-public-secrets",
20714
+ label: "No secret keys in browser-visible settings (NEXT_PUBLIC_\u2026 and similar)",
20715
+ impact: "Nothing changes on its own: Aura can\u2019t move a key for you, and lists what to do below.",
20716
+ read: async (ctx) => (await exposedSecrets(ctx)).length ? { state: "off" } : { state: "on" },
20717
+ enable: async () => ({ ok: false, unavailable: "needs you to move the key (see Left for you)" })
20718
+ }
20719
+ ],
20720
+ async leftForYou(ctx) {
20721
+ const left = [];
20722
+ const exposed = await exposedSecrets(ctx);
20723
+ if (exposed.length)
20724
+ left.push(
20725
+ `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\`).`
20726
+ );
20727
+ left.push("Turn on two-factor login for your Vercel account: Vercel \u2192 Account Settings \u2192 Authentication.");
20728
+ return left;
20729
+ }
20730
+ };
20731
+ function findLinks(dir) {
20732
+ try {
20733
+ return readdirSync2(dir).map((d) => path4.join(dir, d, ".vercel", "project.json"));
20734
+ } catch {
20735
+ return [];
20736
+ }
20737
+ }
20738
+
20739
+ // src/connectors/index.ts
20740
+ var CONNECTORS = [github, supabase, vercel];
20741
+ function connectorForAction(actionId) {
20742
+ return CONNECTORS.find((c) => c.actionId === actionId);
20743
+ }
20744
+
20745
+ // src/templates.ts
20746
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
20747
+ import path7 from "path";
20748
+
20207
20749
  // src/core/config.ts
20750
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
20751
+ import os2 from "os";
20752
+ import path5 from "path";
20208
20753
  var DEFAULT_CONFIG = {
20209
20754
  version: 1,
20210
20755
  agents: [],
@@ -20213,45 +20758,45 @@ var DEFAULT_CONFIG = {
20213
20758
  ciMaxDrop: 0
20214
20759
  };
20215
20760
  function auraPath(root, ...parts) {
20216
- return path2.join(root, AURA_DIR, ...parts);
20761
+ return path5.join(root, AURA_DIR, ...parts);
20217
20762
  }
20218
20763
  function ensureAuraDir(root) {
20219
- mkdirSync(auraPath(root), { recursive: true });
20764
+ mkdirSync2(auraPath(root), { recursive: true });
20220
20765
  }
20221
20766
  function loadConfig(root) {
20222
20767
  const file2 = auraPath(root, "config.json");
20223
- if (!existsSync2(file2)) return { ...DEFAULT_CONFIG };
20768
+ if (!existsSync4(file2)) return { ...DEFAULT_CONFIG };
20224
20769
  try {
20225
- return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync2(file2, "utf8")) };
20770
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync4(file2, "utf8")) };
20226
20771
  } catch {
20227
20772
  return { ...DEFAULT_CONFIG };
20228
20773
  }
20229
20774
  }
20230
20775
  function saveConfig(root, config2) {
20231
20776
  ensureAuraDir(root);
20232
- writeFileSync(auraPath(root, "config.json"), JSON.stringify(config2, null, 2) + "\n");
20777
+ writeFileSync2(auraPath(root, "config.json"), JSON.stringify(config2, null, 2) + "\n");
20233
20778
  }
20234
20779
  function writeAuraFile(root, name, content) {
20235
20780
  ensureAuraDir(root);
20236
20781
  const file2 = auraPath(root, name);
20237
- mkdirSync(path2.dirname(file2), { recursive: true });
20238
- writeFileSync(file2, content);
20782
+ mkdirSync2(path5.dirname(file2), { recursive: true });
20783
+ writeFileSync2(file2, content);
20239
20784
  return file2;
20240
20785
  }
20241
- var homeAuraDir = () => path2.join(os.homedir(), ".aura");
20242
- var credentialsFile = () => path2.join(homeAuraDir(), "credentials.json");
20786
+ var homeAuraDir = () => path5.join(os2.homedir(), ".aura");
20787
+ var credentialsFile = () => path5.join(homeAuraDir(), "credentials.json");
20243
20788
  function getApiKey() {
20244
20789
  if (process.env.AURA_KEY) return process.env.AURA_KEY.trim();
20245
20790
  try {
20246
- const data = JSON.parse(readFileSync2(credentialsFile(), "utf8"));
20791
+ const data = JSON.parse(readFileSync4(credentialsFile(), "utf8"));
20247
20792
  return typeof data.key === "string" ? data.key : null;
20248
20793
  } catch {
20249
20794
  return null;
20250
20795
  }
20251
20796
  }
20252
20797
  function saveApiKey(key) {
20253
- mkdirSync(homeAuraDir(), { recursive: true });
20254
- writeFileSync(credentialsFile(), JSON.stringify({ key }, null, 2) + "\n", { mode: 384 });
20798
+ mkdirSync2(homeAuraDir(), { recursive: true });
20799
+ writeFileSync2(credentialsFile(), JSON.stringify({ key }, null, 2) + "\n", { mode: 384 });
20255
20800
  try {
20256
20801
  chmodSync(credentialsFile(), 384);
20257
20802
  } catch {
@@ -20261,7 +20806,7 @@ function getApiUrl(config2) {
20261
20806
  return (process.env.AURA_API_URL || config2?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
20262
20807
  }
20263
20808
  function auraBinDir() {
20264
- return path2.join(homeAuraDir(), "bin");
20809
+ return path5.join(homeAuraDir(), "bin");
20265
20810
  }
20266
20811
  var AURA_GITIGNORE = `# Aura local files: they can describe security weaknesses, so they stay on your machine.
20267
20812
  report.md
@@ -20273,15 +20818,15 @@ tasks/
20273
20818
  `;
20274
20819
  function ensureGitignore(root) {
20275
20820
  const file2 = auraPath(root, ".gitignore");
20276
- if (existsSync2(file2) && readFileSync2(file2, "utf8") === AURA_GITIGNORE) return false;
20821
+ if (existsSync4(file2) && readFileSync4(file2, "utf8") === AURA_GITIGNORE) return false;
20277
20822
  ensureAuraDir(root);
20278
- writeFileSync(file2, AURA_GITIGNORE);
20823
+ writeFileSync2(file2, AURA_GITIGNORE);
20279
20824
  return true;
20280
20825
  }
20281
20826
 
20282
20827
  // src/core/stack.ts
20283
- import { existsSync as existsSync3 } from "fs";
20284
- import path3 from "path";
20828
+ import { existsSync as existsSync5 } from "fs";
20829
+ import path6 from "path";
20285
20830
  function allDeps(pkg) {
20286
20831
  return { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
20287
20832
  }
@@ -20405,7 +20950,7 @@ function findEnvRefs(files, read2) {
20405
20950
  }
20406
20951
  function detectStack(root, files, pkg, read2) {
20407
20952
  const deps2 = allDeps(pkg);
20408
- const has = (f) => files.includes(f) || existsSync3(path3.join(root, f));
20953
+ const has = (f) => files.includes(f) || existsSync5(path6.join(root, f));
20409
20954
  const hasDir = (d) => files.some((f) => f.startsWith(`${d}/`));
20410
20955
  let packageManager = null;
20411
20956
  const pmField = pkg?.packageManager?.split("@")[0];
@@ -20430,6 +20975,7 @@ function detectStack(root, files, pkg, read2) {
20430
20975
  }
20431
20976
  if (has("vercel.json") || has(".vercel/project.json")) services.add("Vercel");
20432
20977
  if (hasDir("supabase")) services.add("Supabase");
20978
+ if (has("vercel.json") || has(".vercel/project.json")) services.add("Vercel");
20433
20979
  services.add("GitHub");
20434
20980
  let nextjs = null;
20435
20981
  if (deps2.next) {
@@ -20439,9 +20985,9 @@ function detectStack(root, files, pkg, read2) {
20439
20985
  const pages = hasDir(`${base}pages`);
20440
20986
  nextjs = { version: deps2.next.replace(/^[^\d]*/, ""), router: app && pages ? "both" : app ? "app" : pages ? "pages" : null, srcDir };
20441
20987
  }
20442
- let supabase = null;
20988
+ let supabase2 = null;
20443
20989
  if (services.has("Supabase")) {
20444
- supabase = { migrationsDir: hasDir("supabase/migrations") ? "supabase/migrations" : null, hasConfig: has("supabase/config.toml") };
20990
+ supabase2 = { migrationsDir: hasDir("supabase/migrations") ? "supabase/migrations" : null, hasConfig: has("supabase/config.toml") };
20445
20991
  }
20446
20992
  let testRunner = null;
20447
20993
  if (deps2.vitest || files.some((f) => /^vitest\.config\./.test(f))) testRunner = "vitest";
@@ -20456,7 +21002,7 @@ function detectStack(root, files, pkg, read2) {
20456
21002
  frameworks,
20457
21003
  services: [...services].sort(),
20458
21004
  nextjs,
20459
- supabase,
21005
+ supabase: supabase2,
20460
21006
  typescript: has("tsconfig.json"),
20461
21007
  testRunner,
20462
21008
  envVars,
@@ -20500,7 +21046,7 @@ function pmCommands(pm) {
20500
21046
  // package.json
20501
21047
  var package_default = {
20502
21048
  name: "getaura",
20503
- version: "0.1.7",
21049
+ version: "0.1.9",
20504
21050
  description: "Aura helps you and your coding agent follow best practices while you vibe code.",
20505
21051
  type: "module",
20506
21052
  bin: {
@@ -20581,19 +21127,19 @@ function fill(text, vars) {
20581
21127
  return text.replace(/\{\{([a-z_]+)\}\}/g, (m, name) => KNOWN.has(name) ? vars[name] ?? "" : m);
20582
21128
  }
20583
21129
  function readTemplate(rel) {
20584
- return readFileSync3(path4.join(templatesDir(), rel), "utf8");
21130
+ return readFileSync5(path7.join(templatesDir(), rel), "utf8");
20585
21131
  }
20586
21132
  function render(rel, vars) {
20587
21133
  return fill(readTemplate(rel), vars);
20588
21134
  }
20589
21135
  function templateExists(rel) {
20590
- return existsSync4(path4.join(templatesDir(), rel));
21136
+ return existsSync6(path7.join(templatesDir(), rel));
20591
21137
  }
20592
21138
  function briefSummary(root, pkg) {
20593
- const fallbackName = pkg?.name ? pkg.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : path4.basename(root);
21139
+ const fallbackName = pkg?.name ? pkg.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : path7.basename(root);
20594
21140
  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");
21141
+ if (!existsSync6(file2)) return { name: fallbackName, summary: pkg?.description ?? "See `.aura/brief.md` once the product brief is added." };
21142
+ const text = readFileSync5(file2, "utf8");
20597
21143
  const title = text.match(/^#\s+(.+)$/m)?.[1]?.replace(/[:\-–—]?\s*product brief$/i, "").trim();
20598
21144
  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
21145
  const sentences = paragraph.replace(/\s+/g, " ").match(/[^.!?]+[.!?]+/g) ?? [paragraph];
@@ -20758,7 +21304,6 @@ var ACTIONS = [
20758
21304
  },
20759
21305
  // Accounts and ownership
20760
21306
  { 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
21307
  { id: "transfer-ownership", title: "Move accounts to a company owner", category: "security", delivery: "guide", resolves: [] }
20763
21308
  ];
20764
21309
  var FOUNDATION_BUNDLE = ["add-brief", "add-agent-rules", "install-skills", "token-efficiency", "add-ci", "add-env-example", "add-aura-workflow"];
@@ -20773,6 +21318,15 @@ function getAction(id) {
20773
21318
  };
20774
21319
  }
20775
21320
  if (/^rotate-[a-z]+-(?:key|token)$/.test(id)) return { id, title: "Rotate an exposed key", category: "security", delivery: "guide", resolves: [] };
21321
+ const connector = connectorForAction(id);
21322
+ if (connector)
21323
+ return {
21324
+ id,
21325
+ title: `Turn on ${connector.name} security features`,
21326
+ category: "security",
21327
+ delivery: "guide",
21328
+ resolves: [`security.${connector.id}-settings`]
21329
+ };
20776
21330
  return ACTIONS.find((a) => a.id === id);
20777
21331
  }
20778
21332
  var CATEGORY_PRIORITY = ["security", "agent-setup", "foundations", "structure"];
@@ -20808,15 +21362,16 @@ function nextSteps(scan, opts = {}) {
20808
21362
  if (!action2) continue;
20809
21363
  const { delivery, fellBack } = effectiveDelivery(action2, scan);
20810
21364
  const worst = fs.map((f) => f.severity).sort((a, b) => SEVERITY_ORDER.indexOf(a) - SEVERITY_ORDER.indexOf(b))[0];
21365
+ const connected = Boolean(connectorForAction(actionId));
20811
21366
  steps.push({
20812
21367
  actionId,
20813
21368
  title: action2.title,
20814
21369
  delivery,
20815
- effort: EFFORT[delivery],
21370
+ effort: connected ? "one-click" : EFFORT[delivery],
20816
21371
  category: action2.category,
20817
21372
  why: fs[0].why,
20818
21373
  impact: impactOf(scan.checks, action2.resolves),
20819
- command: commandFor(actionId, delivery),
21374
+ command: connected ? `aura apply ${actionId}` : commandFor(actionId, delivery),
20820
21375
  findingIds: fs.map((f) => f.id),
20821
21376
  fellBack,
20822
21377
  severity: worst
@@ -20841,16 +21396,16 @@ function nextSteps(scan, opts = {}) {
20841
21396
  severity: "critical"
20842
21397
  });
20843
21398
  }
20844
- if (opts.accounts && !opts.accounts.githubSecurity) {
21399
+ 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
21400
  steps.push({
20846
- actionId: "github-security",
20847
- title: "Enable GitHub security features",
21401
+ actionId: connector.actionId,
21402
+ title: `Turn on ${connector.name} security features`,
20848
21403
  delivery: "guide",
20849
- effort: "follow-guide",
21404
+ effort: "one-click",
20850
21405
  category: "security",
20851
- why: "Branch protection and secret scanning stop mistakes (and leaked keys) before they reach your main branch.",
21406
+ why: `Aura checks ${connector.name}\u2019s security settings and, once you agree, turns on the ones that are off.`,
20852
21407
  impact: 0,
20853
- command: "aura guide github-security",
21408
+ command: `aura apply ${connector.actionId}`,
20854
21409
  findingIds: [],
20855
21410
  severity: "low"
20856
21411
  });
@@ -20899,12 +21454,12 @@ function nextSteps(scan, opts = {}) {
20899
21454
  }
20900
21455
 
20901
21456
  // src/actions/generators.ts
20902
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
20903
- import path9 from "path";
21457
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
21458
+ import path12 from "path";
20904
21459
 
20905
21460
  // src/checks/agent-setup.ts
20906
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
20907
- import path5 from "path";
21461
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
21462
+ import path8 from "path";
20908
21463
 
20909
21464
  // src/checks/types.ts
20910
21465
  import { createHash } from "crypto";
@@ -20938,18 +21493,18 @@ function lineOf(text, index) {
20938
21493
 
20939
21494
  // src/checks/agent-setup.ts
20940
21495
  function exists(ctx, file2) {
20941
- return existsSync5(path5.join(ctx.root, file2));
21496
+ return existsSync7(path8.join(ctx.root, file2));
20942
21497
  }
20943
21498
  function readDisk(ctx, file2) {
20944
21499
  try {
20945
- return readFileSync4(path5.join(ctx.root, file2), "utf8");
21500
+ return readFileSync6(path8.join(ctx.root, file2), "utf8");
20946
21501
  } catch {
20947
21502
  return "";
20948
21503
  }
20949
21504
  }
20950
21505
  function skillsIn(ctx, dir) {
20951
21506
  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);
21507
+ 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
21508
  } catch {
20954
21509
  return [];
20955
21510
  }
@@ -20966,7 +21521,7 @@ function rulesFileFor(ctx, agent) {
20966
21521
  if (agent === "cursor") {
20967
21522
  if (exists(ctx, ".cursorrules")) return ".cursorrules";
20968
21523
  try {
20969
- const rules = readdirSync2(path5.join(ctx.root, ".cursor/rules")).filter((f) => /\.mdc?$/.test(f));
21524
+ const rules = readdirSync3(path8.join(ctx.root, ".cursor/rules")).filter((f) => /\.mdc?$/.test(f));
20970
21525
  if (rules.length) return `.cursor/rules/${rules[0]}`;
20971
21526
  } catch {
20972
21527
  }
@@ -21111,7 +21666,7 @@ var brief = {
21111
21666
  var agentChecks = [rulesFile, rulesCurrent, skills, auraSkill, brief];
21112
21667
 
21113
21668
  // src/core/graph.ts
21114
- import path6 from "path";
21669
+ import path9 from "path";
21115
21670
  var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
21116
21671
  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
21672
  function parseImports(text) {
@@ -21139,7 +21694,7 @@ function readAliases(read2) {
21139
21694
  for (const [key, targets] of Object.entries(json2.compilerOptions?.paths ?? {})) {
21140
21695
  const target = targets[0];
21141
21696
  if (!key.endsWith("/*") || !target?.endsWith("/*")) continue;
21142
- const resolved = path6.posix.normalize(path6.posix.join(base || ".", target.slice(0, -1)));
21697
+ const resolved = path9.posix.normalize(path9.posix.join(base || ".", target.slice(0, -1)));
21143
21698
  entries.push([key.slice(0, -1), resolved === "." || resolved === "./" ? "" : resolved.replace(/\/?$/, "/")]);
21144
21699
  }
21145
21700
  } catch {
@@ -21150,10 +21705,10 @@ function readAliases(read2) {
21150
21705
  }
21151
21706
  function resolveImport(from, spec, files, aliases) {
21152
21707
  let base = null;
21153
- if (spec.startsWith(".")) base = path6.posix.normalize(path6.posix.join(path6.posix.dirname(from), spec));
21708
+ if (spec.startsWith(".")) base = path9.posix.normalize(path9.posix.join(path9.posix.dirname(from), spec));
21154
21709
  else {
21155
21710
  const alias = aliases.entries.find(([prefix]) => spec.startsWith(prefix));
21156
- if (alias) base = path6.posix.normalize(alias[1] + spec.slice(alias[0].length));
21711
+ if (alias) base = path9.posix.normalize(alias[1] + spec.slice(alias[0].length));
21157
21712
  }
21158
21713
  if (base === null) return null;
21159
21714
  const withoutJs = base.replace(/\.(?:js|jsx|mjs|cjs)$/, "");
@@ -21199,14 +21754,14 @@ function unreachableFiles(files, read2, pkgScripts) {
21199
21754
  }
21200
21755
 
21201
21756
  // src/actions/agent-commands.ts
21202
- import { readFileSync as readFileSync5 } from "fs";
21203
- import path7 from "path";
21757
+ import { readFileSync as readFileSync7 } from "fs";
21758
+ import path10 from "path";
21204
21759
  var AURA_COMMANDS = ["scan", "next", "fix", "setup"];
21205
21760
  function invocation(agent, command) {
21206
21761
  return agent === "claude" ? `/aura:${command}` : agent === "codex" ? `$aura-${command}` : `/aura-${command}`;
21207
21762
  }
21208
21763
  function pluginCommand(command) {
21209
- const text = readFileSync5(path7.join(path7.dirname(templatesDir()), "plugin", "skills", command, "SKILL.md"), "utf8").replace(/\r\n/g, "\n");
21764
+ const text = readFileSync7(path10.join(path10.dirname(templatesDir()), "plugin", "skills", command, "SKILL.md"), "utf8").replace(/\r\n/g, "\n");
21210
21765
  const [, frontmatter = "", body = ""] = text.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/) ?? [];
21211
21766
  const description = frontmatter.match(/^description: (.+)$/m)?.[1] ?? "";
21212
21767
  return { description, body };
@@ -21251,11 +21806,11 @@ function commandList(agent) {
21251
21806
  }
21252
21807
 
21253
21808
  // src/actions/agent-files.ts
21254
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
21255
- import path8 from "path";
21809
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
21810
+ import path11 from "path";
21256
21811
  function current(root, file2) {
21257
- const full = path8.join(root, file2);
21258
- return existsSync6(full) ? readFileSync6(full, "utf8") : null;
21812
+ const full = path11.join(root, file2);
21813
+ return existsSync8(full) ? readFileSync8(full, "utf8") : null;
21259
21814
  }
21260
21815
  function upsertBlock(existing, body) {
21261
21816
  const block = `${AURA_BLOCK_START}
@@ -21299,8 +21854,8 @@ function skillChanges(agents, names) {
21299
21854
 
21300
21855
  // src/actions/generators.ts
21301
21856
  function read(root, file2) {
21302
- const full = path9.join(root, file2);
21303
- return existsSync7(full) ? readFileSync7(full, "utf8") : null;
21857
+ const full = path12.join(root, file2);
21858
+ return existsSync9(full) ? readFileSync9(full, "utf8") : null;
21304
21859
  }
21305
21860
  function withScripts(root, scripts, overwrite = false) {
21306
21861
  const text = read(root, "package.json");
@@ -21321,13 +21876,13 @@ function findingMeta(scan, checkId, key) {
21321
21876
  return scan.checks.find((c) => c.checkId === checkId)?.findings[0]?.meta?.[key];
21322
21877
  }
21323
21878
  var addBrief = ({ root }) => {
21324
- const brief2 = BRIEF_PATHS.find((f) => existsSync7(path9.join(root, f)));
21879
+ const brief2 = BRIEF_PATHS.find((f) => existsSync9(path12.join(root, f)));
21325
21880
  if (!brief2) return { skip: "There is no product brief yet. Run `aura init` to draft and confirm it first." };
21326
21881
  const changes = [
21327
21882
  { path: brief2, content: read(root, brief2) },
21328
21883
  { path: ".aura/.gitignore", content: AURA_GITIGNORE }
21329
21884
  ];
21330
- if (existsSync7(auraPath(root, "config.json"))) changes.push({ path: ".aura/config.json", content: read(root, ".aura/config.json") });
21885
+ if (existsSync9(auraPath(root, "config.json"))) changes.push({ path: ".aura/config.json", content: read(root, ".aura/config.json") });
21331
21886
  return { changes, details: [`- Adds \`${brief2}\`, \`.aura/config.json\` and \`.aura/.gitignore\` (keeps Aura's local reports out of git)`] };
21332
21887
  };
21333
21888
  var addAgentRules = ({ root, agents, vars }) => {
@@ -21346,14 +21901,14 @@ var installSkills = ({ agents, skills: skills2 }) => {
21346
21901
  };
21347
21902
  var tokenEfficiency = (g) => {
21348
21903
  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) });
21904
+ if (!existsSync9(path12.join(g.root, "docs/index.md"))) changes.push({ path: "docs/index.md", content: render("docs/index.md", g.vars) });
21350
21905
  changes.push(...rulesChanges(g.root, g.agents, g.vars));
21351
21906
  return { changes, details: ["- `docs/index.md`: a map of the repo your agent reads first", "- Lean-context rules in the agent rules file"] };
21352
21907
  };
21353
21908
  var addCi = ({ root, ctx, vars }) => {
21354
21909
  const existing = ctx.files.filter((f) => /^\.github\/workflows\//.test(f));
21355
21910
  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.` };
21911
+ if (existsSync9(path12.join(root, file2))) return { skip: `${file2} already exists.` };
21357
21912
  return {
21358
21913
  changes: [{ path: file2, content: render("github/workflows/ci.yml", vars) }],
21359
21914
  details: [`- \`${file2}\`: lint, type check, tests and build on every pull request`]
@@ -21377,7 +21932,7 @@ var setupTesting = ({ root, ctx, vars }) => {
21377
21932
  };
21378
21933
  };
21379
21934
  var addEnvExample = ({ root, scan }) => {
21380
- const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync7(path9.join(root, f))) ?? ".env.example";
21935
+ const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync9(path12.join(root, f))) ?? ".env.example";
21381
21936
  const existing = read(root, file2);
21382
21937
  const declared = new Set(
21383
21938
  (existing ?? "").split(/\r?\n/).map((l) => l.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1]).filter(Boolean)
@@ -21434,7 +21989,7 @@ var addLinting = ({ root, ctx }) => {
21434
21989
  return { changes, devDeps, details };
21435
21990
  };
21436
21991
  var addPreCommit = ({ root, ctx, vars }) => {
21437
- if (existsSync7(path9.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21992
+ if (existsSync9(path12.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21438
21993
  const changes = [{ path: ".husky/pre-commit", content: render("configs/husky-pre-commit", vars) }];
21439
21994
  const scripts = withScripts(root, { prepare: "husky" }, false);
21440
21995
  if (scripts) changes.push(scripts);
@@ -21464,7 +22019,7 @@ ${current2.trim()}
21464
22019
  };
21465
22020
  };
21466
22021
  var enableDependencyUpdates = ({ root, vars }) => {
21467
- if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync7(path9.join(root, f)))) {
22022
+ if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync9(path12.join(root, f)))) {
21468
22023
  return { skip: "Dependabot or Renovate is already configured." };
21469
22024
  }
21470
22025
  return {
@@ -21544,7 +22099,7 @@ function planMoves(root, ctx, moves) {
21544
22099
  const alias = aliasFor(toFile);
21545
22100
  if (alias) return alias[0] + noExt.slice(alias[1].length);
21546
22101
  }
21547
- let rel = path9.posix.relative(path9.posix.dirname(fromFile), noExt);
22102
+ let rel = path12.posix.relative(path12.posix.dirname(fromFile), noExt);
21548
22103
  if (!rel.startsWith(".")) rel = `./${rel}`;
21549
22104
  return rel;
21550
22105
  };
@@ -21578,7 +22133,7 @@ function planMoves(root, ctx, moves) {
21578
22133
  }
21579
22134
  var moveMisplacedFiles = ({ root, ctx, scan }) => {
21580
22135
  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);
22136
+ const moves = raw.map((m) => m.split("=>")).filter(([, to]) => !existsSync9(path12.join(root, to))).slice(0, MAX_MOVES);
21582
22137
  if (moves.length === 0) return { skip: "No files to move (or their targets already exist)." };
21583
22138
  return { changes: planMoves(root, ctx, moves), details: moves.map(([from, to]) => `- \`${from}\` \u2192 \`${to}\``), verifyTypes: true };
21584
22139
  };
@@ -21669,7 +22224,7 @@ var AuraClient = class _AuraClient {
21669
22224
  get hasKey() {
21670
22225
  return Boolean(this.key);
21671
22226
  }
21672
- async request(method, path25, body, timeoutMs = 6e4) {
22227
+ async request(method, path27, body, timeoutMs = 6e4) {
21673
22228
  if (!this.key) throw new AuraApiError("no_key", "No pilot key");
21674
22229
  let payload;
21675
22230
  if (body !== void 0) {
@@ -21679,7 +22234,7 @@ var AuraClient = class _AuraClient {
21679
22234
  }
21680
22235
  let res;
21681
22236
  try {
21682
- res = await fetch(`${this.baseUrl}/v1/${path25}`, {
22237
+ res = await fetch(`${this.baseUrl}/v1/${path27}`, {
21683
22238
  method,
21684
22239
  headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json", "user-agent": `getaura/${AURA_VERSION}` },
21685
22240
  body: payload,
@@ -21743,13 +22298,13 @@ var say = {
21743
22298
  };
21744
22299
 
21745
22300
  // src/fix-tracking.ts
21746
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
22301
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
21747
22302
  var FILE = "fixes.json";
21748
22303
  var MAX_AGE_MS = 60 * 24 * 60 * 60 * 1e3;
21749
22304
  var MAX_CHECKS_PER_SCAN = 5;
21750
22305
  function loadTrackedFixes(root) {
21751
22306
  try {
21752
- return existsSync8(auraPath(root, FILE)) ? JSON.parse(readFileSync8(auraPath(root, FILE), "utf8")) : [];
22307
+ return existsSync10(auraPath(root, FILE)) ? JSON.parse(readFileSync10(auraPath(root, FILE), "utf8")) : [];
21753
22308
  } catch {
21754
22309
  return [];
21755
22310
  }
@@ -21857,70 +22412,6 @@ async function recordAction(root, config2, actionId, report, startedAt, via) {
21857
22412
  }
21858
22413
  }
21859
22414
 
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
22415
  // src/checks/foundations.ts
21925
22416
  import YAML from "yaml";
21926
22417
  var ciWorkflow = {
@@ -22120,10 +22611,10 @@ var typecheck = {
22120
22611
  applies: (ctx) => ctx.stack.typescript,
22121
22612
  async run(ctx) {
22122
22613
  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." };
22614
+ const { existsSync: existsSync21 } = await import("fs");
22615
+ const path27 = await import("path");
22616
+ const tsc = path27.join(ctx.root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
22617
+ if (!existsSync21(tsc)) return { status: "unknown", summary: "Dependencies not installed, so the type check was skipped." };
22127
22618
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: ctx.root, timeoutMs: 18e4 });
22128
22619
  if (r.timedOut) return { status: "unknown", summary: "Type check timed out." };
22129
22620
  if (r.code === 0) return { status: "pass", summary: "No type errors.", engine: "tsc" };
@@ -22143,6 +22634,73 @@ var typecheck = {
22143
22634
  };
22144
22635
  var foundationChecks = [ciWorkflow, tests, readme, envExample, lockfile, lintFormat, typecheck];
22145
22636
 
22637
+ // src/connectors/snapshot.ts
22638
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
22639
+ var REFUSAL_MS = 30 * 24 * 60 * 60 * 1e3;
22640
+ function withRefusals(states, snap, now = Date.now()) {
22641
+ return Object.fromEntries(
22642
+ Object.entries(states).map(([id, s]) => {
22643
+ const refusal = snap?.refused?.[id];
22644
+ return [id, s.state === "off" && refusal && now - Date.parse(refusal.at) < REFUSAL_MS ? { state: "unavailable", why: refusal.why } : s];
22645
+ })
22646
+ );
22647
+ }
22648
+ var FILE2 = "cache/connectors.json";
22649
+ function loadSnapshots(root) {
22650
+ try {
22651
+ return existsSync11(auraPath(root, FILE2)) ? JSON.parse(readFileSync11(auraPath(root, FILE2), "utf8")) : {};
22652
+ } catch {
22653
+ return {};
22654
+ }
22655
+ }
22656
+ function saveSnapshot(root, toolId, states, refusedNow = {}) {
22657
+ const all = loadSnapshots(root);
22658
+ const now = (/* @__PURE__ */ new Date()).toISOString();
22659
+ const refused = { ...all[toolId]?.refused };
22660
+ for (const [id, why] of Object.entries(refusedNow)) refused[id] = { why, at: now };
22661
+ for (const [id, s] of Object.entries(states)) if (s.state === "on") delete refused[id];
22662
+ all[toolId] = {
22663
+ checkedAt: now,
22664
+ settings: Object.fromEntries(Object.entries(states).map(([id, s]) => [id, s.state])),
22665
+ ...Object.keys(refused).length ? { refused } : {}
22666
+ };
22667
+ writeAuraFile(root, FILE2, `${JSON.stringify(all, null, 2)}
22668
+ `);
22669
+ }
22670
+ var isStale = (snap, now = Date.now()) => !snap || now - Date.parse(snap.checkedAt) > 24 * 60 * 60 * 1e3;
22671
+
22672
+ // src/checks/security/tool-settings.ts
22673
+ function toolSettingsCheck(connector) {
22674
+ return {
22675
+ id: `security.${connector.id}-settings`,
22676
+ category: "security",
22677
+ title: `${connector.name} security settings`,
22678
+ weight: 3,
22679
+ applies: (ctx) => {
22680
+ const snap = loadSnapshots(ctx.root)[connector.id];
22681
+ return Boolean(snap && Object.values(snap.settings).some((s) => s !== "unavailable"));
22682
+ },
22683
+ run(ctx) {
22684
+ const snap = loadSnapshots(ctx.root)[connector.id];
22685
+ const available = connector.settings.filter((s) => snap.settings[s.id] === "on" || snap.settings[s.id] === "off");
22686
+ const off = available.filter((s) => snap.settings[s.id] === "off");
22687
+ const engine = `${connector.name}, checked ${snap.checkedAt.slice(0, 10)}`;
22688
+ if (off.length === 0) return { status: "pass", summary: `All ${available.length} available ${connector.name} security settings are on.`, engine };
22689
+ const finding = makeFinding(this, {
22690
+ severity: off.some((s) => /protect|push|secret/.test(s.id)) ? "medium" : "low",
22691
+ title: `${off.length} ${connector.name} security setting(s) off`,
22692
+ detail: `Off in ${connector.name}: ${off.map((s) => s.label).join("; ")}.`,
22693
+ why: "These settings catch leaked keys, vulnerable packages and risky changes before they cause harm, and they cost nothing to turn on.",
22694
+ locations: [],
22695
+ actionId: connector.actionId,
22696
+ key: `${connector.id}-settings`
22697
+ });
22698
+ const score = (available.length - off.length) / available.length;
22699
+ return { status: score === 0 ? "fail" : "partial", score, summary: finding.title, findings: [finding], engine };
22700
+ }
22701
+ };
22702
+ }
22703
+
22146
22704
  // src/checks/security/api-routes.ts
22147
22705
  var AUTH_SIGNALS = [
22148
22706
  /\bauth\.getUser\s*\(/,
@@ -22249,9 +22807,9 @@ var apiRoutes = {
22249
22807
 
22250
22808
  // src/core/tools.ts
22251
22809
  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";
22810
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync3, mkdtempSync, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
22811
+ import os3 from "os";
22812
+ import path13 from "path";
22255
22813
  var GITLEAKS_SHA = {
22256
22814
  darwin_arm64: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
22257
22815
  darwin_x64: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
@@ -22269,7 +22827,7 @@ var OSV_SHA = {
22269
22827
  windows_arm64: "ca1379da0e408279ef2c85cde0846a903495738a91c7b065a208048437c1686c"
22270
22828
  };
22271
22829
  function tarCommand(platform = process.platform, systemRoot = process.env.SystemRoot) {
22272
- return platform === "win32" ? path11.win32.join(systemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
22830
+ return platform === "win32" ? path13.win32.join(systemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
22273
22831
  }
22274
22832
  var TOOLS = {
22275
22833
  gitleaks: {
@@ -22305,8 +22863,8 @@ function ensureTool(name, noDownload = false) {
22305
22863
  async function resolveTool(name, noDownload) {
22306
22864
  const spec = TOOLS[name];
22307
22865
  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 };
22866
+ const cached2 = path13.join(auraBinDir(), `${name}-${spec.version}`, exe);
22867
+ if (existsSync12(cached2)) return { path: cached2, version: spec.version };
22310
22868
  if (await commandExists(name)) {
22311
22869
  const v = await run(name, ["version"]);
22312
22870
  return { path: name, version: (v.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? "system").trim() };
@@ -22326,20 +22884,20 @@ async function download(name, spec, target) {
22326
22884
  const buf = Buffer.from(await res.arrayBuffer());
22327
22885
  const digest = createHash3("sha256").update(buf).digest("hex");
22328
22886
  if (digest !== asset.sha256) throw new Error(`Checksum mismatch for ${asset.file}`);
22329
- const dir = path11.dirname(target);
22887
+ const dir = path13.dirname(target);
22330
22888
  mkdirSync3(dir, { recursive: true });
22331
- const tmp = mkdtempSync(path11.join(os2.tmpdir(), `aura-${name}-`));
22889
+ const tmp = mkdtempSync(path13.join(os3.tmpdir(), `aura-${name}-`));
22332
22890
  try {
22333
22891
  if (asset.archive === null) {
22334
22892
  writeFileSync3(target, buf);
22335
22893
  } else {
22336
- const archivePath = path11.join(tmp, asset.file);
22894
+ const archivePath = path13.join(tmp, asset.file);
22337
22895
  writeFileSync3(archivePath, buf);
22338
22896
  const r = await run(tarCommand(), ["-xf", archivePath, "-C", tmp]);
22339
22897
  if (r.code !== 0) return null;
22340
- const exe = readdirSync3(tmp).find((f) => f === name || f === `${name}.exe`);
22898
+ const exe = readdirSync4(tmp).find((f) => f === name || f === `${name}.exe`);
22341
22899
  if (!exe) return null;
22342
- renameSync(path11.join(tmp, exe), target);
22900
+ renameSync(path13.join(tmp, exe), target);
22343
22901
  }
22344
22902
  chmodSync2(target, 493);
22345
22903
  return { path: target, version: spec.version };
@@ -22610,9 +23168,9 @@ var rls = {
22610
23168
  };
22611
23169
 
22612
23170
  // 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";
23171
+ import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync3 } from "fs";
23172
+ import os4 from "os";
23173
+ import path14 from "path";
22616
23174
 
22617
23175
  // src/core/ignore.ts
22618
23176
  function globToRegex(glob) {
@@ -22650,15 +23208,15 @@ async function gitleaksHistory(ctx) {
22650
23208
  if (!ctx.tracked) return null;
22651
23209
  const bin = await ensureTool("gitleaks", ctx.options.noDownload);
22652
23210
  if (!bin) return null;
22653
- const dir = mkdtempSync2(path12.join(os3.tmpdir(), "aura-gitleaks-"));
22654
- const report = path12.join(dir, "report.json");
23211
+ const dir = mkdtempSync2(path14.join(os4.tmpdir(), "aura-gitleaks-"));
23212
+ const report = path14.join(dir, "report.json");
22655
23213
  try {
22656
23214
  const r = await run(bin.path, ["git", "--no-banner", "--redact", "--report-format", "json", "--report-path", report, "--exit-code", "0", "."], {
22657
23215
  cwd: ctx.root,
22658
23216
  timeoutMs: 18e4
22659
23217
  });
22660
- if (r.code !== 0 || !existsSync10(report)) return null;
22661
- return { hits: JSON.parse(readFileSync9(report, "utf8")), engine: `gitleaks ${bin.version}` };
23218
+ if (r.code !== 0 || !existsSync13(report)) return null;
23219
+ return { hits: JSON.parse(readFileSync12(report, "utf8")), engine: `gitleaks ${bin.version}` };
22662
23220
  } catch {
22663
23221
  return null;
22664
23222
  } finally {
@@ -22770,10 +23328,10 @@ var envFiles = {
22770
23328
  };
22771
23329
 
22772
23330
  // src/checks/security.ts
22773
- var securityChecks = [secrets, envFiles, deps, rls, apiRoutes, headers];
23331
+ var securityChecks = [secrets, envFiles, deps, rls, apiRoutes, headers, ...CONNECTORS.map(toolSettingsCheck)];
22774
23332
 
22775
23333
  // src/checks/structure.ts
22776
- import path13 from "path";
23334
+ import path15 from "path";
22777
23335
  var GENERATED = /(?:^|\/)(?:supabase\/migrations|generated|__generated__|\.generated)\/|\.(?:d\.ts|min\.js)$|(?:^|\/)(?:database|supabase)\.types\.ts$|(?:^|\/)components\/ui\//;
22778
23336
  function appSource(ctx) {
22779
23337
  return ctx.files.filter((f) => isSourceFile(f) && !GENERATED.test(f));
@@ -22862,7 +23420,7 @@ var duplicates = {
22862
23420
  const findings = pairs.map(
22863
23421
  ([a, b, sim]) => makeFinding(this, {
22864
23422
  severity: "low",
22865
- title: `${path13.basename(a)} and ${path13.basename(b)} are near-duplicates`,
23423
+ title: `${path15.basename(a)} and ${path15.basename(b)} are near-duplicates`,
22866
23424
  detail: `${a} and ${b} are ${Math.round(sim * 100)}% the same.`,
22867
23425
  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
23426
  locations: [{ file: a }, { file: b }],
@@ -22876,7 +23434,7 @@ var duplicates = {
22876
23434
  };
22877
23435
  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
23436
  function suggestTarget(file2, srcDir) {
22879
- const base = path13.posix.basename(file2);
23437
+ const base = path15.posix.basename(file2);
22880
23438
  const prefix = srcDir ? "src/" : "";
22881
23439
  const isComponent = /^[A-Z][A-Za-z0-9]*\.(?:tsx|jsx)$/.test(base);
22882
23440
  if (isComponent) return `${prefix}components/${base}`;
@@ -22989,21 +23547,21 @@ async function runScan(root, options, onProgress) {
22989
23547
  }
22990
23548
 
22991
23549
  // src/commands/fix.ts
22992
- import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
22993
- import path17 from "path";
23550
+ import { existsSync as existsSync18, readFileSync as readFileSync19, writeFileSync as writeFileSync6 } from "fs";
23551
+ import path19 from "path";
22994
23552
 
22995
23553
  // 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";
23554
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync13, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
23555
+ import path16 from "path";
22998
23556
  var MAX_OUTPUT = 12e3;
22999
23557
  var MAX_FILE_LINES = 800;
23000
23558
  var FORBIDDEN = /^(?:\.git|node_modules|\.next|\.vercel)(?:\/|$)|(?:^|\/)\.env(?:\.(?!example$|sample$|template$)[^/]+)?$/;
23001
23559
  function safePath(root, rel) {
23002
23560
  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}`);
23561
+ const normalized = path16.posix.normalize(rel.replace(/\\/g, "/").replace(/^\.\//, ""));
23562
+ if (normalized.startsWith("../") || normalized === ".." || path16.isAbsolute(normalized)) throw new Error(`Path is outside the repo: ${rel}`);
23005
23563
  if (FORBIDDEN.test(normalized)) throw new Error(`Aura doesn\u2019t let the agent touch ${normalized}.`);
23006
- const full = path14.join(root, normalized);
23564
+ const full = path16.join(root, normalized);
23007
23565
  if (!isInside(root, full)) throw new Error(`Path is outside the repo: ${rel}`);
23008
23566
  return normalized;
23009
23567
  }
@@ -23024,10 +23582,10 @@ async function runTool(root, call, allowedCommands2) {
23024
23582
  }
23025
23583
  case "read_file": {
23026
23584
  const rel = safePath(root, input2.path);
23027
- const full = path14.join(root, rel);
23028
- if (!existsSync11(full)) return { output: `File not found: ${rel}` };
23585
+ const full = path16.join(root, rel);
23586
+ if (!existsSync14(full)) return { output: `File not found: ${rel}` };
23029
23587
  if (statSync2(full).size > 1e6) return { output: `${rel} is too large to read.` };
23030
- const lines = readFileSync10(full, "utf8").split("\n");
23588
+ const lines = readFileSync13(full, "utf8").split("\n");
23031
23589
  const start = Math.max(1, Number(input2.startLine) || 1);
23032
23590
  const end = Math.min(lines.length, Number(input2.endLine) || start + MAX_FILE_LINES - 1);
23033
23591
  const body = lines.slice(start - 1, end).map((l, i) => `${start + i}: ${l}`).join("\n");
@@ -23044,7 +23602,7 @@ async function runTool(root, call, allowedCommands2) {
23044
23602
  if (FORBIDDEN.test(f) || glob && !glob.test(f)) continue;
23045
23603
  let text;
23046
23604
  try {
23047
- text = readFileSync10(path14.join(root, f), "utf8");
23605
+ text = readFileSync13(path16.join(root, f), "utf8");
23048
23606
  } catch {
23049
23607
  continue;
23050
23608
  }
@@ -23063,14 +23621,14 @@ async function runTool(root, call, allowedCommands2) {
23063
23621
  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
23622
  };
23065
23623
  }
23066
- mkdirSync4(path14.dirname(path14.join(root, rel)), { recursive: true });
23067
- writeFileSync4(path14.join(root, rel), content);
23624
+ mkdirSync4(path16.dirname(path16.join(root, rel)), { recursive: true });
23625
+ writeFileSync4(path16.join(root, rel), content);
23068
23626
  return { output: `Wrote ${rel} (${content.split("\n").length} lines).`, touched: rel };
23069
23627
  }
23070
23628
  case "delete_file": {
23071
23629
  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));
23630
+ if (!existsSync14(path16.join(root, rel))) return { output: `File not found: ${rel}` };
23631
+ rmSync4(path16.join(root, rel));
23074
23632
  return { output: `Deleted ${rel}.`, touched: rel };
23075
23633
  }
23076
23634
  case "run_command": {
@@ -23092,7 +23650,7 @@ ${(r.stdout + "\n" + r.stderr).trim().slice(-6e3)}`;
23092
23650
 
23093
23651
  // src/api/explain.ts
23094
23652
  import { createHash as createHash4 } from "crypto";
23095
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
23653
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
23096
23654
 
23097
23655
  // src/report.ts
23098
23656
  import pc2 from "picocolors";
@@ -23281,7 +23839,7 @@ var CACHE_FILE = "cache/explanations.json";
23281
23839
  var MAX_EXPLAIN = 25;
23282
23840
  function loadCache(root) {
23283
23841
  try {
23284
- return JSON.parse(readFileSync11(auraPath(root, CACHE_FILE), "utf8"));
23842
+ return JSON.parse(readFileSync14(auraPath(root, CACHE_FILE), "utf8"));
23285
23843
  } catch {
23286
23844
  return {};
23287
23845
  }
@@ -23302,8 +23860,8 @@ function excerptFor(root, f, context = 4) {
23302
23860
  }
23303
23861
  function productContext(root) {
23304
23862
  const file2 = auraPath(root, "brief.md");
23305
- if (!existsSync12(file2)) return void 0;
23306
- return redact(readFileSync11(file2, "utf8")).slice(0, 1800);
23863
+ if (!existsSync15(file2)) return void 0;
23864
+ return redact(readFileSync14(file2, "utf8")).slice(0, 1800);
23307
23865
  }
23308
23866
  async function addExplanations(root, scan, client) {
23309
23867
  const cache = loadCache(root);
@@ -23347,19 +23905,22 @@ async function addExplanations(root, scan, client) {
23347
23905
 
23348
23906
  // src/commands/simple.ts
23349
23907
  import { execFileSync } from "child_process";
23350
- import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync15 } from "fs";
23351
- import path16 from "path";
23908
+ import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync18 } from "fs";
23909
+ import path18 from "path";
23352
23910
  import YAML2 from "yaml";
23353
23911
 
23354
23912
  // src/commands/inventory.ts
23355
- import { readFileSync as readFileSync12 } from "fs";
23913
+ import { readFileSync as readFileSync15 } from "fs";
23356
23914
  function accountState(root, services) {
23357
23915
  const inv = loadInventory(root);
23358
- return { inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg), githubSecurity: Boolean(inv.githubSecurity) };
23916
+ return {
23917
+ inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg),
23918
+ secured: { ...inv.secured, github: Boolean(inv.githubSecurity || inv.secured?.github) }
23919
+ };
23359
23920
  }
23360
23921
  function loadInventory(root) {
23361
23922
  try {
23362
- return JSON.parse(readFileSync12(auraPath(root, "inventory.json"), "utf8"));
23923
+ return JSON.parse(readFileSync15(auraPath(root, "inventory.json"), "utf8"));
23363
23924
  } catch {
23364
23925
  return { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), services: {} };
23365
23926
  }
@@ -23446,10 +24007,10 @@ async function inventoryCommand(flags) {
23446
24007
  }
23447
24008
 
23448
24009
  // src/history.ts
23449
- import { readFileSync as readFileSync13 } from "fs";
24010
+ import { readFileSync as readFileSync16 } from "fs";
23450
24011
  function loadHistory(root) {
23451
24012
  try {
23452
- const data = JSON.parse(readFileSync13(auraPath(root, "history.json"), "utf8"));
24013
+ const data = JSON.parse(readFileSync16(auraPath(root, "history.json"), "utf8"));
23453
24014
  return Array.isArray(data) ? data : [];
23454
24015
  } catch {
23455
24016
  return [];
@@ -23497,10 +24058,10 @@ function banner(colors = pc3.isColorSupported) {
23497
24058
  // src/api/connect.ts
23498
24059
  import { spawn as spawn2 } from "child_process";
23499
24060
  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");
24061
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync17, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
24062
+ import os5 from "os";
24063
+ import path17 from "path";
24064
+ var pendingFile = () => path17.join(os5.homedir(), ".aura", "pending-connect.json");
23504
24065
  async function postJson(apiUrl, route, body) {
23505
24066
  let res;
23506
24067
  try {
@@ -23519,14 +24080,14 @@ async function postJson(apiUrl, route, body) {
23519
24080
  }
23520
24081
  function loadPending(apiUrl, now) {
23521
24082
  try {
23522
- const p2 = JSON.parse(readFileSync14(pendingFile(), "utf8"));
24083
+ const p2 = JSON.parse(readFileSync17(pendingFile(), "utf8"));
23523
24084
  if (p2.apiUrl === apiUrl && Date.parse(p2.expiresAt) - now > 6e4) return p2;
23524
24085
  } catch {
23525
24086
  }
23526
24087
  return null;
23527
24088
  }
23528
24089
  function clearPendingConnect() {
23529
- if (existsSync13(pendingFile())) rmSync5(pendingFile(), { force: true });
24090
+ if (existsSync16(pendingFile())) rmSync5(pendingFile(), { force: true });
23530
24091
  }
23531
24092
  async function startOrResumeConnect(apiUrl, now = Date.now()) {
23532
24093
  const existing = loadPending(apiUrl, now);
@@ -23538,7 +24099,7 @@ async function startOrResumeConnect(apiUrl, now = Date.now()) {
23538
24099
  });
23539
24100
  const res = ConnectStartResponse.parse(await postJson(apiUrl, "connect", { publicKey }));
23540
24101
  const pending = { apiUrl, privateKey, ...res };
23541
- mkdirSync5(path15.dirname(pendingFile()), { recursive: true });
24102
+ mkdirSync5(path17.dirname(pendingFile()), { recursive: true });
23542
24103
  writeFileSync5(pendingFile(), JSON.stringify(pending, null, 2) + "\n", { mode: 384 });
23543
24104
  return { ...pending, resumed: false };
23544
24105
  }
@@ -23631,7 +24192,7 @@ function stepRun(step, hasKey) {
23631
24192
  }
23632
24193
  function hint(step, hasKey) {
23633
24194
  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";
24195
+ 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
24196
  return [what, step.impact > 0 ? `+${step.impact} points` : "", effortLabel(step.effort)].filter(Boolean).join(" \xB7 ");
23636
24197
  }
23637
24198
  async function pickAndRunSteps(steps, limit = 10) {
@@ -23657,7 +24218,10 @@ async function pickAndRunSteps(steps, limit = 10) {
23657
24218
  console.log(`
23658
24219
  ${pc.magenta("\u2726")} ${pc.bold(step.title)}`);
23659
24220
  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({});
24221
+ const code = run2.kind === "apply" ? (
24222
+ // Settings changes always ask first, even when picked here; pull requests don't need to.
24223
+ await applyCommand(run2.actionId, { yes: !connectorForAction(run2.actionId), via: "checklist" })
24224
+ ) : 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
24225
  if (run2.kind === "guide" || run2.kind === "inventory") {
23662
24226
  const root = await findRepoRoot();
23663
24227
  await recordUsage(root, loadConfig(root), {
@@ -23681,9 +24245,9 @@ ${pc.bold(" Done")}`);
23681
24245
  // src/commands/simple.ts
23682
24246
  async function latestScan(root) {
23683
24247
  const file2 = auraPath(root, "scan.json");
23684
- if (existsSync14(file2)) {
24248
+ if (existsSync17(file2)) {
23685
24249
  try {
23686
- const scan = JSON.parse(readFileSync15(file2, "utf8"));
24250
+ const scan = JSON.parse(readFileSync18(file2, "utf8"));
23687
24251
  const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23688
24252
  const dirty = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23689
24253
  if (scan.commit === head && !dirty && Date.now() - Date.parse(scan.scannedAt) < 24 * 36e5) return scan;
@@ -23798,9 +24362,9 @@ async function configCommand(key, value) {
23798
24362
  return 0;
23799
24363
  }
23800
24364
  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");
24365
+ const dir = path18.join(templatesDir(), "guides");
24366
+ return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((f) => {
24367
+ const text = readFileSync18(path18.join(dir, f), "utf8");
23804
24368
  const fm = text.match(/^---\n([\s\S]*?)\n---\n?/);
23805
24369
  const meta3 = fm ? YAML2.parse(fm[1]) : {};
23806
24370
  return { id: f.replace(/\.md$/, ""), title: meta3.title ?? f, summary: meta3.summary ?? "", body: fm ? text.slice(fm[0].length) : text };
@@ -23868,7 +24432,7 @@ function allowedCommands(root, pm, typescript) {
23868
24432
  const pkg = readJson(root, "package.json");
23869
24433
  const run2 = pmCommands(pm).run;
23870
24434
  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`);
24435
+ if (typescript && !pkg?.scripts?.typecheck && existsSync18(path19.join(root, "node_modules/.bin/tsc"))) cmds.push(`${pmCommands(pm).exec} tsc --noEmit`);
23872
24436
  return cmds;
23873
24437
  }
23874
24438
  function taskFor(root, scan, action2, findings) {
@@ -23919,8 +24483,8 @@ async function untrackEnvFiles(root, findings) {
23919
24483
  const files = findings.filter((f) => f.checkId === "security.env-files").map((f) => f.locations[0].file);
23920
24484
  for (const f of files) await git(root, "rm", "-q", "--cached", "--", f);
23921
24485
  if (files.length) {
23922
- const gi = path17.join(root, ".gitignore");
23923
- const current2 = existsSync15(gi) ? readFileSync16(gi, "utf8") : "";
24486
+ const gi = path19.join(root, ".gitignore");
24487
+ const current2 = existsSync18(gi) ? readFileSync19(gi, "utf8") : "";
23924
24488
  if (!/^\.env\*?$/m.test(current2)) writeFileSync6(gi, `${current2.trimEnd()}
23925
24489
 
23926
24490
  # Environment files hold secrets
@@ -24097,6 +24661,128 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
24097
24661
  }
24098
24662
  }
24099
24663
 
24664
+ // src/connectors/engine.ts
24665
+ async function readSettings(connector, ctx) {
24666
+ const states = await Promise.all(connector.settings.map((s) => s.read(ctx).catch(() => ({ state: "off" }))));
24667
+ return Object.fromEntries(connector.settings.map((s, i) => [s.id, states[i]]));
24668
+ }
24669
+ async function enableSettings(connector, ctx, states) {
24670
+ const results = [];
24671
+ for (const setting of connector.settings) {
24672
+ const s = states[setting.id];
24673
+ if (s.state === "on") results.push({ id: setting.id, result: "already-on" });
24674
+ else if (s.state === "unavailable") results.push({ id: setting.id, result: "unavailable", why: s.why });
24675
+ else {
24676
+ const out = await setting.enable(ctx).catch(() => ({ ok: false }));
24677
+ results.push(
24678
+ 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` }
24679
+ );
24680
+ }
24681
+ }
24682
+ return results;
24683
+ }
24684
+ var securityDone = (results) => results.every((r) => r.result !== "failed");
24685
+ async function secureCommand(connector, flags, report = {}, fake) {
24686
+ const root = await findRepoRoot();
24687
+ report.delivery = "settings";
24688
+ const connection = await connector.connect(root, fake);
24689
+ if (!connection.ok) {
24690
+ say.error(connection.problem);
24691
+ Object.assign(report, { outcome: "failed", errorType: connection.errorType });
24692
+ return 1;
24693
+ }
24694
+ const { ctx, target } = connection;
24695
+ const states = withRefusals(await readSettings(connector, ctx), loadSnapshots(root)[connector.id]);
24696
+ saveSnapshot(root, connector.id, states);
24697
+ const toTurnOn = connector.settings.filter((s) => states[s.id].state === "off").map((s) => s.id);
24698
+ console.log(`
24699
+ ${pc.magenta("\u2726")} ${pc.bold(`${connector.name} security for ${target}`)}`);
24700
+ for (const setting of connector.settings) {
24701
+ const s = states[setting.id];
24702
+ 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}`);
24703
+ console.log(` ${setting.label}
24704
+ ${line}`);
24705
+ }
24706
+ for (const line of await connector.explain?.(ctx, toTurnOn) ?? []) say.dim(` ${line}`);
24707
+ const label = (id) => connector.settings.find((s) => s.id === id).label;
24708
+ const impact = (id) => connector.settings.find((s) => s.id === id).impact;
24709
+ if (toTurnOn.length && !flags.dryRun) {
24710
+ console.log(`
24711
+ ${pc.bold(" What changes for you")}`);
24712
+ for (const id of toTurnOn) console.log(` \u2022 ${impact(id)}`);
24713
+ }
24714
+ if (flags.dryRun) {
24715
+ say.dim(toTurnOn.length ? "\nDry run: nothing was changed." : "\nEverything Aura can turn on here is already on.");
24716
+ return 0;
24717
+ }
24718
+ let chosen = toTurnOn;
24719
+ if (toTurnOn.length && !flags.yes) {
24720
+ if (!interactive()) {
24721
+ say.info(`
24722
+ Nothing was changed. Show the user the changes above, and once they agree run: aura apply ${connector.actionId} --yes`);
24723
+ report.outcome = "shown";
24724
+ return 0;
24725
+ }
24726
+ chosen = unwrap(
24727
+ await p.multiselect({
24728
+ message: `Which should Aura turn on in ${connector.name}? (untick any you don\u2019t want)`,
24729
+ options: toTurnOn.map((id) => ({ value: id, label: label(id) })),
24730
+ initialValues: toTurnOn,
24731
+ required: false
24732
+ })
24733
+ );
24734
+ if (chosen.length === 0) {
24735
+ say.dim("Nothing was changed.");
24736
+ report.outcome = "cancelled";
24737
+ return 0;
24738
+ }
24739
+ }
24740
+ const skipped = toTurnOn.filter((id) => !chosen.includes(id));
24741
+ let results;
24742
+ if (toTurnOn.length === 0) {
24743
+ say.ok("Everything Aura can turn on here is already on.");
24744
+ results = await enableSettings(connector, ctx, states);
24745
+ report.outcome = "no_changes";
24746
+ } else {
24747
+ const attempt = Object.fromEntries(
24748
+ Object.entries(states).map(([id, s]) => [id, skipped.includes(id) ? { state: "unavailable", why: "you chose to leave it off" } : s])
24749
+ );
24750
+ results = await enableSettings(connector, ctx, attempt);
24751
+ console.log("");
24752
+ for (const r of results.filter((x) => chosen.includes(x.id))) {
24753
+ if (r.result === "turned-on") say.ok(`${label(r.id)}: on`);
24754
+ else if (r.result === "unavailable") say.dim(`\u2013 ${label(r.id)}: ${r.why}`);
24755
+ else say.warn(`${label(r.id)}: ${r.why}.`);
24756
+ }
24757
+ report.outcome = results.some((r) => r.result === "failed") ? "failed" : "settings_changed";
24758
+ }
24759
+ const refusedNow = Object.fromEntries(results.filter((r) => r.result === "unavailable" && states[r.id].state === "off" && r.why).map((r) => [r.id, r.why]));
24760
+ saveSnapshot(
24761
+ root,
24762
+ connector.id,
24763
+ Object.fromEntries(
24764
+ Object.entries(states).map(([id, s]) => {
24765
+ const r = results.find((x) => x.id === id);
24766
+ return [id, r?.result === "turned-on" ? { state: "on" } : refusedNow[id] ? { state: "unavailable", why: refusedNow[id] } : s];
24767
+ })
24768
+ ),
24769
+ refusedNow
24770
+ );
24771
+ const left = await connector.leftForYou(ctx, results);
24772
+ if (left.length) {
24773
+ console.log(`
24774
+ ${pc.bold(" Left for you")}`);
24775
+ for (const line of left) console.log(` \u2022 ${line}`);
24776
+ }
24777
+ const inv = loadInventory(root);
24778
+ inv.secured = { ...inv.secured, [connector.id]: securityDone(results) };
24779
+ if (connector.id === "github") inv.githubSecurity = inv.secured.github;
24780
+ inv.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
24781
+ writeAuraFile(root, "inventory.json", `${JSON.stringify(inv, null, 2)}
24782
+ `);
24783
+ return results.some((r) => r.result === "failed") ? 1 : 0;
24784
+ }
24785
+
24100
24786
  // src/commands/apply.ts
24101
24787
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
24102
24788
  async function applyCommand(actionId, flags) {
@@ -24116,6 +24802,8 @@ async function applyAction(actionId, flags, report) {
24116
24802
  }
24117
24803
  const scan = await latestScan(root);
24118
24804
  const { delivery, fellBack } = effectiveDelivery(action2, scan);
24805
+ const connector = connectorForAction(actionId);
24806
+ if (connector) return secureCommand(connector, flags, report);
24119
24807
  if (delivery === "guide") return guideCommand(actionId, {});
24120
24808
  if (delivery === "agent-fix") {
24121
24809
  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 +24837,7 @@ async function applyAction(actionId, flags, report) {
24149
24837
  console.log(`
24150
24838
  ${pc.bold(action2.title)}`);
24151
24839
  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}`);
24840
+ console.log(` ${c.content === null ? pc.red("delete") : existsSync19(path20.join(root, c.path)) ? pc.yellow("update") : pc.green("add ")} ${c.path}`);
24153
24841
  const pm = pmCommands(scan.stack.packageManager);
24154
24842
  if (plan.devDeps?.length) console.log(` ${pc.cyan("install")} ${plan.devDeps.join(", ")} ${pc.dim(`(${pm.addDev})`)}`);
24155
24843
  for (const cmd of plan.commands ?? []) console.log(` ${pc.cyan("run ")} ${cmd.join(" ")}`);
@@ -24181,8 +24869,8 @@ ${pc.bold(action2.title)}`);
24181
24869
  report.outcome = "cancelled";
24182
24870
  return 0;
24183
24871
  }
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);
24872
+ const tsc = path20.join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
24873
+ const checkTypes = plan.verifyTypes && scan.stack.typescript && existsSync19(tsc);
24186
24874
  const typeErrors = async () => {
24187
24875
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: root, timeoutMs: 3e5 });
24188
24876
  return (r.stdout.match(/error TS\d+/g) ?? []).length;
@@ -24291,17 +24979,17 @@ async function undoPlan(root, plan) {
24291
24979
  for (const file2 of touched) {
24292
24980
  const inHead = (await git(root, "cat-file", "-e", `HEAD:${file2}`)).code === 0;
24293
24981
  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 });
24982
+ else if (plan.changes.some((c) => c.path === file2)) rmSync6(path20.join(root, file2), { force: true });
24295
24983
  }
24296
24984
  }
24297
24985
 
24298
24986
  // src/commands/init.ts
24299
- import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
24987
+ import { existsSync as existsSync20, readFileSync as readFileSync21 } from "fs";
24300
24988
  import { spawnSync as spawnSync3 } from "child_process";
24301
- import path22 from "path";
24989
+ import path24 from "path";
24302
24990
 
24303
24991
  // src/brief.ts
24304
- import path19 from "path";
24992
+ import path21 from "path";
24305
24993
  function repoSummary(ctx) {
24306
24994
  const routes = ctx.files.filter((f) => /^(?:src\/)?app\/(?:.+\/)?(?:page|route)\.[cm]?[jt]sx?$/.test(f) || /^(?:src\/)?pages\/.+\.[cm]?[jt]sx?$/.test(f)).map(
24307
24995
  (f) => "/" + f.replace(/^(?:src\/)?(?:app|pages)\//, "").replace(/(?:^|\/)(?:page|route|index)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/, "").replace(/\([^)]+\)\//g, "")
@@ -24317,7 +25005,7 @@ function repoSummary(ctx) {
24317
25005
  const readme2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f));
24318
25006
  const readmeText = readme2 ? ctx.read(readme2) ?? "" : "";
24319
25007
  return {
24320
- name: ctx.pkg?.name ?? path19.basename(ctx.root),
25008
+ name: ctx.pkg?.name ?? path21.basename(ctx.root),
24321
25009
  packageJson: ctx.pkg ? {
24322
25010
  name: ctx.pkg.name,
24323
25011
  description: ctx.pkg.description,
@@ -24410,10 +25098,10 @@ function installFallback(result, platform = process.platform) {
24410
25098
 
24411
25099
  // src/core/claude-plugin.ts
24412
25100
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
24413
- import os5 from "os";
24414
- import path20 from "path";
25101
+ import os6 from "os";
25102
+ import path22 from "path";
24415
25103
  var CLAUDE_PLUGIN_ID = "aura@aura";
24416
- var marketplaceDir = () => path20.join(os5.homedir(), ".aura", "claude-marketplace");
25104
+ var marketplaceDir = () => path22.join(os6.homedir(), ".aura", "claude-marketplace");
24417
25105
  var MARKETPLACE = {
24418
25106
  name: "aura",
24419
25107
  description: "Aura for Claude Code",
@@ -24428,8 +25116,8 @@ var MARKETPLACE = {
24428
25116
  };
24429
25117
  function writeMarketplace() {
24430
25118
  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)}
25119
+ mkdirSync6(path22.join(dir, ".claude-plugin"), { recursive: true });
25120
+ writeFileSync7(path22.join(dir, ".claude-plugin", "marketplace.json"), `${JSON.stringify(MARKETPLACE, null, 2)}
24433
25121
  `);
24434
25122
  return dir;
24435
25123
  }
@@ -24532,11 +25220,30 @@ function describeInferred(inferred) {
24532
25220
 
24533
25221
  // src/commands/scan.ts
24534
25222
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
24535
- import os6 from "os";
24536
- import path21 from "path";
25223
+ import os7 from "os";
25224
+ import path23 from "path";
25225
+
25226
+ // src/connectors/refresh.ts
25227
+ import { readFileSync as readFileSync20 } from "fs";
25228
+ async function refreshToolSettings(root) {
25229
+ if (process.env.AURA_NO_CONNECTORS) return;
25230
+ let previous = null;
25231
+ try {
25232
+ previous = JSON.parse(readFileSync20(auraPath(root, "scan.json"), "utf8"));
25233
+ } catch {
25234
+ }
25235
+ const snapshots = loadSnapshots(root);
25236
+ for (const connector of CONNECTORS) {
25237
+ if (!(previous ? connector.detect(previous) : connector.id === "github") || !isStale(snapshots[connector.id])) continue;
25238
+ const connection = await connector.connect(root).catch(() => null);
25239
+ if (connection?.ok) saveSnapshot(root, connector.id, withRefusals(await readSettings(connector, connection.ctx), snapshots[connector.id]));
25240
+ }
25241
+ }
25242
+
25243
+ // src/commands/scan.ts
24537
25244
  async function scanRef(root, ref, opts) {
24538
- const dir = mkdtempSync3(path21.join(os6.tmpdir(), "aura-base-"));
24539
- const wt = path21.join(dir, "tree");
25245
+ const dir = mkdtempSync3(path23.join(os7.tmpdir(), "aura-base-"));
25246
+ const wt = path23.join(dir, "tree");
24540
25247
  const add = await run("git", ["worktree", "add", "--detach", wt, ref], { cwd: root });
24541
25248
  if (add.code !== 0) {
24542
25249
  rmSync7(dir, { recursive: true, force: true });
@@ -24557,6 +25264,7 @@ async function scanCommand(flags) {
24557
25264
  const opts = { fast: Boolean(flags.fast || flags.base), noDownload: flags.download === false };
24558
25265
  const s = spin(!quiet && !flags.ci);
24559
25266
  s.start("Scanning");
25267
+ if (!flags.ci) await refreshToolSettings(root);
24560
25268
  const scan = await runScan(root, opts, (m) => s.message(`Checking: ${m}`));
24561
25269
  s.stop(`Scanned ${scan.checks.filter((c) => c.status !== "not-applicable").length} checks`);
24562
25270
  const client = AuraClient.from(config2);
@@ -24625,7 +25333,7 @@ async function scanCommand(flags) {
24625
25333
  // src/commands/init.ts
24626
25334
  var AGENT_IDS = ["claude", "codex", "cursor", "pi"];
24627
25335
  function detectAgents(root) {
24628
- return detectAgentsFrom((rel) => existsSync17(path22.join(root, rel)));
25336
+ return detectAgentsFrom((rel) => existsSync20(path24.join(root, rel)));
24629
25337
  }
24630
25338
  function openEditor(file2) {
24631
25339
  const fallback = process.platform === "win32" ? "notepad" : spawnSync3("sh", ["-c", "command -v nano"]).status === 0 ? "nano" : "vi";
@@ -24740,13 +25448,13 @@ async function initCommand(flags) {
24740
25448
  let brief2 = null;
24741
25449
  let keptBrief = false;
24742
25450
  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 })))) {
25451
+ brief2 = readFileSync21(flags.briefFile, "utf8");
25452
+ } else if (!flags.answers && existsSync20(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
24745
25453
  keptBrief = true;
24746
25454
  } else {
24747
25455
  let answers = null;
24748
25456
  if (flags.answers) {
24749
- answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync17(flags.answers, "utf8")));
25457
+ answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync21(flags.answers, "utf8")));
24750
25458
  } else if (tty) {
24751
25459
  const inferred = inferAnswers(ctx);
24752
25460
  p.note(describeInferred(inferred).join("\n"), "Here\u2019s what Aura found about your app");
@@ -24878,7 +25586,7 @@ ${pc.magenta("\u2726")} ${pc.bold("Give the user the aura command")}`);
24878
25586
  }
24879
25587
 
24880
25588
  // src/commands/summary.ts
24881
- import path23 from "path";
25589
+ import path25 from "path";
24882
25590
  async function activity(root, since) {
24883
25591
  const log = await git(root, "log", `--since=${since.toISOString()}`, "--no-merges", "--format=%H%x09%an%x09%s");
24884
25592
  const commits = log.stdout.split("\n").filter(Boolean).slice(0, 300).map((l) => {
@@ -24935,7 +25643,7 @@ async function summaryCommand(flags) {
24935
25643
  const beforeIds = new Set(before ? allFindings(before).map((f) => f.id) : []);
24936
25644
  const nowIds = new Set(allFindings(now).map((f) => f.id));
24937
25645
  const req = {
24938
- repo: path23.basename(root),
25646
+ repo: path25.basename(root),
24939
25647
  since: since.toISOString(),
24940
25648
  until: until.toISOString(),
24941
25649
  product: productContext(root),
@@ -24983,11 +25691,11 @@ ${body}`);
24983
25691
  import pc4 from "picocolors";
24984
25692
 
24985
25693
  // 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";
25694
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "fs";
25695
+ import os8 from "os";
25696
+ import path26 from "path";
24989
25697
  var DAY_MS = 24 * 60 * 60 * 1e3;
24990
- var cacheFile = () => path24.join(os7.homedir(), ".aura", "update-check.json");
25698
+ var cacheFile = () => path26.join(os8.homedir(), ".aura", "update-check.json");
24991
25699
  function isNewer(a, b) {
24992
25700
  if (!/^\d+\.\d+\.\d+$/.test(a) || !/^\d+\.\d+\.\d+$/.test(b)) return false;
24993
25701
  const [x, y] = [a, b].map((v) => v.split(".").map(Number));
@@ -25007,13 +25715,13 @@ async function newerVersion(opts = {}) {
25007
25715
  const now = opts.now ?? Date.now();
25008
25716
  let latest = null;
25009
25717
  try {
25010
- const cached2 = JSON.parse(readFileSync18(cacheFile(), "utf8"));
25718
+ const cached2 = JSON.parse(readFileSync22(cacheFile(), "utf8"));
25011
25719
  if (now - cached2.checkedAt < DAY_MS) latest = cached2.latest;
25012
25720
  else throw new Error("stale");
25013
25721
  } catch {
25014
25722
  latest = await (opts.fetchLatest ?? fetchLatest)();
25015
25723
  try {
25016
- mkdirSync7(path24.dirname(cacheFile()), { recursive: true });
25724
+ mkdirSync7(path26.dirname(cacheFile()), { recursive: true });
25017
25725
  writeFileSync9(cacheFile(), JSON.stringify({ checkedAt: now, latest }));
25018
25726
  } catch {
25019
25727
  }