getaura 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,8 +9,8 @@ var __export = (target, all) => {
9
9
  import { Command, Option } from "commander";
10
10
 
11
11
  // src/commands/apply.ts
12
- import { existsSync as existsSync15, rmSync as rmSync6 } from "fs";
13
- import path17 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 = {
@@ -36,6 +36,25 @@ function uniqueSkillDirs(agents) {
36
36
  }
37
37
  var AURA_BLOCK_START = "<!-- aura:start -->";
38
38
  var AURA_BLOCK_END = "<!-- aura:end -->";
39
+ function detectAgentsFrom(exists2) {
40
+ const found = [];
41
+ if (exists2("CLAUDE.md") || exists2(".claude")) found.push("claude");
42
+ if (exists2(".codex")) found.push("codex");
43
+ if (exists2(".cursor") || exists2(".cursorrules")) found.push("cursor");
44
+ if (exists2(".pi")) found.push("pi");
45
+ if (found.length === 0 && exists2("AGENTS.md")) found.push("codex");
46
+ return found;
47
+ }
48
+ function runningAgent(env = process.env) {
49
+ if (env.CLAUDECODE) return "claude";
50
+ if (env.CODEX_THREAD_ID) return "codex";
51
+ if (env.CURSOR_AGENT) return "cursor";
52
+ if (env.PI_CODING_AGENT) return "pi";
53
+ return null;
54
+ }
55
+ function inCI(env = process.env) {
56
+ return Boolean(env.CI) && !runningAgent(env);
57
+ }
39
58
 
40
59
  // ../shared/src/types.ts
41
60
  var CATEGORIES = ["security", "agent-setup", "foundations", "structure"];
@@ -990,10 +1009,10 @@ function mergeDefs(...defs) {
990
1009
  function cloneDef(schema) {
991
1010
  return mergeDefs(schema._zod.def);
992
1011
  }
993
- function getElementAtPath(obj, path22) {
994
- if (!path22)
1012
+ function getElementAtPath(obj, path27) {
1013
+ if (!path27)
995
1014
  return obj;
996
- return path22.reduce((acc, key) => acc?.[key], obj);
1015
+ return path27.reduce((acc, key) => acc?.[key], obj);
997
1016
  }
998
1017
  function promiseAllObject(promisesObj) {
999
1018
  const keys = Object.keys(promisesObj);
@@ -1333,11 +1352,11 @@ function explicitlyAborted(x, startIndex = 0) {
1333
1352
  }
1334
1353
  return false;
1335
1354
  }
1336
- function prefixIssues(path22, issues) {
1355
+ function prefixIssues(path27, issues) {
1337
1356
  return issues.map((iss) => {
1338
1357
  var _a3;
1339
1358
  (_a3 = iss).path ?? (_a3.path = []);
1340
- iss.path.unshift(path22);
1359
+ iss.path.unshift(path27);
1341
1360
  return iss;
1342
1361
  });
1343
1362
  }
@@ -1787,16 +1806,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
1787
1806
  }
1788
1807
  function formatError(error62, mapper = (issue2) => issue2.message) {
1789
1808
  const fieldErrors = { _errors: [] };
1790
- const processError = (error63, path22 = []) => {
1809
+ const processError = (error63, path27 = []) => {
1791
1810
  for (const issue2 of error63.issues) {
1792
1811
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1793
- issue2.errors.map((issues) => processError({ issues }, [...path22, ...issue2.path]));
1812
+ issue2.errors.map((issues) => processError({ issues }, [...path27, ...issue2.path]));
1794
1813
  } else if (issue2.code === "invalid_key") {
1795
- processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1814
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1796
1815
  } else if (issue2.code === "invalid_element") {
1797
- processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1816
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1798
1817
  } else {
1799
- const fullpath = [...path22, ...issue2.path];
1818
+ const fullpath = [...path27, ...issue2.path];
1800
1819
  if (fullpath.length === 0) {
1801
1820
  fieldErrors._errors.push(mapper(issue2));
1802
1821
  } else {
@@ -1835,17 +1854,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
1835
1854
  }
1836
1855
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
1837
1856
  const result = { errors: [] };
1838
- const processError = (error63, path22 = []) => {
1857
+ const processError = (error63, path27 = []) => {
1839
1858
  var _a3;
1840
1859
  for (const issue2 of error63.issues) {
1841
1860
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1842
- issue2.errors.map((issues) => processError({ issues }, [...path22, ...issue2.path]));
1861
+ issue2.errors.map((issues) => processError({ issues }, [...path27, ...issue2.path]));
1843
1862
  } else if (issue2.code === "invalid_key") {
1844
- processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1863
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1845
1864
  } else if (issue2.code === "invalid_element") {
1846
- processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1865
+ processError({ issues: issue2.issues }, [...path27, ...issue2.path]);
1847
1866
  } else {
1848
- const fullpath = [...path22, ...issue2.path];
1867
+ const fullpath = [...path27, ...issue2.path];
1849
1868
  if (fullpath.length === 0) {
1850
1869
  result.errors.push(mapper(issue2));
1851
1870
  continue;
@@ -1884,8 +1903,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
1884
1903
  }
1885
1904
  function toDotPath(_path) {
1886
1905
  const segs = [];
1887
- const path22 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1888
- for (const seg of path22) {
1906
+ const path27 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1907
+ for (const seg of path27) {
1889
1908
  if (typeof seg === "number")
1890
1909
  segs.push(`[${seg}]`);
1891
1910
  else if (typeof seg === "symbol")
@@ -2019,18 +2038,18 @@ var validateAsync = async (schema, value, _ctx) => {
2019
2038
  return result.issues.length === 0;
2020
2039
  };
2021
2040
  var _encode = (_Err) => {
2022
- const parse3 = _parse(_Err);
2041
+ const parse5 = _parse(_Err);
2023
2042
  const fn = (schema, value, _ctx, _params) => {
2024
2043
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
2025
- return parse3(schema, value, ctx, finalizeParams(fn, _params));
2044
+ return parse5(schema, value, ctx, finalizeParams(fn, _params));
2026
2045
  };
2027
2046
  return fn;
2028
2047
  };
2029
2048
  var encode = /* @__PURE__ */ _encode($ZodRealError);
2030
2049
  var _decode = (_Err) => {
2031
- const parse3 = _parse(_Err);
2050
+ const parse5 = _parse(_Err);
2032
2051
  const fn = (schema, value, _ctx, _params) => {
2033
- return parse3(schema, value, _ctx, finalizeParams(fn, _params));
2052
+ return parse5(schema, value, _ctx, finalizeParams(fn, _params));
2034
2053
  };
2035
2054
  return fn;
2036
2055
  };
@@ -18987,13 +19006,13 @@ function resolveRef(ref, ctx) {
18987
19006
  if (!ref.startsWith("#")) {
18988
19007
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
18989
19008
  }
18990
- const path22 = ref.slice(1).split("/").filter(Boolean);
18991
- if (path22.length === 0) {
19009
+ const path27 = ref.slice(1).split("/").filter(Boolean);
19010
+ if (path27.length === 0) {
18992
19011
  return ctx.rootSchema;
18993
19012
  }
18994
19013
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
18995
- if (path22[0] === defsKey) {
18996
- const key = path22[1] === void 0 ? void 0 : decodeJSONPointerSegment(path22[1]);
19014
+ if (path27[0] === defsKey) {
19015
+ const key = path27[1] === void 0 ? void 0 : decodeJSONPointerSegment(path27[1]);
18997
19016
  if (!key || !ctx.defs[key]) {
18998
19017
  throw new Error(`Reference not found: ${ref}`);
18999
19018
  }
@@ -19939,14 +19958,59 @@ var SummaryRequest = external_exports.object({
19939
19958
  resolvedFindings: external_exports.array(external_exports.string().max(300)).max(50)
19940
19959
  });
19941
19960
  var SummaryResponse = external_exports.object({ summary: external_exports.string() });
19961
+ var SETUP_STEPS = [
19962
+ "started",
19963
+ "key_connected",
19964
+ "brief_written",
19965
+ "skill_installed",
19966
+ "command_installed",
19967
+ "command_install_failed",
19968
+ "first_scan"
19969
+ ];
19970
+ var FIX_OUTCOMES = [
19971
+ "pr_opened",
19972
+ "committed",
19973
+ "no_changes",
19974
+ "failed",
19975
+ "cancelled",
19976
+ "task_written",
19977
+ "shown",
19978
+ "settings_changed",
19979
+ "merged",
19980
+ "closed",
19981
+ "resolved",
19982
+ "not_resolved"
19983
+ ];
19942
19984
  var TelemetryRequest = external_exports.object({
19943
- event: external_exports.enum(["scan", "action"]),
19985
+ event: external_exports.enum(["scan", "action", "setup", "fix_result", "error"]),
19944
19986
  auraVersion: external_exports.string().max(40),
19945
19987
  /** Anonymous, stable per-repo id (hash of the first commit), never the repo name. */
19946
19988
  repoId: external_exports.string().max(80),
19989
+ /** Who ran Aura: a person at a terminal, a coding agent or CI. */
19990
+ source: external_exports.enum(["terminal", "agent", "ci"]).optional(),
19991
+ /** The coding agent that ran this command, when it could be told. */
19992
+ agent: external_exports.enum(["claude", "codex", "cursor", "pi"]).optional(),
19993
+ /** The coding agents set up in this repo. */
19994
+ agents: external_exports.array(external_exports.enum(["claude", "codex", "cursor", "pi"])).max(4).optional(),
19995
+ os: external_exports.enum(["darwin", "win32", "linux", "other"]).optional(),
19996
+ nodeVersion: external_exports.string().regex(/^v?\d+\.\d+\.\d+$/).optional(),
19947
19997
  score: external_exports.number().optional(),
19948
19998
  categories: external_exports.record(external_exports.string(), external_exports.number().nullable()).optional(),
19949
- actionId: external_exports.string().max(80).optional()
19999
+ /** Findings by severity, and the ids of checks that didn't fully pass (never file names). */
20000
+ findings: external_exports.record(severity, external_exports.number().int().nonnegative()).optional(),
20001
+ failingChecks: external_exports.array(external_exports.string().regex(/^[a-z-]+\.[a-z-]+$/)).max(40).optional(),
20002
+ actionId: external_exports.string().regex(/^[a-z0-9-]{1,80}$/).optional(),
20003
+ step: external_exports.enum(SETUP_STEPS).optional(),
20004
+ outcome: external_exports.enum(FIX_OUTCOMES).optional(),
20005
+ delivery: external_exports.enum(["pr", "agent-fix", "agent-task", "guide", "checklist", "settings"]).optional(),
20006
+ /** Whether the user picked it from the next-steps checklist or ran the command. */
20007
+ via: external_exports.enum(["checklist", "command"]).optional(),
20008
+ durationMs: external_exports.number().int().nonnegative().max(864e5).optional(),
20009
+ agentSteps: external_exports.number().int().nonnegative().max(1e3).optional(),
20010
+ scoreChange: external_exports.number().optional(),
20011
+ command: external_exports.string().regex(/^[a-z-]{1,30}$/).optional(),
20012
+ /** The kind of error (its class or code), never its message, which can contain paths. */
20013
+ errorType: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,59}$/).optional()
19950
20014
  });
19951
20015
  var MeResponse = external_exports.object({
19952
20016
  name: external_exports.string(),
@@ -19969,7 +20033,6 @@ var ConnectStartResponse = external_exports.object({
19969
20033
  var ConnectPollRequest = external_exports.object({ deviceSecret: external_exports.string().min(20).max(200) });
19970
20034
  var ConnectPollResponse = external_exports.discriminatedUnion("status", [
19971
20035
  external_exports.object({ status: external_exports.literal("pending") }),
19972
- external_exports.object({ status: external_exports.literal("skipped") }),
19973
20036
  external_exports.object({ status: external_exports.literal("expired") }),
19974
20037
  /** `encryptedKey` is base64 RSA-OAEP (SHA-256) ciphertext of the pilot key. */
19975
20038
  external_exports.object({ status: external_exports.literal("complete"), encryptedKey: external_exports.string(), name: external_exports.string() })
@@ -20015,36 +20078,24 @@ function grade(score) {
20015
20078
  return { label: "Generational aura debt", sentence: "You have generational aura debt", color: "red" };
20016
20079
  }
20017
20080
 
20018
- // src/templates.ts
20019
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
20020
- import path4 from "path";
20021
-
20022
- // src/core/config.ts
20023
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
20024
- import os from "os";
20025
- import path2 from "path";
20026
-
20027
- // src/core/repo.ts
20028
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
20029
- import path from "path";
20030
- import { fileURLToPath } from "url";
20031
-
20032
20081
  // src/core/exec.ts
20033
20082
  import { spawn, spawnSync } from "child_process";
20034
- var WINDOWS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "node", "tar", "where"]);
20083
+ var WINDOWS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "node", "tar", "where", "powershell"]);
20035
20084
  function quoteForCmd(arg) {
20036
20085
  return /^[\w@%+=:,./\\-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`;
20037
20086
  }
20038
20087
  function spawnPlan(cmd, args, platform = process.platform) {
20039
- const direct = platform !== "win32" || cmd.includes("/") || cmd.includes("\\") || WINDOWS_PROGRAMS.has(cmd.toLowerCase());
20040
- return direct ? { args, shell: false } : { args: args.map(quoteForCmd), shell: true };
20088
+ const isPath = cmd.includes("/") || cmd.includes("\\");
20089
+ const script = /\.(?:cmd|bat)$/i.test(cmd);
20090
+ const direct = platform !== "win32" || isPath && !script || WINDOWS_PROGRAMS.has(cmd.toLowerCase());
20091
+ return direct ? { command: cmd, args, shell: false } : { command: quoteForCmd(cmd), args: args.map(quoteForCmd), shell: true };
20041
20092
  }
20042
20093
  function run(cmd, args, opts = {}) {
20043
20094
  return new Promise((resolve) => {
20044
20095
  let child;
20045
20096
  try {
20046
20097
  const plan = spawnPlan(cmd, args);
20047
- child = spawn(cmd, plan.args, { cwd: opts.cwd, env: opts.env ?? process.env, stdio: ["pipe", "pipe", "pipe"], shell: plan.shell });
20098
+ child = spawn(plan.command, plan.args, { cwd: opts.cwd, env: opts.env ?? process.env, stdio: ["pipe", "pipe", "pipe"], shell: plan.shell });
20048
20099
  } catch (err) {
20049
20100
  resolve({ code: 127, stdout: "", stderr: String(err) });
20050
20101
  return;
@@ -20075,7 +20126,14 @@ async function commandExists(cmd) {
20075
20126
  return probe.code === 0;
20076
20127
  }
20077
20128
 
20129
+ // src/core/git.ts
20130
+ import { mkdirSync, rmSync, writeFileSync } from "fs";
20131
+ import path2 from "path";
20132
+
20078
20133
  // src/core/repo.ts
20134
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
20135
+ import path from "path";
20136
+ import { fileURLToPath } from "url";
20079
20137
  var AURA_DIR = ".aura";
20080
20138
  var ALWAYS_IGNORED = ["node_modules", ".next", ".git", "dist", "build", "out", ".vercel", ".turbo", "coverage", ".aura"];
20081
20139
  async function findRepoRoot(cwd = process.cwd()) {
@@ -20140,7 +20198,545 @@ var SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/;
20140
20198
  var isSourceFile = (f) => SOURCE_EXT.test(f) && !f.endsWith(".d.ts");
20141
20199
  var isTestFile = (f) => /(?:^|\/)(?:__tests__|tests?|e2e)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/.test(f);
20142
20200
 
20201
+ // src/core/git.ts
20202
+ async function git(root, ...args) {
20203
+ return run("git", args, { cwd: root });
20204
+ }
20205
+ function ghInstallHint(platform = process.platform) {
20206
+ const how = platform === "win32" ? "Install it with `winget install --id GitHub.cli`" : platform === "darwin" ? "Install it with `brew install gh` (or download it from https://cli.github.com)" : "Install it from https://cli.github.com";
20207
+ return `${how}, open a new terminal, then run \`gh auth login\`.`;
20208
+ }
20209
+ async function ghStatus(root) {
20210
+ const v = await run("gh", ["--version"]);
20211
+ if (v.code !== 0) return { installed: false, authed: false, hasRemote: false };
20212
+ const auth = await run("gh", ["auth", "status"], { cwd: root });
20213
+ const remote = await git(root, "remote", "get-url", "origin");
20214
+ return { installed: true, authed: auth.code === 0, hasRemote: remote.code === 0 && /github\.com/.test(remote.stdout) };
20215
+ }
20216
+ var AURA_OWNED = /^(?:\.aura\/|\.claude\/skills\/aura\/|\.agents\/skills\/aura(?:-(?:scan|next|fix|setup))?\/|\.cursor\/commands\/aura-[a-z]+\.md$|\.pi\/prompts\/aura-[a-z]+\.md$|\.claude\/settings\.json$)/;
20217
+ async function dirtyFiles(root) {
20218
+ const r = await git(root, "status", "--porcelain", "-uall");
20219
+ return r.stdout.split("\n").filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, "").split(" -> ").pop()).filter((f) => !AURA_OWNED.test(f));
20220
+ }
20221
+ async function currentBranch(root) {
20222
+ return (await git(root, "rev-parse", "--abbrev-ref", "HEAD")).stdout.trim();
20223
+ }
20224
+ async function freeBranchName(root, base) {
20225
+ for (let i = 1; i < 50; i++) {
20226
+ const name = i === 1 ? base : `${base}-${i}`;
20227
+ const local = await git(root, "rev-parse", "--verify", "--quiet", `refs/heads/${name}`);
20228
+ const remote = await git(root, "ls-remote", "--exit-code", "--heads", "origin", name);
20229
+ if (local.code !== 0 && remote.code !== 0) return name;
20230
+ }
20231
+ return `${base}-${Date.now()}`;
20232
+ }
20233
+ function writeChanges(root, changes) {
20234
+ for (const c of changes) {
20235
+ const full = path2.join(root, c.path);
20236
+ if (!isInside(root, full)) throw new Error(`Refusing to write outside the repo: ${c.path}`);
20237
+ if (c.content === null) rmSync(full, { force: true });
20238
+ else {
20239
+ mkdirSync(path2.dirname(full), { recursive: true });
20240
+ writeFileSync(full, c.content);
20241
+ }
20242
+ }
20243
+ }
20244
+ async function commitPushPr(root, opts) {
20245
+ const commit = await git(root, "commit", "-m", opts.title, "-m", "Opened by Aura (https://github.com/JohnFazio1/aura).");
20246
+ const base = { branch: opts.branch, url: null };
20247
+ if (commit.code !== 0) {
20248
+ const out = commit.stderr || commit.stdout;
20249
+ const error62 = /tell me who you are|user\.email/i.test(out) ? 'git doesn\u2019t know your name and email yet. Run these two commands:\n git config --global user.name "Your Name"\n git config --global user.email "you@example.com"' : out;
20250
+ return { ...base, committed: false, pushed: false, error: error62 };
20251
+ }
20252
+ if (!opts.openPr) return { ...base, committed: true, pushed: false };
20253
+ const push = await git(root, "push", "-u", "origin", opts.branch);
20254
+ if (push.code !== 0) return { ...base, committed: true, pushed: false, error: push.stderr };
20255
+ const pr = await run("gh", ["pr", "create", "--base", opts.base, "--head", opts.branch, "--title", opts.title, "--body-file", "-"], {
20256
+ cwd: root,
20257
+ input: opts.body
20258
+ });
20259
+ if (pr.code !== 0) return { ...base, committed: true, pushed: true, error: pr.stderr };
20260
+ return { ...base, url: pr.stdout.trim().split("\n").pop() ?? null, committed: true, pushed: true };
20261
+ }
20262
+
20263
+ // src/connectors/github.ts
20264
+ var PAID_SECRETS = "needs GitHub Secret Protection (a paid add-on) on private repositories";
20265
+ var PAID_RULES = "needs a paid GitHub plan (Pro is about $4/month) on private repositories";
20266
+ var PUBLIC_ONLY = "only available on public repositories";
20267
+ var RULESET_NAME = "Protect main (Aura)";
20268
+ var parse3 = (r) => {
20269
+ try {
20270
+ return JSON.parse(r.stdout);
20271
+ } catch {
20272
+ return null;
20273
+ }
20274
+ };
20275
+ var needsUpgrade = (r) => /Upgrade to GitHub Pro|HTTP 403|not available|Advanced Security/i.test(r.stderr + r.stdout);
20276
+ var done = (r) => r.code === 0 ? { ok: true } : { ok: false };
20277
+ async function ciChecks(ctx) {
20278
+ const r = await ctx.gh([
20279
+ "api",
20280
+ `repos/${ctx.repo}/commits/${ctx.defaultBranch}/check-runs`,
20281
+ "--jq",
20282
+ '.check_runs[] | select(.app.slug == "github-actions") | .name'
20283
+ ]);
20284
+ return r.code === 0 ? [
20285
+ ...new Set(
20286
+ r.stdout.split("\n").map((s) => s.trim()).filter(Boolean)
20287
+ )
20288
+ ] : [];
20289
+ }
20290
+ function protectMainRuleset(checks) {
20291
+ return {
20292
+ name: RULESET_NAME,
20293
+ target: "branch",
20294
+ enforcement: "active",
20295
+ conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
20296
+ rules: [
20297
+ { type: "deletion" },
20298
+ { type: "non_fast_forward" },
20299
+ {
20300
+ // No approvals required, so someone working alone can still merge their own pull requests.
20301
+ type: "pull_request",
20302
+ parameters: {
20303
+ required_approving_review_count: 0,
20304
+ dismiss_stale_reviews_on_push: false,
20305
+ require_code_owner_review: false,
20306
+ require_last_push_approval: false,
20307
+ required_review_thread_resolution: false
20308
+ }
20309
+ },
20310
+ ...checks.length ? [
20311
+ {
20312
+ type: "required_status_checks",
20313
+ parameters: { strict_required_status_checks_policy: false, required_status_checks: checks.map((context) => ({ context })) }
20314
+ }
20315
+ ] : []
20316
+ ]
20317
+ };
20318
+ }
20319
+ var secretState = (key) => async (ctx) => {
20320
+ const security = parse3(await ctx.get(`repos/${ctx.repo}`))?.security_and_analysis;
20321
+ if (security?.[key]?.status === "enabled") return { state: "on" };
20322
+ return security?.[key] || !ctx.isPrivate ? { state: "off" } : { state: "unavailable", why: PAID_SECRETS };
20323
+ };
20324
+ var enableSecret = (key) => async (ctx) => {
20325
+ const r = await ctx.gh(
20326
+ ["api", "-X", "PATCH", `repos/${ctx.repo}`, "--input", "-"],
20327
+ JSON.stringify({ security_and_analysis: { [key]: { status: "enabled" } } })
20328
+ );
20329
+ return r.code === 0 ? { ok: true } : { ok: false, unavailable: ctx.isPrivate && needsUpgrade(r) ? PAID_SECRETS : void 0 };
20330
+ };
20331
+ var github = {
20332
+ id: "github",
20333
+ name: "GitHub",
20334
+ actionId: "github-security",
20335
+ // Offered for every repo; connecting finds out whether it's on GitHub.
20336
+ detect: () => true,
20337
+ async connect(root, fake) {
20338
+ const cli = fake;
20339
+ const gh = cli ?? ((args, input2) => run("gh", args, { cwd: root, input: input2, timeoutMs: 3e4 }));
20340
+ if (!cli) {
20341
+ const status = await ghStatus(root);
20342
+ if (!status.installed) return { ok: false, problem: `The GitHub CLI isn\u2019t installed. ${ghInstallHint()}`, errorType: "CliNotInstalled" };
20343
+ if (!status.authed) return { ok: false, problem: "The GitHub CLI isn\u2019t signed in. Run `gh auth login`, then try again.", errorType: "CliNotSignedIn" };
20344
+ if (!status.hasRemote)
20345
+ return { ok: false, problem: "This repo isn\u2019t on GitHub yet. Push it to GitHub first, for example with `gh repo create`.", errorType: "NoProject" };
20346
+ }
20347
+ const info = parse3(await gh(["repo", "view", "--json", "nameWithOwner,isPrivate,defaultBranchRef,viewerPermission"]));
20348
+ if (!info?.nameWithOwner) return { ok: false, problem: "Couldn\u2019t read this repo from GitHub. Check `gh repo view` works here.", errorType: "NoProject" };
20349
+ if (info.viewerPermission !== "ADMIN")
20350
+ return {
20351
+ ok: false,
20352
+ problem: `Changing these settings needs admin access to ${info.nameWithOwner}. Ask the repo\u2019s owner to run \`aura apply github-security\`.`,
20353
+ errorType: "NotAdmin"
20354
+ };
20355
+ const cache = /* @__PURE__ */ new Map();
20356
+ const ctx = {
20357
+ repo: info.nameWithOwner,
20358
+ isPrivate: Boolean(info.isPrivate),
20359
+ defaultBranch: info.defaultBranchRef?.name || "main",
20360
+ gh,
20361
+ get: (path27) => {
20362
+ if (!cache.has(path27)) cache.set(path27, gh(["api", path27]));
20363
+ return cache.get(path27);
20364
+ }
20365
+ };
20366
+ return { ok: true, ctx, target: `${ctx.repo} ${ctx.isPrivate ? "(private)" : "(public)"}` };
20367
+ },
20368
+ settings: [
20369
+ {
20370
+ id: "dependabot-alerts",
20371
+ label: "Dependabot alerts (warns about vulnerable packages)",
20372
+ impact: "GitHub emails you when a package your app uses has a known security hole.",
20373
+ read: async (ctx) => (await ctx.get(`repos/${ctx.repo}/vulnerability-alerts`)).code === 0 ? { state: "on" } : { state: "off" },
20374
+ enable: async (ctx) => done(await ctx.gh(["api", "-X", "PUT", `repos/${ctx.repo}/vulnerability-alerts`]))
20375
+ },
20376
+ {
20377
+ id: "dependabot-fixes",
20378
+ label: "Dependabot security fixes (opens PRs to update them)",
20379
+ impact: "GitHub opens pull requests that update vulnerable packages. Nothing merges without you.",
20380
+ read: async (ctx) => parse3(await ctx.get(`repos/${ctx.repo}/automated-security-fixes`))?.enabled ? { state: "on" } : { state: "off" },
20381
+ enable: async (ctx) => done(await ctx.gh(["api", "-X", "PUT", `repos/${ctx.repo}/automated-security-fixes`]))
20382
+ },
20383
+ {
20384
+ id: "secret-scanning",
20385
+ label: "Secret scanning (finds keys pushed to GitHub)",
20386
+ impact: "GitHub alerts you when a key or password is pushed to the repo.",
20387
+ read: secretState("secret_scanning"),
20388
+ enable: enableSecret("secret_scanning")
20389
+ },
20390
+ {
20391
+ id: "push-protection",
20392
+ label: "Push protection (blocks pushes that contain a key)",
20393
+ impact: "A push that contains a key is refused until you remove it.",
20394
+ read: secretState("secret_scanning_push_protection"),
20395
+ enable: enableSecret("secret_scanning_push_protection")
20396
+ },
20397
+ {
20398
+ id: "private-reporting",
20399
+ label: "Private vulnerability reporting",
20400
+ impact: "Security researchers can report a problem to you privately instead of in a public issue.",
20401
+ read: async (ctx) => ctx.isPrivate ? { state: "unavailable", why: PUBLIC_ONLY } : parse3(await ctx.get(`repos/${ctx.repo}/private-vulnerability-reporting`))?.enabled ? { state: "on" } : { state: "off" },
20402
+ enable: async (ctx) => done(await ctx.gh(["api", "-X", "PUT", `repos/${ctx.repo}/private-vulnerability-reporting`]))
20403
+ },
20404
+ {
20405
+ id: "protect-main",
20406
+ label: "Protect the main branch (pull requests and passing checks required, no force pushes or deletion)",
20407
+ impact: "You can\u2019t push straight to main any more: changes go through pull requests (you can still merge your own) once the checks pass.",
20408
+ async read(ctx) {
20409
+ const rules = parse3(await ctx.get(`repos/${ctx.repo}/rules/branches/${ctx.defaultBranch}`));
20410
+ const protection = await ctx.get(`repos/${ctx.repo}/branches/${ctx.defaultBranch}/protection`);
20411
+ if (Array.isArray(rules) && rules.some((r) => r.type === "pull_request") || protection.code === 0) return { state: "on" };
20412
+ if (ctx.isPrivate && /Upgrade to GitHub Pro/i.test(protection.stderr)) return { state: "unavailable", why: PAID_RULES };
20413
+ return { state: "off" };
20414
+ },
20415
+ async enable(ctx) {
20416
+ const r = await ctx.gh(["api", "-X", "POST", `repos/${ctx.repo}/rulesets`, "--input", "-"], JSON.stringify(protectMainRuleset(await ciChecks(ctx))));
20417
+ return r.code === 0 ? { ok: true } : { ok: false, unavailable: ctx.isPrivate && needsUpgrade(r) ? PAID_RULES : void 0 };
20418
+ }
20419
+ }
20420
+ ],
20421
+ async explain(ctx, toTurnOn) {
20422
+ if (!toTurnOn.includes("protect-main")) return [];
20423
+ const checks = await ciChecks(ctx);
20424
+ return checks.length ? [`Pull requests to main will need these checks to pass: ${checks.join(", ")}.`] : [];
20425
+ },
20426
+ async leftForYou(ctx, results) {
20427
+ const left = ["Turn on two-factor login for your GitHub account: https://github.com/settings/security"];
20428
+ const r = await ctx.gh(["api", `repos/${ctx.repo}/collaborators`, "--jq", '.[] | "\\(.login) (\\(.role_name))"']);
20429
+ const people = r.code === 0 ? r.stdout.split("\n").filter(Boolean) : [];
20430
+ if (people.length)
20431
+ left.push(`People with access: ${people.join(", ")}. Remove anyone who no longer needs it: https://github.com/${ctx.repo}/settings/access`);
20432
+ if (results.some((x) => x.id === "secret-scanning" && x.result === "unavailable"))
20433
+ left.push("Aura\u2019s own secret check still runs on every `aura scan`, and before each commit if you add the pre-commit hook.");
20434
+ return left;
20435
+ }
20436
+ };
20437
+
20438
+ // src/connectors/supabase.ts
20439
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
20440
+ import os from "os";
20441
+ import path3 from "path";
20442
+ var TOKEN = /^sbp_(?:oauth_|v0_)?[a-f0-9]{40}$/;
20443
+ function decodeToken(raw) {
20444
+ const value = raw?.trim() ?? "";
20445
+ const token = value.startsWith("go-keyring-base64:") ? Buffer.from(value.slice("go-keyring-base64:".length), "base64").toString("utf8").trim() : value;
20446
+ return TOKEN.test(token) ? token : null;
20447
+ }
20448
+ function windowsCredentialScript(target) {
20449
+ return `
20450
+ $sig = @'
20451
+ [DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredRead(string target, int type, int flags, out IntPtr cred);
20452
+ [DllImport("advapi32.dll")] public static extern void CredFree(IntPtr cred);
20453
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] public struct CREDENTIAL { public int Flags; public int Type; public string TargetName; public string Comment; public long LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist; public int AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName; }
20454
+ '@
20455
+ Add-Type -MemberDefinition $sig -Namespace AuraCred -Name Win
20456
+ $p = [IntPtr]::Zero
20457
+ if ([AuraCred.Win]::CredRead('${target}', 1, 0, [ref]$p)) {
20458
+ $c = [Runtime.InteropServices.Marshal]::PtrToStructure($p, [type][AuraCred.Win+CREDENTIAL])
20459
+ $b = New-Object byte[] $c.CredentialBlobSize
20460
+ [Runtime.InteropServices.Marshal]::Copy($c.CredentialBlob, $b, 0, $b.Length)
20461
+ [Text.Encoding]::UTF8.GetString($b)
20462
+ [AuraCred.Win]::CredFree($p)
20463
+ }`;
20464
+ }
20465
+ async function supabaseToken() {
20466
+ const fromEnv = decodeToken(process.env.SUPABASE_ACCESS_TOKEN);
20467
+ if (fromEnv) return fromEnv;
20468
+ const home = path3.join(os.homedir(), ".supabase");
20469
+ const profile = process.env.SUPABASE_PROFILE || (existsSync2(path3.join(home, "profile")) ? readFileSync2(path3.join(home, "profile"), "utf8").trim() : "") || "supabase";
20470
+ for (const account of [profile, "access-token"]) {
20471
+ const r = process.platform === "darwin" ? await run("security", ["find-generic-password", "-s", "Supabase CLI", "-a", account, "-w"]) : process.platform === "win32" ? await run("powershell", [
20472
+ "-NoProfile",
20473
+ "-NonInteractive",
20474
+ "-EncodedCommand",
20475
+ Buffer.from(windowsCredentialScript(`Supabase CLI:${account}`), "utf16le").toString("base64")
20476
+ ]) : await run("secret-tool", ["lookup", "service", "Supabase CLI", "username", account]);
20477
+ const token = r.code === 0 ? decodeToken(r.stdout) : null;
20478
+ if (token) return token;
20479
+ }
20480
+ const file2 = path3.join(home, "access-token");
20481
+ return existsSync2(file2) ? decodeToken(readFileSync2(file2, "utf8")) : null;
20482
+ }
20483
+ var realApi = (token) => async (method, endpoint, body) => {
20484
+ const res = await fetch(`https://api.supabase.com${endpoint}`, {
20485
+ method,
20486
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
20487
+ body: body ? JSON.stringify(body) : void 0,
20488
+ signal: AbortSignal.timeout(3e4)
20489
+ });
20490
+ return { status: res.status, json: await res.json().catch(() => null) };
20491
+ };
20492
+ var PRO_ONLY = "needs a paid Supabase plan (Pro or higher)";
20493
+ var authConfig = (ctx) => ctx.get(`/v1/projects/${ctx.ref}/config/auth`);
20494
+ async function patchAuth(ctx, body) {
20495
+ const r = await ctx.api("PATCH", `/v1/projects/${ctx.ref}/config/auth`, body);
20496
+ if (r.status >= 200 && r.status < 300) return { ok: true };
20497
+ return { ok: false, unavailable: /plan|pro\b|upgrade|paid/i.test(JSON.stringify(r.json ?? "")) ? PRO_ONLY : void 0 };
20498
+ }
20499
+ var authFlag = (check2) => async (ctx) => check2(await authConfig(ctx) ?? {}) ? { state: "on" } : { state: "off" };
20500
+ async function advisorProblems(ctx) {
20501
+ const lints = (await ctx.get(`/v1/projects/${ctx.ref}/advisors/security`))?.lints ?? [];
20502
+ return lints.filter((l) => l.level === "ERROR" || l.level === "WARN");
20503
+ }
20504
+ var supabase = {
20505
+ id: "supabase",
20506
+ name: "Supabase",
20507
+ actionId: "supabase-security",
20508
+ detect: (scan) => scan.stack.services.includes("Supabase"),
20509
+ async connect(root, fake) {
20510
+ let api = fake;
20511
+ if (!api) {
20512
+ const token = await supabaseToken();
20513
+ if (!token)
20514
+ return {
20515
+ ok: false,
20516
+ problem: "Aura couldn\u2019t find your Supabase login. Install the Supabase CLI and run `supabase login`, then try again.",
20517
+ errorType: "CliNotSignedIn"
20518
+ };
20519
+ api = realApi(token);
20520
+ }
20521
+ const refFile = path3.join(root, "supabase", ".temp", "project-ref");
20522
+ const ref = process.env.SUPABASE_PROJECT_ID || (existsSync2(refFile) ? readFileSync2(refFile, "utf8").trim() : "");
20523
+ if (!/^[a-z]{20}$/.test(ref))
20524
+ return { ok: false, problem: "This folder isn\u2019t linked to a Supabase project yet. Run `supabase link`, then try again.", errorType: "NoProject" };
20525
+ const cache = /* @__PURE__ */ new Map();
20526
+ const call = api;
20527
+ const ctx = {
20528
+ ref,
20529
+ api: call,
20530
+ get: (endpoint) => {
20531
+ if (!cache.has(endpoint))
20532
+ cache.set(
20533
+ endpoint,
20534
+ call("GET", endpoint).then((r) => r.status === 200 ? r.json : null)
20535
+ );
20536
+ return cache.get(endpoint);
20537
+ }
20538
+ };
20539
+ const project = await ctx.get(`/v1/projects/${ref}`);
20540
+ if (!project?.name) return { ok: false, problem: `Couldn\u2019t read the Supabase project ${ref}. Check your account can open it.`, errorType: "NoProject" };
20541
+ return { ok: true, ctx, target: project.name };
20542
+ },
20543
+ settings: [
20544
+ {
20545
+ id: "email-confirmation",
20546
+ label: "Email confirmation (new users confirm their email before signing in)",
20547
+ impact: "New users must click a link in an email before they can sign in. People already signed up aren\u2019t affected.",
20548
+ read: authFlag((c) => c.mailer_autoconfirm === false),
20549
+ enable: (ctx) => patchAuth(ctx, { mailer_autoconfirm: false })
20550
+ },
20551
+ {
20552
+ id: "password-length",
20553
+ label: "Passwords of at least 8 characters",
20554
+ impact: "New and changed passwords must be at least 8 characters. Existing passwords keep working.",
20555
+ read: authFlag((c) => (c.password_min_length ?? 0) >= 8),
20556
+ enable: (ctx) => patchAuth(ctx, { password_min_length: 8 })
20557
+ },
20558
+ {
20559
+ id: "leaked-passwords",
20560
+ label: "Leaked password protection (refuses passwords found in data breaches)",
20561
+ impact: "Passwords that appear in known data breaches are refused when someone signs up or changes their password.",
20562
+ read: authFlag((c) => c.password_hibp_enabled === true),
20563
+ enable: (ctx) => patchAuth(ctx, { password_hibp_enabled: true })
20564
+ },
20565
+ {
20566
+ id: "app-mfa",
20567
+ label: "Authenticator app sign-in for your users (two-factor, optional for them)",
20568
+ impact: "Your users can protect their account with an authenticator app. It stays optional, and your app needs a screen for it before anyone can use it.",
20569
+ read: authFlag((c) => c.mfa_totp_enroll_enabled === true && c.mfa_totp_verify_enabled === true),
20570
+ enable: (ctx) => patchAuth(ctx, { mfa_totp_enroll_enabled: true, mfa_totp_verify_enabled: true })
20571
+ },
20572
+ {
20573
+ id: "secure-email-change",
20574
+ label: "Secure email change (both the old and new address confirm)",
20575
+ impact: "Changing an account\u2019s email needs confirming from both the old and the new address, so a stolen session can\u2019t take over the account.",
20576
+ read: authFlag((c) => c.mailer_secure_email_change_enabled === true),
20577
+ enable: (ctx) => patchAuth(ctx, { mailer_secure_email_change_enabled: true })
20578
+ },
20579
+ {
20580
+ id: "database-encryption",
20581
+ label: "Encrypted database connections only",
20582
+ impact: "Direct connections to your database must be encrypted. Turning this on restarts the database for a few seconds.",
20583
+ read: async (ctx) => (await ctx.get(`/v1/projects/${ctx.ref}/ssl-enforcement`))?.currentConfig?.database === true ? { state: "on" } : { state: "off" },
20584
+ async enable(ctx) {
20585
+ const r = await ctx.api("PUT", `/v1/projects/${ctx.ref}/ssl-enforcement`, { requestedConfig: { database: true } });
20586
+ return r.status >= 200 && r.status < 300 ? { ok: true } : { ok: false };
20587
+ }
20588
+ },
20589
+ {
20590
+ id: "security-advisor",
20591
+ label: "No problems in Supabase\u2019s security advisor",
20592
+ impact: "Nothing changes on its own: these are fixed in your database code, and Aura lists them below.",
20593
+ read: async (ctx) => (await advisorProblems(ctx)).length ? { state: "off" } : { state: "on" },
20594
+ enable: async () => ({ ok: false, unavailable: "fixed in your database code (see Left for you)" })
20595
+ }
20596
+ ],
20597
+ async leftForYou(ctx, results) {
20598
+ const left = [];
20599
+ const problems = await advisorProblems(ctx);
20600
+ if (problems.length) {
20601
+ const titles = [...new Set(problems.map((p2) => p2.title ?? p2.name ?? "Security problem"))].slice(0, 5);
20602
+ left.push(
20603
+ `Supabase\u2019s security advisor found ${problems.length} problem(s): ${titles.join("; ")}. Fix them in a migration (\`aura fix turn-on-rls\` covers missing row-level security), or see https://supabase.com/dashboard/project/${ctx.ref}/advisors/security`
20604
+ );
20605
+ }
20606
+ if (results.some((r) => r.id === "leaked-passwords" && r.result === "unavailable"))
20607
+ left.push("Leaked password protection needs Supabase Pro. Until then, the 8-character minimum helps.");
20608
+ left.push("Turn on two-factor login for your Supabase account: Supabase dashboard \u2192 Account \u2192 Security.");
20609
+ return left;
20610
+ }
20611
+ };
20612
+
20613
+ // src/connectors/vercel.ts
20614
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
20615
+ import path4 from "path";
20616
+ var PUBLIC_PREFIX = /^(?:NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_|PUBLIC_)/;
20617
+ var SECRET_NAME = /SECRET|SERVICE_ROLE|PRIVATE|PASSWORD|_TOKEN$|API_KEY$/;
20618
+ var parse4 = (r) => {
20619
+ try {
20620
+ return r.code === 0 ? JSON.parse(r.stdout) : null;
20621
+ } catch {
20622
+ return null;
20623
+ }
20624
+ };
20625
+ async function exposedSecrets(ctx) {
20626
+ const envs = (await ctx.get(`/v10/projects/${ctx.projectId}/env?decrypt=true${ctx.team.replace("?", "&")}`))?.envs ?? [];
20627
+ return [...new Set(envs.filter((e) => PUBLIC_PREFIX.test(e.key) && (SECRET_NAME.test(e.key) || e.value && containsSecret(e.value))).map((e) => e.key))];
20628
+ }
20629
+ async function patchProject(ctx, body) {
20630
+ const r = await ctx.vercel(["api", `/v9/projects/${ctx.projectId}${ctx.team}`, "-X", "PATCH", "--input", "-", "--raw"], JSON.stringify(body));
20631
+ if (r.code === 0) return { ok: true };
20632
+ return { ok: false, unavailable: /plan|upgrade|pro\b|enterprise/i.test(r.stderr + r.stdout) ? "not available on your Vercel plan" : void 0 };
20633
+ }
20634
+ var vercel = {
20635
+ id: "vercel",
20636
+ name: "Vercel",
20637
+ actionId: "vercel-security",
20638
+ detect: (scan) => scan.stack.services.includes("Vercel"),
20639
+ async connect(root, fake) {
20640
+ const cli = fake ?? ((args, input2) => run("vercel", args, { cwd: root, input: input2, timeoutMs: 6e4 }));
20641
+ if (!fake) {
20642
+ if (!await commandExists("vercel"))
20643
+ return {
20644
+ ok: false,
20645
+ problem: "The Vercel CLI isn\u2019t installed. Install it with `npm install -g vercel`, then run `vercel login`.",
20646
+ errorType: "CliNotInstalled"
20647
+ };
20648
+ if ((await cli(["whoami"])).code !== 0)
20649
+ return { ok: false, problem: "The Vercel CLI isn\u2019t signed in. Run `vercel login`, then try again.", errorType: "CliNotSignedIn" };
20650
+ }
20651
+ const linked = [path4.join(root, ".vercel", "project.json"), ...["apps", "packages"].flatMap((d) => findLinks(path4.join(root, d)))].find(
20652
+ (f) => existsSync3(f)
20653
+ );
20654
+ let project = null;
20655
+ try {
20656
+ project = linked ? JSON.parse(readFileSync3(linked, "utf8")) : null;
20657
+ } catch {
20658
+ project = null;
20659
+ }
20660
+ if (!project?.projectId)
20661
+ return { ok: false, problem: "This folder isn\u2019t linked to a Vercel project yet. Run `vercel link`, then try again.", errorType: "NoProject" };
20662
+ const cache = /* @__PURE__ */ new Map();
20663
+ const ctx = {
20664
+ projectId: project.projectId,
20665
+ team: project.orgId?.startsWith("team_") ? `?teamId=${project.orgId}` : "",
20666
+ vercel: cli,
20667
+ get: (endpoint) => {
20668
+ if (!cache.has(endpoint)) cache.set(endpoint, cli(["api", endpoint, "--raw"]).then(parse4));
20669
+ return cache.get(endpoint);
20670
+ }
20671
+ };
20672
+ const name = (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.name;
20673
+ if (!name)
20674
+ return { ok: false, problem: "Couldn\u2019t read this project from Vercel. Check you have access to it with `vercel project ls`.", errorType: "NoProject" };
20675
+ return { ok: true, ctx, target: name };
20676
+ },
20677
+ settings: [
20678
+ {
20679
+ id: "preview-protection",
20680
+ label: "Protect preview links (only your Vercel team can open them)",
20681
+ impact: "Preview and deployment links need a Vercel login to open. Your live site on your own domain stays public.",
20682
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.ssoProtection ? { state: "on" } : { state: "off" },
20683
+ enable: (ctx) => patchProject(ctx, { ssoProtection: { deploymentType: "all_except_custom_domains" } })
20684
+ },
20685
+ {
20686
+ id: "git-fork-protection",
20687
+ label: "Fork protection (pull requests from forks don\u2019t deploy automatically)",
20688
+ impact: "A pull request from someone else\u2019s copy of your repo only deploys after you approve it, so it can\u2019t read your settings.",
20689
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.gitForkProtection ? { state: "on" } : { state: "off" },
20690
+ enable: (ctx) => patchProject(ctx, { gitForkProtection: true })
20691
+ },
20692
+ {
20693
+ id: "protected-sourcemaps",
20694
+ label: "Protected source maps (your original code isn\u2019t served to the public)",
20695
+ impact: "Source maps, which reveal your original code, are only served to people signed in to your Vercel team.",
20696
+ read: async (ctx) => (await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`))?.protectedSourcemaps ? { state: "on" } : { state: "off" },
20697
+ enable: (ctx) => patchProject(ctx, { protectedSourcemaps: true })
20698
+ },
20699
+ {
20700
+ id: "no-public-secrets",
20701
+ label: "No secret keys in browser-visible settings (NEXT_PUBLIC_\u2026 and similar)",
20702
+ impact: "Nothing changes on its own: Aura can\u2019t move a key for you, and lists what to do below.",
20703
+ read: async (ctx) => (await exposedSecrets(ctx)).length ? { state: "off" } : { state: "on" },
20704
+ enable: async () => ({ ok: false, unavailable: "needs you to move the key (see Left for you)" })
20705
+ }
20706
+ ],
20707
+ async leftForYou(ctx) {
20708
+ const left = [];
20709
+ const exposed = await exposedSecrets(ctx);
20710
+ if (exposed.length)
20711
+ left.push(
20712
+ `These settings are visible to anyone using your site: ${exposed.join(", ")}. Rename each without the public prefix, read it only in server code, then rotate the key in its dashboard (\`aura guide rotate-generic-key\`).`
20713
+ );
20714
+ left.push("Turn on two-factor login for your Vercel account: Vercel \u2192 Account Settings \u2192 Authentication.");
20715
+ return left;
20716
+ }
20717
+ };
20718
+ function findLinks(dir) {
20719
+ try {
20720
+ return readdirSync2(dir).map((d) => path4.join(dir, d, ".vercel", "project.json"));
20721
+ } catch {
20722
+ return [];
20723
+ }
20724
+ }
20725
+
20726
+ // src/connectors/index.ts
20727
+ var CONNECTORS = [github, supabase, vercel];
20728
+ function connectorForAction(actionId) {
20729
+ return CONNECTORS.find((c) => c.actionId === actionId);
20730
+ }
20731
+
20732
+ // src/templates.ts
20733
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
20734
+ import path7 from "path";
20735
+
20143
20736
  // src/core/config.ts
20737
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
20738
+ import os2 from "os";
20739
+ import path5 from "path";
20144
20740
  var DEFAULT_CONFIG = {
20145
20741
  version: 1,
20146
20742
  agents: [],
@@ -20149,45 +20745,45 @@ var DEFAULT_CONFIG = {
20149
20745
  ciMaxDrop: 0
20150
20746
  };
20151
20747
  function auraPath(root, ...parts) {
20152
- return path2.join(root, AURA_DIR, ...parts);
20748
+ return path5.join(root, AURA_DIR, ...parts);
20153
20749
  }
20154
20750
  function ensureAuraDir(root) {
20155
- mkdirSync(auraPath(root), { recursive: true });
20751
+ mkdirSync2(auraPath(root), { recursive: true });
20156
20752
  }
20157
20753
  function loadConfig(root) {
20158
20754
  const file2 = auraPath(root, "config.json");
20159
- if (!existsSync2(file2)) return { ...DEFAULT_CONFIG };
20755
+ if (!existsSync4(file2)) return { ...DEFAULT_CONFIG };
20160
20756
  try {
20161
- return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync2(file2, "utf8")) };
20757
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync4(file2, "utf8")) };
20162
20758
  } catch {
20163
20759
  return { ...DEFAULT_CONFIG };
20164
20760
  }
20165
20761
  }
20166
20762
  function saveConfig(root, config2) {
20167
20763
  ensureAuraDir(root);
20168
- writeFileSync(auraPath(root, "config.json"), JSON.stringify(config2, null, 2) + "\n");
20764
+ writeFileSync2(auraPath(root, "config.json"), JSON.stringify(config2, null, 2) + "\n");
20169
20765
  }
20170
20766
  function writeAuraFile(root, name, content) {
20171
20767
  ensureAuraDir(root);
20172
20768
  const file2 = auraPath(root, name);
20173
- mkdirSync(path2.dirname(file2), { recursive: true });
20174
- writeFileSync(file2, content);
20769
+ mkdirSync2(path5.dirname(file2), { recursive: true });
20770
+ writeFileSync2(file2, content);
20175
20771
  return file2;
20176
20772
  }
20177
- var homeAuraDir = () => path2.join(os.homedir(), ".aura");
20178
- var credentialsFile = () => path2.join(homeAuraDir(), "credentials.json");
20773
+ var homeAuraDir = () => path5.join(os2.homedir(), ".aura");
20774
+ var credentialsFile = () => path5.join(homeAuraDir(), "credentials.json");
20179
20775
  function getApiKey() {
20180
20776
  if (process.env.AURA_KEY) return process.env.AURA_KEY.trim();
20181
20777
  try {
20182
- const data = JSON.parse(readFileSync2(credentialsFile(), "utf8"));
20778
+ const data = JSON.parse(readFileSync4(credentialsFile(), "utf8"));
20183
20779
  return typeof data.key === "string" ? data.key : null;
20184
20780
  } catch {
20185
20781
  return null;
20186
20782
  }
20187
20783
  }
20188
20784
  function saveApiKey(key) {
20189
- mkdirSync(homeAuraDir(), { recursive: true });
20190
- writeFileSync(credentialsFile(), JSON.stringify({ key }, null, 2) + "\n", { mode: 384 });
20785
+ mkdirSync2(homeAuraDir(), { recursive: true });
20786
+ writeFileSync2(credentialsFile(), JSON.stringify({ key }, null, 2) + "\n", { mode: 384 });
20191
20787
  try {
20192
20788
  chmodSync(credentialsFile(), 384);
20193
20789
  } catch {
@@ -20197,26 +20793,27 @@ function getApiUrl(config2) {
20197
20793
  return (process.env.AURA_API_URL || config2?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
20198
20794
  }
20199
20795
  function auraBinDir() {
20200
- return path2.join(homeAuraDir(), "bin");
20796
+ return path5.join(homeAuraDir(), "bin");
20201
20797
  }
20202
20798
  var AURA_GITIGNORE = `# Aura local files: they can describe security weaknesses, so they stay on your machine.
20203
20799
  report.md
20204
20800
  scan.json
20205
20801
  history.json
20802
+ fixes.json
20206
20803
  cache/
20207
20804
  tasks/
20208
20805
  `;
20209
20806
  function ensureGitignore(root) {
20210
20807
  const file2 = auraPath(root, ".gitignore");
20211
- if (existsSync2(file2) && readFileSync2(file2, "utf8") === AURA_GITIGNORE) return false;
20808
+ if (existsSync4(file2) && readFileSync4(file2, "utf8") === AURA_GITIGNORE) return false;
20212
20809
  ensureAuraDir(root);
20213
- writeFileSync(file2, AURA_GITIGNORE);
20810
+ writeFileSync2(file2, AURA_GITIGNORE);
20214
20811
  return true;
20215
20812
  }
20216
20813
 
20217
20814
  // src/core/stack.ts
20218
- import { existsSync as existsSync3 } from "fs";
20219
- import path3 from "path";
20815
+ import { existsSync as existsSync5 } from "fs";
20816
+ import path6 from "path";
20220
20817
  function allDeps(pkg) {
20221
20818
  return { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
20222
20819
  }
@@ -20267,6 +20864,12 @@ var ENV_SERVICES = [
20267
20864
  [/^SLACK_/, "Slack"]
20268
20865
  ];
20269
20866
  var BUILTIN_ENV = /* @__PURE__ */ new Set([
20867
+ "USERPROFILE",
20868
+ "APPDATA",
20869
+ "LOCALAPPDATA",
20870
+ "SYSTEMROOT",
20871
+ "COMSPEC",
20872
+ "PROGRAMFILES",
20270
20873
  "NODE_ENV",
20271
20874
  "CI",
20272
20875
  "PORT",
@@ -20288,7 +20891,7 @@ var BUILTIN_ENV = /* @__PURE__ */ new Set([
20288
20891
  "TMPDIR",
20289
20892
  "HOSTNAME"
20290
20893
  ]);
20291
- var ENV_REF = /process\.env\.([A-Z][A-Z0-9_]*)|process\.env\[\s*['"]([A-Z][A-Z0-9_]*)['"]\s*\]|import\.meta\.env\.([A-Z][A-Z0-9_]*)|Deno\.env\.get\(\s*['"]([A-Z][A-Z0-9_]*)['"]\s*\)/g;
20894
+ var ENV_REF = /process\.env\.([A-Za-z_][A-Za-z0-9_]*)|process\.env\[\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]\s*\]|import\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)|Deno\.env\.get\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]\s*\)/g;
20292
20895
  function serviceForEnv(name) {
20293
20896
  return ENV_SERVICES.find(([re]) => re.test(name))?.[1];
20294
20897
  }
@@ -20320,7 +20923,7 @@ function findEnvRefs(files, read2) {
20320
20923
  if (!text) continue;
20321
20924
  for (const m of text.matchAll(ENV_REF)) {
20322
20925
  const name = m[1] ?? m[2] ?? m[3] ?? m[4];
20323
- if (!name || BUILTIN_ENV.has(name) || inStringLiteral(text, m.index)) continue;
20926
+ if (!name || /[a-z]/.test(name) || BUILTIN_ENV.has(name) || inStringLiteral(text, m.index)) continue;
20324
20927
  if (!map2.has(name)) map2.set(name, /* @__PURE__ */ new Set());
20325
20928
  map2.get(name).add(f);
20326
20929
  }
@@ -20334,7 +20937,7 @@ function findEnvRefs(files, read2) {
20334
20937
  }
20335
20938
  function detectStack(root, files, pkg, read2) {
20336
20939
  const deps2 = allDeps(pkg);
20337
- const has = (f) => files.includes(f) || existsSync3(path3.join(root, f));
20940
+ const has = (f) => files.includes(f) || existsSync5(path6.join(root, f));
20338
20941
  const hasDir = (d) => files.some((f) => f.startsWith(`${d}/`));
20339
20942
  let packageManager = null;
20340
20943
  const pmField = pkg?.packageManager?.split("@")[0];
@@ -20359,6 +20962,7 @@ function detectStack(root, files, pkg, read2) {
20359
20962
  }
20360
20963
  if (has("vercel.json") || has(".vercel/project.json")) services.add("Vercel");
20361
20964
  if (hasDir("supabase")) services.add("Supabase");
20965
+ if (has("vercel.json") || has(".vercel/project.json")) services.add("Vercel");
20362
20966
  services.add("GitHub");
20363
20967
  let nextjs = null;
20364
20968
  if (deps2.next) {
@@ -20368,9 +20972,9 @@ function detectStack(root, files, pkg, read2) {
20368
20972
  const pages = hasDir(`${base}pages`);
20369
20973
  nextjs = { version: deps2.next.replace(/^[^\d]*/, ""), router: app && pages ? "both" : app ? "app" : pages ? "pages" : null, srcDir };
20370
20974
  }
20371
- let supabase = null;
20975
+ let supabase2 = null;
20372
20976
  if (services.has("Supabase")) {
20373
- supabase = { migrationsDir: hasDir("supabase/migrations") ? "supabase/migrations" : null, hasConfig: has("supabase/config.toml") };
20977
+ supabase2 = { migrationsDir: hasDir("supabase/migrations") ? "supabase/migrations" : null, hasConfig: has("supabase/config.toml") };
20374
20978
  }
20375
20979
  let testRunner = null;
20376
20980
  if (deps2.vitest || files.some((f) => /^vitest\.config\./.test(f))) testRunner = "vitest";
@@ -20385,7 +20989,7 @@ function detectStack(root, files, pkg, read2) {
20385
20989
  frameworks,
20386
20990
  services: [...services].sort(),
20387
20991
  nextjs,
20388
- supabase,
20992
+ supabase: supabase2,
20389
20993
  typescript: has("tsconfig.json"),
20390
20994
  testRunner,
20391
20995
  envVars,
@@ -20429,7 +21033,7 @@ function pmCommands(pm) {
20429
21033
  // package.json
20430
21034
  var package_default = {
20431
21035
  name: "getaura",
20432
- version: "0.1.6",
21036
+ version: "0.1.8",
20433
21037
  description: "Aura helps you and your coding agent follow best practices while you vibe code.",
20434
21038
  type: "module",
20435
21039
  bin: {
@@ -20438,7 +21042,9 @@ var package_default = {
20438
21042
  files: [
20439
21043
  "dist",
20440
21044
  "templates",
20441
- "README.md"
21045
+ "README.md",
21046
+ ".claude-plugin",
21047
+ "plugin"
20442
21048
  ],
20443
21049
  engines: {
20444
21050
  node: ">=22"
@@ -20508,19 +21114,19 @@ function fill(text, vars) {
20508
21114
  return text.replace(/\{\{([a-z_]+)\}\}/g, (m, name) => KNOWN.has(name) ? vars[name] ?? "" : m);
20509
21115
  }
20510
21116
  function readTemplate(rel) {
20511
- return readFileSync3(path4.join(templatesDir(), rel), "utf8");
21117
+ return readFileSync5(path7.join(templatesDir(), rel), "utf8");
20512
21118
  }
20513
21119
  function render(rel, vars) {
20514
21120
  return fill(readTemplate(rel), vars);
20515
21121
  }
20516
21122
  function templateExists(rel) {
20517
- return existsSync4(path4.join(templatesDir(), rel));
21123
+ return existsSync6(path7.join(templatesDir(), rel));
20518
21124
  }
20519
21125
  function briefSummary(root, pkg) {
20520
- const fallbackName = pkg?.name ? pkg.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : path4.basename(root);
21126
+ const fallbackName = pkg?.name ? pkg.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : path7.basename(root);
20521
21127
  const file2 = auraPath(root, "brief.md");
20522
- if (!existsSync4(file2)) return { name: fallbackName, summary: pkg?.description ?? "See `.aura/brief.md` once the product brief is added." };
20523
- const text = readFileSync3(file2, "utf8");
21128
+ if (!existsSync6(file2)) return { name: fallbackName, summary: pkg?.description ?? "See `.aura/brief.md` once the product brief is added." };
21129
+ const text = readFileSync5(file2, "utf8");
20524
21130
  const title = text.match(/^#\s+(.+)$/m)?.[1]?.replace(/[:\-–—]?\s*product brief$/i, "").trim();
20525
21131
  const paragraph = text.split(/\n\s*\n/).map((p2) => p2.trim()).find((p2) => p2 && !p2.startsWith("#") && !p2.startsWith(">") && !p2.startsWith("-") && !p2.startsWith("|") && p2.length > 40) ?? "";
20526
21132
  const sentences = paragraph.replace(/\s+/g, " ").match(/[^.!?]+[.!?]+/g) ?? [paragraph];
@@ -20553,7 +21159,7 @@ function templateVars(root, scan, pkg, extra = {}) {
20553
21159
  scriptLine(pkg, cmds.run, "lint", "lint the code"),
20554
21160
  scriptLine(pkg, cmds.run, "typecheck", "check types") ?? (scan.stack.typescript ? `- \`${cmds.exec} tsc --noEmit\`: check types` : null),
20555
21161
  scriptLine(pkg, cmds.run, "build", "production build"),
20556
- "- `aura scan`: check the Aura score"
21162
+ "- `aura scan`: check the Aura score (coding agents: `aura scan --json`, which never waits for input)"
20557
21163
  ].filter(Boolean);
20558
21164
  const nodeMajor = pkg?.engines?.node?.match(/(\d{2})/)?.[1];
20559
21165
  return {
@@ -20685,7 +21291,6 @@ var ACTIONS = [
20685
21291
  },
20686
21292
  // Accounts and ownership
20687
21293
  { id: "inventory", title: "Complete the keys and accounts inventory", category: "security", delivery: "checklist", resolves: [] },
20688
- { id: "github-security", title: "Enable GitHub security features", category: "security", delivery: "guide", resolves: [] },
20689
21294
  { id: "transfer-ownership", title: "Move accounts to a company owner", category: "security", delivery: "guide", resolves: [] }
20690
21295
  ];
20691
21296
  var FOUNDATION_BUNDLE = ["add-brief", "add-agent-rules", "install-skills", "token-efficiency", "add-ci", "add-env-example", "add-aura-workflow"];
@@ -20700,6 +21305,15 @@ function getAction(id) {
20700
21305
  };
20701
21306
  }
20702
21307
  if (/^rotate-[a-z]+-(?:key|token)$/.test(id)) return { id, title: "Rotate an exposed key", category: "security", delivery: "guide", resolves: [] };
21308
+ const connector = connectorForAction(id);
21309
+ if (connector)
21310
+ return {
21311
+ id,
21312
+ title: `Turn on ${connector.name} security features`,
21313
+ category: "security",
21314
+ delivery: "guide",
21315
+ resolves: [`security.${connector.id}-settings`]
21316
+ };
20703
21317
  return ACTIONS.find((a) => a.id === id);
20704
21318
  }
20705
21319
  var CATEGORY_PRIORITY = ["security", "agent-setup", "foundations", "structure"];
@@ -20735,15 +21349,16 @@ function nextSteps(scan, opts = {}) {
20735
21349
  if (!action2) continue;
20736
21350
  const { delivery, fellBack } = effectiveDelivery(action2, scan);
20737
21351
  const worst = fs.map((f) => f.severity).sort((a, b) => SEVERITY_ORDER.indexOf(a) - SEVERITY_ORDER.indexOf(b))[0];
21352
+ const connected = Boolean(connectorForAction(actionId));
20738
21353
  steps.push({
20739
21354
  actionId,
20740
21355
  title: action2.title,
20741
21356
  delivery,
20742
- effort: EFFORT[delivery],
21357
+ effort: connected ? "one-click" : EFFORT[delivery],
20743
21358
  category: action2.category,
20744
21359
  why: fs[0].why,
20745
21360
  impact: impactOf(scan.checks, action2.resolves),
20746
- command: commandFor(actionId, delivery),
21361
+ command: connected ? `aura apply ${actionId}` : commandFor(actionId, delivery),
20747
21362
  findingIds: fs.map((f) => f.id),
20748
21363
  fellBack,
20749
21364
  severity: worst
@@ -20768,16 +21383,16 @@ function nextSteps(scan, opts = {}) {
20768
21383
  severity: "critical"
20769
21384
  });
20770
21385
  }
20771
- if (opts.accounts && !opts.accounts.githubSecurity) {
21386
+ for (const connector of opts.accounts ? CONNECTORS.filter((c) => c.detect(scan) && !opts.accounts.secured[c.id] && !steps.some((s) => s.actionId === c.actionId)) : []) {
20772
21387
  steps.push({
20773
- actionId: "github-security",
20774
- title: "Enable GitHub security features",
21388
+ actionId: connector.actionId,
21389
+ title: `Turn on ${connector.name} security features`,
20775
21390
  delivery: "guide",
20776
- effort: "follow-guide",
21391
+ effort: "one-click",
20777
21392
  category: "security",
20778
- why: "Branch protection and secret scanning stop mistakes (and leaked keys) before they reach your main branch.",
21393
+ why: `Aura checks ${connector.name}\u2019s security settings and, once you agree, turns on the ones that are off.`,
20779
21394
  impact: 0,
20780
- command: "aura guide github-security",
21395
+ command: `aura apply ${connector.actionId}`,
20781
21396
  findingIds: [],
20782
21397
  severity: "low"
20783
21398
  });
@@ -20826,12 +21441,12 @@ function nextSteps(scan, opts = {}) {
20826
21441
  }
20827
21442
 
20828
21443
  // src/actions/generators.ts
20829
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
20830
- import path8 from "path";
21444
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
21445
+ import path12 from "path";
20831
21446
 
20832
21447
  // src/checks/agent-setup.ts
20833
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
20834
- import path5 from "path";
21448
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
21449
+ import path8 from "path";
20835
21450
 
20836
21451
  // src/checks/types.ts
20837
21452
  import { createHash } from "crypto";
@@ -20865,38 +21480,35 @@ function lineOf(text, index) {
20865
21480
 
20866
21481
  // src/checks/agent-setup.ts
20867
21482
  function exists(ctx, file2) {
20868
- return existsSync5(path5.join(ctx.root, file2));
21483
+ return existsSync7(path8.join(ctx.root, file2));
20869
21484
  }
20870
21485
  function readDisk(ctx, file2) {
20871
21486
  try {
20872
- return readFileSync4(path5.join(ctx.root, file2), "utf8");
21487
+ return readFileSync6(path8.join(ctx.root, file2), "utf8");
20873
21488
  } catch {
20874
21489
  return "";
20875
21490
  }
20876
21491
  }
20877
21492
  function skillsIn(ctx, dir) {
20878
21493
  try {
20879
- return readdirSync2(path5.join(ctx.root, dir), { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync5(path5.join(ctx.root, dir, d.name, "SKILL.md"))).map((d) => d.name);
21494
+ return readdirSync3(path8.join(ctx.root, dir), { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync7(path8.join(ctx.root, dir, d.name, "SKILL.md"))).map((d) => d.name);
20880
21495
  } catch {
20881
21496
  return [];
20882
21497
  }
20883
21498
  }
20884
21499
  function targetAgents(ctx) {
20885
21500
  if (ctx.config.agents.length) return ctx.config.agents;
20886
- const seen = [];
20887
- if (exists(ctx, "CLAUDE.md") || exists(ctx, ".claude")) seen.push("claude");
20888
- if (exists(ctx, "AGENTS.md")) seen.push("codex");
20889
- if (exists(ctx, ".cursor") || exists(ctx, ".cursorrules")) seen.push("cursor");
20890
- if (exists(ctx, ".pi")) seen.push("pi");
20891
- return seen;
21501
+ return detectAgentsFrom((rel) => exists(ctx, rel));
20892
21502
  }
20893
21503
  function rulesFileFor(ctx, agent) {
20894
21504
  const preferred = AGENT_LAYOUT[agent].rulesFile;
20895
21505
  if (exists(ctx, preferred)) return preferred;
21506
+ if (agent === "claude" && exists(ctx, ".claude/CLAUDE.md")) return ".claude/CLAUDE.md";
21507
+ if (agent === "pi" && exists(ctx, "CLAUDE.md")) return "CLAUDE.md";
20896
21508
  if (agent === "cursor") {
20897
21509
  if (exists(ctx, ".cursorrules")) return ".cursorrules";
20898
21510
  try {
20899
- const rules = readdirSync2(path5.join(ctx.root, ".cursor/rules")).filter((f) => /\.mdc?$/.test(f));
21511
+ const rules = readdirSync3(path8.join(ctx.root, ".cursor/rules")).filter((f) => /\.mdc?$/.test(f));
20900
21512
  if (rules.length) return `.cursor/rules/${rules[0]}`;
20901
21513
  } catch {
20902
21514
  }
@@ -20973,7 +21585,7 @@ var skills = {
20973
21585
  const agents = targetAgents(ctx);
20974
21586
  const dirs = agents.length ? uniqueSkillDirs(agents) : ALL_SKILLS_DIRS;
20975
21587
  const starterNames = new Set(STARTER_SKILLS.map((s) => s.name));
20976
- const perDir = dirs.map((d) => ({ dir: d, skills: skillsIn(ctx, d).filter((s) => s !== "aura") }));
21588
+ const perDir = dirs.map((d) => ({ dir: d, skills: skillsIn(ctx, d).filter((s) => s !== "aura" && !s.startsWith("aura-")) }));
20977
21589
  const best = Math.max(0, ...perDir.map((p2) => p2.skills.length));
20978
21590
  const weakest = agents.length ? Math.min(...perDir.map((p2) => p2.skills.length)) : best;
20979
21591
  if (weakest >= 3) return { status: "pass", summary: `${weakest}+ skills installed.` };
@@ -21023,13 +21635,13 @@ var brief = {
21023
21635
  weight: 5,
21024
21636
  run(ctx) {
21025
21637
  const file2 = BRIEF_PATHS.find((f) => exists(ctx, f));
21026
- const words = file2 ? readDisk(ctx, file2).split(/\s+/).filter(Boolean).length : 0;
21638
+ const words2 = file2 ? readDisk(ctx, file2).split(/\s+/).filter(Boolean).length : 0;
21027
21639
  const committed = file2 && ctx.tracked ? ctx.tracked.has(file2) : true;
21028
- if (file2 && words >= 80 && committed) return { status: "pass", summary: `${file2} (${words} words).` };
21640
+ if (file2 && words2 >= 80 && committed) return { status: "pass", summary: `${file2} (${words2} words).` };
21029
21641
  const finding = makeFinding(this, {
21030
21642
  severity: "medium",
21031
21643
  title: !file2 ? "No product brief" : !committed ? "Product brief isn\u2019t committed" : "Product brief is too short",
21032
- detail: !file2 ? "There is no product brief describing what the app does, who it\u2019s for and what data it handles." : !committed ? `${file2} exists on your machine but isn\u2019t committed, so your agent in other sessions and other people can\u2019t see it.` : `${file2} has only ${words} words.`,
21644
+ detail: !file2 ? "There is no product brief describing what the app does, who it\u2019s for and what data it handles." : !committed ? `${file2} exists on your machine but isn\u2019t committed, so your agent in other sessions and other people can\u2019t see it.` : `${file2} has only ${words2} words.`,
21033
21645
  why: "The brief grounds every agent session in what you\u2019re actually building, and becomes page one of any handoff to engineers.",
21034
21646
  locations: file2 ? [{ file: file2 }] : [],
21035
21647
  actionId: "add-brief",
@@ -21041,7 +21653,7 @@ var brief = {
21041
21653
  var agentChecks = [rulesFile, rulesCurrent, skills, auraSkill, brief];
21042
21654
 
21043
21655
  // src/core/graph.ts
21044
- import path6 from "path";
21656
+ import path9 from "path";
21045
21657
  var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
21046
21658
  var IMPORT_RE = /(?:import|export)\s+(?:type\s+)?(?:[\w*{}\s,$]+?\s+from\s+)?(['"])([^'"\n]+)\1|import\s*\(\s*(['"])([^'"\n]+)\3\s*\)|require\s*\(\s*(['"])([^'"\n]+)\5\s*\)/g;
21047
21659
  function parseImports(text) {
@@ -21069,7 +21681,7 @@ function readAliases(read2) {
21069
21681
  for (const [key, targets] of Object.entries(json2.compilerOptions?.paths ?? {})) {
21070
21682
  const target = targets[0];
21071
21683
  if (!key.endsWith("/*") || !target?.endsWith("/*")) continue;
21072
- const resolved = path6.posix.normalize(path6.posix.join(base || ".", target.slice(0, -1)));
21684
+ const resolved = path9.posix.normalize(path9.posix.join(base || ".", target.slice(0, -1)));
21073
21685
  entries.push([key.slice(0, -1), resolved === "." || resolved === "./" ? "" : resolved.replace(/\/?$/, "/")]);
21074
21686
  }
21075
21687
  } catch {
@@ -21080,10 +21692,10 @@ function readAliases(read2) {
21080
21692
  }
21081
21693
  function resolveImport(from, spec, files, aliases) {
21082
21694
  let base = null;
21083
- if (spec.startsWith(".")) base = path6.posix.normalize(path6.posix.join(path6.posix.dirname(from), spec));
21695
+ if (spec.startsWith(".")) base = path9.posix.normalize(path9.posix.join(path9.posix.dirname(from), spec));
21084
21696
  else {
21085
21697
  const alias = aliases.entries.find(([prefix]) => spec.startsWith(prefix));
21086
- if (alias) base = path6.posix.normalize(alias[1] + spec.slice(alias[0].length));
21698
+ if (alias) base = path9.posix.normalize(alias[1] + spec.slice(alias[0].length));
21087
21699
  }
21088
21700
  if (base === null) return null;
21089
21701
  const withoutJs = base.replace(/\.(?:js|jsx|mjs|cjs)$/, "");
@@ -21128,12 +21740,64 @@ function unreachableFiles(files, read2, pkgScripts) {
21128
21740
  return source.filter((f) => !seen.has(f));
21129
21741
  }
21130
21742
 
21743
+ // src/actions/agent-commands.ts
21744
+ import { readFileSync as readFileSync7 } from "fs";
21745
+ import path10 from "path";
21746
+ var AURA_COMMANDS = ["scan", "next", "fix", "setup"];
21747
+ function invocation(agent, command) {
21748
+ return agent === "claude" ? `/aura:${command}` : agent === "codex" ? `$aura-${command}` : `/aura-${command}`;
21749
+ }
21750
+ function pluginCommand(command) {
21751
+ const text = readFileSync7(path10.join(path10.dirname(templatesDir()), "plugin", "skills", command, "SKILL.md"), "utf8").replace(/\r\n/g, "\n");
21752
+ const [, frontmatter = "", body = ""] = text.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/) ?? [];
21753
+ const description = frontmatter.match(/^description: (.+)$/m)?.[1] ?? "";
21754
+ return { description, body };
21755
+ }
21756
+ function adapt(agent, body) {
21757
+ const args = agent === "pi" ? "Fix: $@\n\n" : "Fix the action or finding the user named after the command, if any.\n\n";
21758
+ return body.replace(/\/aura:(scan|next|fix|setup)\b/g, (_, c) => invocation(agent, c)).replace("Fix: $ARGUMENTS\n\n", args).replace("--agent claude", `--agent ${agent}`);
21759
+ }
21760
+ function commandChanges(agents) {
21761
+ const changes = [];
21762
+ for (const agent of new Set(agents)) {
21763
+ for (const command of AURA_COMMANDS) {
21764
+ const { description, body } = pluginCommand(command);
21765
+ const text = adapt(agent, body);
21766
+ if (agent === "cursor") changes.push({ path: `.cursor/commands/aura-${command}.md`, content: `# Aura: ${description}
21767
+
21768
+ ${text}` });
21769
+ if (agent === "pi") changes.push({ path: `.pi/prompts/aura-${command}.md`, content: `---
21770
+ description: ${description}
21771
+ ---
21772
+
21773
+ ${text}` });
21774
+ if (agent === "codex") {
21775
+ const dir = `.agents/skills/aura-${command}`;
21776
+ changes.push({
21777
+ path: `${dir}/SKILL.md`,
21778
+ content: `---
21779
+ name: aura-${command}
21780
+ description: ${description} Only when the user runs ${invocation(agent, command)}.
21781
+ ---
21782
+
21783
+ ${text}`
21784
+ });
21785
+ changes.push({ path: `${dir}/agents/openai.yaml`, content: "policy:\n allow_implicit_invocation: false\n" });
21786
+ }
21787
+ }
21788
+ }
21789
+ return changes;
21790
+ }
21791
+ function commandList(agent) {
21792
+ return AURA_COMMANDS.map((c) => invocation(agent, c)).join(", ");
21793
+ }
21794
+
21131
21795
  // src/actions/agent-files.ts
21132
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
21133
- import path7 from "path";
21796
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
21797
+ import path11 from "path";
21134
21798
  function current(root, file2) {
21135
- const full = path7.join(root, file2);
21136
- return existsSync6(full) ? readFileSync5(full, "utf8") : null;
21799
+ const full = path11.join(root, file2);
21800
+ return existsSync8(full) ? readFileSync8(full, "utf8") : null;
21137
21801
  }
21138
21802
  function upsertBlock(existing, body) {
21139
21803
  const block = `${AURA_BLOCK_START}
@@ -21177,8 +21841,8 @@ function skillChanges(agents, names) {
21177
21841
 
21178
21842
  // src/actions/generators.ts
21179
21843
  function read(root, file2) {
21180
- const full = path8.join(root, file2);
21181
- return existsSync7(full) ? readFileSync6(full, "utf8") : null;
21844
+ const full = path12.join(root, file2);
21845
+ return existsSync9(full) ? readFileSync9(full, "utf8") : null;
21182
21846
  }
21183
21847
  function withScripts(root, scripts, overwrite = false) {
21184
21848
  const text = read(root, "package.json");
@@ -21199,13 +21863,13 @@ function findingMeta(scan, checkId, key) {
21199
21863
  return scan.checks.find((c) => c.checkId === checkId)?.findings[0]?.meta?.[key];
21200
21864
  }
21201
21865
  var addBrief = ({ root }) => {
21202
- const brief2 = BRIEF_PATHS.find((f) => existsSync7(path8.join(root, f)));
21866
+ const brief2 = BRIEF_PATHS.find((f) => existsSync9(path12.join(root, f)));
21203
21867
  if (!brief2) return { skip: "There is no product brief yet. Run `aura init` to draft and confirm it first." };
21204
21868
  const changes = [
21205
21869
  { path: brief2, content: read(root, brief2) },
21206
21870
  { path: ".aura/.gitignore", content: AURA_GITIGNORE }
21207
21871
  ];
21208
- if (existsSync7(auraPath(root, "config.json"))) changes.push({ path: ".aura/config.json", content: read(root, ".aura/config.json") });
21872
+ if (existsSync9(auraPath(root, "config.json"))) changes.push({ path: ".aura/config.json", content: read(root, ".aura/config.json") });
21209
21873
  return { changes, details: [`- Adds \`${brief2}\`, \`.aura/config.json\` and \`.aura/.gitignore\` (keeps Aura's local reports out of git)`] };
21210
21874
  };
21211
21875
  var addAgentRules = ({ root, agents, vars }) => {
@@ -21214,22 +21878,24 @@ var addAgentRules = ({ root, agents, vars }) => {
21214
21878
  };
21215
21879
  var installSkills = ({ agents, skills: skills2 }) => {
21216
21880
  const names = [.../* @__PURE__ */ new Set([...skills2 ?? STARTER_SKILLS.map((s) => s.name), "aura"])];
21217
- const changes = skillChanges(agents, names);
21218
21881
  return {
21219
- changes,
21220
- details: names.map((n) => `- \`${n}\`: ${STARTER_SKILLS.find((s) => s.name === n)?.description ?? "check the Aura score and fix findings."}`)
21882
+ changes: [...skillChanges(agents, names), ...commandChanges(agents)],
21883
+ details: [
21884
+ ...names.map((n) => `- \`${n}\`: ${STARTER_SKILLS.find((s) => s.name === n)?.description ?? "check the Aura score and fix findings."}`),
21885
+ ...agents.filter((a) => a !== "claude").map((a) => `- Aura's commands for ${AGENT_LABELS[a]}: ${commandList(a)}`)
21886
+ ]
21221
21887
  };
21222
21888
  };
21223
21889
  var tokenEfficiency = (g) => {
21224
21890
  const changes = [];
21225
- if (!existsSync7(path8.join(g.root, "docs/index.md"))) changes.push({ path: "docs/index.md", content: render("docs/index.md", g.vars) });
21891
+ if (!existsSync9(path12.join(g.root, "docs/index.md"))) changes.push({ path: "docs/index.md", content: render("docs/index.md", g.vars) });
21226
21892
  changes.push(...rulesChanges(g.root, g.agents, g.vars));
21227
21893
  return { changes, details: ["- `docs/index.md`: a map of the repo your agent reads first", "- Lean-context rules in the agent rules file"] };
21228
21894
  };
21229
21895
  var addCi = ({ root, ctx, vars }) => {
21230
21896
  const existing = ctx.files.filter((f) => /^\.github\/workflows\//.test(f));
21231
21897
  const file2 = existing.includes(".github/workflows/ci.yml") ? ".github/workflows/checks.yml" : ".github/workflows/ci.yml";
21232
- if (existsSync7(path8.join(root, file2))) return { skip: `${file2} already exists.` };
21898
+ if (existsSync9(path12.join(root, file2))) return { skip: `${file2} already exists.` };
21233
21899
  return {
21234
21900
  changes: [{ path: file2, content: render("github/workflows/ci.yml", vars) }],
21235
21901
  details: [`- \`${file2}\`: lint, type check, tests and build on every pull request`]
@@ -21253,7 +21919,7 @@ var setupTesting = ({ root, ctx, vars }) => {
21253
21919
  };
21254
21920
  };
21255
21921
  var addEnvExample = ({ root, scan }) => {
21256
- const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync7(path8.join(root, f))) ?? ".env.example";
21922
+ const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync9(path12.join(root, f))) ?? ".env.example";
21257
21923
  const existing = read(root, file2);
21258
21924
  const declared = new Set(
21259
21925
  (existing ?? "").split(/\r?\n/).map((l) => l.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1]).filter(Boolean)
@@ -21310,7 +21976,7 @@ var addLinting = ({ root, ctx }) => {
21310
21976
  return { changes, devDeps, details };
21311
21977
  };
21312
21978
  var addPreCommit = ({ root, ctx, vars }) => {
21313
- if (existsSync7(path8.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21979
+ if (existsSync9(path12.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21314
21980
  const changes = [{ path: ".husky/pre-commit", content: render("configs/husky-pre-commit", vars) }];
21315
21981
  const scripts = withScripts(root, { prepare: "husky" }, false);
21316
21982
  if (scripts) changes.push(scripts);
@@ -21325,10 +21991,10 @@ var addReadme = ({ root, ctx, vars }) => {
21325
21991
  const file2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f)) ?? "README.md";
21326
21992
  const current2 = read(root, file2);
21327
21993
  const boilerplate = current2 && /create-next-app|bootstrapped with/i.test(current2);
21328
- const words = current2?.split(/\s+/).filter(Boolean).length ?? 0;
21329
- if (current2 && !boilerplate && words >= 80) return { skip: `${file2} already describes the app.` };
21994
+ const words2 = current2?.split(/\s+/).filter(Boolean).length ?? 0;
21995
+ if (current2 && !boilerplate && words2 >= 80) return { skip: `${file2} already describes the app.` };
21330
21996
  let content = render("readme/README.md", vars);
21331
- if (current2 && !boilerplate && words > 0) content = `${content.trimEnd()}
21997
+ if (current2 && !boilerplate && words2 > 0) content = `${content.trimEnd()}
21332
21998
 
21333
21999
  ## Notes
21334
22000
 
@@ -21340,7 +22006,7 @@ ${current2.trim()}
21340
22006
  };
21341
22007
  };
21342
22008
  var enableDependencyUpdates = ({ root, vars }) => {
21343
- if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync7(path8.join(root, f)))) {
22009
+ if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync9(path12.join(root, f)))) {
21344
22010
  return { skip: "Dependabot or Renovate is already configured." };
21345
22011
  }
21346
22012
  return {
@@ -21420,7 +22086,7 @@ function planMoves(root, ctx, moves) {
21420
22086
  const alias = aliasFor(toFile);
21421
22087
  if (alias) return alias[0] + noExt.slice(alias[1].length);
21422
22088
  }
21423
- let rel = path8.posix.relative(path8.posix.dirname(fromFile), noExt);
22089
+ let rel = path12.posix.relative(path12.posix.dirname(fromFile), noExt);
21424
22090
  if (!rel.startsWith(".")) rel = `./${rel}`;
21425
22091
  return rel;
21426
22092
  };
@@ -21454,7 +22120,7 @@ function planMoves(root, ctx, moves) {
21454
22120
  }
21455
22121
  var moveMisplacedFiles = ({ root, ctx, scan }) => {
21456
22122
  const raw = findingMeta(scan, "structure.layout", "moves") ?? [];
21457
- const moves = raw.map((m) => m.split("=>")).filter(([, to]) => !existsSync7(path8.join(root, to))).slice(0, MAX_MOVES);
22123
+ const moves = raw.map((m) => m.split("=>")).filter(([, to]) => !existsSync9(path12.join(root, to))).slice(0, MAX_MOVES);
21458
22124
  if (moves.length === 0) return { skip: "No files to move (or their targets already exist)." };
21459
22125
  return { changes: planMoves(root, ctx, moves), details: moves.map(([from, to]) => `- \`${from}\` \u2192 \`${to}\``), verifyTypes: true };
21460
22126
  };
@@ -21545,7 +22211,7 @@ var AuraClient = class _AuraClient {
21545
22211
  get hasKey() {
21546
22212
  return Boolean(this.key);
21547
22213
  }
21548
- async request(method, path22, body, timeoutMs = 6e4) {
22214
+ async request(method, path27, body, timeoutMs = 6e4) {
21549
22215
  if (!this.key) throw new AuraApiError("no_key", "No pilot key");
21550
22216
  let payload;
21551
22217
  if (body !== void 0) {
@@ -21555,7 +22221,7 @@ var AuraClient = class _AuraClient {
21555
22221
  }
21556
22222
  let res;
21557
22223
  try {
21558
- res = await fetch(`${this.baseUrl}/v1/${path22}`, {
22224
+ res = await fetch(`${this.baseUrl}/v1/${path27}`, {
21559
22225
  method,
21560
22226
  headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json", "user-agent": `getaura/${AURA_VERSION}` },
21561
22227
  body: payload,
@@ -21580,81 +22246,157 @@ var AuraClient = class _AuraClient {
21580
22246
 
21581
22247
  // src/api/telemetry.ts
21582
22248
  import { createHash as createHash2 } from "crypto";
21583
- async function repoId(root) {
21584
- const r = await run("git", ["rev-list", "--max-parents=0", "HEAD"], { cwd: root });
21585
- const seed = r.code === 0 ? r.stdout.trim().split("\n")[0] : root;
21586
- return createHash2("sha256").update(seed).digest("hex").slice(0, 24);
21587
- }
21588
- async function sendTelemetry(root, client, event) {
21589
- if (!client.hasKey) return;
21590
- try {
21591
- await client.request("POST", "telemetry", { ...event, auraVersion: AURA_VERSION, repoId: await repoId(root) }, 5e3);
21592
- } catch {
21593
- }
21594
- }
21595
22249
 
21596
- // src/core/git.ts
21597
- import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
21598
- import path9 from "path";
21599
- async function git(root, ...args) {
21600
- return run("git", args, { cwd: root });
22250
+ // src/ui.ts
22251
+ import * as p from "@clack/prompts";
22252
+ import pc from "picocolors";
22253
+ function interactive() {
22254
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !inCI() && !runningAgent());
21601
22255
  }
21602
- function ghInstallHint(platform = process.platform) {
21603
- 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";
21604
- return `${how}, open a new terminal, then run \`gh auth login\`.`;
22256
+ var UserCancelled = class extends Error {
22257
+ };
22258
+ function unwrap(value) {
22259
+ if (p.isCancel(value)) {
22260
+ p.cancel("Cancelled.");
22261
+ throw new UserCancelled();
22262
+ }
22263
+ return value;
21605
22264
  }
21606
- async function ghStatus(root) {
21607
- const v = await run("gh", ["--version"]);
21608
- if (v.code !== 0) return { installed: false, authed: false, hasRemote: false };
21609
- const auth = await run("gh", ["auth", "status"], { cwd: root });
21610
- const remote = await git(root, "remote", "get-url", "origin");
21611
- return { installed: true, authed: auth.code === 0, hasRemote: remote.code === 0 && /github\.com/.test(remote.stdout) };
21612
- }
21613
- var AURA_OWNED = /^(?:\.aura\/|\.claude\/skills\/aura\/|\.agents\/skills\/aura\/)/;
21614
- async function dirtyFiles(root) {
21615
- const r = await git(root, "status", "--porcelain", "-uall");
21616
- return r.stdout.split("\n").filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, "").split(" -> ").pop()).filter((f) => !AURA_OWNED.test(f));
22265
+ function spin(enabled) {
22266
+ if (!enabled || !process.stdout.isTTY) {
22267
+ return { start: (_m) => {
22268
+ }, message: (_m) => {
22269
+ }, stop: (_m) => {
22270
+ }, error: (_m) => {
22271
+ } };
22272
+ }
22273
+ const s = p.spinner();
22274
+ return { start: (m) => s.start(m), message: (m) => s.message(m), stop: (m) => s.stop(m), error: (m) => s.error(m) };
21617
22275
  }
21618
- async function currentBranch(root) {
21619
- return (await git(root, "rev-parse", "--abbrev-ref", "HEAD")).stdout.trim();
22276
+ function printJson(value) {
22277
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
21620
22278
  }
21621
- async function freeBranchName(root, base) {
21622
- for (let i = 1; i < 50; i++) {
21623
- const name = i === 1 ? base : `${base}-${i}`;
21624
- const local = await git(root, "rev-parse", "--verify", "--quiet", `refs/heads/${name}`);
21625
- const remote = await git(root, "ls-remote", "--exit-code", "--heads", "origin", name);
21626
- if (local.code !== 0 && remote.code !== 0) return name;
22279
+ var say = {
22280
+ info: (m) => console.log(m),
22281
+ ok: (m) => console.log(`${pc.green("\u2714")} ${m}`),
22282
+ warn: (m) => console.error(`${pc.yellow("!")} ${m}`),
22283
+ error: (m) => console.error(`${pc.red("\u2716")} ${m}`),
22284
+ dim: (m) => console.log(pc.dim(m))
22285
+ };
22286
+
22287
+ // src/fix-tracking.ts
22288
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
22289
+ var FILE = "fixes.json";
22290
+ var MAX_AGE_MS = 60 * 24 * 60 * 60 * 1e3;
22291
+ var MAX_CHECKS_PER_SCAN = 5;
22292
+ function loadTrackedFixes(root) {
22293
+ try {
22294
+ return existsSync10(auraPath(root, FILE)) ? JSON.parse(readFileSync10(auraPath(root, FILE), "utf8")) : [];
22295
+ } catch {
22296
+ return [];
21627
22297
  }
21628
- return `${base}-${Date.now()}`;
21629
22298
  }
21630
- function writeChanges(root, changes) {
21631
- for (const c of changes) {
21632
- const full = path9.join(root, c.path);
21633
- if (!isInside(root, full)) throw new Error(`Refusing to write outside the repo: ${c.path}`);
21634
- if (c.content === null) rmSync(full, { force: true });
21635
- else {
21636
- mkdirSync2(path9.dirname(full), { recursive: true });
21637
- writeFileSync2(full, c.content);
21638
- }
22299
+ function save(root, fixes) {
22300
+ writeAuraFile(root, FILE, JSON.stringify(fixes, null, 2));
22301
+ }
22302
+ function trackFix(root, config2, fix) {
22303
+ if (!sharesUsage(config2)) return;
22304
+ save(root, [...loadTrackedFixes(root).filter((f) => f.branch !== fix.branch), { ...fix, openedAt: (/* @__PURE__ */ new Date()).toISOString() }]);
22305
+ }
22306
+ async function prState(root, branch) {
22307
+ const r = await run("gh", ["pr", "view", branch, "--json", "state,mergeCommit", "--jq", '[.state, .mergeCommit.oid // ""] | join(" ")'], {
22308
+ cwd: root,
22309
+ timeoutMs: 15e3
22310
+ });
22311
+ const [state, sha] = r.stdout.trim().split(" ");
22312
+ if (r.code !== 0) return null;
22313
+ if (state === "OPEN" || state === "CLOSED") return { state };
22314
+ if (state !== "MERGED") return null;
22315
+ const inCheckout = Boolean(sha) && (await run("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { cwd: root })).code === 0;
22316
+ return { state, inCheckout };
22317
+ }
22318
+ async function followUpFixes(root, config2, scan, state = prState) {
22319
+ const tracked = loadTrackedFixes(root);
22320
+ if (tracked.length === 0 || !sharesUsage(config2)) return;
22321
+ const current2 = new Set(scan.checks.flatMap((c) => c.findings.map((f) => f.id)));
22322
+ const keep = [];
22323
+ for (const [i, fix] of tracked.entries()) {
22324
+ if (Date.now() - Date.parse(fix.openedAt) > MAX_AGE_MS) continue;
22325
+ const s = i < MAX_CHECKS_PER_SCAN ? await state(root, fix.branch) : null;
22326
+ if (s?.state === "MERGED") {
22327
+ if (!fix.merged) await recordUsage(root, config2, { event: "fix_result", actionId: fix.actionId, outcome: "merged" });
22328
+ if (!s.inCheckout) {
22329
+ keep.push({ ...fix, merged: true });
22330
+ continue;
22331
+ }
22332
+ const stillThere = fix.findingIds.some((id) => current2.has(id));
22333
+ await recordUsage(root, config2, {
22334
+ event: "fix_result",
22335
+ actionId: fix.actionId,
22336
+ outcome: stillThere ? "not_resolved" : "resolved",
22337
+ scoreChange: scan.score - fix.scoreBefore
22338
+ });
22339
+ } else if (s?.state === "CLOSED") {
22340
+ await recordUsage(root, config2, { event: "fix_result", actionId: fix.actionId, outcome: "closed" });
22341
+ } else keep.push(fix);
21639
22342
  }
22343
+ save(root, keep);
21640
22344
  }
21641
- async function commitPushPr(root, opts) {
21642
- const commit = await git(root, "commit", "-m", opts.title, "-m", "Opened by Aura (https://github.com/JohnFazio1/aura).");
21643
- const base = { branch: opts.branch, url: null };
21644
- if (commit.code !== 0) {
21645
- const out = commit.stderr || commit.stdout;
21646
- const error62 = /tell me who you are|user\.email/i.test(out) ? 'git doesn\u2019t know your name and email yet. Run: git config --global user.name "Your Name" && git config --global user.email "you@example.com"' : out;
21647
- return { ...base, committed: false, pushed: false, error: error62 };
22345
+
22346
+ // src/api/telemetry.ts
22347
+ function sharesUsage(config2, key = getApiKey()) {
22348
+ if (!key) return false;
22349
+ if (config2.shareUsage !== void 0) return config2.shareUsage;
22350
+ return key.startsWith("aura_live_") || config2.telemetry === true;
22351
+ }
22352
+ function runSource() {
22353
+ return inCI() ? "ci" : interactive() ? "terminal" : "agent";
22354
+ }
22355
+ function osName() {
22356
+ return process.platform === "darwin" || process.platform === "win32" || process.platform === "linux" ? process.platform : "other";
22357
+ }
22358
+ async function repoId(root) {
22359
+ const r = await run("git", ["rev-list", "--max-parents=0", "HEAD"], { cwd: root });
22360
+ const seed = r.code === 0 ? r.stdout.trim().split("\n")[0] : root;
22361
+ return createHash2("sha256").update(seed).digest("hex").slice(0, 24);
22362
+ }
22363
+ async function recordUsage(root, config2, event) {
22364
+ if (!sharesUsage(config2)) return;
22365
+ try {
22366
+ const body = {
22367
+ ...event,
22368
+ auraVersion: AURA_VERSION,
22369
+ repoId: await repoId(root),
22370
+ source: runSource(),
22371
+ agent: runningAgent() ?? void 0,
22372
+ agents: config2.agents.length ? config2.agents : void 0,
22373
+ os: osName(),
22374
+ nodeVersion: process.version
22375
+ };
22376
+ await AuraClient.from(config2).request("POST", "telemetry", body, 5e3);
22377
+ } catch {
21648
22378
  }
21649
- if (!opts.openPr) return { ...base, committed: true, pushed: false };
21650
- const push = await git(root, "push", "-u", "origin", opts.branch);
21651
- if (push.code !== 0) return { ...base, committed: true, pushed: false, error: push.stderr };
21652
- const pr = await run("gh", ["pr", "create", "--base", opts.base, "--head", opts.branch, "--title", opts.title, "--body-file", "-"], {
21653
- cwd: root,
21654
- input: opts.body
22379
+ }
22380
+ function errorKind(err) {
22381
+ const e = err;
22382
+ const kind = typeof e?.code === "string" ? e.code : typeof e?.name === "string" ? e.name : "Error";
22383
+ return /^[A-Za-z_][A-Za-z0-9_]{0,59}$/.test(kind) ? kind : "Error";
22384
+ }
22385
+ async function recordAction(root, config2, actionId, report, startedAt, via) {
22386
+ if (!report.outcome) return;
22387
+ await recordUsage(root, config2, {
22388
+ event: "action",
22389
+ actionId,
22390
+ outcome: report.outcome,
22391
+ delivery: report.delivery,
22392
+ via,
22393
+ durationMs: Date.now() - startedAt,
22394
+ agentSteps: report.agentSteps,
22395
+ errorType: report.errorType
21655
22396
  });
21656
- if (pr.code !== 0) return { ...base, committed: true, pushed: true, error: pr.stderr };
21657
- return { ...base, url: pr.stdout.trim().split("\n").pop() ?? null, committed: true, pushed: true };
22397
+ if (report.outcome === "pr_opened" && report.branch) {
22398
+ trackFix(root, config2, { actionId, branch: report.branch, findingIds: report.findingIds ?? [], scoreBefore: report.scoreBefore ?? 0 });
22399
+ }
21658
22400
  }
21659
22401
 
21660
22402
  // src/checks/foundations.ts
@@ -21747,13 +22489,13 @@ var readme = {
21747
22489
  run(ctx) {
21748
22490
  const file2 = ctx.files.find((f) => /^readme(\.md|\.mdx|\.txt)?$/i.test(f));
21749
22491
  const text = file2 ? ctx.read(file2) ?? "" : "";
21750
- const words = text.split(/\s+/).filter(Boolean).length;
21751
- if (file2 && !BOILERPLATE.test(text) && words >= 80) return { status: "pass", summary: `${file2} (${words} words).` };
22492
+ const words2 = text.split(/\s+/).filter(Boolean).length;
22493
+ if (file2 && !BOILERPLATE.test(text) && words2 >= 80) return { status: "pass", summary: `${file2} (${words2} words).` };
21752
22494
  const boilerplate = Boolean(file2) && BOILERPLATE.test(text);
21753
22495
  const finding = makeFinding(this, {
21754
22496
  severity: "low",
21755
22497
  title: !file2 ? "No README" : boilerplate ? "README is still the starter template" : "README is very short",
21756
- detail: !file2 ? "There is no README describing what the app is, how to run it or how it is deployed." : boilerplate ? "The README is the default text from create-next-app and says nothing about this app." : `The README has only ${words} words, so it can't explain how to run or deploy the app.`,
22498
+ detail: !file2 ? "There is no README describing what the app is, how to run it or how it is deployed." : boilerplate ? "The README is the default text from create-next-app and says nothing about this app." : `The README has only ${words2} words, so it can't explain how to run or deploy the app.`,
21757
22499
  why: "A good README is the first thing a new engineer or your coding agent reads; without it they guess.",
21758
22500
  locations: file2 ? [{ file: file2 }] : [],
21759
22501
  actionId: "add-readme",
@@ -21856,10 +22598,10 @@ var typecheck = {
21856
22598
  applies: (ctx) => ctx.stack.typescript,
21857
22599
  async run(ctx) {
21858
22600
  if (ctx.options.fast) return { status: "unknown", summary: "Skipped in fast mode." };
21859
- const { existsSync: existsSync17 } = await import("fs");
21860
- const path22 = await import("path");
21861
- const tsc = path22.join(ctx.root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
21862
- if (!existsSync17(tsc)) return { status: "unknown", summary: "Dependencies not installed, so the type check was skipped." };
22601
+ const { existsSync: existsSync21 } = await import("fs");
22602
+ const path27 = await import("path");
22603
+ const tsc = path27.join(ctx.root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
22604
+ if (!existsSync21(tsc)) return { status: "unknown", summary: "Dependencies not installed, so the type check was skipped." };
21863
22605
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: ctx.root, timeoutMs: 18e4 });
21864
22606
  if (r.timedOut) return { status: "unknown", summary: "Type check timed out." };
21865
22607
  if (r.code === 0) return { status: "pass", summary: "No type errors.", engine: "tsc" };
@@ -21879,6 +22621,56 @@ var typecheck = {
21879
22621
  };
21880
22622
  var foundationChecks = [ciWorkflow, tests, readme, envExample, lockfile, lintFormat, typecheck];
21881
22623
 
22624
+ // src/connectors/snapshot.ts
22625
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
22626
+ var FILE2 = "cache/connectors.json";
22627
+ function loadSnapshots(root) {
22628
+ try {
22629
+ return existsSync11(auraPath(root, FILE2)) ? JSON.parse(readFileSync11(auraPath(root, FILE2), "utf8")) : {};
22630
+ } catch {
22631
+ return {};
22632
+ }
22633
+ }
22634
+ function saveSnapshot(root, toolId, states) {
22635
+ const all = loadSnapshots(root);
22636
+ all[toolId] = { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), settings: Object.fromEntries(Object.entries(states).map(([id, s]) => [id, s.state])) };
22637
+ writeAuraFile(root, FILE2, `${JSON.stringify(all, null, 2)}
22638
+ `);
22639
+ }
22640
+ var isStale = (snap, now = Date.now()) => !snap || now - Date.parse(snap.checkedAt) > 24 * 60 * 60 * 1e3;
22641
+
22642
+ // src/checks/security/tool-settings.ts
22643
+ function toolSettingsCheck(connector) {
22644
+ return {
22645
+ id: `security.${connector.id}-settings`,
22646
+ category: "security",
22647
+ title: `${connector.name} security settings`,
22648
+ weight: 3,
22649
+ applies: (ctx) => {
22650
+ const snap = loadSnapshots(ctx.root)[connector.id];
22651
+ return Boolean(snap && Object.values(snap.settings).some((s) => s !== "unavailable"));
22652
+ },
22653
+ run(ctx) {
22654
+ const snap = loadSnapshots(ctx.root)[connector.id];
22655
+ const available = connector.settings.filter((s) => snap.settings[s.id] === "on" || snap.settings[s.id] === "off");
22656
+ const off = available.filter((s) => snap.settings[s.id] === "off");
22657
+ const engine = `${connector.name}, checked ${snap.checkedAt.slice(0, 10)}`;
22658
+ if (off.length === 0) return { status: "pass", summary: `All ${available.length} available ${connector.name} security settings are on.`, engine };
22659
+ const finding = makeFinding(this, {
22660
+ severity: off.some((s) => /protect|push|secret/.test(s.id)) ? "medium" : "low",
22661
+ title: `${off.length} ${connector.name} security setting(s) off`,
22662
+ detail: `Off in ${connector.name}: ${off.map((s) => s.label).join("; ")}.`,
22663
+ why: "These settings catch leaked keys, vulnerable packages and risky changes before they cause harm, and they cost nothing to turn on.",
22664
+ locations: [],
22665
+ actionId: connector.actionId,
22666
+ key: `${connector.id}-settings`
22667
+ });
22668
+ const score = (available.length - off.length) / available.length;
22669
+ return { status: score === 0 ? "fail" : "partial", score, summary: finding.title, findings: [finding], engine };
22670
+ }
22671
+ };
22672
+ }
22673
+
21882
22674
  // src/checks/security/api-routes.ts
21883
22675
  var AUTH_SIGNALS = [
21884
22676
  /\bauth\.getUser\s*\(/,
@@ -21985,9 +22777,9 @@ var apiRoutes = {
21985
22777
 
21986
22778
  // src/core/tools.ts
21987
22779
  import { createHash as createHash3 } from "crypto";
21988
- import { chmodSync as chmodSync2, existsSync as existsSync8, mkdirSync as mkdirSync3, mkdtempSync, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
21989
- import os2 from "os";
21990
- import path10 from "path";
22780
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync3, mkdtempSync, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
22781
+ import os3 from "os";
22782
+ import path13 from "path";
21991
22783
  var GITLEAKS_SHA = {
21992
22784
  darwin_arm64: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
21993
22785
  darwin_x64: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
@@ -22004,13 +22796,16 @@ var OSV_SHA = {
22004
22796
  windows_amd64: "e0ed7644118b717b028c249ee9d3515024e55e8510747ca08906eb96765354d6",
22005
22797
  windows_arm64: "ca1379da0e408279ef2c85cde0846a903495738a91c7b065a208048437c1686c"
22006
22798
  };
22799
+ function tarCommand(platform = process.platform, systemRoot = process.env.SystemRoot) {
22800
+ return platform === "win32" ? path13.win32.join(systemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
22801
+ }
22007
22802
  var TOOLS = {
22008
22803
  gitleaks: {
22009
22804
  version: "8.30.1",
22010
22805
  asset(platform, arch) {
22011
22806
  const p2 = platform === "win32" ? "windows" : platform;
22012
- const a = arch === "arm64" ? "arm64" : "x64";
22013
- const sha = GITLEAKS_SHA[`${p2}_${a}`];
22807
+ const a = arch === "arm64" ? "arm64" : arch === "x64" ? "x64" : null;
22808
+ const sha = a && GITLEAKS_SHA[`${p2}_${a}`];
22014
22809
  if (!sha) return null;
22015
22810
  const archive = p2 === "windows" ? "zip" : "tar.gz";
22016
22811
  return { file: `gitleaks_8.30.1_${p2}_${a}.${archive}`, sha256: sha, archive };
@@ -22021,8 +22816,8 @@ var TOOLS = {
22021
22816
  version: "2.6.0",
22022
22817
  asset(platform, arch) {
22023
22818
  const p2 = platform === "win32" ? "windows" : platform;
22024
- const a = arch === "arm64" ? "arm64" : "amd64";
22025
- const sha = OSV_SHA[`${p2}_${a}`];
22819
+ const a = arch === "arm64" ? "arm64" : arch === "x64" ? "amd64" : null;
22820
+ const sha = a && OSV_SHA[`${p2}_${a}`];
22026
22821
  if (!sha) return null;
22027
22822
  return { file: `osv-scanner_${p2}_${a}${p2 === "windows" ? ".exe" : ""}`, sha256: sha, archive: null };
22028
22823
  },
@@ -22038,8 +22833,8 @@ function ensureTool(name, noDownload = false) {
22038
22833
  async function resolveTool(name, noDownload) {
22039
22834
  const spec = TOOLS[name];
22040
22835
  const exe = process.platform === "win32" ? `${name}.exe` : name;
22041
- const cached2 = path10.join(auraBinDir(), `${name}-${spec.version}`, exe);
22042
- if (existsSync8(cached2)) return { path: cached2, version: spec.version };
22836
+ const cached2 = path13.join(auraBinDir(), `${name}-${spec.version}`, exe);
22837
+ if (existsSync12(cached2)) return { path: cached2, version: spec.version };
22043
22838
  if (await commandExists(name)) {
22044
22839
  const v = await run(name, ["version"]);
22045
22840
  return { path: name, version: (v.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? "system").trim() };
@@ -22059,20 +22854,20 @@ async function download(name, spec, target) {
22059
22854
  const buf = Buffer.from(await res.arrayBuffer());
22060
22855
  const digest = createHash3("sha256").update(buf).digest("hex");
22061
22856
  if (digest !== asset.sha256) throw new Error(`Checksum mismatch for ${asset.file}`);
22062
- const dir = path10.dirname(target);
22857
+ const dir = path13.dirname(target);
22063
22858
  mkdirSync3(dir, { recursive: true });
22064
- const tmp = mkdtempSync(path10.join(os2.tmpdir(), `aura-${name}-`));
22859
+ const tmp = mkdtempSync(path13.join(os3.tmpdir(), `aura-${name}-`));
22065
22860
  try {
22066
22861
  if (asset.archive === null) {
22067
22862
  writeFileSync3(target, buf);
22068
22863
  } else {
22069
- const archivePath = path10.join(tmp, asset.file);
22864
+ const archivePath = path13.join(tmp, asset.file);
22070
22865
  writeFileSync3(archivePath, buf);
22071
- const r = await run("tar", ["-xf", archivePath, "-C", tmp]);
22866
+ const r = await run(tarCommand(), ["-xf", archivePath, "-C", tmp]);
22072
22867
  if (r.code !== 0) return null;
22073
- const exe = readdirSync3(tmp).find((f) => f === name || f === `${name}.exe`);
22868
+ const exe = readdirSync4(tmp).find((f) => f === name || f === `${name}.exe`);
22074
22869
  if (!exe) return null;
22075
- renameSync(path10.join(tmp, exe), target);
22870
+ renameSync(path13.join(tmp, exe), target);
22076
22871
  }
22077
22872
  chmodSync2(target, 493);
22078
22873
  return { path: target, version: spec.version };
@@ -22343,9 +23138,9 @@ var rls = {
22343
23138
  };
22344
23139
 
22345
23140
  // src/checks/security/secrets.ts
22346
- import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
22347
- import os3 from "os";
22348
- import path11 from "path";
23141
+ import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync3 } from "fs";
23142
+ import os4 from "os";
23143
+ import path14 from "path";
22349
23144
 
22350
23145
  // src/core/ignore.ts
22351
23146
  function globToRegex(glob) {
@@ -22383,15 +23178,15 @@ async function gitleaksHistory(ctx) {
22383
23178
  if (!ctx.tracked) return null;
22384
23179
  const bin = await ensureTool("gitleaks", ctx.options.noDownload);
22385
23180
  if (!bin) return null;
22386
- const dir = mkdtempSync2(path11.join(os3.tmpdir(), "aura-gitleaks-"));
22387
- const report = path11.join(dir, "report.json");
23181
+ const dir = mkdtempSync2(path14.join(os4.tmpdir(), "aura-gitleaks-"));
23182
+ const report = path14.join(dir, "report.json");
22388
23183
  try {
22389
23184
  const r = await run(bin.path, ["git", "--no-banner", "--redact", "--report-format", "json", "--report-path", report, "--exit-code", "0", "."], {
22390
23185
  cwd: ctx.root,
22391
23186
  timeoutMs: 18e4
22392
23187
  });
22393
- if (r.code !== 0 || !existsSync9(report)) return null;
22394
- return { hits: JSON.parse(readFileSync7(report, "utf8")), engine: `gitleaks ${bin.version}` };
23188
+ if (r.code !== 0 || !existsSync13(report)) return null;
23189
+ return { hits: JSON.parse(readFileSync12(report, "utf8")), engine: `gitleaks ${bin.version}` };
22395
23190
  } catch {
22396
23191
  return null;
22397
23192
  } finally {
@@ -22503,10 +23298,10 @@ var envFiles = {
22503
23298
  };
22504
23299
 
22505
23300
  // src/checks/security.ts
22506
- var securityChecks = [secrets, envFiles, deps, rls, apiRoutes, headers];
23301
+ var securityChecks = [secrets, envFiles, deps, rls, apiRoutes, headers, ...CONNECTORS.map(toolSettingsCheck)];
22507
23302
 
22508
23303
  // src/checks/structure.ts
22509
- import path12 from "path";
23304
+ import path15 from "path";
22510
23305
  var GENERATED = /(?:^|\/)(?:supabase\/migrations|generated|__generated__|\.generated)\/|\.(?:d\.ts|min\.js)$|(?:^|\/)(?:database|supabase)\.types\.ts$|(?:^|\/)components\/ui\//;
22511
23306
  function appSource(ctx) {
22512
23307
  return ctx.files.filter((f) => isSourceFile(f) && !GENERATED.test(f));
@@ -22595,7 +23390,7 @@ var duplicates = {
22595
23390
  const findings = pairs.map(
22596
23391
  ([a, b, sim]) => makeFinding(this, {
22597
23392
  severity: "low",
22598
- title: `${path12.basename(a)} and ${path12.basename(b)} are near-duplicates`,
23393
+ title: `${path15.basename(a)} and ${path15.basename(b)} are near-duplicates`,
22599
23394
  detail: `${a} and ${b} are ${Math.round(sim * 100)}% the same.`,
22600
23395
  why: "Duplicates drift apart over time: a bug gets fixed in one copy and not the other, and your agent can\u2019t tell which one is right.",
22601
23396
  locations: [{ file: a }, { file: b }],
@@ -22609,7 +23404,7 @@ var duplicates = {
22609
23404
  };
22610
23405
  var ROOT_ALLOWED = /^(?:[^/]+\.config\.[cm]?[jt]s|middleware\.[cm]?[jt]s|proxy\.[cm]?[jt]s|instrumentation(?:-client)?\.[cm]?[jt]s|next-env\.d\.ts|global\.d\.ts|env\.d\.ts|sentry\.[\w.]+\.[cm]?[jt]s)$/;
22611
23406
  function suggestTarget(file2, srcDir) {
22612
- const base = path12.posix.basename(file2);
23407
+ const base = path15.posix.basename(file2);
22613
23408
  const prefix = srcDir ? "src/" : "";
22614
23409
  const isComponent = /^[A-Z][A-Za-z0-9]*\.(?:tsx|jsx)$/.test(base);
22615
23410
  if (isComponent) return `${prefix}components/${base}`;
@@ -22721,59 +23516,22 @@ async function runScan(root, options, onProgress) {
22721
23516
  };
22722
23517
  }
22723
23518
 
22724
- // src/ui.ts
22725
- import * as p from "@clack/prompts";
22726
- import pc from "picocolors";
22727
- function interactive() {
22728
- return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
22729
- }
22730
- var UserCancelled = class extends Error {
22731
- };
22732
- function unwrap(value) {
22733
- if (p.isCancel(value)) {
22734
- p.cancel("Cancelled.");
22735
- throw new UserCancelled();
22736
- }
22737
- return value;
22738
- }
22739
- function spin(enabled) {
22740
- if (!enabled || !process.stdout.isTTY) {
22741
- return { start: (_m) => {
22742
- }, message: (_m) => {
22743
- }, stop: (_m) => {
22744
- }, error: (_m) => {
22745
- } };
22746
- }
22747
- const s = p.spinner();
22748
- return { start: (m) => s.start(m), message: (m) => s.message(m), stop: (m) => s.stop(m), error: (m) => s.error(m) };
22749
- }
22750
- function printJson(value) {
22751
- process.stdout.write(JSON.stringify(value, null, 2) + "\n");
22752
- }
22753
- var say = {
22754
- info: (m) => console.log(m),
22755
- ok: (m) => console.log(`${pc.green("\u2714")} ${m}`),
22756
- warn: (m) => console.error(`${pc.yellow("!")} ${m}`),
22757
- error: (m) => console.error(`${pc.red("\u2716")} ${m}`),
22758
- dim: (m) => console.log(pc.dim(m))
22759
- };
22760
-
22761
23519
  // src/commands/fix.ts
22762
- import { existsSync as existsSync14, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
22763
- import path16 from "path";
23520
+ import { existsSync as existsSync18, readFileSync as readFileSync19, writeFileSync as writeFileSync6 } from "fs";
23521
+ import path19 from "path";
22764
23522
 
22765
23523
  // src/agent/tools.ts
22766
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync8, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
22767
- import path13 from "path";
23524
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync13, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
23525
+ import path16 from "path";
22768
23526
  var MAX_OUTPUT = 12e3;
22769
23527
  var MAX_FILE_LINES = 800;
22770
23528
  var FORBIDDEN = /^(?:\.git|node_modules|\.next|\.vercel)(?:\/|$)|(?:^|\/)\.env(?:\.(?!example$|sample$|template$)[^/]+)?$/;
22771
23529
  function safePath(root, rel) {
22772
23530
  if (typeof rel !== "string" || !rel.trim()) throw new Error("A file path is required.");
22773
- const normalized = path13.posix.normalize(rel.replace(/\\/g, "/").replace(/^\.\//, ""));
22774
- if (normalized.startsWith("../") || normalized === ".." || path13.isAbsolute(normalized)) throw new Error(`Path is outside the repo: ${rel}`);
23531
+ const normalized = path16.posix.normalize(rel.replace(/\\/g, "/").replace(/^\.\//, ""));
23532
+ if (normalized.startsWith("../") || normalized === ".." || path16.isAbsolute(normalized)) throw new Error(`Path is outside the repo: ${rel}`);
22775
23533
  if (FORBIDDEN.test(normalized)) throw new Error(`Aura doesn\u2019t let the agent touch ${normalized}.`);
22776
- const full = path13.join(root, normalized);
23534
+ const full = path16.join(root, normalized);
22777
23535
  if (!isInside(root, full)) throw new Error(`Path is outside the repo: ${rel}`);
22778
23536
  return normalized;
22779
23537
  }
@@ -22794,10 +23552,10 @@ async function runTool(root, call, allowedCommands2) {
22794
23552
  }
22795
23553
  case "read_file": {
22796
23554
  const rel = safePath(root, input2.path);
22797
- const full = path13.join(root, rel);
22798
- if (!existsSync10(full)) return { output: `File not found: ${rel}` };
23555
+ const full = path16.join(root, rel);
23556
+ if (!existsSync14(full)) return { output: `File not found: ${rel}` };
22799
23557
  if (statSync2(full).size > 1e6) return { output: `${rel} is too large to read.` };
22800
- const lines = readFileSync8(full, "utf8").split("\n");
23558
+ const lines = readFileSync13(full, "utf8").split("\n");
22801
23559
  const start = Math.max(1, Number(input2.startLine) || 1);
22802
23560
  const end = Math.min(lines.length, Number(input2.endLine) || start + MAX_FILE_LINES - 1);
22803
23561
  const body = lines.slice(start - 1, end).map((l, i) => `${start + i}: ${l}`).join("\n");
@@ -22814,7 +23572,7 @@ async function runTool(root, call, allowedCommands2) {
22814
23572
  if (FORBIDDEN.test(f) || glob && !glob.test(f)) continue;
22815
23573
  let text;
22816
23574
  try {
22817
- text = readFileSync8(path13.join(root, f), "utf8");
23575
+ text = readFileSync13(path16.join(root, f), "utf8");
22818
23576
  } catch {
22819
23577
  continue;
22820
23578
  }
@@ -22833,14 +23591,14 @@ async function runTool(root, call, allowedCommands2) {
22833
23591
  output: "Refused: the content contains a [REDACTED:\u2026] placeholder. Secrets are hidden from you on purpose. Read the value from an environment variable (process.env.NAME) instead of writing it."
22834
23592
  };
22835
23593
  }
22836
- mkdirSync4(path13.dirname(path13.join(root, rel)), { recursive: true });
22837
- writeFileSync4(path13.join(root, rel), content);
23594
+ mkdirSync4(path16.dirname(path16.join(root, rel)), { recursive: true });
23595
+ writeFileSync4(path16.join(root, rel), content);
22838
23596
  return { output: `Wrote ${rel} (${content.split("\n").length} lines).`, touched: rel };
22839
23597
  }
22840
23598
  case "delete_file": {
22841
23599
  const rel = safePath(root, input2.path);
22842
- if (!existsSync10(path13.join(root, rel))) return { output: `File not found: ${rel}` };
22843
- rmSync4(path13.join(root, rel));
23600
+ if (!existsSync14(path16.join(root, rel))) return { output: `File not found: ${rel}` };
23601
+ rmSync4(path16.join(root, rel));
22844
23602
  return { output: `Deleted ${rel}.`, touched: rel };
22845
23603
  }
22846
23604
  case "run_command": {
@@ -22862,7 +23620,7 @@ ${(r.stdout + "\n" + r.stderr).trim().slice(-6e3)}`;
22862
23620
 
22863
23621
  // src/api/explain.ts
22864
23622
  import { createHash as createHash4 } from "crypto";
22865
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
23623
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
22866
23624
 
22867
23625
  // src/report.ts
22868
23626
  import pc2 from "picocolors";
@@ -23051,7 +23809,7 @@ var CACHE_FILE = "cache/explanations.json";
23051
23809
  var MAX_EXPLAIN = 25;
23052
23810
  function loadCache(root) {
23053
23811
  try {
23054
- return JSON.parse(readFileSync9(auraPath(root, CACHE_FILE), "utf8"));
23812
+ return JSON.parse(readFileSync14(auraPath(root, CACHE_FILE), "utf8"));
23055
23813
  } catch {
23056
23814
  return {};
23057
23815
  }
@@ -23072,8 +23830,8 @@ function excerptFor(root, f, context = 4) {
23072
23830
  }
23073
23831
  function productContext(root) {
23074
23832
  const file2 = auraPath(root, "brief.md");
23075
- if (!existsSync11(file2)) return void 0;
23076
- return redact(readFileSync9(file2, "utf8")).slice(0, 1800);
23833
+ if (!existsSync15(file2)) return void 0;
23834
+ return redact(readFileSync14(file2, "utf8")).slice(0, 1800);
23077
23835
  }
23078
23836
  async function addExplanations(root, scan, client) {
23079
23837
  const cache = loadCache(root);
@@ -23117,19 +23875,22 @@ async function addExplanations(root, scan, client) {
23117
23875
 
23118
23876
  // src/commands/simple.ts
23119
23877
  import { execFileSync } from "child_process";
23120
- import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
23121
- import path15 from "path";
23878
+ import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync18 } from "fs";
23879
+ import path18 from "path";
23122
23880
  import YAML2 from "yaml";
23123
23881
 
23124
23882
  // src/commands/inventory.ts
23125
- import { readFileSync as readFileSync10 } from "fs";
23883
+ import { readFileSync as readFileSync15 } from "fs";
23126
23884
  function accountState(root, services) {
23127
23885
  const inv = loadInventory(root);
23128
- return { inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg), githubSecurity: Boolean(inv.githubSecurity) };
23886
+ return {
23887
+ inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg),
23888
+ secured: { ...inv.secured, github: Boolean(inv.githubSecurity || inv.secured?.github) }
23889
+ };
23129
23890
  }
23130
23891
  function loadInventory(root) {
23131
23892
  try {
23132
- return JSON.parse(readFileSync10(auraPath(root, "inventory.json"), "utf8"));
23893
+ return JSON.parse(readFileSync15(auraPath(root, "inventory.json"), "utf8"));
23133
23894
  } catch {
23134
23895
  return { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), services: {} };
23135
23896
  }
@@ -23216,10 +23977,10 @@ async function inventoryCommand(flags) {
23216
23977
  }
23217
23978
 
23218
23979
  // src/history.ts
23219
- import { readFileSync as readFileSync11 } from "fs";
23980
+ import { readFileSync as readFileSync16 } from "fs";
23220
23981
  function loadHistory(root) {
23221
23982
  try {
23222
- const data = JSON.parse(readFileSync11(auraPath(root, "history.json"), "utf8"));
23983
+ const data = JSON.parse(readFileSync16(auraPath(root, "history.json"), "utf8"));
23223
23984
  return Array.isArray(data) ? data : [];
23224
23985
  } catch {
23225
23986
  return [];
@@ -23267,10 +24028,10 @@ function banner(colors = pc3.isColorSupported) {
23267
24028
  // src/api/connect.ts
23268
24029
  import { spawn as spawn2 } from "child_process";
23269
24030
  import { constants, generateKeyPairSync, privateDecrypt } from "crypto";
23270
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
23271
- import os4 from "os";
23272
- import path14 from "path";
23273
- var pendingFile = () => path14.join(os4.homedir(), ".aura", "pending-connect.json");
24031
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync17, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
24032
+ import os5 from "os";
24033
+ import path17 from "path";
24034
+ var pendingFile = () => path17.join(os5.homedir(), ".aura", "pending-connect.json");
23274
24035
  async function postJson(apiUrl, route, body) {
23275
24036
  let res;
23276
24037
  try {
@@ -23289,14 +24050,14 @@ async function postJson(apiUrl, route, body) {
23289
24050
  }
23290
24051
  function loadPending(apiUrl, now) {
23291
24052
  try {
23292
- const p2 = JSON.parse(readFileSync12(pendingFile(), "utf8"));
24053
+ const p2 = JSON.parse(readFileSync17(pendingFile(), "utf8"));
23293
24054
  if (p2.apiUrl === apiUrl && Date.parse(p2.expiresAt) - now > 6e4) return p2;
23294
24055
  } catch {
23295
24056
  }
23296
24057
  return null;
23297
24058
  }
23298
24059
  function clearPendingConnect() {
23299
- if (existsSync12(pendingFile())) rmSync5(pendingFile(), { force: true });
24060
+ if (existsSync16(pendingFile())) rmSync5(pendingFile(), { force: true });
23300
24061
  }
23301
24062
  async function startOrResumeConnect(apiUrl, now = Date.now()) {
23302
24063
  const existing = loadPending(apiUrl, now);
@@ -23308,7 +24069,7 @@ async function startOrResumeConnect(apiUrl, now = Date.now()) {
23308
24069
  });
23309
24070
  const res = ConnectStartResponse.parse(await postJson(apiUrl, "connect", { publicKey }));
23310
24071
  const pending = { apiUrl, privateKey, ...res };
23311
- mkdirSync5(path14.dirname(pendingFile()), { recursive: true });
24072
+ mkdirSync5(path17.dirname(pendingFile()), { recursive: true });
23312
24073
  writeFileSync5(pendingFile(), JSON.stringify(pending, null, 2) + "\n", { mode: 384 });
23313
24074
  return { ...pending, resumed: false };
23314
24075
  }
@@ -23333,7 +24094,7 @@ async function waitForConnect(pending, waitSeconds, sleep = (ms) => new Promise(
23333
24094
  }
23334
24095
  }
23335
24096
  function openInBrowser(url2) {
23336
- if (process.env.AURA_NO_BROWSER || process.env.CI) return;
24097
+ if (process.env.AURA_NO_BROWSER || inCI()) return;
23337
24098
  const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
23338
24099
  try {
23339
24100
  const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
@@ -23362,7 +24123,7 @@ ${pc.magenta("\u2726")} ${pc.bold("Connect your Aura pilot key")}`);
23362
24123
  console.log(pc.dim(` Check the page shows the code ${pending.code}. Press Ctrl+C to stop.
23363
24124
  `));
23364
24125
  } else {
23365
- console.log(` Ask the user to open this link and paste their pilot key there (or choose to continue without one):`);
24126
+ console.log(" Ask the user to open this link and paste their pilot key there:");
23366
24127
  console.log(` ${pending.url}`);
23367
24128
  console.log(` Don\u2019t ask for the key in chat. The page sends it to this computer encrypted. Code: ${pending.code}`);
23368
24129
  console.log(` Waiting up to ${Math.round(wait)} seconds\u2026
@@ -23371,7 +24132,7 @@ ${pc.magenta("\u2726")} ${pc.bold("Connect your Aura pilot key")}`);
23371
24132
  const s = spin(opts.person);
23372
24133
  s.start("Waiting for you to finish in the browser");
23373
24134
  const outcome = await waitForConnect(pending, wait);
23374
- s.stop(outcome.status === "complete" ? "Key received" : outcome.status === "skipped" ? "Continuing without a key" : "Stopped waiting");
24135
+ s.stop(outcome.status === "complete" ? "Key received" : "Stopped waiting");
23375
24136
  return outcome;
23376
24137
  } catch (err) {
23377
24138
  return { status: "error", message: friendlyApiError(err) };
@@ -23379,18 +24140,15 @@ ${pc.magenta("\u2726")} ${pc.bold("Connect your Aura pilot key")}`);
23379
24140
  }
23380
24141
  function explainSignIn(outcome) {
23381
24142
  switch (outcome.status) {
23382
- case "skipped":
23383
- say.dim("No pilot key: Aura will still score your repo, with standard explanations. Add one later with `aura login`.");
23384
- return true;
23385
24143
  case "pending":
23386
24144
  say.warn("Still waiting for the user to finish in the browser. When they have, run the same command again: it carries on with the same link.");
23387
- return false;
24145
+ break;
23388
24146
  case "expired":
23389
24147
  say.warn("The sign-in link expired. Run the same command again for a new one.");
23390
- return false;
24148
+ break;
23391
24149
  case "error":
23392
24150
  say.warn(`Couldn\u2019t start sign-in: ${outcome.message}`);
23393
- return false;
24151
+ break;
23394
24152
  }
23395
24153
  }
23396
24154
 
@@ -23404,7 +24162,7 @@ function stepRun(step, hasKey) {
23404
24162
  }
23405
24163
  function hint(step, hasKey) {
23406
24164
  const run2 = stepRun(step, hasKey);
23407
- const what = run2.kind === "apply" ? "opens a pull request" : run2.kind === "fix" ? run2.withOwnAgent ? "writes a task for your coding agent" : "Aura\u2019s agent fixes it in a pull request" : run2.kind === "guide" ? "shows step-by-step instructions" : "a few questions";
24165
+ const what = connectorForAction(step.actionId) ? `turns on ${connectorForAction(step.actionId).name}\u2019s security settings, after you agree` : run2.kind === "apply" ? "opens a pull request" : run2.kind === "fix" ? run2.withOwnAgent ? "writes a task for your coding agent" : "Aura\u2019s agent fixes it in a pull request" : run2.kind === "guide" ? "shows step-by-step instructions" : "a few questions";
23408
24166
  return [what, step.impact > 0 ? `+${step.impact} points` : "", effortLabel(step.effort)].filter(Boolean).join(" \xB7 ");
23409
24167
  }
23410
24168
  async function pickAndRunSteps(steps, limit = 10) {
@@ -23430,7 +24188,20 @@ async function pickAndRunSteps(steps, limit = 10) {
23430
24188
  console.log(`
23431
24189
  ${pc.magenta("\u2726")} ${pc.bold(step.title)}`);
23432
24190
  const run2 = stepRun(step, hasKey);
23433
- const code = run2.kind === "apply" ? await applyCommand(run2.actionId, { yes: true }) : run2.kind === "fix" ? await fixCommand(run2.actionId, run2.withOwnAgent ? { agent: true } : { yes: true }) : run2.kind === "guide" ? await guideCommand(run2.topic, {}) : await inventoryCommand({});
24191
+ const code = run2.kind === "apply" ? (
24192
+ // Settings changes always ask first, even when picked here; pull requests don't need to.
24193
+ await applyCommand(run2.actionId, { yes: !connectorForAction(run2.actionId), via: "checklist" })
24194
+ ) : run2.kind === "fix" ? await fixCommand(run2.actionId, run2.withOwnAgent ? { agent: true, via: "checklist" } : { yes: true, via: "checklist" }) : run2.kind === "guide" ? await guideCommand(run2.topic, {}) : await inventoryCommand({});
24195
+ if (run2.kind === "guide" || run2.kind === "inventory") {
24196
+ const root = await findRepoRoot();
24197
+ await recordUsage(root, loadConfig(root), {
24198
+ event: "action",
24199
+ actionId: step.actionId,
24200
+ delivery: run2.kind === "guide" ? "guide" : "checklist",
24201
+ via: "checklist",
24202
+ outcome: "shown"
24203
+ });
24204
+ }
23434
24205
  results.push({ title: step.title, ok: code === 0 });
23435
24206
  }
23436
24207
  if (results.length > 1) {
@@ -23444,9 +24215,9 @@ ${pc.bold(" Done")}`);
23444
24215
  // src/commands/simple.ts
23445
24216
  async function latestScan(root) {
23446
24217
  const file2 = auraPath(root, "scan.json");
23447
- if (existsSync13(file2)) {
24218
+ if (existsSync17(file2)) {
23448
24219
  try {
23449
- const scan = JSON.parse(readFileSync13(file2, "utf8"));
24220
+ const scan = JSON.parse(readFileSync18(file2, "utf8"));
23450
24221
  const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23451
24222
  const dirty = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23452
24223
  if (scan.commit === head && !dirty && Date.now() - Date.parse(scan.scannedAt) < 24 * 36e5) return scan;
@@ -23481,14 +24252,14 @@ async function loginCommand(flags) {
23481
24252
  let key = flags.key ?? process.env.AURA_KEY;
23482
24253
  if (!key && flags.paste && person) key = unwrap(await p.password({ message: "Paste your Aura pilot key", mask: "\u2022" }));
23483
24254
  if (!key) {
23484
- if (process.env.CI) {
24255
+ if (inCI()) {
23485
24256
  say.error("Pass your key with --key or set AURA_KEY.");
23486
24257
  return 1;
23487
24258
  }
23488
24259
  const outcome = await signInWithBrowser(apiUrl, { person, waitSeconds: parseWait(flags.wait) });
23489
24260
  if (outcome.status !== "complete") {
23490
24261
  explainSignIn(outcome);
23491
- return outcome.status === "skipped" ? 0 : 1;
24262
+ return 1;
23492
24263
  }
23493
24264
  key = outcome.key;
23494
24265
  }
@@ -23534,8 +24305,8 @@ async function configCommand(key, value) {
23534
24305
  const bool = (v) => ["on", "true", "yes", "1"].includes(v.toLowerCase());
23535
24306
  switch (key) {
23536
24307
  case "telemetry":
23537
- if (value === void 0) return say.info(config2.telemetry ? "on" : "off"), 0;
23538
- config2.telemetry = bool(value);
24308
+ if (value === void 0) return say.info(sharesUsage(config2) ? "on" : "off"), 0;
24309
+ config2.shareUsage = bool(value);
23539
24310
  break;
23540
24311
  case "ciMaxDrop":
23541
24312
  case "ci-max-drop":
@@ -23561,9 +24332,9 @@ async function configCommand(key, value) {
23561
24332
  return 0;
23562
24333
  }
23563
24334
  function loadGuides() {
23564
- const dir = path15.join(templatesDir(), "guides");
23565
- return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => {
23566
- const text = readFileSync13(path15.join(dir, f), "utf8");
24335
+ const dir = path18.join(templatesDir(), "guides");
24336
+ return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((f) => {
24337
+ const text = readFileSync18(path18.join(dir, f), "utf8");
23567
24338
  const fm = text.match(/^---\n([\s\S]*?)\n---\n?/);
23568
24339
  const meta3 = fm ? YAML2.parse(fm[1]) : {};
23569
24340
  return { id: f.replace(/\.md$/, ""), title: meta3.title ?? f, summary: meta3.summary ?? "", body: fm ? text.slice(fm[0].length) : text };
@@ -23631,7 +24402,7 @@ function allowedCommands(root, pm, typescript) {
23631
24402
  const pkg = readJson(root, "package.json");
23632
24403
  const run2 = pmCommands(pm).run;
23633
24404
  const cmds = ["test", "lint", "typecheck", "build"].filter((s) => pkg?.scripts?.[s]).map((s) => `${run2} ${s}`);
23634
- if (typescript && !pkg?.scripts?.typecheck && existsSync14(path16.join(root, "node_modules/.bin/tsc"))) cmds.push(`${pmCommands(pm).exec} tsc --noEmit`);
24405
+ if (typescript && !pkg?.scripts?.typecheck && existsSync18(path19.join(root, "node_modules/.bin/tsc"))) cmds.push(`${pmCommands(pm).exec} tsc --noEmit`);
23635
24406
  return cmds;
23636
24407
  }
23637
24408
  function taskFor(root, scan, action2, findings) {
@@ -23682,8 +24453,8 @@ async function untrackEnvFiles(root, findings) {
23682
24453
  const files = findings.filter((f) => f.checkId === "security.env-files").map((f) => f.locations[0].file);
23683
24454
  for (const f of files) await git(root, "rm", "-q", "--cached", "--", f);
23684
24455
  if (files.length) {
23685
- const gi = path16.join(root, ".gitignore");
23686
- const current2 = existsSync14(gi) ? readFileSync14(gi, "utf8") : "";
24456
+ const gi = path19.join(root, ".gitignore");
24457
+ const current2 = existsSync18(gi) ? readFileSync19(gi, "utf8") : "";
23687
24458
  if (!/^\.env\*?$/m.test(current2)) writeFileSync6(gi, `${current2.trimEnd()}
23688
24459
 
23689
24460
  # Environment files hold secrets
@@ -23695,6 +24466,15 @@ async function untrackEnvFiles(root, findings) {
23695
24466
  return files;
23696
24467
  }
23697
24468
  async function fixCommand(target, flags) {
24469
+ const started = Date.now();
24470
+ const report = { delivery: flags.agent ? "agent-task" : "agent-fix" };
24471
+ const actionId = { value: target };
24472
+ const code = await fixAction(target, flags, report, actionId);
24473
+ const root = await findRepoRoot();
24474
+ await recordAction(root, loadConfig(root), actionId.value, report, started, flags.via ?? "command");
24475
+ return code;
24476
+ }
24477
+ async function fixAction(target, flags, report, resolvedId) {
23698
24478
  const root = await findRepoRoot();
23699
24479
  const config2 = loadConfig(root);
23700
24480
  const scan = await latestScan(root);
@@ -23704,8 +24484,10 @@ async function fixCommand(target, flags) {
23704
24484
  return 1;
23705
24485
  }
23706
24486
  const { action: action2, findings } = resolved;
24487
+ resolvedId.value = action2.id;
23707
24488
  if (findings.length === 0 && !["write-critical-tests"].includes(action2.id)) {
23708
24489
  say.info(`Aura found nothing to fix for "${action2.title}".`);
24490
+ report.outcome = "no_changes";
23709
24491
  return 0;
23710
24492
  }
23711
24493
  const task = taskFor(root, scan, action2, findings);
@@ -23715,11 +24497,13 @@ async function fixCommand(target, flags) {
23715
24497
  console.log(`
23716
24498
  Ask your coding agent:
23717
24499
  ${pc.cyan(`Complete the Aura task in ${file2}`)}`);
24500
+ report.outcome = "task_written";
23718
24501
  return 0;
23719
24502
  }
23720
24503
  const client = AuraClient.from(config2);
23721
24504
  if (!client.hasKey) {
23722
24505
  say.error("Aura\u2019s agent needs a pilot key (aura login). Or run with --agent to hand this to your own coding agent.");
24506
+ Object.assign(report, { outcome: "failed", errorType: "NoPilotKey" });
23723
24507
  return 1;
23724
24508
  }
23725
24509
  console.log(`
@@ -23732,6 +24516,7 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23732
24516
  const dirty = await dirtyFiles(root);
23733
24517
  if (dirty.length) {
23734
24518
  say.error(`You have uncommitted changes (${dirty.slice(0, 5).join(", ")}). Commit or stash them first.`);
24519
+ Object.assign(report, { outcome: "failed", errorType: "UncommittedChanges" });
23735
24520
  return 1;
23736
24521
  }
23737
24522
  const openPr = flags.pr !== false;
@@ -23739,15 +24524,19 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23739
24524
  const gh = await ghStatus(root);
23740
24525
  if (!gh.installed || !gh.authed || !gh.hasRemote) {
23741
24526
  say.error("Opening a PR needs the GitHub CLI, signed in, and a GitHub remote. Run with --no-pr to keep the change on a local branch.");
24527
+ Object.assign(report, { outcome: "failed", errorType: gh.installed ? "GhNotReady" : "GhNotInstalled" });
23742
24528
  return 1;
23743
24529
  }
23744
24530
  }
23745
- if (interactive() && !flags.yes && !unwrap(await p.confirm({ message: "Start Aura\u2019s agent on a new branch? It reads only the files it needs, with secrets hidden." })))
24531
+ if (interactive() && !flags.yes && !unwrap(await p.confirm({ message: "Start Aura\u2019s agent on a new branch? It reads only the files it needs, with secrets hidden." }))) {
24532
+ report.outcome = "cancelled";
23746
24533
  return 0;
24534
+ }
23747
24535
  const original = await currentBranch(root);
23748
24536
  const branch = await freeBranchName(root, `aura/fix-${action2.id}`);
23749
24537
  if ((await git(root, "switch", "-c", branch)).code !== 0) {
23750
24538
  say.error(`Couldn\u2019t create branch ${branch}.`);
24539
+ Object.assign(report, { outcome: "failed", errorType: "BranchFailed" });
23751
24540
  return 1;
23752
24541
  }
23753
24542
  const s = spin(true);
@@ -23757,6 +24546,7 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23757
24546
  try {
23758
24547
  s.start("Aura\u2019s agent is working");
23759
24548
  for (let step = 0; step <= MAX_STEPS && !finish; step++) {
24549
+ report.agentSteps = step + 1;
23760
24550
  const res = await client.request("POST", "agent/step", { task, messages, step }, 18e4);
23761
24551
  messages = [...messages, ...res.responseMessages];
23762
24552
  if (res.toolCalls.length === 0) break;
@@ -23785,6 +24575,7 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23785
24575
  say.warn("Aura\u2019s agent didn\u2019t change anything. " + (finish?.summary ?? ""));
23786
24576
  await git(root, "switch", original);
23787
24577
  await git(root, "branch", "-D", branch);
24578
+ report.outcome = "no_changes";
23788
24579
  return 1;
23789
24580
  }
23790
24581
  const guides = [
@@ -23819,11 +24610,12 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23819
24610
  else if (result.error) say.warn(`Committed on ${branch}, but couldn\u2019t open the PR: ${result.error.trim()}`);
23820
24611
  else say.ok(`Committed on branch ${branch}.`);
23821
24612
  if (finish?.risks.length) say.info(pc.dim(`Check before merging: ${finish.risks.join(" ")}`));
23822
- if (config2.telemetry) await sendTelemetry(root, client, { event: "action", actionId: action2.id });
24613
+ Object.assign(report, { outcome: result.url ? "pr_opened" : "committed", branch, scoreBefore: scan.score, findingIds: findings.map((f) => f.id) });
23823
24614
  return 0;
23824
24615
  } catch (err) {
23825
24616
  s.error("Aura\u2019s agent hit a problem");
23826
24617
  say.error(friendlyApiError(err));
24618
+ Object.assign(report, { outcome: "failed", errorType: errorKind(err) });
23827
24619
  await git(root, "reset", "-q");
23828
24620
  for (const f of touched) {
23829
24621
  const inHead = (await git(root, "cat-file", "-e", `HEAD:${f}`)).code === 0;
@@ -23839,9 +24631,132 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23839
24631
  }
23840
24632
  }
23841
24633
 
24634
+ // src/connectors/engine.ts
24635
+ async function readSettings(connector, ctx) {
24636
+ const states = await Promise.all(connector.settings.map((s) => s.read(ctx).catch(() => ({ state: "off" }))));
24637
+ return Object.fromEntries(connector.settings.map((s, i) => [s.id, states[i]]));
24638
+ }
24639
+ async function enableSettings(connector, ctx, states) {
24640
+ const results = [];
24641
+ for (const setting of connector.settings) {
24642
+ const s = states[setting.id];
24643
+ if (s.state === "on") results.push({ id: setting.id, result: "already-on" });
24644
+ else if (s.state === "unavailable") results.push({ id: setting.id, result: "unavailable", why: s.why });
24645
+ else {
24646
+ const out = await setting.enable(ctx).catch(() => ({ ok: false }));
24647
+ results.push(
24648
+ out.ok ? { id: setting.id, result: "turned-on" } : out.unavailable ? { id: setting.id, result: "unavailable", why: out.unavailable } : { id: setting.id, result: "failed", why: `${connector.name} refused the change` }
24649
+ );
24650
+ }
24651
+ }
24652
+ return results;
24653
+ }
24654
+ var securityDone = (results) => results.every((r) => r.result !== "failed");
24655
+ async function secureCommand(connector, flags, report = {}, fake) {
24656
+ const root = await findRepoRoot();
24657
+ report.delivery = "settings";
24658
+ const connection = await connector.connect(root, fake);
24659
+ if (!connection.ok) {
24660
+ say.error(connection.problem);
24661
+ Object.assign(report, { outcome: "failed", errorType: connection.errorType });
24662
+ return 1;
24663
+ }
24664
+ const { ctx, target } = connection;
24665
+ const states = await readSettings(connector, ctx);
24666
+ saveSnapshot(root, connector.id, states);
24667
+ const toTurnOn = connector.settings.filter((s) => states[s.id].state === "off").map((s) => s.id);
24668
+ console.log(`
24669
+ ${pc.magenta("\u2726")} ${pc.bold(`${connector.name} security for ${target}`)}`);
24670
+ for (const setting of connector.settings) {
24671
+ const s = states[setting.id];
24672
+ const line = s.state === "on" ? pc.green("\u2714 already on") : s.state === "off" ? pc.cyan("\u2192 Aura will turn this on") : pc.dim(`\u2013 not available: ${s.why}`);
24673
+ console.log(` ${setting.label}
24674
+ ${line}`);
24675
+ }
24676
+ for (const line of await connector.explain?.(ctx, toTurnOn) ?? []) say.dim(` ${line}`);
24677
+ const label = (id) => connector.settings.find((s) => s.id === id).label;
24678
+ const impact = (id) => connector.settings.find((s) => s.id === id).impact;
24679
+ if (toTurnOn.length && !flags.dryRun) {
24680
+ console.log(`
24681
+ ${pc.bold(" What changes for you")}`);
24682
+ for (const id of toTurnOn) console.log(` \u2022 ${impact(id)}`);
24683
+ }
24684
+ if (flags.dryRun) {
24685
+ say.dim(toTurnOn.length ? "\nDry run: nothing was changed." : "\nEverything Aura can turn on here is already on.");
24686
+ return 0;
24687
+ }
24688
+ let chosen = toTurnOn;
24689
+ if (toTurnOn.length && !flags.yes) {
24690
+ if (!interactive()) {
24691
+ say.info(`
24692
+ Nothing was changed. Show the user the changes above, and once they agree run: aura apply ${connector.actionId} --yes`);
24693
+ report.outcome = "shown";
24694
+ return 0;
24695
+ }
24696
+ chosen = unwrap(
24697
+ await p.multiselect({
24698
+ message: `Which should Aura turn on in ${connector.name}? (untick any you don\u2019t want)`,
24699
+ options: toTurnOn.map((id) => ({ value: id, label: label(id) })),
24700
+ initialValues: toTurnOn,
24701
+ required: false
24702
+ })
24703
+ );
24704
+ if (chosen.length === 0) {
24705
+ say.dim("Nothing was changed.");
24706
+ report.outcome = "cancelled";
24707
+ return 0;
24708
+ }
24709
+ }
24710
+ const skipped = toTurnOn.filter((id) => !chosen.includes(id));
24711
+ let results;
24712
+ if (toTurnOn.length === 0) {
24713
+ say.ok("Everything Aura can turn on here is already on.");
24714
+ results = await enableSettings(connector, ctx, states);
24715
+ report.outcome = "no_changes";
24716
+ } else {
24717
+ const attempt = Object.fromEntries(
24718
+ Object.entries(states).map(([id, s]) => [id, skipped.includes(id) ? { state: "unavailable", why: "you chose to leave it off" } : s])
24719
+ );
24720
+ results = await enableSettings(connector, ctx, attempt);
24721
+ console.log("");
24722
+ for (const r of results.filter((x) => chosen.includes(x.id))) {
24723
+ if (r.result === "turned-on") say.ok(`${label(r.id)}: on`);
24724
+ else if (r.result === "unavailable") say.dim(`\u2013 ${label(r.id)}: ${r.why}`);
24725
+ else say.warn(`${label(r.id)}: ${r.why}.`);
24726
+ }
24727
+ report.outcome = results.some((r) => r.result === "failed") ? "failed" : "settings_changed";
24728
+ }
24729
+ saveSnapshot(
24730
+ root,
24731
+ connector.id,
24732
+ Object.fromEntries(Object.entries(states).map(([id, s]) => [id, results.find((r) => r.id === id)?.result === "turned-on" ? { state: "on" } : s]))
24733
+ );
24734
+ const left = await connector.leftForYou(ctx, results);
24735
+ if (left.length) {
24736
+ console.log(`
24737
+ ${pc.bold(" Left for you")}`);
24738
+ for (const line of left) console.log(` \u2022 ${line}`);
24739
+ }
24740
+ const inv = loadInventory(root);
24741
+ inv.secured = { ...inv.secured, [connector.id]: securityDone(results) };
24742
+ if (connector.id === "github") inv.githubSecurity = inv.secured.github;
24743
+ inv.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
24744
+ writeAuraFile(root, "inventory.json", `${JSON.stringify(inv, null, 2)}
24745
+ `);
24746
+ return results.some((r) => r.result === "failed") ? 1 : 0;
24747
+ }
24748
+
23842
24749
  // src/commands/apply.ts
23843
24750
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
23844
24751
  async function applyCommand(actionId, flags) {
24752
+ const started = Date.now();
24753
+ const report = { delivery: "pr" };
24754
+ const code = await applyAction(actionId, flags, report);
24755
+ const root = await findRepoRoot();
24756
+ await recordAction(root, loadConfig(root), actionId, report, started, flags.via ?? "command");
24757
+ return code;
24758
+ }
24759
+ async function applyAction(actionId, flags, report) {
23845
24760
  const root = await findRepoRoot();
23846
24761
  const action2 = getAction(actionId);
23847
24762
  if (!action2) {
@@ -23850,10 +24765,12 @@ async function applyCommand(actionId, flags) {
23850
24765
  }
23851
24766
  const scan = await latestScan(root);
23852
24767
  const { delivery, fellBack } = effectiveDelivery(action2, scan);
24768
+ const connector = connectorForAction(actionId);
24769
+ if (connector) return secureCommand(connector, flags, report);
23853
24770
  if (delivery === "guide") return guideCommand(actionId, {});
23854
24771
  if (delivery === "agent-fix") {
23855
24772
  if (fellBack) say.info(pc.dim(`Aura only opens template PRs on stacks it recognizes (Next.js). Handing "${actionId}" to Aura\u2019s agent instead.`));
23856
- return fixCommand(actionId, { yes: flags.yes, dryRun: flags.dryRun, pr: flags.pr });
24773
+ return fixCommand(actionId, { yes: flags.yes, dryRun: flags.dryRun, pr: flags.pr, via: flags.via });
23857
24774
  }
23858
24775
  if (delivery !== "pr") {
23859
24776
  say.error(`"${actionId}" isn\u2019t a PR action. Try aura ${delivery === "checklist" ? "inventory" : "next"}.`);
@@ -23875,18 +24792,15 @@ async function applyCommand(actionId, flags) {
23875
24792
  }
23876
24793
  const g = { root, scan, ctx, agents: [...agents], vars: templateVars(root, scan, ctx.pkg), skills: skills2 };
23877
24794
  const plan = actionId === "foundation" ? foundationPlan(g) : GENERATORS[actionId]?.(g) ?? { skip: "Nothing to do." };
23878
- if ("skip" in plan) {
23879
- say.info(plan.skip);
23880
- return 0;
23881
- }
23882
- if (plan.changes.length === 0 && !plan.commands?.length && !plan.devDeps?.length) {
23883
- say.info("Nothing to change.");
24795
+ if ("skip" in plan || plan.changes.length === 0 && !plan.commands?.length && !plan.devDeps?.length) {
24796
+ say.info("skip" in plan ? plan.skip : "Nothing to change.");
24797
+ report.outcome = "no_changes";
23884
24798
  return 0;
23885
24799
  }
23886
24800
  console.log(`
23887
24801
  ${pc.bold(action2.title)}`);
23888
24802
  for (const c of plan.changes)
23889
- console.log(` ${c.content === null ? pc.red("delete") : existsSync15(path17.join(root, c.path)) ? pc.yellow("update") : pc.green("add ")} ${c.path}`);
24803
+ console.log(` ${c.content === null ? pc.red("delete") : existsSync19(path20.join(root, c.path)) ? pc.yellow("update") : pc.green("add ")} ${c.path}`);
23890
24804
  const pm = pmCommands(scan.stack.packageManager);
23891
24805
  if (plan.devDeps?.length) console.log(` ${pc.cyan("install")} ${plan.devDeps.join(", ")} ${pc.dim(`(${pm.addDev})`)}`);
23892
24806
  for (const cmd of plan.commands ?? []) console.log(` ${pc.cyan("run ")} ${cmd.join(" ")}`);
@@ -23899,6 +24813,7 @@ ${pc.bold(action2.title)}`);
23899
24813
  say.error(
23900
24814
  `You have uncommitted changes (${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? "\u2026" : ""}). Commit or stash them first so they don\u2019t end up in Aura\u2019s PR.`
23901
24815
  );
24816
+ Object.assign(report, { outcome: "failed", errorType: "UncommittedChanges" });
23902
24817
  return 1;
23903
24818
  }
23904
24819
  const openPr = flags.pr !== false;
@@ -23909,13 +24824,16 @@ ${pc.bold(action2.title)}`);
23909
24824
  !gh.installed ? `The GitHub CLI isn\u2019t installed. ${ghInstallHint()}` : !gh.authed ? "Run `gh auth login` first." : "This repo has no GitHub remote. Push it to GitHub first."
23910
24825
  );
23911
24826
  say.dim("Or run with --no-pr to commit the change on a local branch.");
24827
+ Object.assign(report, { outcome: "failed", errorType: gh.installed ? "GhNotReady" : "GhNotInstalled" });
23912
24828
  return 1;
23913
24829
  }
23914
24830
  }
23915
- if (interactive() && !flags.yes && !unwrap(await p.confirm({ message: openPr ? "Create a branch and open a pull request?" : "Create a branch and commit?" })))
24831
+ if (interactive() && !flags.yes && !unwrap(await p.confirm({ message: openPr ? "Create a branch and open a pull request?" : "Create a branch and commit?" }))) {
24832
+ report.outcome = "cancelled";
23916
24833
  return 0;
23917
- const tsc = path17.join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
23918
- const checkTypes = plan.verifyTypes && scan.stack.typescript && existsSync15(tsc);
24834
+ }
24835
+ const tsc = path20.join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
24836
+ const checkTypes = plan.verifyTypes && scan.stack.typescript && existsSync19(tsc);
23919
24837
  const typeErrors = async () => {
23920
24838
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: root, timeoutMs: 3e5 });
23921
24839
  return (r.stdout.match(/error TS\d+/g) ?? []).length;
@@ -23926,6 +24844,7 @@ ${pc.bold(action2.title)}`);
23926
24844
  const sw = await git(root, "switch", "-c", branch);
23927
24845
  if (sw.code !== 0) {
23928
24846
  say.error(`Couldn\u2019t create branch ${branch}: ${sw.stderr.trim()}`);
24847
+ Object.assign(report, { outcome: "failed", errorType: "BranchFailed" });
23929
24848
  return 1;
23930
24849
  }
23931
24850
  try {
@@ -23953,6 +24872,7 @@ ${r.stderr.slice(-1500)}`);
23953
24872
  say.info("Nothing changed, so there\u2019s no PR to open.");
23954
24873
  await git(root, "switch", original);
23955
24874
  await git(root, "branch", "-D", branch);
24875
+ report.outcome = "no_changes";
23956
24876
  return 0;
23957
24877
  }
23958
24878
  const prTemplate = `pr/${actionId}.md`;
@@ -23963,22 +24883,31 @@ ${plan.details.join("\n")}`;
23963
24883
  const result = await commitPushPr(root, { branch, base: original, title: `Aura: ${action2.title}`, body, openPr });
23964
24884
  if (result.error && result.committed) {
23965
24885
  say.error(`Committed on branch ${branch}, but couldn\u2019t ${result.pushed ? "open the pull request" : "push it"}: ${result.error.trim()}`);
23966
- say.dim(`Finish by hand: git push -u origin ${branch} && gh pr create --head ${branch}`);
24886
+ say.dim(`Finish by hand with these two commands:
24887
+ git push -u origin ${branch}
24888
+ gh pr create --head ${branch}`);
24889
+ Object.assign(report, { outcome: "failed", errorType: result.pushed ? "PrFailed" : "PushFailed" });
23967
24890
  return 1;
23968
24891
  }
23969
24892
  if (result.error) throw new Error(result.error.trim());
23970
24893
  if (result.url) say.ok(`Opened ${result.url}`);
23971
24894
  else say.ok(`Committed on branch ${branch}.`);
24895
+ Object.assign(report, {
24896
+ outcome: result.url ? "pr_opened" : "committed",
24897
+ branch,
24898
+ scoreBefore: scan.score,
24899
+ findingIds: nextSteps(scan, { includeFoundationBundle: true }).find((s) => s.actionId === actionId)?.findingIds ?? []
24900
+ });
23972
24901
  if (plan.changes.some((c) => /^\.aura\/|\/skills\/aura\//.test(c.path))) {
23973
24902
  say.dim(
23974
24903
  openPr ? "Your brief and Aura files are in this PR. Merge it, then run `git pull` to have them locally again." : `Your brief and Aura files are on ${branch}. Merge that branch to have them locally again.`
23975
24904
  );
23976
24905
  }
23977
24906
  if (plan.changes.some((c) => c.path === ".github/workflows/aura.yml") && openPr && !flags.yes) await offerPilotSecret(root);
23978
- if (config2.telemetry) await sendTelemetry(root, AuraClient.from(config2), { event: "action", actionId });
23979
24907
  return 0;
23980
24908
  } catch (err) {
23981
24909
  say.error(err.message);
24910
+ Object.assign(report, { outcome: "failed", errorType: errorKind(err) });
23982
24911
  await undoPlan(root, plan);
23983
24912
  await git(root, "switch", original);
23984
24913
  await git(root, "branch", "-D", branch);
@@ -24013,16 +24942,17 @@ async function undoPlan(root, plan) {
24013
24942
  for (const file2 of touched) {
24014
24943
  const inHead = (await git(root, "cat-file", "-e", `HEAD:${file2}`)).code === 0;
24015
24944
  if (inHead) await git(root, "checkout", "HEAD", "--", file2);
24016
- else if (plan.changes.some((c) => c.path === file2)) rmSync6(path17.join(root, file2), { force: true });
24945
+ else if (plan.changes.some((c) => c.path === file2)) rmSync6(path20.join(root, file2), { force: true });
24017
24946
  }
24018
24947
  }
24019
24948
 
24020
24949
  // src/commands/init.ts
24021
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
24950
+ import { existsSync as existsSync20, readFileSync as readFileSync21 } from "fs";
24022
24951
  import { spawnSync as spawnSync3 } from "child_process";
24952
+ import path24 from "path";
24023
24953
 
24024
24954
  // src/brief.ts
24025
- import path18 from "path";
24955
+ import path21 from "path";
24026
24956
  function repoSummary(ctx) {
24027
24957
  const routes = ctx.files.filter((f) => /^(?:src\/)?app\/(?:.+\/)?(?:page|route)\.[cm]?[jt]sx?$/.test(f) || /^(?:src\/)?pages\/.+\.[cm]?[jt]sx?$/.test(f)).map(
24028
24958
  (f) => "/" + f.replace(/^(?:src\/)?(?:app|pages)\//, "").replace(/(?:^|\/)(?:page|route|index)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/, "").replace(/\([^)]+\)\//g, "")
@@ -24038,7 +24968,7 @@ function repoSummary(ctx) {
24038
24968
  const readme2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f));
24039
24969
  const readmeText = readme2 ? ctx.read(readme2) ?? "" : "";
24040
24970
  return {
24041
- name: ctx.pkg?.name ?? path18.basename(ctx.root),
24971
+ name: ctx.pkg?.name ?? path21.basename(ctx.root),
24042
24972
  packageJson: ctx.pkg ? {
24043
24973
  name: ctx.pkg.name,
24044
24974
  description: ctx.pkg.description,
@@ -24129,13 +25059,154 @@ function installFallback(result, platform = process.platform) {
24129
25059
  return `${admin} \`npx getaura <command>\` in place of \`aura <command>\` (for example \`npx getaura scan\`).`;
24130
25060
  }
24131
25061
 
25062
+ // src/core/claude-plugin.ts
25063
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
25064
+ import os6 from "os";
25065
+ import path22 from "path";
25066
+ var CLAUDE_PLUGIN_ID = "aura@aura";
25067
+ var marketplaceDir = () => path22.join(os6.homedir(), ".aura", "claude-marketplace");
25068
+ var MARKETPLACE = {
25069
+ name: "aura",
25070
+ description: "Aura for Claude Code",
25071
+ owner: { name: "Aura" },
25072
+ plugins: [
25073
+ {
25074
+ name: "aura",
25075
+ description: "Slash commands for Aura: /aura:scan, /aura:next, /aura:fix and /aura:setup.",
25076
+ source: { source: "npm", package: "getaura" }
25077
+ }
25078
+ ]
25079
+ };
25080
+ function writeMarketplace() {
25081
+ const dir = marketplaceDir();
25082
+ mkdirSync6(path22.join(dir, ".claude-plugin"), { recursive: true });
25083
+ writeFileSync7(path22.join(dir, ".claude-plugin", "marketplace.json"), `${JSON.stringify(MARKETPLACE, null, 2)}
25084
+ `);
25085
+ return dir;
25086
+ }
25087
+ function manualInstall() {
25088
+ return [`claude plugin marketplace add "${writeMarketplace()}"`, `claude plugin install ${CLAUDE_PLUGIN_ID} --scope project`];
25089
+ }
25090
+ async function installClaudePlugin(root) {
25091
+ if (!await commandExists("claude")) return "no-claude";
25092
+ const dir = writeMarketplace();
25093
+ const add = await run("claude", ["plugin", "marketplace", "add", dir], { cwd: root, timeoutMs: 12e4 });
25094
+ if (add.code !== 0) return "failed";
25095
+ const install = await run("claude", ["plugin", "install", CLAUDE_PLUGIN_ID, "--scope", "project"], { cwd: root, timeoutMs: 18e4 });
25096
+ return install.code === 0 ? "installed" : "failed";
25097
+ }
25098
+
25099
+ // src/onboarding.ts
25100
+ var DATA_LABELS2 = {
25101
+ personal: "personal info (names, emails)",
25102
+ payments: "payments",
25103
+ health: "health",
25104
+ legal: "legal",
25105
+ children: "children\u2019s data",
25106
+ none: "none of these"
25107
+ };
25108
+ var BOILERPLATE2 = /bootstrapped with|create-next-app|getting started|this template|vite \+|turborepo|starter kit/i;
25109
+ function whatItDoes(ctx) {
25110
+ const description = ctx.pkg?.description?.trim();
25111
+ if (description && description.length >= 15 && !BOILERPLATE2.test(description)) return { value: description, from: "package.json" };
25112
+ const readme2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f));
25113
+ const readmeText = readme2 ? ctx.read(readme2) ?? "" : "";
25114
+ if (/create-next-app|create-react-app|create-vite|create-t3-app|bootstrapped with/i.test(readmeText)) return null;
25115
+ const paragraphs = readmeText.replace(/\r\n/g, "\n").split(/\n\s*\n/);
25116
+ for (const raw of paragraphs) {
25117
+ const text = raw.replace(/\s+/g, " ").trim();
25118
+ if (!text || /^(?:#|!\[|\[|<|```|[-*|>]|\d+\.)/.test(text) || text.length < 30 || BOILERPLATE2.test(text)) continue;
25119
+ return { value: text.length > 300 ? `${text.slice(0, 297).replace(/\s+\S*$/, "")}\u2026` : text, from: "the README" };
25120
+ }
25121
+ return null;
25122
+ }
25123
+ var PAYMENT_PACKAGES = [
25124
+ [/^stripe$|^@stripe\//, "Stripe"],
25125
+ [/^@lemonsqueezy\//, "Lemon Squeezy"],
25126
+ [/^@paddle\//, "Paddle"],
25127
+ [/^@polar-sh\//, "Polar"],
25128
+ [/^@revenuecat\/|^react-native-purchases$/, "RevenueCat"]
25129
+ ];
25130
+ var AUTH_PACKAGES = [
25131
+ [/^@supabase\//, "Supabase"],
25132
+ [/^@clerk\//, "Clerk"],
25133
+ [/^next-auth$|^@auth\//, "Auth.js"],
25134
+ [/^firebase$|^firebase-admin$/, "Firebase"],
25135
+ [/^better-auth$/, "Better Auth"],
25136
+ [/^@kinde-oss\//, "Kinde"],
25137
+ [/^@auth0\//, "Auth0"]
25138
+ ];
25139
+ var words = (list) => new RegExp(`(?<![a-z])(?:${list})(?![a-z])`, "i");
25140
+ var SENSITIVE = [
25141
+ ["health", words("patients?|diagnos[a-z]*|medical|medications?|prescriptions?|symptoms?|health_records?"), "health"],
25142
+ ["legal", words("case_number|lawsuits?|attorneys?|legal_matters?|court[a-z]*"), "legal"],
25143
+ ["children", words("child|children|kids?|minors?|guardians?|parental_consent"), "children"]
25144
+ ];
25145
+ function schemaText(ctx) {
25146
+ const files = ctx.files.filter((f) => /\.sql$|schema\.prisma$|(?:^|\/)schema\.[cm]?[jt]s$|(?:^|\/)db\/schema\//.test(f) && !/node_modules\//.test(f));
25147
+ return files.slice(0, 200).map((f) => ctx.read(f) ?? "").join("\n");
25148
+ }
25149
+ var matchPackage = (deps2, list) => list.find(([re]) => deps2.some((d) => re.test(d)))?.[1];
25150
+ function inferAnswers(ctx) {
25151
+ const deps2 = Object.keys(allDeps(ctx.pkg));
25152
+ const payment = matchPackage(deps2, PAYMENT_PACKAGES);
25153
+ const paymentPage = ctx.files.find((f) => /(?:^|\/)(?:pricing|checkout|billing|subscribe)(?:\/|\.|$)/i.test(f));
25154
+ const chargesMoney = payment ? { value: true, from: `${payment} is installed` } : paymentPage ? { value: true, from: `there\u2019s a ${paymentPage.match(/pricing|checkout|billing|subscribe/i)[0].toLowerCase()} page` } : { value: false, from: "no payment provider or pricing page found" };
25155
+ const kinds = [];
25156
+ const reasons = [];
25157
+ const auth = matchPackage(deps2, AUTH_PACKAGES);
25158
+ const schema = schemaText(ctx);
25159
+ if (auth || /\b(?:users?|profiles?|accounts?)\b[\s\S]{0,400}\bemail\b/i.test(schema)) {
25160
+ kinds.push("personal");
25161
+ reasons.push(auth ? `sign-in with ${auth}` : "a users table with emails");
25162
+ }
25163
+ if (chargesMoney.value) {
25164
+ kinds.push("payments");
25165
+ reasons.push("it charges money");
25166
+ }
25167
+ for (const [kind, re, label] of SENSITIVE) {
25168
+ if (re.test(schema)) {
25169
+ kinds.push(kind);
25170
+ reasons.push(`${label} words in the database tables`);
25171
+ }
25172
+ }
25173
+ const dataHandled = kinds.length ? { value: kinds, from: reasons.join("; ") } : { value: ["none"], from: "no sign-in, payments or sensitive tables found" };
25174
+ return { whatItDoes: whatItDoes(ctx), chargesMoney, dataHandled };
25175
+ }
25176
+ function describeInferred(inferred) {
25177
+ return [
25178
+ `What it does: ${inferred.whatItDoes ? `${inferred.whatItDoes.value} (from ${inferred.whatItDoes.from})` : "not found, please describe it"}`,
25179
+ `Charges money: ${inferred.chargesMoney.value ? "yes" : "no"} (${inferred.chargesMoney.from})`,
25180
+ `Data it handles: ${inferred.dataHandled.value.map((k) => DATA_LABELS2[k]).join(", ")} (${inferred.dataHandled.from})`
25181
+ ];
25182
+ }
25183
+
25184
+ // src/commands/scan.ts
25185
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
25186
+ import os7 from "os";
25187
+ import path23 from "path";
25188
+
25189
+ // src/connectors/refresh.ts
25190
+ import { readFileSync as readFileSync20 } from "fs";
25191
+ async function refreshToolSettings(root) {
25192
+ if (process.env.AURA_NO_CONNECTORS) return;
25193
+ let previous = null;
25194
+ try {
25195
+ previous = JSON.parse(readFileSync20(auraPath(root, "scan.json"), "utf8"));
25196
+ } catch {
25197
+ }
25198
+ const snapshots = loadSnapshots(root);
25199
+ for (const connector of CONNECTORS) {
25200
+ if (!(previous ? connector.detect(previous) : connector.id === "github") || !isStale(snapshots[connector.id])) continue;
25201
+ const connection = await connector.connect(root).catch(() => null);
25202
+ if (connection?.ok) saveSnapshot(root, connector.id, await readSettings(connector, connection.ctx));
25203
+ }
25204
+ }
25205
+
24132
25206
  // src/commands/scan.ts
24133
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "fs";
24134
- import os5 from "os";
24135
- import path19 from "path";
24136
25207
  async function scanRef(root, ref, opts) {
24137
- const dir = mkdtempSync3(path19.join(os5.tmpdir(), "aura-base-"));
24138
- const wt = path19.join(dir, "tree");
25208
+ const dir = mkdtempSync3(path23.join(os7.tmpdir(), "aura-base-"));
25209
+ const wt = path23.join(dir, "tree");
24139
25210
  const add = await run("git", ["worktree", "add", "--detach", wt, ref], { cwd: root });
24140
25211
  if (add.code !== 0) {
24141
25212
  rmSync7(dir, { recursive: true, force: true });
@@ -24149,12 +25220,14 @@ async function scanRef(root, ref, opts) {
24149
25220
  }
24150
25221
  }
24151
25222
  async function scanCommand(flags) {
25223
+ const started = Date.now();
24152
25224
  const root = await findRepoRoot();
24153
25225
  const config2 = loadConfig(root);
24154
25226
  const quiet = Boolean(flags.json);
24155
25227
  const opts = { fast: Boolean(flags.fast || flags.base), noDownload: flags.download === false };
24156
25228
  const s = spin(!quiet && !flags.ci);
24157
25229
  s.start("Scanning");
25230
+ if (!flags.ci) await refreshToolSettings(root);
24158
25231
  const scan = await runScan(root, opts, (m) => s.message(`Checking: ${m}`));
24159
25232
  s.stop(`Scanned ${scan.checks.filter((c) => c.status !== "not-applicable").length} checks`);
24160
25233
  const client = AuraClient.from(config2);
@@ -24176,8 +25249,22 @@ async function scanCommand(flags) {
24176
25249
  writeAuraFile(root, "scan.json", JSON.stringify(scan, null, 2));
24177
25250
  writeAuraFile(root, "report.md", renderMarkdown(scan, steps));
24178
25251
  appendHistory(root, scan);
24179
- if (config2.telemetry)
24180
- await sendTelemetry(root, client, { event: "scan", score: scan.score, categories: Object.fromEntries(scan.categories.map((c) => [c.category, c.score])) });
25252
+ const findings = scan.checks.flatMap((c) => c.findings);
25253
+ await recordUsage(root, config2, {
25254
+ event: "scan",
25255
+ score: scan.score,
25256
+ categories: Object.fromEntries(scan.categories.map((c) => [c.category, c.score])),
25257
+ findings: {
25258
+ critical: findings.filter((f) => f.severity === "critical").length,
25259
+ high: findings.filter((f) => f.severity === "high").length,
25260
+ medium: findings.filter((f) => f.severity === "medium").length,
25261
+ low: findings.filter((f) => f.severity === "low").length,
25262
+ info: findings.filter((f) => f.severity === "info").length
25263
+ },
25264
+ failingChecks: scan.checks.filter((c) => c.status === "fail" || c.status === "partial").map((c) => c.checkId),
25265
+ durationMs: Date.now() - started
25266
+ });
25267
+ if (!flags.ci) await followUpFixes(root, config2, scan);
24181
25268
  let exitCode = 0;
24182
25269
  let base = null;
24183
25270
  if (flags.base) {
@@ -24188,7 +25275,7 @@ async function scanCommand(flags) {
24188
25275
  const baseIds = new Set(base ? base.checks.flatMap((c) => c.findings.map((f) => f.id)) : []);
24189
25276
  const newCritical = base ? scan.checks.flatMap((c) => c.findings).filter((f) => f.severity === "critical" && !baseIds.has(f.id)) : [];
24190
25277
  if (base && (base.score - scan.score > maxDrop || newCritical.length > 0)) exitCode = 1;
24191
- if (flags.commentFile) writeFileSync7(flags.commentFile, renderPrComment(scan, base, maxDrop));
25278
+ if (flags.commentFile) writeFileSync8(flags.commentFile, renderPrComment(scan, base, maxDrop));
24192
25279
  if (quiet) {
24193
25280
  printJson({ ...scan, nextSteps: steps, base: base ? { score: base.score, categories: base.categories } : void 0 });
24194
25281
  return exitCode;
@@ -24209,31 +25296,34 @@ async function scanCommand(flags) {
24209
25296
  // src/commands/init.ts
24210
25297
  var AGENT_IDS = ["claude", "codex", "cursor", "pi"];
24211
25298
  function detectAgents(root) {
24212
- const found = [];
24213
- if (existsSync16(`${root}/CLAUDE.md`) || existsSync16(`${root}/.claude`)) found.push("claude");
24214
- if (existsSync16(`${root}/AGENTS.md`)) found.push("codex");
24215
- if (existsSync16(`${root}/.cursor`) || existsSync16(`${root}/.cursorrules`)) found.push("cursor");
24216
- if (existsSync16(`${root}/.pi`)) found.push("pi");
24217
- return found;
25299
+ return detectAgentsFrom((rel) => existsSync20(path24.join(root, rel)));
24218
25300
  }
24219
25301
  function openEditor(file2) {
24220
- const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "nano");
24221
- const r = spawnSync3(editor, [file2], { stdio: "inherit", shell: process.platform === "win32" });
24222
- return r.status === 0;
25302
+ const fallback = process.platform === "win32" ? "notepad" : spawnSync3("sh", ["-c", "command -v nano"]).status === 0 ? "nano" : "vi";
25303
+ const editor = process.env.VISUAL || process.env.EDITOR || fallback;
25304
+ return spawnSync3(`${editor} "${file2}"`, { stdio: "inherit", shell: true }).status === 0;
25305
+ }
25306
+ function answersTemplate(inferred) {
25307
+ return JSON.stringify({
25308
+ whatItDoes: inferred.whatItDoes?.value ?? "\u2026",
25309
+ audience: "\u2026",
25310
+ chargesMoney: inferred.chargesMoney.value,
25311
+ dataHandled: inferred.dataHandled.value,
25312
+ userRegions: ["\u2026"]
25313
+ });
24223
25314
  }
24224
- var EXAMPLE_ANSWERS = '{"whatItDoes": "\u2026", "audience": "\u2026", "chargesMoney": false, "dataHandled": ["personal"], "userRegions": ["Canada"]}';
24225
- function briefHandoff() {
25315
+ function briefHandoff(inferred) {
24226
25316
  return [
24227
25317
  "",
24228
- `${pc.magenta("\u2726")} ${pc.bold("Next: write the product brief")}`,
24229
- " Ask the user these questions. If you are a coding agent, suggest answers from the code and let the user confirm or correct them:",
24230
- " 1. What does the app do?",
24231
- " 2. Who is it for?",
24232
- " 3. Does it charge money?",
24233
- " 4. What data does it handle? Any of: personal, payments, health, legal, children, none",
24234
- " 5. Where are its users? (countries or regions)",
24235
- " Save the answers to .aura/answers.json in this shape:",
24236
- ` ${EXAMPLE_ANSWERS}`,
25318
+ `${pc.magenta("\u2726")} ${pc.bold("Next: check the product brief with the user")}`,
25319
+ " Aura worked these out from the code. Show them to the user and correct anything they say is wrong:",
25320
+ ...describeInferred(inferred).map((l) => ` - ${l}`),
25321
+ " Then ask the user:",
25322
+ " - Who is the app for?",
25323
+ " - Where are its users? (countries or regions)",
25324
+ ' Save the answers to .aura/answers.json. Start from this and replace each "\u2026":',
25325
+ ` ${answersTemplate(inferred)}`,
25326
+ " dataHandled can include: personal, payments, health, legal, children, none.",
24237
25327
  " Then run: npx getaura@latest init --yes --answers .aura/answers.json",
24238
25328
  ""
24239
25329
  ].join("\n");
@@ -24262,6 +25352,8 @@ async function initCommand(flags) {
24262
25352
  else if (!gh.hasRemote) say.warn("This repo has no GitHub remote yet. Push it to GitHub before applying fixes (`gh repo create`).");
24263
25353
  else say.ok("git and the GitHub CLI are ready.");
24264
25354
  const config2 = loadConfig(root);
25355
+ if (flags.telemetry !== void 0) config2.shareUsage = flags.telemetry;
25356
+ const setupStep = (step) => recordUsage(root, config2, { event: "setup", step });
24265
25357
  let agents = (flags.agent ?? []).flatMap((a) => a.split(",")).map((a) => a.trim().toLowerCase());
24266
25358
  const unknown2 = agents.filter((a) => !AGENT_IDS.includes(a));
24267
25359
  if (unknown2.length) {
@@ -24283,20 +25375,15 @@ async function initCommand(flags) {
24283
25375
  }
24284
25376
  config2.agents = agents;
24285
25377
  let key = flags.key ?? getApiKey();
24286
- let explained = false;
24287
- if (!key && !process.env.CI && !config2.noKey) {
25378
+ let connectedNow = false;
25379
+ if (!key && !inCI()) {
24288
25380
  const outcome = await signInWithBrowser(getApiUrl(config2), { person: interactive(), waitSeconds: parseWait(flags.wait) });
24289
- if (outcome.status === "complete") {
24290
- key = outcome.key;
24291
- delete config2.noKey;
24292
- } else {
24293
- if (outcome.status === "skipped") config2.noKey = true;
24294
- if (!explainSignIn(outcome)) {
24295
- if (outcome.status === "pending") return 1;
24296
- say.dim("Continuing without a pilot key for now. Connect one later with `aura login`.");
24297
- }
24298
- explained = true;
25381
+ if (outcome.status !== "complete") {
25382
+ explainSignIn(outcome);
25383
+ return 1;
24299
25384
  }
25385
+ key = outcome.key;
25386
+ connectedNow = true;
24300
25387
  }
24301
25388
  let client = AuraClient.from(config2);
24302
25389
  if (key) {
@@ -24305,11 +25392,18 @@ async function initCommand(flags) {
24305
25392
  const me = await client.request("GET", "me");
24306
25393
  saveApiKey(key);
24307
25394
  say.ok(`Signed in as ${me.name}.`);
25395
+ if (sharesUsage(config2, key)) {
25396
+ say.dim(
25397
+ "During the pilot, Aura shares usage data with the Aura team: scores, fixes and errors, never your code. Turn it off with `aura config telemetry off`."
25398
+ );
25399
+ await setupStep("started");
25400
+ if (connectedNow) await setupStep("key_connected");
25401
+ }
24308
25402
  } catch (err) {
24309
25403
  say.warn(`${friendlyApiError(err)} Continuing without Aura\u2019s agent.`);
24310
25404
  client = new AuraClient(getApiUrl(config2), null);
24311
25405
  }
24312
- } else if (!explained) {
25406
+ } else {
24313
25407
  say.dim("No pilot key: Aura will still score your repo, with standard explanations.");
24314
25408
  }
24315
25409
  const ctx = await buildContext(root, { fast: true, noDownload: true }, config2);
@@ -24317,35 +25411,49 @@ async function initCommand(flags) {
24317
25411
  let brief2 = null;
24318
25412
  let keptBrief = false;
24319
25413
  if (flags.briefFile) {
24320
- brief2 = readFileSync15(flags.briefFile, "utf8");
24321
- } else if (!flags.answers && existsSync16(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
25414
+ brief2 = readFileSync21(flags.briefFile, "utf8");
25415
+ } else if (!flags.answers && existsSync20(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
24322
25416
  keptBrief = true;
24323
25417
  } else {
24324
25418
  let answers = null;
24325
25419
  if (flags.answers) {
24326
- answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync15(flags.answers, "utf8")));
25420
+ answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync21(flags.answers, "utf8")));
24327
25421
  } else if (tty) {
24328
- p.note("A few questions so Aura understands your app. Your answers go into the product brief.", "About your app");
24329
- const whatItDoes = unwrap(await p.text({ message: "What does your app do?", placeholder: "e.g. Lets dog owners book and pay for walks" }));
24330
- const audience = unwrap(await p.text({ message: "Who is it for?", placeholder: "e.g. Busy dog owners in Toronto" }));
24331
- const chargesMoney = unwrap(await p.confirm({ message: "Does it charge money?", initialValue: ctx.stack.services.includes("Stripe") }));
24332
- const dataHandled = unwrap(
25422
+ const inferred = inferAnswers(ctx);
25423
+ p.note(describeInferred(inferred).join("\n"), "Here\u2019s what Aura found about your app");
25424
+ const fix = new Set(
25425
+ unwrap(
25426
+ await p.multiselect({
25427
+ message: "Anything Aura got wrong? Select it to correct it, or press enter if it\u2019s all right.",
25428
+ options: [
25429
+ { value: "whatItDoes", label: "What it does" },
25430
+ { value: "chargesMoney", label: "Charges money" },
25431
+ { value: "dataHandled", label: "Data it handles" }
25432
+ ],
25433
+ required: false
25434
+ })
25435
+ )
25436
+ );
25437
+ const whatItDoes2 = inferred.whatItDoes && !fix.has("whatItDoes") ? inferred.whatItDoes.value : unwrap(
25438
+ await p.text({
25439
+ message: "What does your app do?",
25440
+ placeholder: "e.g. Lets dog owners book and pay for walks",
25441
+ initialValue: inferred.whatItDoes?.value
25442
+ })
25443
+ );
25444
+ const chargesMoney = fix.has("chargesMoney") ? unwrap(await p.confirm({ message: "Does it charge money?", initialValue: !inferred.chargesMoney.value })) : inferred.chargesMoney.value;
25445
+ const dataHandled = fix.has("dataHandled") ? unwrap(
24333
25446
  await p.multiselect({
24334
25447
  message: "What data does it handle?",
24335
- options: [
24336
- { value: "personal", label: "Personal info (names, emails)" },
24337
- { value: "payments", label: "Payments" },
24338
- { value: "health", label: "Health" },
24339
- { value: "legal", label: "Legal" },
24340
- { value: "children", label: "Children\u2019s data" },
24341
- { value: "none", label: "None of these" }
24342
- ],
25448
+ options: Object.entries(DATA_LABELS2).map(([value, label]) => ({ value, label: label[0].toUpperCase() + label.slice(1) })),
25449
+ initialValues: inferred.dataHandled.value,
24343
25450
  required: true
24344
25451
  })
24345
- );
25452
+ ) : inferred.dataHandled.value;
25453
+ const audience = unwrap(await p.text({ message: "Who is it for?", placeholder: "e.g. Busy dog owners in Toronto" }));
24346
25454
  const regions = unwrap(await p.text({ message: "Where are your users?", placeholder: "e.g. Canada, United States" }));
24347
25455
  answers = {
24348
- whatItDoes,
25456
+ whatItDoes: whatItDoes2,
24349
25457
  audience,
24350
25458
  chargesMoney,
24351
25459
  dataHandled,
@@ -24368,13 +25476,14 @@ async function initCommand(flags) {
24368
25476
  }
24369
25477
  brief2 ??= offlineBrief(answers, summary, ctx);
24370
25478
  } else {
24371
- console.log(briefHandoff());
25479
+ console.log(briefHandoff(inferAnswers(ctx)));
24372
25480
  }
24373
25481
  }
24374
25482
  if (keptBrief) {
24375
25483
  say.ok("Kept your existing product brief (.aura/brief.md).");
24376
25484
  } else if (brief2) {
24377
25485
  writeAuraFile(root, "brief.md", brief2);
25486
+ await setupStep("brief_written");
24378
25487
  if (tty) {
24379
25488
  p.note(brief2.split("\n").slice(0, 24).join("\n") + (brief2.split("\n").length > 24 ? "\n\u2026" : ""), "Product brief draft");
24380
25489
  const choice = unwrap(
@@ -24390,22 +25499,28 @@ async function initCommand(flags) {
24390
25499
  }
24391
25500
  say.ok("Saved the product brief to .aura/brief.md. It gets committed in the first-run PR (aura apply foundation).");
24392
25501
  }
24393
- writeChanges(root, skillChanges(agents, ["aura"]));
25502
+ writeChanges(root, [...skillChanges(agents, ["aura"]), ...commandChanges(agents)]);
24394
25503
  say.ok(`Installed the Aura skill for ${agents.map((a) => AGENT_LABELS[a]).join(", ")}.`);
24395
- if (flags.telemetry !== void 0) config2.telemetry = flags.telemetry;
24396
- else if (tty) {
24397
- config2.telemetry = unwrap(
24398
- await p.confirm({ message: "Share your Aura score and which actions you take with the Aura team? (Never code. Off by default.)", initialValue: false })
24399
- );
25504
+ await setupStep("skill_installed");
25505
+ if (agents.includes("claude") && !inCI() && !process.env.AURA_NO_CLAUDE_PLUGIN) {
25506
+ const plugin = await installClaudePlugin(root);
25507
+ if (plugin === "installed")
25508
+ say.ok(
25509
+ "Added Aura\u2019s slash commands to Claude Code: /aura:scan, /aura:next, /aura:fix and /aura:setup. Restart Claude Code (or run /reload-plugins) to use them."
25510
+ );
25511
+ else say.dim(`To add Aura\u2019s slash commands (/aura:scan, /aura:fix\u2026) to Claude Code, run these two commands:
25512
+ ${manualInstall().join("\n ")}`);
24400
25513
  }
25514
+ for (const agent of agents.filter((a) => a !== "claude")) say.ok(`Added Aura\u2019s commands for ${AGENT_LABELS[agent]}: ${commandList(agent)}.`);
24401
25515
  saveConfig(root, config2);
24402
25516
  ensureGitignore(root);
24403
- if (!process.env.CI && !await hasAuraCommand()) {
25517
+ if (!inCI() && !await hasAuraCommand()) {
24404
25518
  if (tty) {
24405
25519
  if (unwrap(await p.confirm({ message: "Install the `aura` command so you can run `aura scan` any time?", initialValue: true }))) {
24406
25520
  const s = spin(true);
24407
25521
  s.start("Installing the aura command");
24408
25522
  const installed = await installAuraCommand();
25523
+ await setupStep(installed.ok ? "command_installed" : "command_install_failed");
24409
25524
  if (installed.ok)
24410
25525
  s.stop(installed.onPath ? "Installed. Run `aura scan` from any folder." : "Installed. Open a new terminal window, then run `aura scan`.");
24411
25526
  else {
@@ -24428,11 +25543,13 @@ ${pc.magenta("\u2726")} ${pc.bold("Give the user the aura command")}`);
24428
25543
  return 0;
24429
25544
  }
24430
25545
  if (tty) p.outro("Running your first scan\u2026");
24431
- return scanCommand({});
25546
+ const scanned = await scanCommand({});
25547
+ await setupStep("first_scan");
25548
+ return scanned;
24432
25549
  }
24433
25550
 
24434
25551
  // src/commands/summary.ts
24435
- import path20 from "path";
25552
+ import path25 from "path";
24436
25553
  async function activity(root, since) {
24437
25554
  const log = await git(root, "log", `--since=${since.toISOString()}`, "--no-merges", "--format=%H%x09%an%x09%s");
24438
25555
  const commits = log.stdout.split("\n").filter(Boolean).slice(0, 300).map((l) => {
@@ -24489,7 +25606,7 @@ async function summaryCommand(flags) {
24489
25606
  const beforeIds = new Set(before ? allFindings(before).map((f) => f.id) : []);
24490
25607
  const nowIds = new Set(allFindings(now).map((f) => f.id));
24491
25608
  const req = {
24492
- repo: path20.basename(root),
25609
+ repo: path25.basename(root),
24493
25610
  since: since.toISOString(),
24494
25611
  until: until.toISOString(),
24495
25612
  product: productContext(root),
@@ -24537,11 +25654,11 @@ ${body}`);
24537
25654
  import pc4 from "picocolors";
24538
25655
 
24539
25656
  // src/core/update-check.ts
24540
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "fs";
24541
- import os6 from "os";
24542
- import path21 from "path";
25657
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "fs";
25658
+ import os8 from "os";
25659
+ import path26 from "path";
24543
25660
  var DAY_MS = 24 * 60 * 60 * 1e3;
24544
- var cacheFile = () => path21.join(os6.homedir(), ".aura", "update-check.json");
25661
+ var cacheFile = () => path26.join(os8.homedir(), ".aura", "update-check.json");
24545
25662
  function isNewer(a, b) {
24546
25663
  if (!/^\d+\.\d+\.\d+$/.test(a) || !/^\d+\.\d+\.\d+$/.test(b)) return false;
24547
25664
  const [x, y] = [a, b].map((v) => v.split(".").map(Number));
@@ -24561,21 +25678,21 @@ async function newerVersion(opts = {}) {
24561
25678
  const now = opts.now ?? Date.now();
24562
25679
  let latest = null;
24563
25680
  try {
24564
- const cached2 = JSON.parse(readFileSync16(cacheFile(), "utf8"));
25681
+ const cached2 = JSON.parse(readFileSync22(cacheFile(), "utf8"));
24565
25682
  if (now - cached2.checkedAt < DAY_MS) latest = cached2.latest;
24566
25683
  else throw new Error("stale");
24567
25684
  } catch {
24568
25685
  latest = await (opts.fetchLatest ?? fetchLatest)();
24569
25686
  try {
24570
- mkdirSync6(path21.dirname(cacheFile()), { recursive: true });
24571
- writeFileSync8(cacheFile(), JSON.stringify({ checkedAt: now, latest }));
25687
+ mkdirSync7(path26.dirname(cacheFile()), { recursive: true });
25688
+ writeFileSync9(cacheFile(), JSON.stringify({ checkedAt: now, latest }));
24572
25689
  } catch {
24573
25690
  }
24574
25691
  }
24575
25692
  return latest && isNewer(latest, opts.current ?? AURA_VERSION) ? latest : null;
24576
25693
  }
24577
25694
  function startUpdateCheck() {
24578
- if (!process.stdout.isTTY || process.env.CI || process.env.AURA_NO_UPDATE_CHECK) return Promise.resolve(null);
25695
+ if (!process.stdout.isTTY || inCI() || runningAgent() || process.env.AURA_NO_UPDATE_CHECK) return Promise.resolve(null);
24579
25696
  return newerVersion().catch(() => null);
24580
25697
  }
24581
25698
  function updateNotice(latest, argv1 = process.argv[1] ?? "") {
@@ -24601,6 +25718,12 @@ function action(fn) {
24601
25718
  say.error(err.message);
24602
25719
  if (process.env.AURA_DEBUG) console.error(err);
24603
25720
  process.exitCode = 1;
25721
+ const command = args.at(-1) instanceof Command ? args.at(-1).name() : "unknown";
25722
+ try {
25723
+ const root = await findRepoRoot();
25724
+ await recordUsage(root, loadConfig(root), { event: "error", command, errorType: errorKind(err) });
25725
+ } catch {
25726
+ }
24604
25727
  }
24605
25728
  const latest = await update;
24606
25729
  if (latest) console.error(pc4.dim(`